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