View Javadoc

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