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