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.DateGenerator;
35  import org.eclipse.jetty.http.HttpContent;
36  import org.eclipse.jetty.http.HttpContent.ResourceAsHttpContent;
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      private final boolean  _useFileMappedBuffer;
61      
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 flushCache()
147     {
148         if (_cache!=null)
149         {
150             while (_cache.size()>0)
151             {
152                 for (String path : _cache.keySet())
153                 {
154                     Content content = _cache.remove(path);
155                     if (content!=null)
156                         content.invalidate();
157                 }
158             }
159         }
160     }
161 
162     /* ------------------------------------------------------------ */
163     /** Get a Entry from the cache.
164      * Get either a valid entry object or create a new one if possible.
165      *
166      * @param pathInContext The key into the cache
167      * @return The entry matching <code>pathInContext</code>, or a new entry 
168      * if no matching entry was found. If the content exists but is not cachable, 
169      * then a {@link ResourceAsHttpContent} instance is return. If 
170      * the resource does not exist, then null is returned.
171      * @throws IOException Problem loading the resource
172      */
173     public HttpContent lookup(String pathInContext)
174         throws IOException
175     {
176         // Is the content in this cache?
177         Content content =_cache.get(pathInContext);
178         if (content!=null && (content).isValid())
179             return content;
180        
181         // try loading the content from our factory.
182         Resource resource=_factory.getResource(pathInContext);
183         HttpContent loaded = load(pathInContext,resource);
184         if (loaded!=null)
185             return loaded;
186         
187         // Is the content in the parent cache?
188         if (_parent!=null)
189         {
190             HttpContent httpContent=_parent.lookup(pathInContext);
191             if (httpContent!=null)
192                 return httpContent;
193         }
194         
195         return null;
196     }
197     
198     /* ------------------------------------------------------------ */
199     /**
200      * @param resource
201      * @return True if the resource is cacheable. The default implementation tests the cache sizes.
202      */
203     protected boolean isCacheable(Resource resource)
204     {
205         long len = resource.length();
206 
207         // Will it fit in the cache?
208         return  (len>0 && len<_maxCachedFileSize && len<_maxCacheSize);
209     }
210     
211     /* ------------------------------------------------------------ */
212     private HttpContent load(String pathInContext, Resource resource)
213         throws IOException
214     {
215         Content content=null;
216         
217         if (resource==null || !resource.exists())
218             return null;
219         
220         // Will it fit in the cache?
221         if (!resource.isDirectory() && isCacheable(resource))
222         {   
223             // Create the Content (to increment the cache sizes before adding the content 
224             content = new Content(pathInContext,resource);
225 
226             // reduce the cache to an acceptable size.
227             shrinkCache();
228 
229             // Add it to the cache.
230             Content added = _cache.putIfAbsent(pathInContext,content);
231             if (added!=null)
232             {
233                 content.invalidate();
234                 content=added;
235             }
236 
237             return content;
238         }
239         
240         return new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),getMaxCachedFileSize(),_etagSupported);
241         
242     }
243     
244     /* ------------------------------------------------------------ */
245     private void shrinkCache()
246     {
247         // While we need to shrink
248         while (_cache.size()>0 && (_cachedFiles.get()>_maxCachedFiles || _cachedSize.get()>_maxCacheSize))
249         {
250             // Scan the entire cache and generate an ordered list by last accessed time.
251             SortedSet<Content> sorted= new TreeSet<Content>(
252                     new Comparator<Content>()
253                     {
254                         public int compare(Content c1, Content c2)
255                         {
256                             if (c1._lastAccessed<c2._lastAccessed)
257                                 return -1;
258                             
259                             if (c1._lastAccessed>c2._lastAccessed)
260                                 return 1;
261 
262                             if (c1._length<c2._length)
263                                 return -1;
264                             
265                             return c1._key.compareTo(c2._key);
266                         }
267                     });
268             for (Content content : _cache.values())
269                 sorted.add(content);
270             
271             // Invalidate least recently used first
272             for (Content content : sorted)
273             {
274                 if (_cachedFiles.get()<=_maxCachedFiles && _cachedSize.get()<=_maxCacheSize)
275                     break;
276                 if (content==_cache.remove(content.getKey()))
277                     content.invalidate();
278             }
279         }
280     }
281     
282     /* ------------------------------------------------------------ */
283     protected ByteBuffer getIndirectBuffer(Resource resource)
284     {
285         try
286         {
287             return BufferUtil.toBuffer(resource,true);
288         }
289         catch(IOException|IllegalArgumentException e)
290         {
291             LOG.warn(e);
292             return null;
293         }
294     }
295 
296     /* ------------------------------------------------------------ */
297     protected ByteBuffer getDirectBuffer(Resource resource)
298     {
299         try
300         {
301             if (_useFileMappedBuffer && resource.getFile()!=null) 
302                 return BufferUtil.toMappedBuffer(resource.getFile());
303             
304             return BufferUtil.toBuffer(resource,true);
305         }
306         catch(IOException|IllegalArgumentException e)
307         {
308             LOG.warn(e);
309             return null;
310         }
311     }
312 
313     /* ------------------------------------------------------------ */
314     @Override
315     public String toString()
316     {
317         return "ResourceCache["+_parent+","+_factory+"]@"+hashCode();
318     }
319     
320     /* ------------------------------------------------------------ */
321     /* ------------------------------------------------------------ */
322     /** MetaData associated with a context Resource.
323      */
324     public class Content implements HttpContent
325     {
326         final Resource _resource;
327         final int _length;
328         final String _key;
329         final long _lastModified;
330         final ByteBuffer _lastModifiedBytes;
331         final ByteBuffer _contentType;
332         final String _etag;
333         
334         volatile long _lastAccessed;
335         AtomicReference<ByteBuffer> _indirectBuffer=new AtomicReference<ByteBuffer>();
336         AtomicReference<ByteBuffer> _directBuffer=new AtomicReference<ByteBuffer>();
337 
338         /* ------------------------------------------------------------ */
339         Content(String pathInContext,Resource resource)
340         {
341             _key=pathInContext;
342             _resource=resource;
343 
344             String mimeType = _mimeTypes.getMimeByExtension(_resource.toString());
345             _contentType=(mimeType==null?null:BufferUtil.toBuffer(mimeType));
346             boolean exists=resource.exists();
347             _lastModified=exists?resource.lastModified():-1;
348             _lastModifiedBytes=_lastModified<0?null:BufferUtil.toBuffer(DateGenerator.formatDate(_lastModified));
349             
350             _length=exists?(int)resource.length():0;
351             _cachedSize.addAndGet(_length);
352             _cachedFiles.incrementAndGet();
353             _lastAccessed=System.currentTimeMillis();
354             
355             _etag=ResourceCache.this._etagSupported?resource.getWeakETag():null;
356         }
357 
358 
359         /* ------------------------------------------------------------ */
360         public String getKey()
361         {
362             return _key;
363         }
364 
365         /* ------------------------------------------------------------ */
366         public boolean isCached()
367         {
368             return _key!=null;
369         }
370         
371         /* ------------------------------------------------------------ */
372         public boolean isMiss()
373         {
374             return false;
375         }
376 
377         /* ------------------------------------------------------------ */
378         @Override
379         public Resource getResource()
380         {
381             return _resource;
382         }
383 
384         /* ------------------------------------------------------------ */
385         @Override
386         public String getETag()
387         {
388             return _etag;
389         }
390         
391         /* ------------------------------------------------------------ */
392         boolean isValid()
393         {
394             if (_lastModified==_resource.lastModified() && _length==_resource.length())
395             {
396                 _lastAccessed=System.currentTimeMillis();
397                 return true;
398             }
399 
400             if (this==_cache.remove(_key))
401                 invalidate();
402             return false;
403         }
404 
405         /* ------------------------------------------------------------ */
406         protected void invalidate()
407         {
408             // Invalidate it
409             _cachedSize.addAndGet(-_length);
410             _cachedFiles.decrementAndGet();
411             _resource.close(); 
412         }
413 
414         /* ------------------------------------------------------------ */
415         @Override
416         public String getLastModified()
417         {
418             return BufferUtil.toString(_lastModifiedBytes);
419         }
420 
421         /* ------------------------------------------------------------ */
422         @Override
423         public String getContentType()
424         {
425             return BufferUtil.toString(_contentType);
426         }
427 
428         /* ------------------------------------------------------------ */
429         @Override
430         public void release()
431         {
432             // don't release while cached. Release when invalidated.
433         }
434 
435         /* ------------------------------------------------------------ */
436         @Override
437         public ByteBuffer getIndirectBuffer()
438         {
439             ByteBuffer buffer = _indirectBuffer.get();
440             if (buffer==null)
441             {
442                 ByteBuffer buffer2=ResourceCache.this.getIndirectBuffer(_resource);
443                 
444                 if (buffer2==null)
445                     LOG.warn("Could not load "+this);
446                 else if (_indirectBuffer.compareAndSet(null,buffer2))
447                     buffer=buffer2;
448                 else
449                     buffer=_indirectBuffer.get();
450             }
451             if (buffer==null)
452                 return null;
453             return buffer.slice();
454         }
455         
456 
457         /* ------------------------------------------------------------ */
458         @Override
459         public ByteBuffer getDirectBuffer()
460         {
461             ByteBuffer buffer = _directBuffer.get();
462             if (buffer==null)
463             {
464                 ByteBuffer buffer2=ResourceCache.this.getDirectBuffer(_resource);
465 
466                 if (buffer2==null)
467                     LOG.warn("Could not load "+this);
468                 else if (_directBuffer.compareAndSet(null,buffer2))
469                     buffer=buffer2;
470                 else
471                     buffer=_directBuffer.get();
472             }
473             if (buffer==null)
474                 return null;
475             return buffer.asReadOnlyBuffer();
476         }
477         
478         /* ------------------------------------------------------------ */
479         @Override
480         public long getContentLength()
481         {
482             return _length;
483         }
484 
485         /* ------------------------------------------------------------ */
486         @Override
487         public InputStream getInputStream() throws IOException
488         {
489             ByteBuffer indirect = getIndirectBuffer();
490             if (indirect!=null && indirect.hasArray())
491                 return new ByteArrayInputStream(indirect.array(),indirect.arrayOffset()+indirect.position(),indirect.remaining());
492            
493             return _resource.getInputStream();
494         }   
495         
496         /* ------------------------------------------------------------ */
497         @Override
498         public ReadableByteChannel getReadableByteChannel() throws IOException
499         {
500             return _resource.getReadableByteChannel();
501         }
502 
503 
504         /* ------------------------------------------------------------ */
505         @Override
506         public String toString()
507         {
508             return String.format("%s %s %d %s %s",_resource,_resource.exists(),_resource.lastModified(),_contentType,_lastModifiedBytes);
509         }   
510     }
511 }