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