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