View Javadoc

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