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             if (len<0)
292             {
293                 Log.warn("invalid resource: "+String.valueOf(resource)+" "+len);
294                 return null;
295             }
296             Buffer buffer = new IndirectNIOBuffer(len);
297             InputStream is = resource.getInputStream();
298             buffer.readFrom(is,len);
299             is.close();
300             return buffer;
301         }
302         catch(IOException e)
303         {
304             Log.warn(e);
305             return null;
306         }
307     }
308 
309     /* ------------------------------------------------------------ */
310     protected Buffer getDirectBuffer(Resource resource)
311     {
312         try
313         {
314             if (_useFileMappedBuffer && resource.getFile()!=null) 
315                 return new DirectNIOBuffer(resource.getFile());
316 
317             int len=(int)resource.length();
318             if (len<0)
319             {
320                 Log.warn("invalid resource: "+String.valueOf(resource)+" "+len);
321                 return null;
322             }
323             Buffer buffer = new DirectNIOBuffer(len);
324             InputStream is = resource.getInputStream();
325             buffer.readFrom(is,len);
326             is.close();
327             return buffer;
328         }
329         catch(IOException e)
330         {
331             Log.warn(e);
332             return null;
333         }
334     }
335 
336     /* ------------------------------------------------------------ */
337     public String toString()
338     {
339         return "ResourceCache["+_parent+","+_factory+"]@"+hashCode();
340     }
341     
342     /* ------------------------------------------------------------ */
343     /* ------------------------------------------------------------ */
344     /** MetaData associated with a context Resource.
345      */
346     public class Content implements HttpContent
347     {
348         final Resource _resource;
349         final int _length;
350         final String _key;
351         final long _lastModified;
352         final Buffer _lastModifiedBytes;
353         final Buffer _contentType;
354         
355         volatile long _lastAccessed;
356         AtomicReference<Buffer> _indirectBuffer=new AtomicReference<Buffer>();
357         AtomicReference<Buffer> _directBuffer=new AtomicReference<Buffer>();
358 
359         /* ------------------------------------------------------------ */
360         Content(String pathInContext,Resource resource)
361         {
362             _key=pathInContext;
363             _resource=resource;
364 
365             _contentType=_mimeTypes.getMimeByExtension(_resource.toString());
366             boolean exists=resource.exists();
367             _lastModified=exists?resource.lastModified():-1;
368             _lastModifiedBytes=_lastModified<0?null:new ByteArrayBuffer(HttpFields.formatDate(_lastModified));
369             
370             _length=exists?(int)resource.length():0;
371             _cachedSize.addAndGet(_length);
372             _cachedFiles.incrementAndGet();
373             _lastAccessed=System.currentTimeMillis();
374         }
375 
376 
377         /* ------------------------------------------------------------ */
378         public String getKey()
379         {
380             return _key;
381         }
382 
383         /* ------------------------------------------------------------ */
384         public boolean isCached()
385         {
386             return _key!=null;
387         }
388         
389         /* ------------------------------------------------------------ */
390         public boolean isMiss()
391         {
392             return false;
393         }
394 
395         /* ------------------------------------------------------------ */
396         public Resource getResource()
397         {
398             return _resource;
399         }
400         
401         /* ------------------------------------------------------------ */
402         boolean isValid()
403         {
404             if (_lastModified==_resource.lastModified())
405             {
406                 _lastAccessed=System.currentTimeMillis();
407                 return true;
408             }
409 
410             if (this==_cache.remove(_key))
411                 invalidate();
412             return false;
413         }
414 
415         /* ------------------------------------------------------------ */
416         protected void invalidate()
417         {
418             // Invalidate it
419             _cachedSize.addAndGet(-_length);
420             _cachedFiles.decrementAndGet();
421             _resource.release(); 
422         }
423 
424         /* ------------------------------------------------------------ */
425         public Buffer getLastModified()
426         {
427             return _lastModifiedBytes;
428         }
429 
430         /* ------------------------------------------------------------ */
431         public Buffer getContentType()
432         {
433             return _contentType;
434         }
435 
436         /* ------------------------------------------------------------ */
437         public void release()
438         {
439             // don't release while cached. Release when invalidated.
440         }
441 
442         /* ------------------------------------------------------------ */
443         public Buffer getIndirectBuffer()
444         {
445             Buffer buffer = _indirectBuffer.get();
446             if (buffer==null)
447             {
448                 Buffer buffer2=ResourceCache.this.getIndirectBuffer(_resource);
449                 
450                 if (buffer2==null)
451                     Log.warn("Could not load "+this);
452                 else if (_indirectBuffer.compareAndSet(null,buffer2))
453                     buffer=buffer2;
454                 else
455                     buffer=_indirectBuffer.get();
456             }
457             if (buffer==null)
458                 return null;
459             return new View(buffer);
460         }
461         
462 
463         /* ------------------------------------------------------------ */
464         public Buffer getDirectBuffer()
465         {
466             Buffer buffer = _directBuffer.get();
467             if (buffer==null)
468             {
469                 Buffer buffer2=ResourceCache.this.getDirectBuffer(_resource);
470 
471                 if (buffer2==null)
472                     Log.warn("Could not load "+this);
473                 else if (_directBuffer.compareAndSet(null,buffer2))
474                     buffer=buffer2;
475                 else
476                     buffer=_directBuffer.get();
477             }
478             if (buffer==null)
479                 return null;
480                         
481             return new View(buffer);
482         }
483         
484         /* ------------------------------------------------------------ */
485         public long getContentLength()
486         {
487             return _length;
488         }
489 
490         /* ------------------------------------------------------------ */
491         public InputStream getInputStream() throws IOException
492         {
493             Buffer indirect = getIndirectBuffer();
494             if (indirect!=null && indirect.array()!=null)
495                 return new ByteArrayInputStream(indirect.array(),indirect.getIndex(),indirect.length());
496            
497             return _resource.getInputStream();
498         }   
499 
500         /* ------------------------------------------------------------ */
501         @Override
502         public String toString()
503         {
504             return String.format("%s %s %d %s %s",_resource,_resource.exists(),_resource.lastModified(),_contentType,_lastModifiedBytes);
505         }   
506     }
507 }