View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 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.HttpContent;
35  import org.eclipse.jetty.http.HttpContent.ResourceAsHttpContent;
36  import org.eclipse.jetty.http.HttpFields;
37  import org.eclipse.jetty.http.MimeTypes;
38  import org.eclipse.jetty.util.BufferUtil;
39  import org.eclipse.jetty.util.log.Log;
40  import org.eclipse.jetty.util.log.Logger;
41  import org.eclipse.jetty.util.resource.Resource;
42  import org.eclipse.jetty.util.resource.ResourceFactory;
43  
44  
45  /* ------------------------------------------------------------ */
46  /** 
47   * 
48   */
49  public class ResourceCache
50  {
51      private static final Logger LOG = Log.getLogger(ResourceCache.class);
52  
53      private final ConcurrentMap<String,Content> _cache;
54      private final AtomicInteger _cachedSize;
55      private final AtomicInteger _cachedFiles;
56      private final ResourceFactory _factory;
57      private final ResourceCache _parent;
58      private final MimeTypes _mimeTypes;
59      private final boolean _etagSupported;
60      private final boolean  _useFileMappedBuffer;
61      
62      private int _maxCachedFileSize =4*1024*1024;
63      private int _maxCachedFiles=2048;
64      private int _maxCacheSize =32*1024*1024;
65      
66      /* ------------------------------------------------------------ */
67      /** Constructor.
68       * @param mimeTypes Mimetype to use for meta data
69       */
70      public ResourceCache(ResourceCache parent, ResourceFactory factory, MimeTypes mimeTypes,boolean useFileMappedBuffer,boolean etags)
71      {
72          _factory = factory;
73          _cache=new ConcurrentHashMap<String,Content>();
74          _cachedSize=new AtomicInteger();
75          _cachedFiles=new AtomicInteger();
76          _mimeTypes=mimeTypes;
77          _parent=parent;
78          _useFileMappedBuffer=useFileMappedBuffer;
79          _etagSupported=etags;
80      }
81  
82      /* ------------------------------------------------------------ */
83      public int getCachedSize()
84      {
85          return _cachedSize.get();
86      }
87      
88      /* ------------------------------------------------------------ */
89      public int getCachedFiles()
90      {
91          return _cachedFiles.get();
92      }
93      
94      /* ------------------------------------------------------------ */
95      public int getMaxCachedFileSize()
96      {
97          return _maxCachedFileSize;
98      }
99  
100     /* ------------------------------------------------------------ */
101     public void setMaxCachedFileSize(int maxCachedFileSize)
102     {
103         _maxCachedFileSize = maxCachedFileSize;
104         shrinkCache();
105     }
106 
107     /* ------------------------------------------------------------ */
108     public int getMaxCacheSize()
109     {
110         return _maxCacheSize;
111     }
112 
113     /* ------------------------------------------------------------ */
114     public void setMaxCacheSize(int maxCacheSize)
115     {
116         _maxCacheSize = maxCacheSize;
117         shrinkCache();
118     }
119 
120     /* ------------------------------------------------------------ */
121     /**
122      * @return Returns the maxCachedFiles.
123      */
124     public int getMaxCachedFiles()
125     {
126         return _maxCachedFiles;
127     }
128     
129     /* ------------------------------------------------------------ */
130     /**
131      * @param maxCachedFiles The maxCachedFiles to set.
132      */
133     public void setMaxCachedFiles(int maxCachedFiles)
134     {
135         _maxCachedFiles = maxCachedFiles;
136         shrinkCache();
137     }
138 
139     /* ------------------------------------------------------------ */
140     public boolean isUseFileMappedBuffer()
141     {
142         return _useFileMappedBuffer;
143     }
144 
145     /* ------------------------------------------------------------ */
146     public void flushCache()
147     {
148         if (_cache!=null)
149         {
150             while (_cache.size()>0)
151             {
152                 for (String path : _cache.keySet())
153                 {
154                     Content content = _cache.remove(path);
155                     if (content!=null)
156                         content.invalidate();
157                 }
158             }
159         }
160     }
161 
162     /* ------------------------------------------------------------ */
163     /** Get a Entry from the cache.
164      * Get either a valid entry object or create a new one if possible.
165      *
166      * @param pathInContext The key into the cache
167      * @return The entry matching <code>pathInContext</code>, or a new entry 
168      * if no matching entry was found. If the content exists but is not cachable, 
169      * then a {@link ResourceAsHttpContent} instance is return. If 
170      * the resource does not exist, then null is returned.
171      * @throws IOException Problem loading the resource
172      */
173     public HttpContent lookup(String pathInContext)
174         throws IOException
175     {
176         // Is the content in this cache?
177         Content content =_cache.get(pathInContext);
178         if (content!=null && (content).isValid())
179             return content;
180        
181         // try loading the content from our factory.
182         Resource resource=_factory.getResource(pathInContext);
183         HttpContent loaded = load(pathInContext,resource);
184         if (loaded!=null)
185             return loaded;
186         
187         // Is the content in the parent cache?
188         if (_parent!=null)
189         {
190             HttpContent httpContent=_parent.lookup(pathInContext);
191             if (httpContent!=null)
192                 return httpContent;
193         }
194         
195         return null;
196     }
197     
198     /* ------------------------------------------------------------ */
199     /**
200      * @param resource
201      * @return True if the resource is cacheable. The default implementation tests the cache sizes.
202      */
203     protected boolean isCacheable(Resource resource)
204     {
205         long len = resource.length();
206 
207         // Will it fit in the cache?
208         return  (len>0 && len<_maxCachedFileSize && len<_maxCacheSize);
209     }
210     
211     /* ------------------------------------------------------------ */
212     private HttpContent load(String pathInContext, Resource resource)
213         throws IOException
214     {
215         Content content=null;
216         
217         if (resource==null || !resource.exists())
218             return null;
219         
220         // Will it fit in the cache?
221         if (!resource.isDirectory() && isCacheable(resource))
222         {   
223             // Create the Content (to increment the cache sizes before adding the content 
224             content = new Content(pathInContext,resource);
225 
226             // reduce the cache to an acceptable size.
227             shrinkCache();
228 
229             // Add it to the cache.
230             Content added = _cache.putIfAbsent(pathInContext,content);
231             if (added!=null)
232             {
233                 content.invalidate();
234                 content=added;
235             }
236 
237             return content;
238         }
239         
240         return new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),getMaxCachedFileSize(),_etagSupported);
241         
242     }
243     
244     /* ------------------------------------------------------------ */
245     private void shrinkCache()
246     {
247         // While we need to shrink
248         while (_cache.size()>0 && (_cachedFiles.get()>_maxCachedFiles || _cachedSize.get()>_maxCacheSize))
249         {
250             // Scan the entire cache and generate an ordered list by last accessed time.
251             SortedSet<Content> sorted= new TreeSet<Content>(
252                     new Comparator<Content>()
253                     {
254                         public int compare(Content c1, Content c2)
255                         {
256                             if (c1._lastAccessed<c2._lastAccessed)
257                                 return -1;
258                             
259                             if (c1._lastAccessed>c2._lastAccessed)
260                                 return 1;
261 
262                             if (c1._length<c2._length)
263                                 return -1;
264                             
265                             return c1._key.compareTo(c2._key);
266                         }
267                     });
268             for (Content content : _cache.values())
269                 sorted.add(content);
270             
271             // Invalidate least recently used first
272             for (Content content : sorted)
273             {
274                 if (_cachedFiles.get()<=_maxCachedFiles && _cachedSize.get()<=_maxCacheSize)
275                     break;
276                 if (content==_cache.remove(content.getKey()))
277                     content.invalidate();
278             }
279         }
280     }
281     
282     /* ------------------------------------------------------------ */
283     protected ByteBuffer getIndirectBuffer(Resource resource)
284     {
285         try
286         {
287             int len=(int)resource.length();
288             if (len<0)
289             {
290                 LOG.warn("invalid resource: "+String.valueOf(resource)+" "+len);
291                 return null;
292             }
293             ByteBuffer buffer = BufferUtil.allocate(len);
294             int pos=BufferUtil.flipToFill(buffer);
295             if (resource.getFile()!=null)
296                 BufferUtil.readFrom(resource.getFile(),buffer);
297             else
298             {
299                 InputStream is = resource.getInputStream();
300                 BufferUtil.readFrom(is,len,buffer);
301                 is.close();
302             }
303             BufferUtil.flipToFlush(buffer,pos);
304             return buffer;
305         }
306         catch(IOException e)
307         {
308             LOG.warn(e);
309             return null;
310         }
311     }
312 
313     /* ------------------------------------------------------------ */
314     protected ByteBuffer getDirectBuffer(Resource resource)
315     {
316         try
317         {
318             if (_useFileMappedBuffer && resource.getFile()!=null) 
319                 return BufferUtil.toBuffer(resource.getFile());
320             
321             int len=(int)resource.length();
322             if (len<0)
323             {
324                 LOG.warn("invalid resource: "+String.valueOf(resource)+" "+len);
325                 return null;
326             }
327             ByteBuffer buffer = BufferUtil.allocateDirect(len);
328 
329             int pos=BufferUtil.flipToFill(buffer);
330             if (resource.getFile()!=null)
331                 BufferUtil.readFrom(resource.getFile(),buffer);
332             else
333             {
334                 InputStream is = resource.getInputStream();
335                 BufferUtil.readFrom(is,len,buffer);
336                 is.close();
337             }
338             BufferUtil.flipToFlush(buffer,pos);
339             
340             return buffer;
341         }
342         catch(IOException e)
343         {
344             LOG.warn(e);
345             return null;
346         }
347     }
348 
349     /* ------------------------------------------------------------ */
350     @Override
351     public String toString()
352     {
353         return "ResourceCache["+_parent+","+_factory+"]@"+hashCode();
354     }
355     
356     /* ------------------------------------------------------------ */
357     /* ------------------------------------------------------------ */
358     /** MetaData associated with a context Resource.
359      */
360     public class Content implements HttpContent
361     {
362         final Resource _resource;
363         final int _length;
364         final String _key;
365         final long _lastModified;
366         final ByteBuffer _lastModifiedBytes;
367         final ByteBuffer _contentType;
368         final String _etag;
369         
370         volatile long _lastAccessed;
371         AtomicReference<ByteBuffer> _indirectBuffer=new AtomicReference<ByteBuffer>();
372         AtomicReference<ByteBuffer> _directBuffer=new AtomicReference<ByteBuffer>();
373 
374         /* ------------------------------------------------------------ */
375         Content(String pathInContext,Resource resource)
376         {
377             _key=pathInContext;
378             _resource=resource;
379 
380             String mimeType = _mimeTypes.getMimeByExtension(_resource.toString());
381             _contentType=(mimeType==null?null:BufferUtil.toBuffer(mimeType));
382             boolean exists=resource.exists();
383             _lastModified=exists?resource.lastModified():-1;
384             _lastModifiedBytes=_lastModified<0?null:BufferUtil.toBuffer(HttpFields.formatDate(_lastModified));
385             
386             _length=exists?(int)resource.length():0;
387             _cachedSize.addAndGet(_length);
388             _cachedFiles.incrementAndGet();
389             _lastAccessed=System.currentTimeMillis();
390             
391             _etag=ResourceCache.this._etagSupported?resource.getWeakETag():null;
392         }
393 
394 
395         /* ------------------------------------------------------------ */
396         public String getKey()
397         {
398             return _key;
399         }
400 
401         /* ------------------------------------------------------------ */
402         public boolean isCached()
403         {
404             return _key!=null;
405         }
406         
407         /* ------------------------------------------------------------ */
408         public boolean isMiss()
409         {
410             return false;
411         }
412 
413         /* ------------------------------------------------------------ */
414         @Override
415         public Resource getResource()
416         {
417             return _resource;
418         }
419 
420         /* ------------------------------------------------------------ */
421         @Override
422         public String getETag()
423         {
424             return _etag;
425         }
426         
427         /* ------------------------------------------------------------ */
428         boolean isValid()
429         {
430             if (_lastModified==_resource.lastModified() && _length==_resource.length())
431             {
432                 _lastAccessed=System.currentTimeMillis();
433                 return true;
434             }
435 
436             if (this==_cache.remove(_key))
437                 invalidate();
438             return false;
439         }
440 
441         /* ------------------------------------------------------------ */
442         protected void invalidate()
443         {
444             // Invalidate it
445             _cachedSize.addAndGet(-_length);
446             _cachedFiles.decrementAndGet();
447             _resource.close(); 
448         }
449 
450         /* ------------------------------------------------------------ */
451         @Override
452         public String getLastModified()
453         {
454             return BufferUtil.toString(_lastModifiedBytes);
455         }
456 
457         /* ------------------------------------------------------------ */
458         @Override
459         public String getContentType()
460         {
461             return BufferUtil.toString(_contentType);
462         }
463 
464         /* ------------------------------------------------------------ */
465         @Override
466         public void release()
467         {
468             // don't release while cached. Release when invalidated.
469         }
470 
471         /* ------------------------------------------------------------ */
472         @Override
473         public ByteBuffer getIndirectBuffer()
474         {
475             ByteBuffer buffer = _indirectBuffer.get();
476             if (buffer==null)
477             {
478                 ByteBuffer buffer2=ResourceCache.this.getIndirectBuffer(_resource);
479                 
480                 if (buffer2==null)
481                     LOG.warn("Could not load "+this);
482                 else if (_indirectBuffer.compareAndSet(null,buffer2))
483                     buffer=buffer2;
484                 else
485                     buffer=_indirectBuffer.get();
486             }
487             if (buffer==null)
488                 return null;
489             return buffer.slice();
490         }
491         
492 
493         /* ------------------------------------------------------------ */
494         @Override
495         public ByteBuffer getDirectBuffer()
496         {
497             ByteBuffer buffer = _directBuffer.get();
498             if (buffer==null)
499             {
500                 ByteBuffer buffer2=ResourceCache.this.getDirectBuffer(_resource);
501 
502                 if (buffer2==null)
503                     LOG.warn("Could not load "+this);
504                 else if (_directBuffer.compareAndSet(null,buffer2))
505                     buffer=buffer2;
506                 else
507                     buffer=_directBuffer.get();
508             }
509             if (buffer==null)
510                 return null;
511             return buffer.asReadOnlyBuffer();
512         }
513         
514         /* ------------------------------------------------------------ */
515         @Override
516         public long getContentLength()
517         {
518             return _length;
519         }
520 
521         /* ------------------------------------------------------------ */
522         @Override
523         public InputStream getInputStream() throws IOException
524         {
525             ByteBuffer indirect = getIndirectBuffer();
526             if (indirect!=null && indirect.hasArray())
527                 return new ByteArrayInputStream(indirect.array(),indirect.arrayOffset()+indirect.position(),indirect.remaining());
528            
529             return _resource.getInputStream();
530         }   
531         
532         /* ------------------------------------------------------------ */
533         @Override
534         public ReadableByteChannel getReadableByteChannel() throws IOException
535         {
536             return _resource.getReadableByteChannel();
537         }
538 
539 
540         /* ------------------------------------------------------------ */
541         @Override
542         public String toString()
543         {
544             return String.format("%s %s %d %s %s",_resource,_resource.exists(),_resource.lastModified(),_contentType,_lastModifiedBytes);
545         }   
546     }
547 }