View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 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.nio.ByteBuffer;
25  import java.nio.channels.ReadableByteChannel;
26  import java.util.Comparator;
27  import java.util.SortedSet;
28  import java.util.TreeSet;
29  import java.util.concurrent.ConcurrentHashMap;
30  import java.util.concurrent.ConcurrentMap;
31  import java.util.concurrent.atomic.AtomicInteger;
32  import java.util.concurrent.atomic.AtomicReference;
33  
34  import org.eclipse.jetty.http.HttpContent;
35  import org.eclipse.jetty.http.HttpContent.ResourceAsHttpContent;
36  import org.eclipse.jetty.http.HttpFields;
37  import org.eclipse.jetty.http.MimeTypes;
38  import org.eclipse.jetty.util.BufferUtil;
39  import org.eclipse.jetty.util.log.Log;
40  import org.eclipse.jetty.util.log.Logger;
41  import org.eclipse.jetty.util.resource.Resource;
42  import org.eclipse.jetty.util.resource.ResourceFactory;
43  
44  
45  /* ------------------------------------------------------------ */
46  /** 
47   * 
48   */
49  public class ResourceCache
50  {
51      private static final Logger LOG = Log.getLogger(ResourceCache.class);
52  
53      private final ConcurrentMap<String,Content> _cache;
54      private final AtomicInteger _cachedSize;
55      private final AtomicInteger _cachedFiles;
56      private final ResourceFactory _factory;
57      private final ResourceCache _parent;
58      private final MimeTypes _mimeTypes;
59      private final boolean _etagSupported;
60  
61      private boolean  _useFileMappedBuffer=true;
62      private int _maxCachedFileSize =4*1024*1024;
63      private int _maxCachedFiles=2048;
64      private int _maxCacheSize =32*1024*1024;
65      
66      /* ------------------------------------------------------------ */
67      /** Constructor.
68       * @param mimeTypes Mimetype to use for meta data
69       */
70      public ResourceCache(ResourceCache parent, ResourceFactory factory, MimeTypes mimeTypes,boolean useFileMappedBuffer,boolean etags)
71      {
72          _factory = factory;
73          _cache=new ConcurrentHashMap<String,Content>();
74          _cachedSize=new AtomicInteger();
75          _cachedFiles=new AtomicInteger();
76          _mimeTypes=mimeTypes;
77          _parent=parent;
78          _useFileMappedBuffer=useFileMappedBuffer;
79          _etagSupported=etags;
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 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(),_etagSupported);
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 ByteBuffer 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             ByteBuffer buffer = BufferUtil.allocate(len);
300             int pos=BufferUtil.flipToFill(buffer);
301             if (resource.getFile()!=null)
302                 BufferUtil.readFrom(resource.getFile(),buffer);
303             else
304             {
305                 InputStream is = resource.getInputStream();
306                 BufferUtil.readFrom(is,len,buffer);
307                 is.close();
308             }
309             BufferUtil.flipToFlush(buffer,pos);
310             return buffer;
311         }
312         catch(IOException e)
313         {
314             LOG.warn(e);
315             return null;
316         }
317     }
318 
319     /* ------------------------------------------------------------ */
320     protected ByteBuffer getDirectBuffer(Resource resource)
321     {
322         try
323         {
324             if (_useFileMappedBuffer && resource.getFile()!=null) 
325                 return BufferUtil.toBuffer(resource.getFile());
326             
327             int len=(int)resource.length();
328             if (len<0)
329             {
330                 LOG.warn("invalid resource: "+String.valueOf(resource)+" "+len);
331                 return null;
332             }
333             ByteBuffer buffer = BufferUtil.allocateDirect(len);
334 
335             int pos=BufferUtil.flipToFill(buffer);
336             if (resource.getFile()!=null)
337                 BufferUtil.readFrom(resource.getFile(),buffer);
338             else
339             {
340                 InputStream is = resource.getInputStream();
341                 BufferUtil.readFrom(is,len,buffer);
342                 is.close();
343             }
344             BufferUtil.flipToFlush(buffer,pos);
345             
346             return buffer;
347         }
348         catch(IOException e)
349         {
350             LOG.warn(e);
351             return null;
352         }
353     }
354 
355     /* ------------------------------------------------------------ */
356     @Override
357     public String toString()
358     {
359         return "ResourceCache["+_parent+","+_factory+"]@"+hashCode();
360     }
361     
362     /* ------------------------------------------------------------ */
363     /* ------------------------------------------------------------ */
364     /** MetaData associated with a context Resource.
365      */
366     public class Content implements HttpContent
367     {
368         final Resource _resource;
369         final int _length;
370         final String _key;
371         final long _lastModified;
372         final ByteBuffer _lastModifiedBytes;
373         final ByteBuffer _contentType;
374         final String _etag;
375         
376         volatile long _lastAccessed;
377         AtomicReference<ByteBuffer> _indirectBuffer=new AtomicReference<ByteBuffer>();
378         AtomicReference<ByteBuffer> _directBuffer=new AtomicReference<ByteBuffer>();
379 
380         /* ------------------------------------------------------------ */
381         Content(String pathInContext,Resource resource)
382         {
383             _key=pathInContext;
384             _resource=resource;
385 
386             String mimeType = _mimeTypes.getMimeByExtension(_resource.toString());
387             _contentType=(mimeType==null?null:BufferUtil.toBuffer(mimeType));
388             boolean exists=resource.exists();
389             _lastModified=exists?resource.lastModified():-1;
390             _lastModifiedBytes=_lastModified<0?null:BufferUtil.toBuffer(HttpFields.formatDate(_lastModified));
391             
392             _length=exists?(int)resource.length():0;
393             _cachedSize.addAndGet(_length);
394             _cachedFiles.incrementAndGet();
395             _lastAccessed=System.currentTimeMillis();
396             
397             _etag=ResourceCache.this._etagSupported?resource.getWeakETag():null;
398         }
399 
400 
401         /* ------------------------------------------------------------ */
402         public String getKey()
403         {
404             return _key;
405         }
406 
407         /* ------------------------------------------------------------ */
408         public boolean isCached()
409         {
410             return _key!=null;
411         }
412         
413         /* ------------------------------------------------------------ */
414         public boolean isMiss()
415         {
416             return false;
417         }
418 
419         /* ------------------------------------------------------------ */
420         @Override
421         public Resource getResource()
422         {
423             return _resource;
424         }
425 
426         /* ------------------------------------------------------------ */
427         @Override
428         public String getETag()
429         {
430             return _etag;
431         }
432         
433         /* ------------------------------------------------------------ */
434         boolean isValid()
435         {
436             if (_lastModified==_resource.lastModified() && _length==_resource.length())
437             {
438                 _lastAccessed=System.currentTimeMillis();
439                 return true;
440             }
441 
442             if (this==_cache.remove(_key))
443                 invalidate();
444             return false;
445         }
446 
447         /* ------------------------------------------------------------ */
448         protected void invalidate()
449         {
450             // Invalidate it
451             _cachedSize.addAndGet(-_length);
452             _cachedFiles.decrementAndGet();
453             _resource.release(); 
454         }
455 
456         /* ------------------------------------------------------------ */
457         @Override
458         public String getLastModified()
459         {
460             return BufferUtil.toString(_lastModifiedBytes);
461         }
462 
463         /* ------------------------------------------------------------ */
464         @Override
465         public String getContentType()
466         {
467             return BufferUtil.toString(_contentType);
468         }
469 
470         /* ------------------------------------------------------------ */
471         @Override
472         public void release()
473         {
474             // don't release while cached. Release when invalidated.
475         }
476 
477         /* ------------------------------------------------------------ */
478         @Override
479         public ByteBuffer getIndirectBuffer()
480         {
481             ByteBuffer buffer = _indirectBuffer.get();
482             if (buffer==null)
483             {
484                 ByteBuffer buffer2=ResourceCache.this.getIndirectBuffer(_resource);
485                 
486                 if (buffer2==null)
487                     LOG.warn("Could not load "+this);
488                 else if (_indirectBuffer.compareAndSet(null,buffer2))
489                     buffer=buffer2;
490                 else
491                     buffer=_indirectBuffer.get();
492             }
493             if (buffer==null)
494                 return null;
495             return buffer.asReadOnlyBuffer();
496         }
497         
498 
499         /* ------------------------------------------------------------ */
500         @Override
501         public ByteBuffer getDirectBuffer()
502         {
503             ByteBuffer buffer = _directBuffer.get();
504             if (buffer==null)
505             {
506                 ByteBuffer buffer2=ResourceCache.this.getDirectBuffer(_resource);
507 
508                 if (buffer2==null)
509                     LOG.warn("Could not load "+this);
510                 else if (_directBuffer.compareAndSet(null,buffer2))
511                     buffer=buffer2;
512                 else
513                     buffer=_directBuffer.get();
514             }
515             if (buffer==null)
516                 return null;
517             return buffer.asReadOnlyBuffer();
518         }
519         
520         /* ------------------------------------------------------------ */
521         @Override
522         public long getContentLength()
523         {
524             return _length;
525         }
526 
527         /* ------------------------------------------------------------ */
528         @Override
529         public InputStream getInputStream() throws IOException
530         {
531             ByteBuffer indirect = getIndirectBuffer();
532             if (indirect!=null && indirect.hasArray())
533                 return new ByteArrayInputStream(indirect.array(),indirect.arrayOffset()+indirect.position(),indirect.remaining());
534            
535             return _resource.getInputStream();
536         }   
537         
538         /* ------------------------------------------------------------ */
539         @Override
540         public ReadableByteChannel getReadableByteChannel() throws IOException
541         {
542             return _resource.getReadableByteChannel();
543         }
544 
545 
546         /* ------------------------------------------------------------ */
547         @Override
548         public String toString()
549         {
550             return String.format("%s %s %d %s %s",_resource,_resource.exists(),_resource.lastModified(),_contentType,_lastModifiedBytes);
551         }   
552     }
553 }