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