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