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