View Javadoc

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