View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
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  // TODO rename to ContentCache
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      /** Constructor.
70       * @param parent the parent resource cache
71       * @param factory the resource factory
72       * @param mimeTypes Mimetype to use for meta data
73       * @param useFileMappedBuffer true to file memory mapped buffers
74       * @param etags true to support etags 
75       * @param gzip true to support gzip 
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      * @return Returns the maxCachedFiles.
131      */
132     public int getMaxCachedFiles()
133     {
134         return _maxCachedFiles;
135     }
136     
137     /* ------------------------------------------------------------ */
138     /**
139      * @param maxCachedFiles The maxCachedFiles to set.
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     /** Get a Entry from the cache.
180      * Get either a valid entry object or create a new one if possible.
181      *
182      * @param pathInContext The key into the cache
183      * @return The entry matching <code>pathInContext</code>, or a new entry 
184      * if no matching entry was found. If the content exists but is not cachable, 
185      * then a {@link ResourceHttpContent} instance is return. If 
186      * the resource does not exist, then null is returned.
187      * @throws IOException Problem loading the resource
188      */
189     @Override
190     public HttpContent getContent(String pathInContext)
191         throws IOException
192     {
193         // Is the content in this cache?
194         CachedHttpContent content =_cache.get(pathInContext);
195         if (content!=null && (content).isValid())
196             return content;
197        
198         // try loading the content from our factory.
199         Resource resource=_factory.getResource(pathInContext);
200         HttpContent loaded = load(pathInContext,resource);
201         if (loaded!=null)
202             return loaded;
203         
204         // Is the content in the parent cache?
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      * @param resource the resource to test
218      * @return True if the resource is cacheable. The default implementation tests the cache sizes.
219      */
220     protected boolean isCacheable(Resource resource)
221     {
222         if (_maxCachedFiles<=0)
223             return false;
224         
225         long len = resource.length();
226 
227         // Will it fit in the cache?
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         // Will it fit in the cache?
243         if (isCacheable(resource))
244         {   
245             CachedHttpContent content=null;
246             
247             // Look for a gzip resource
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             // reduce the cache to an acceptable size.
274             shrinkCache();
275 
276             // Add it to the cache.
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         // Look for a gzip resource or content
288         String mt = _mimeTypes.getMimeByExtension(pathInContext);
289         if (_gzip)
290         {
291             // Is the gzip content cached?
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             // Is there a gzip resource?
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         // While we need to shrink
311         while (_cache.size()>0 && (_cachedFiles.get()>_maxCachedFiles || _cachedSize.get()>_maxCacheSize))
312         {
313             // Scan the entire cache and generate an ordered list by last accessed time.
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             // Invalidate least recently used first
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     /** MetaData associated with a context Resource.
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             // Invalidate it
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 }