1   // ========================================================================
2   // Copyright 2000-2005 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // Licensed under the Apache License, Version 2.0 (the "License");
5   // you may not use this file except in compliance with the License.
6   // You may obtain a copy of the License at 
7   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  // ========================================================================
14  
15  package org.mortbay.jetty;
16  
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.io.Serializable;
20  import java.util.HashMap;
21  import java.util.Map;
22  
23  import org.mortbay.component.AbstractLifeCycle;
24  import org.mortbay.io.Buffer;
25  import org.mortbay.io.ByteArrayBuffer;
26  import org.mortbay.io.View;
27  import org.mortbay.resource.Resource;
28  import org.mortbay.resource.ResourceFactory;
29  
30  
31  /* ------------------------------------------------------------ */
32  /** 
33   * @author Greg Wilkins
34   */
35  public class ResourceCache extends AbstractLifeCycle implements Serializable
36  {   
37      private int _maxCachedFileSize =1024*1024;
38      private int _maxCachedFiles=2048;
39      private int _maxCacheSize =16*1024*1024;
40      private MimeTypes _mimeTypes;
41      
42      protected transient Map _cache;
43      protected transient int _cachedSize;
44      protected transient int _cachedFiles;
45      protected transient Content _mostRecentlyUsed;
46      protected transient Content _leastRecentlyUsed;
47  
48      /* ------------------------------------------------------------ */
49      /** Constructor.
50       */
51      public ResourceCache(MimeTypes mimeTypes)
52      {
53          _mimeTypes=mimeTypes;
54      }
55  
56      /* ------------------------------------------------------------ */
57      public int getCachedSize()
58      {
59          return _cachedSize;
60      }
61      
62      /* ------------------------------------------------------------ */
63      public int getCachedFiles()
64      {
65          return _cachedFiles;
66      }
67      
68      
69      /* ------------------------------------------------------------ */
70      public int getMaxCachedFileSize()
71      {
72          return _maxCachedFileSize;
73      }
74  
75      /* ------------------------------------------------------------ */
76      public void setMaxCachedFileSize(int maxCachedFileSize)
77      {
78          _maxCachedFileSize = maxCachedFileSize;
79          flushCache();
80      }
81  
82      /* ------------------------------------------------------------ */
83      public int getMaxCacheSize()
84      {
85          return _maxCacheSize;
86      }
87  
88      /* ------------------------------------------------------------ */
89      public void setMaxCacheSize(int maxCacheSize)
90      {
91          _maxCacheSize = maxCacheSize;
92          flushCache();
93      }
94  
95      /* ------------------------------------------------------------ */
96      /**
97       * @return Returns the maxCachedFiles.
98       */
99      public int getMaxCachedFiles()
100     {
101         return _maxCachedFiles;
102     }
103     
104     /* ------------------------------------------------------------ */
105     /**
106      * @param maxCachedFiles The maxCachedFiles to set.
107      */
108     public void setMaxCachedFiles(int maxCachedFiles)
109     {
110         _maxCachedFiles = maxCachedFiles;
111     }
112     
113     /* ------------------------------------------------------------ */
114     public void flushCache()
115     {
116         if (_cache!=null)
117         {
118             synchronized(this)
119             {
120                 _cache.clear();
121                 _cachedSize=0;
122                 _cachedFiles=0;
123                 _mostRecentlyUsed=null;
124                 _leastRecentlyUsed=null;
125             }
126         }
127     }
128 
129     /* ------------------------------------------------------------ */
130     /** Get a Entry from the cache.
131      * Get either a valid entry object or create a new one if possible.
132      *
133      * @param pathInContext The key into the cache
134      * @param factory If no matching entry is found, this {@link ResourceFactory} will be used to create the {@link Resource} 
135      *                for the new enry that is created.
136      * @return The entry matching <code>pathInContext</code>, or a new entry if no matching entry was found
137      */
138     public Content lookup(String pathInContext, ResourceFactory factory)
139         throws IOException
140     {
141         Content content=null;
142         
143         // Look up cache operations
144         synchronized(_cache)
145         {
146             // Look for it in the cache
147             content = (Content)_cache.get(pathInContext);
148         
149             if (content!=null && content.isValid())
150             {
151                 return content;
152             }    
153         }
154         Resource resource=factory.getResource(pathInContext);
155         return load(pathInContext,resource);
156     }
157 
158     /* ------------------------------------------------------------ */
159     public Content lookup(String pathInContext, Resource resource)
160         throws IOException
161     {
162         Content content=null;
163         
164         // Look up cache operations
165         synchronized(_cache)
166         {
167             // Look for it in the cache
168             content = (Content)_cache.get(pathInContext);
169         
170             if (content!=null && content.isValid())
171             {
172                 return content;
173             }    
174         }
175         return load(pathInContext,resource);
176     }
177 
178     /* ------------------------------------------------------------ */
179     private Content load(String pathInContext, Resource resource)
180         throws IOException
181     {
182         Content content=null;
183         if (resource!=null && resource.exists() && !resource.isDirectory())
184         {
185             long len = resource.length();
186             if (len>0 && len<_maxCachedFileSize && len<_maxCacheSize)
187             {   
188                 int must_be_smaller_than=_maxCacheSize-(int)len;
189                 
190                 synchronized(_cache)
191                 {
192                     // check the cache is not full of locked content before loading content
193 
194                     while(_leastRecentlyUsed!=null && (_cachedSize>must_be_smaller_than || (_maxCachedFiles>0 && _cachedFiles>=_maxCachedFiles)))
195                         _leastRecentlyUsed.invalidate();
196                     
197                     if(_cachedSize>must_be_smaller_than || (_maxCachedFiles>0 && _cachedFiles>=_maxCachedFiles))
198                         return null;
199                 }
200                 
201                 content = new Content(resource);
202                 fill(content);
203 
204                 synchronized(_cache)
205                 {
206                     // check that somebody else did not fill this spot.
207                     Content content2 =(Content)_cache.get(pathInContext);
208                     if (content2!=null)
209                     {
210                         content.release();
211                         return content2;
212                     }
213 
214                     while(_leastRecentlyUsed!=null && (_cachedSize>must_be_smaller_than || (_maxCachedFiles>0 && _cachedFiles>=_maxCachedFiles)))
215                         _leastRecentlyUsed.invalidate();
216                     
217                     if(_cachedSize>must_be_smaller_than || (_maxCachedFiles>0 && _cachedFiles>=_maxCachedFiles))
218                         return null; // this could waste an allocated File or DirectBuffer
219                     
220                     content.cache(pathInContext);
221                     
222                     return content;
223                 }
224             }
225         }
226 
227         return null; 
228     }
229 
230     /* ------------------------------------------------------------ */
231     /** Remember a Resource Miss!
232      * @param pathInContext
233      * @param resource
234      * @throws IOException
235      */
236     public void miss(String pathInContext, Resource resource)
237         throws IOException
238     {
239         synchronized(_cache)
240         {
241             while(_maxCachedFiles>0 && _cachedFiles>=_maxCachedFiles && _leastRecentlyUsed!=null)
242                 _leastRecentlyUsed.invalidate();
243             if (_maxCachedFiles>0 && _cachedFiles>=_maxCachedFiles)
244                 return;
245             
246             // check that somebody else did not fill this spot.
247             Miss miss = new Miss(resource);
248             Content content2 =(Content)_cache.get(pathInContext);
249             if (content2!=null)
250             {
251                 miss.release();
252                 return;
253             }
254 
255             miss.cache(pathInContext);
256         }
257     }
258     
259     /* ------------------------------------------------------------ */
260     public synchronized void doStart()
261         throws Exception
262     {
263         _cache=new HashMap();
264         _cachedSize=0;
265         _cachedFiles=0;
266     }
267 
268     /* ------------------------------------------------------------ */
269     /** Stop the context.
270      */
271     public void doStop()
272         throws InterruptedException
273     {
274         flushCache();
275     }
276 
277     /* ------------------------------------------------------------ */
278     protected void fill(Content content)
279         throws IOException
280     {
281         try
282         {
283             InputStream in = content.getResource().getInputStream();
284             int len=(int)content.getResource().length();
285             Buffer buffer = new ByteArrayBuffer(len);
286             buffer.readFrom(in,len);
287             in.close();
288             content.setBuffer(buffer);
289         }
290         finally
291         {
292             content.getResource().release();
293         }
294     }
295     
296     /* ------------------------------------------------------------ */
297     /* ------------------------------------------------------------ */
298     /** MetaData associated with a context Resource.
299      */
300     public class Content implements HttpContent
301     {
302         boolean _locked;
303         String _key;
304         Resource _resource;
305         long _lastModified;
306         Content _prev;
307         Content _next;
308         
309         Buffer _lastModifiedBytes;
310         Buffer _contentType;
311         Buffer _buffer;
312 
313         /* ------------------------------------------------------------ */
314         Content(Resource resource)
315         {
316             _resource=resource;
317 
318             _next=this;
319             _prev=this;
320             _contentType=_mimeTypes.getMimeByExtension(_resource.toString());
321             
322             _lastModified=resource.lastModified();
323         }
324 
325 
326         /* ------------------------------------------------------------ */
327         /**
328          * @return true if the content is locked in the cache
329          */
330         public boolean isLocked()
331         {
332             return _locked;
333         }
334 
335 
336         /* ------------------------------------------------------------ */
337         /**
338          * @param locked true if the content is locked in the cache
339          */
340         public void setLocked(boolean locked)
341         {
342             synchronized (_cache)
343             {
344                 if (_locked && !locked)
345                 {
346                     _locked = locked;
347                     _next=_mostRecentlyUsed;
348                     _mostRecentlyUsed=this;
349                     if (_next!=null)
350                         _next._prev=this;
351                     _prev=null;
352                     if (_leastRecentlyUsed==null)
353                         _leastRecentlyUsed=this;
354                 }
355                 else if (!_locked && locked)
356                 {
357                     if (_prev!=null)
358                         _prev._next=_next;
359                     if (_next!=null)
360                         _next._prev=_prev;
361                     _next=_prev=null;
362                 }
363                 else
364                     _locked = locked;
365             }
366         }
367 
368 
369         /* ------------------------------------------------------------ */
370         void cache(String pathInContext)
371         {
372             _key=pathInContext;
373             
374             if (!_locked)
375             {
376                 _next=_mostRecentlyUsed;
377                 _mostRecentlyUsed=this;
378                 if (_next!=null)
379                     _next._prev=this;
380                 _prev=null;
381                 if (_leastRecentlyUsed==null)
382                     _leastRecentlyUsed=this;
383             }
384             _cache.put(_key,this);
385             if (_buffer!=null)
386                 _cachedSize+=_buffer.length();
387             _cachedFiles++;
388             if (_lastModified!=-1)
389                 _lastModifiedBytes=new ByteArrayBuffer(HttpFields.formatDate(_lastModified,false));
390         }
391 
392         /* ------------------------------------------------------------ */
393         public String getKey()
394         {
395             return _key;
396         }
397 
398         /* ------------------------------------------------------------ */
399         public boolean isCached()
400         {
401             return _key!=null;
402         }
403 
404         /* ------------------------------------------------------------ */
405         public Resource getResource()
406         {
407             return _resource;
408         }
409         
410         /* ------------------------------------------------------------ */
411         boolean isValid()
412         {
413             if (_lastModified==_resource.lastModified())
414             {
415                 if (!_locked && _mostRecentlyUsed!=this)
416                 {
417                     Content tp = _prev;
418                     Content tn = _next;
419 
420                     _next=_mostRecentlyUsed;
421                     _mostRecentlyUsed=this;
422                     if (_next!=null)
423                         _next._prev=this;
424                     _prev=null;
425 
426                     if (tp!=null)
427                         tp._next=tn;
428                     if (tn!=null)
429                         tn._prev=tp;
430 
431                     if (_leastRecentlyUsed==this && tp!=null)
432                         _leastRecentlyUsed=tp;
433                 }
434                 return true;
435             }
436 
437             invalidate();
438             return false;
439         }
440 
441         /* ------------------------------------------------------------ */
442         public void invalidate()
443         {
444             synchronized(this)
445             {
446                 // Invalidate it
447                 _cache.remove(_key);
448                 _key=null;
449                 if (_buffer!=null)
450                     _cachedSize=_cachedSize-(int)_buffer.length();
451                 _cachedFiles--;
452                 
453                 if (_mostRecentlyUsed==this)
454                     _mostRecentlyUsed=_next;
455                 else
456                     _prev._next=_next;
457                 
458                 if (_leastRecentlyUsed==this)
459                     _leastRecentlyUsed=_prev;
460                 else
461                     _next._prev=_prev;
462                 
463                 _prev=null;
464                 _next=null;
465                 _resource=null;
466                 
467             }
468         }
469 
470         /* ------------------------------------------------------------ */
471         public Buffer getLastModified()
472         {
473             return _lastModifiedBytes;
474         }
475 
476         /* ------------------------------------------------------------ */
477         public Buffer getContentType()
478         {
479             return _contentType;
480         }
481 
482         /* ------------------------------------------------------------ */
483         public void setContentType(Buffer type)
484         {
485             _contentType=type;
486         }
487 
488         /* ------------------------------------------------------------ */
489         public void release()
490         {
491         }
492 
493         /* ------------------------------------------------------------ */
494         public Buffer getBuffer()
495         {
496             if (_buffer==null)
497                 return null;
498             return new View(_buffer);
499         }
500         
501         /* ------------------------------------------------------------ */
502         public void setBuffer(Buffer buffer)
503         {
504             _buffer=buffer;
505         }
506 
507         /* ------------------------------------------------------------ */
508         public long getContentLength()
509         {
510             if (_buffer==null)
511                 return -1;
512             return _buffer.length();
513         }
514 
515         /* ------------------------------------------------------------ */
516         public InputStream getInputStream() throws IOException
517         {
518             return _resource.getInputStream();
519         }   
520     }
521     
522 
523     /* ------------------------------------------------------------ */
524     /* ------------------------------------------------------------ */
525     /** MetaData associated with a context Resource.
526      */
527     public class Miss extends Content
528     {
529         Miss(Resource resource)
530         {
531             super(resource);
532         }
533 
534         /* ------------------------------------------------------------ */
535         boolean isValid()
536         {
537             if (_resource.exists())
538             {
539                 invalidate();
540                 return false;
541             }
542             return true;
543         }
544     }
545 }