View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 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.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  // TODO rename to ContentCache
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      /** Constructor.
71       * @param parent the parent resource cache
72       * @param factory the resource factory
73       * @param mimeTypes Mimetype to use for meta data
74       * @param useFileMappedBuffer true to file memory mapped buffers
75       * @param etags true to support etags 
76       * @param gzip true to support gzip 
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      * @return Returns the maxCachedFiles.
132      */
133     public int getMaxCachedFiles()
134     {
135         return _maxCachedFiles;
136     }
137     
138     /* ------------------------------------------------------------ */
139     /**
140      * @param maxCachedFiles The maxCachedFiles to set.
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     /** Get a Entry from the cache.
181      * Get either a valid entry object or create a new one if possible.
182      *
183      * @param pathInContext The key into the cache
184      * @param maxBufferSize The maximum buffer to allocated for this request.  For cached content, a larger buffer may have
185      * previously been allocated and returned by the {@link HttpContent#getDirectBuffer()} or {@link HttpContent#getIndirectBuffer()} calls.
186      * @return The entry matching <code>pathInContext</code>, or a new entry 
187      * if no matching entry was found. If the content exists but is not cachable, 
188      * then a {@link ResourceHttpContent} instance is return. If 
189      * the resource does not exist, then null is returned.
190      * @throws IOException Problem loading the resource
191      */
192     @Override
193     public HttpContent getContent(String pathInContext,int maxBufferSize)
194         throws IOException
195     {
196         // Is the content in this cache?
197         CachedHttpContent content =_cache.get(pathInContext);
198         if (content!=null && (content).isValid())
199             return content;
200        
201         // try loading the content from our factory.
202         Resource resource=_factory.getResource(pathInContext);
203         HttpContent loaded = load(pathInContext,resource,maxBufferSize);
204         if (loaded!=null)
205             return loaded;
206         
207         // Is the content in the parent cache?
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      * @param resource the resource to test
221      * @return True if the resource is cacheable. The default implementation tests the cache sizes.
222      */
223     protected boolean isCacheable(Resource resource)
224     {
225         if (_maxCachedFiles<=0)
226             return false;
227         
228         long len = resource.length();
229 
230         // Will it fit in the cache?
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         // Will it fit in the cache?
245         if (isCacheable(resource))
246         {   
247             CachedHttpContent content=null;
248             
249             // Look for a gzip resource
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             // Add it to the cache.
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         // Look for non Cacheable gzip resource or content
286         String mt = _mimeTypes.getMimeByExtension(pathInContext);
287         if (_gzip)
288         {
289             // Is the gzip content cached?
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             // Is there a gzip resource?
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         // While we need to shrink
309         while (_cache.size()>0 && (_cachedFiles.get()>_maxCachedFiles || _cachedSize.get()>_maxCacheSize))
310         {
311             // Scan the entire cache and generate an ordered list by last accessed time.
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             // Invalidate least recently used first
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         // Only use file mapped buffers for cached resources, otherwise too much virtual memory commitment for
361         // a non shared resource.  Also ignore max buffer size
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             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 }