1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.server;
20
21 import java.io.ByteArrayInputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.nio.ByteBuffer;
25 import java.nio.channels.ReadableByteChannel;
26 import java.util.Comparator;
27 import java.util.SortedSet;
28 import java.util.TreeSet;
29 import java.util.concurrent.ConcurrentHashMap;
30 import java.util.concurrent.ConcurrentMap;
31 import java.util.concurrent.atomic.AtomicInteger;
32 import java.util.concurrent.atomic.AtomicReference;
33
34 import org.eclipse.jetty.http.DateGenerator;
35 import org.eclipse.jetty.http.GzipHttpContent;
36 import org.eclipse.jetty.http.HttpContent;
37 import org.eclipse.jetty.http.HttpField;
38 import org.eclipse.jetty.http.HttpHeader;
39 import org.eclipse.jetty.http.MimeTypes;
40 import org.eclipse.jetty.http.MimeTypes.Type;
41 import org.eclipse.jetty.http.PreEncodedHttpField;
42 import org.eclipse.jetty.http.ResourceHttpContent;
43 import org.eclipse.jetty.util.BufferUtil;
44 import org.eclipse.jetty.util.log.Log;
45 import org.eclipse.jetty.util.log.Logger;
46 import org.eclipse.jetty.util.resource.Resource;
47 import org.eclipse.jetty.util.resource.ResourceFactory;
48
49
50 public class ResourceCache implements HttpContent.Factory
51 {
52 private static final Logger LOG = Log.getLogger(ResourceCache.class);
53
54 private final ConcurrentMap<String,CachedHttpContent> _cache;
55 private final AtomicInteger _cachedSize;
56 private final AtomicInteger _cachedFiles;
57 private final ResourceFactory _factory;
58 private final ResourceCache _parent;
59 private final MimeTypes _mimeTypes;
60 private final boolean _etags;
61 private final boolean _gzip;
62 private final boolean _useFileMappedBuffer;
63
64 private int _maxCachedFileSize =128*1024*1024;
65 private int _maxCachedFiles=2048;
66 private int _maxCacheSize =256*1024*1024;
67
68
69
70
71
72
73
74
75
76
77 public ResourceCache(ResourceCache parent, ResourceFactory factory, MimeTypes mimeTypes,boolean useFileMappedBuffer,boolean etags,boolean gzip)
78 {
79 _factory = factory;
80 _cache=new ConcurrentHashMap<String,CachedHttpContent>();
81 _cachedSize=new AtomicInteger();
82 _cachedFiles=new AtomicInteger();
83 _mimeTypes=mimeTypes;
84 _parent=parent;
85 _useFileMappedBuffer=useFileMappedBuffer;
86 _etags=etags;
87 _gzip=gzip;
88 }
89
90
91 public int getCachedSize()
92 {
93 return _cachedSize.get();
94 }
95
96
97 public int getCachedFiles()
98 {
99 return _cachedFiles.get();
100 }
101
102
103 public int getMaxCachedFileSize()
104 {
105 return _maxCachedFileSize;
106 }
107
108
109 public void setMaxCachedFileSize(int maxCachedFileSize)
110 {
111 _maxCachedFileSize = maxCachedFileSize;
112 shrinkCache();
113 }
114
115
116 public int getMaxCacheSize()
117 {
118 return _maxCacheSize;
119 }
120
121
122 public void setMaxCacheSize(int maxCacheSize)
123 {
124 _maxCacheSize = maxCacheSize;
125 shrinkCache();
126 }
127
128
129
130
131
132 public int getMaxCachedFiles()
133 {
134 return _maxCachedFiles;
135 }
136
137
138
139
140
141 public void setMaxCachedFiles(int maxCachedFiles)
142 {
143 _maxCachedFiles = maxCachedFiles;
144 shrinkCache();
145 }
146
147
148 public boolean isUseFileMappedBuffer()
149 {
150 return _useFileMappedBuffer;
151 }
152
153
154 public void flushCache()
155 {
156 if (_cache!=null)
157 {
158 while (_cache.size()>0)
159 {
160 for (String path : _cache.keySet())
161 {
162 CachedHttpContent content = _cache.remove(path);
163 if (content!=null)
164 content.invalidate();
165 }
166 }
167 }
168 }
169
170
171 @Deprecated
172 public HttpContent lookup(String pathInContext)
173 throws IOException
174 {
175 return getContent(pathInContext);
176 }
177
178
179
180
181
182
183
184
185
186
187
188
189 @Override
190 public HttpContent getContent(String pathInContext)
191 throws IOException
192 {
193
194 CachedHttpContent content =_cache.get(pathInContext);
195 if (content!=null && (content).isValid())
196 return content;
197
198
199 Resource resource=_factory.getResource(pathInContext);
200 HttpContent loaded = load(pathInContext,resource);
201 if (loaded!=null)
202 return loaded;
203
204
205 if (_parent!=null)
206 {
207 HttpContent httpContent=_parent.lookup(pathInContext);
208 if (httpContent!=null)
209 return httpContent;
210 }
211
212 return null;
213 }
214
215
216
217
218
219
220 protected boolean isCacheable(Resource resource)
221 {
222 if (_maxCachedFiles<=0)
223 return false;
224
225 long len = resource.length();
226
227
228 return (len>0 && len<_maxCachedFileSize && len<_maxCacheSize);
229 }
230
231
232 private HttpContent load(String pathInContext, Resource resource)
233 throws IOException
234 {
235
236 if (resource==null || !resource.exists())
237 return null;
238
239 if (resource.isDirectory())
240 return new ResourceHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),getMaxCachedFileSize());
241
242
243 if (isCacheable(resource))
244 {
245 CachedHttpContent content=null;
246
247
248 if (_gzip)
249 {
250 String pathInContextGz=pathInContext+".gz";
251 CachedHttpContent contentGz = _cache.get(pathInContextGz);
252 if (contentGz==null || !contentGz.isValid())
253 {
254 contentGz=null;
255 Resource resourceGz=_factory.getResource(pathInContextGz);
256 if (resourceGz.exists() && resourceGz.lastModified()>=resource.lastModified() && resourceGz.length()<resource.length())
257 {
258 contentGz = new CachedHttpContent(pathInContextGz,resourceGz,null);
259 shrinkCache();
260 CachedHttpContent added = _cache.putIfAbsent(pathInContextGz,contentGz);
261 if (added!=null)
262 {
263 contentGz.invalidate();
264 contentGz=added;
265 }
266 }
267 }
268 content = new CachedHttpContent(pathInContext,resource,contentGz);
269 }
270 else
271 content = new CachedHttpContent(pathInContext,resource,null);
272
273
274 shrinkCache();
275
276
277 CachedHttpContent added = _cache.putIfAbsent(pathInContext,content);
278 if (added!=null)
279 {
280 content.invalidate();
281 content=added;
282 }
283
284 return content;
285 }
286
287
288 String mt = _mimeTypes.getMimeByExtension(pathInContext);
289 if (_gzip)
290 {
291
292 String pathInContextGz=pathInContext+".gz";
293 CachedHttpContent contentGz = _cache.get(pathInContextGz);
294 if (contentGz!=null && contentGz.isValid() && contentGz.getResource().lastModified()>=resource.lastModified())
295 return new ResourceHttpContent(resource,mt,getMaxCachedFileSize(),contentGz);
296
297
298 Resource resourceGz=_factory.getResource(pathInContextGz);
299 if (resourceGz.exists() && resourceGz.lastModified()>=resource.lastModified() && resourceGz.length()<resource.length())
300 return new ResourceHttpContent(resource,mt,getMaxCachedFileSize(),
301 new ResourceHttpContent(resourceGz,_mimeTypes.getMimeByExtension(pathInContextGz),getMaxCachedFileSize()));
302 }
303
304 return new ResourceHttpContent(resource,mt,getMaxCachedFileSize());
305 }
306
307
308 private void shrinkCache()
309 {
310
311 while (_cache.size()>0 && (_cachedFiles.get()>_maxCachedFiles || _cachedSize.get()>_maxCacheSize))
312 {
313
314 SortedSet<CachedHttpContent> sorted= new TreeSet<CachedHttpContent>(
315 new Comparator<CachedHttpContent>()
316 {
317 public int compare(CachedHttpContent c1, CachedHttpContent c2)
318 {
319 if (c1._lastAccessed<c2._lastAccessed)
320 return -1;
321
322 if (c1._lastAccessed>c2._lastAccessed)
323 return 1;
324
325 if (c1._contentLengthValue<c2._contentLengthValue)
326 return -1;
327
328 return c1._key.compareTo(c2._key);
329 }
330 });
331 for (CachedHttpContent content : _cache.values())
332 sorted.add(content);
333
334
335 for (CachedHttpContent content : sorted)
336 {
337 if (_cachedFiles.get()<=_maxCachedFiles && _cachedSize.get()<=_maxCacheSize)
338 break;
339 if (content==_cache.remove(content.getKey()))
340 content.invalidate();
341 }
342 }
343 }
344
345
346 protected ByteBuffer getIndirectBuffer(Resource resource)
347 {
348 try
349 {
350 return BufferUtil.toBuffer(resource,true);
351 }
352 catch(IOException|IllegalArgumentException e)
353 {
354 LOG.warn(e);
355 return null;
356 }
357 }
358
359
360 protected ByteBuffer getDirectBuffer(Resource resource)
361 {
362 try
363 {
364 if (_useFileMappedBuffer && resource.getFile()!=null && resource.length()<Integer.MAX_VALUE)
365 return BufferUtil.toMappedBuffer(resource.getFile());
366
367 return BufferUtil.toBuffer(resource,true);
368 }
369 catch(IOException|IllegalArgumentException e)
370 {
371 LOG.warn(e);
372 return null;
373 }
374 }
375
376
377 @Override
378 public String toString()
379 {
380 return "ResourceCache["+_parent+","+_factory+"]@"+hashCode();
381 }
382
383
384
385
386
387 public class CachedHttpContent implements HttpContent
388 {
389 final String _key;
390 final Resource _resource;
391 final int _contentLengthValue;
392 final HttpField _contentType;
393 final String _characterEncoding;
394 final MimeTypes.Type _mimeType;
395 final HttpField _contentLength;
396 final HttpField _lastModified;
397 final long _lastModifiedValue;
398 final HttpField _etag;
399 final CachedGzipHttpContent _gzipped;
400
401 volatile long _lastAccessed;
402 AtomicReference<ByteBuffer> _indirectBuffer=new AtomicReference<ByteBuffer>();
403 AtomicReference<ByteBuffer> _directBuffer=new AtomicReference<ByteBuffer>();
404
405
406 CachedHttpContent(String pathInContext,Resource resource,CachedHttpContent gzipped)
407 {
408 _key=pathInContext;
409 _resource=resource;
410
411 String contentType = _mimeTypes.getMimeByExtension(_resource.toString());
412 _contentType=contentType==null?null:new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,contentType);
413 _characterEncoding = _contentType==null?null:MimeTypes.getCharsetFromContentType(contentType);
414 _mimeType = _contentType==null?null:MimeTypes.CACHE.get(MimeTypes.getContentTypeWithoutCharset(contentType));
415
416 boolean exists=resource.exists();
417 _lastModifiedValue=exists?resource.lastModified():-1L;
418 _lastModified=_lastModifiedValue==-1?null
419 :new PreEncodedHttpField(HttpHeader.LAST_MODIFIED,DateGenerator.formatDate(_lastModifiedValue));
420
421 _contentLengthValue=exists?(int)resource.length():0;
422 _contentLength=new PreEncodedHttpField(HttpHeader.CONTENT_LENGTH,Long.toString(_contentLengthValue));
423
424 _cachedSize.addAndGet(_contentLengthValue);
425 _cachedFiles.incrementAndGet();
426 _lastAccessed=System.currentTimeMillis();
427
428 _etag=ResourceCache.this._etags?new PreEncodedHttpField(HttpHeader.ETAG,resource.getWeakETag()):null;
429
430 _gzipped=gzipped==null?null:new CachedGzipHttpContent(this,gzipped);
431 }
432
433
434
435 public String getKey()
436 {
437 return _key;
438 }
439
440
441 public boolean isCached()
442 {
443 return _key!=null;
444 }
445
446
447 public boolean isMiss()
448 {
449 return false;
450 }
451
452
453 @Override
454 public Resource getResource()
455 {
456 return _resource;
457 }
458
459
460 @Override
461 public HttpField getETag()
462 {
463 return _etag;
464 }
465
466
467 @Override
468 public String getETagValue()
469 {
470 return _etag.getValue();
471 }
472
473
474 boolean isValid()
475 {
476 if (_lastModifiedValue==_resource.lastModified() && _contentLengthValue==_resource.length())
477 {
478 _lastAccessed=System.currentTimeMillis();
479 return true;
480 }
481
482 if (this==_cache.remove(_key))
483 invalidate();
484 return false;
485 }
486
487
488 protected void invalidate()
489 {
490
491 _cachedSize.addAndGet(-_contentLengthValue);
492 _cachedFiles.decrementAndGet();
493 _resource.close();
494 }
495
496
497 @Override
498 public HttpField getLastModified()
499 {
500 return _lastModified;
501 }
502
503
504 @Override
505 public String getLastModifiedValue()
506 {
507 return _lastModified==null?null:_lastModified.getValue();
508 }
509
510
511
512 @Override
513 public HttpField getContentType()
514 {
515 return _contentType;
516 }
517
518
519 @Override
520 public String getContentTypeValue()
521 {
522 return _contentType==null?null:_contentType.getValue();
523 }
524
525
526 @Override
527 public HttpField getContentEncoding()
528 {
529 return null;
530 }
531
532
533 @Override
534 public String getContentEncodingValue()
535 {
536 return null;
537 }
538
539
540 @Override
541 public String getCharacterEncoding()
542 {
543 return _characterEncoding;
544 }
545
546
547 @Override
548 public Type getMimeType()
549 {
550 return _mimeType;
551 }
552
553
554
555 @Override
556 public void release()
557 {
558 }
559
560
561 @Override
562 public ByteBuffer getIndirectBuffer()
563 {
564 ByteBuffer buffer = _indirectBuffer.get();
565 if (buffer==null)
566 {
567 ByteBuffer buffer2=ResourceCache.this.getIndirectBuffer(_resource);
568
569 if (buffer2==null)
570 LOG.warn("Could not load "+this);
571 else if (_indirectBuffer.compareAndSet(null,buffer2))
572 buffer=buffer2;
573 else
574 buffer=_indirectBuffer.get();
575 }
576 if (buffer==null)
577 return null;
578 return buffer.slice();
579 }
580
581
582
583 @Override
584 public ByteBuffer getDirectBuffer()
585 {
586 ByteBuffer buffer = _directBuffer.get();
587 if (buffer==null)
588 {
589 ByteBuffer buffer2=ResourceCache.this.getDirectBuffer(_resource);
590
591 if (buffer2==null)
592 LOG.warn("Could not load "+this);
593 else if (_directBuffer.compareAndSet(null,buffer2))
594 buffer=buffer2;
595 else
596 buffer=_directBuffer.get();
597 }
598 if (buffer==null)
599 return null;
600 return buffer.asReadOnlyBuffer();
601 }
602
603
604 @Override
605 public HttpField getContentLength()
606 {
607 return _contentLength;
608 }
609
610
611 @Override
612 public long getContentLengthValue()
613 {
614 return _contentLengthValue;
615 }
616
617
618 @Override
619 public InputStream getInputStream() throws IOException
620 {
621 ByteBuffer indirect = getIndirectBuffer();
622 if (indirect!=null && indirect.hasArray())
623 return new ByteArrayInputStream(indirect.array(),indirect.arrayOffset()+indirect.position(),indirect.remaining());
624
625 return _resource.getInputStream();
626 }
627
628
629 @Override
630 public ReadableByteChannel getReadableByteChannel() throws IOException
631 {
632 return _resource.getReadableByteChannel();
633 }
634
635
636 @Override
637 public String toString()
638 {
639 return String.format("CachedContent@%x{r=%s,e=%b,lm=%s,ct=%s,gz=%b}",hashCode(),_resource,_resource.exists(),_lastModified,_contentType,_gzipped!=null);
640 }
641
642
643 @Override
644 public HttpContent getGzipContent()
645 {
646 return (_gzipped!=null && _gzipped.isValid())?_gzipped:null;
647 }
648 }
649
650
651
652
653 public class CachedGzipHttpContent extends GzipHttpContent
654 {
655 private final CachedHttpContent _content;
656 private final CachedHttpContent _contentGz;
657 private final HttpField _etag;
658
659 CachedGzipHttpContent(CachedHttpContent content, CachedHttpContent contentGz)
660 {
661 super(content,contentGz);
662 _content=content;
663 _contentGz=contentGz;
664
665 _etag=(ResourceCache.this._etags)?new PreEncodedHttpField(HttpHeader.ETAG,_content.getResource().getWeakETag("--gzip")):null;
666 }
667
668 public boolean isValid()
669 {
670 return _contentGz.isValid() && _content.isValid() && _content.getResource().lastModified() <= _contentGz.getResource().lastModified();
671 }
672
673 @Override
674 public HttpField getETag()
675 {
676 if (_etag!=null)
677 return _etag;
678 return super.getETag();
679 }
680
681 @Override
682 public String getETagValue()
683 {
684 if (_etag!=null)
685 return _etag.getValue();
686 return super.getETagValue();
687 }
688
689 @Override
690 public String toString()
691 {
692 return "Cached"+super.toString();
693 }
694 }
695
696 }