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
19package org.eclipse.jetty.server;
20
21import java.io.ByteArrayInputStream;
22import java.io.IOException;
23import java.io.InputStream;
24import java.util.Comparator;
25import java.util.SortedSet;
26import java.util.TreeSet;
27import java.util.concurrent.ConcurrentHashMap;
28import java.util.concurrent.ConcurrentMap;
29import java.util.concurrent.atomic.AtomicInteger;
30import java.util.concurrent.atomic.AtomicReference;
31
32import org.eclipse.jetty.http.HttpContent;
33import org.eclipse.jetty.http.HttpContent.ResourceAsHttpContent;
34import org.eclipse.jetty.http.HttpFields;
35import org.eclipse.jetty.http.MimeTypes;
36import org.eclipse.jetty.io.Buffer;
37import org.eclipse.jetty.io.ByteArrayBuffer;
38import org.eclipse.jetty.io.View;
39import org.eclipse.jetty.io.nio.DirectNIOBuffer;
40import org.eclipse.jetty.io.nio.IndirectNIOBuffer;
41import org.eclipse.jetty.util.log.Log;
42import org.eclipse.jetty.util.log.Logger;
43import org.eclipse.jetty.util.resource.Resource;
44import org.eclipse.jetty.util.resource.ResourceFactory;
45
46
47/* ------------------------------------------------------------ */
48/**
49 *
50 */
51public 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     */
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    /* ------------------------------------------------------------ */
85    public int getCachedSize()
86    {
87        return _cachedSize.get();
88    }
89
90    /* ------------------------------------------------------------ */
91    public int getCachedFiles()
92    {
93        return _cachedFiles.get();
94    }
95
96    /* ------------------------------------------------------------ */
97    public int getMaxCachedFileSize()
98    {
99        return _maxCachedFileSize;
100    }
101
102    /* ------------------------------------------------------------ */
103    public void setMaxCachedFileSize(int maxCachedFileSize)
104    {
105        _maxCachedFileSize = maxCachedFileSize;
106        shrinkCache();
107    }
108
109    /* ------------------------------------------------------------ */
110    public int getMaxCacheSize()
111    {
112        return _maxCacheSize;
113    }
114
115    /* ------------------------------------------------------------ */
116    public void setMaxCacheSize(int maxCacheSize)
117    {
118        _maxCacheSize = maxCacheSize;
119        shrinkCache();
120    }
121
122    /* ------------------------------------------------------------ */
123    /**
124     * @return Returns the maxCachedFiles.
125     */
126    public int getMaxCachedFiles()
127    {
128        return _maxCachedFiles;
129    }
130
131    /* ------------------------------------------------------------ */
132    /**
133     * @param maxCachedFiles The maxCachedFiles to set.
134     */
135    public void setMaxCachedFiles(int maxCachedFiles)
136    {
137        _maxCachedFiles = maxCachedFiles;
138        shrinkCache();
139    }
140
141    /* ------------------------------------------------------------ */
142    public boolean isUseFileMappedBuffer()
143    {
144        return _useFileMappedBuffer;
145    }
146
147    /* ------------------------------------------------------------ */
148    public void setUseFileMappedBuffer(boolean useFileMappedBuffer)
149    {
150        _useFileMappedBuffer = useFileMappedBuffer;
151    }
152
153    /* ------------------------------------------------------------ */
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     */
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     */
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    /* ------------------------------------------------------------ */
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    /* ------------------------------------------------------------ */
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    /* ------------------------------------------------------------ */
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    /* ------------------------------------------------------------ */
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
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        /* ------------------------------------------------------------ */
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