001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.markup; 018 019import java.util.Collection; 020import java.util.Iterator; 021import java.util.concurrent.ConcurrentHashMap; 022 023import org.apache.wicket.Application; 024import org.apache.wicket.MarkupContainer; 025import org.apache.wicket.WicketRuntimeException; 026import org.apache.wicket.util.lang.Args; 027import org.apache.wicket.util.listener.IChangeListener; 028import org.apache.wicket.util.watch.IModifiable; 029import org.apache.wicket.util.watch.IModificationWatcher; 030import org.apache.wicket.util.watch.ModificationWatcher; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033 034/** 035 * This is Wicket's default IMarkupCache implementation. It will load the markup and cache it for 036 * fast retrieval. 037 * <p> 038 * If the application is in development mode and a markup file changes, it'll automatically be 039 * removed from the cache and reloaded when needed. 040 * <p> 041 * MarkupCache is registered with {@link MarkupFactory} which in turn is registered with 042 * {@link org.apache.wicket.settings.MarkupSettings} and thus can be replaced with a sub-classed version. 043 * 044 * @see org.apache.wicket.settings.MarkupSettings 045 * @see MarkupFactory 046 * 047 * @author Jonathan Locke 048 * @author Juergen Donnerstag 049 */ 050public class MarkupCache implements IMarkupCache 051{ 052 /** Log for reporting. */ 053 private static final Logger log = LoggerFactory.getLogger(MarkupCache.class); 054 055 /** The actual cache: location => Markup */ 056 private final ICache<String, Markup> markupCache; 057 058 /** 059 * Add extra indirection to the cache: key => location 060 * <p> 061 * Since ConcurrentHashMap does not allow to store null values, we are using Markup.NO_MARKUP 062 * instead. 063 */ 064 private final ICache<String, String> markupKeyCache; 065 066 /** The markup cache key provider used by MarkupCache */ 067 private IMarkupCacheKeyProvider markupCacheKeyProvider; 068 069 /** 070 * Note that you can not use Application.get() since removeMarkup() will be called from a 071 * ModificationWatcher thread which has no associated Application. 072 */ 073 private final Application application; 074 075 /** 076 * A convenient helper to get the markup cache registered with the application. 077 * 078 * @see Application#getMarkupSettings() 079 * @see MarkupFactory#getMarkupCache() 080 * 081 * @return The markup cache registered with the {@link Application} 082 */ 083 public static IMarkupCache get() 084 { 085 return Application.get().getMarkupSettings().getMarkupFactory().getMarkupCache(); 086 } 087 088 /** 089 * Constructor. 090 */ 091 protected MarkupCache() 092 { 093 application = Application.get(); 094 095 markupCache = newCacheImplementation(); 096 if (markupCache == null) 097 { 098 throw new WicketRuntimeException("The map used to cache markup must not be null"); 099 } 100 101 markupKeyCache = newCacheImplementation(); 102 } 103 104 @Override 105 public void clear() 106 { 107 markupCache.clear(); 108 markupKeyCache.clear(); 109 } 110 111 @Override 112 public void shutdown() 113 { 114 markupCache.shutdown(); 115 markupKeyCache.shutdown(); 116 } 117 118 /** 119 * Note that this method will be called from a "cleanup" thread which might not have a thread 120 * local application. 121 */ 122 @Override 123 public final IMarkupFragment removeMarkup(final String cacheKey) 124 { 125 Args.notNull(cacheKey, "cacheKey"); 126 127 if (log.isDebugEnabled()) 128 { 129 log.debug("Removing from cache: " + cacheKey); 130 } 131 132 // Remove the markup from the cache 133 String locationString = markupKeyCache.get(cacheKey); 134 IMarkupFragment markup = (locationString != null ? markupCache.get(locationString) : null); 135 if (markup == null) 136 { 137 return null; 138 } 139 140 // Found an entry: actual markup or Markup.NO_MARKUP. Null values are not possible 141 // because of ConcurrentHashMap. 142 markupCache.remove(locationString); 143 144 if (log.isDebugEnabled()) 145 { 146 log.debug("Removed from cache: " + locationString); 147 } 148 149 // If a base markup file has been removed from the cache then 150 // the derived markup should be removed as well. 151 removeMarkupWhereBaseMarkupIsNoLongerInTheCache(); 152 153 // And now remove all watcher entries associated with markup 154 // resources no longer in the cache. 155 156 // Note that you can not use Application.get() since removeMarkup() will be called from a 157 // ModificationWatcher thread which has no associated Application. 158 159 IModificationWatcher watcher = application.getResourceSettings().getResourceWatcher(false); 160 if (watcher != null) 161 { 162 Iterator<IModifiable> iter = watcher.getEntries().iterator(); 163 while (iter.hasNext()) 164 { 165 IModifiable modifiable = iter.next(); 166 if (modifiable instanceof MarkupResourceStream) 167 { 168 if (!isMarkupCached((MarkupResourceStream)modifiable)) 169 { 170 iter.remove(); 171 172 if (log.isDebugEnabled()) 173 { 174 log.debug("Removed from watcher: " + modifiable); 175 } 176 } 177 } 178 } 179 } 180 181 return markup; 182 } 183 184 private void removeMarkupWhereBaseMarkupIsNoLongerInTheCache() 185 { 186 // Repeat until all dependent resources have been removed (count == 0) 187 int count = 1; 188 while (count > 0) 189 { 190 // Reset prior to next round 191 count = 0; 192 193 // Iterate though all entries of the cache 194 Iterator<Markup> iter = markupCache.getValues().iterator(); 195 while (iter.hasNext()) 196 { 197 Markup markup = iter.next(); 198 199 if ((markup != null) && (markup != Markup.NO_MARKUP)) 200 { 201 // Check if the markup associated with key has a base markup. And if yes, test 202 // if that is cached. If the base markup has been removed, than remove the 203 // derived markup as well. 204 205 MarkupResourceStream resourceStream = markup.getMarkupResourceStream(); 206 if (resourceStream != null) 207 { 208 resourceStream = resourceStream.getBaseMarkupResourceStream(); 209 } 210 211 // Is the base markup available in the cache? 212 if ((resourceStream != null) && !isMarkupCached(resourceStream)) 213 { 214 iter.remove(); 215 count++; 216 217 if (log.isDebugEnabled()) 218 { 219 log.debug("Removed derived markup from cache: " + 220 markup.getMarkupResourceStream()); 221 } 222 } 223 } 224 } 225 } 226 } 227 228 /** 229 * @param resourceStream 230 * @return True if the markup is cached 231 */ 232 private boolean isMarkupCached(final MarkupResourceStream resourceStream) 233 { 234 if (resourceStream != null) 235 { 236 String key = resourceStream.getCacheKey(); 237 if (key != null) 238 { 239 String locationString = markupKeyCache.get(key); 240 if ((locationString != null) && (markupCache.get(locationString) != null)) 241 { 242 return true; 243 } 244 } 245 } 246 return false; 247 } 248 249 @Override 250 public final int size() 251 { 252 return markupCache.size(); 253 } 254 255 /** 256 * Get a unmodifiable map which contains the cached data. The map key is of type String and the 257 * value is of type Markup. 258 * <p> 259 * May be used to debug or iterate the cache content. 260 * 261 * @return cache implementation 262 */ 263 public final ICache<String, Markup> getMarkupCache() 264 { 265 return markupCache; 266 } 267 268 @Override 269 public final Markup getMarkup(final MarkupContainer container, final Class<?> clazz, 270 final boolean enforceReload) 271 { 272 Class<?> containerClass = MarkupFactory.get().getContainerClass(container, clazz); 273 274 // Get the cache key to be associated with the markup resource stream. 275 // If the cacheKey returned == null, than caching is disabled for the resource stream. 276 final String cacheKey = getMarkupCacheKeyProvider(container).getCacheKey(container, 277 containerClass); 278 279 // Is the markup already in the cache? 280 Markup markup = null; 281 if ((enforceReload == false) && (cacheKey != null)) 282 { 283 markup = getMarkupFromCache(cacheKey, container); 284 } 285 286 // If markup not found in cache or cache disabled, than ... 287 if (markup == null) 288 { 289 if (log.isDebugEnabled()) 290 { 291 log.debug("Load markup: cacheKey=" + cacheKey); 292 } 293 294 // Get the markup resource stream for the container 295 final MarkupResourceStream resourceStream = MarkupFactory.get() 296 .getMarkupResourceStream(container, containerClass); 297 298 // Found markup? 299 if (resourceStream != null) 300 { 301 resourceStream.setCacheKey(cacheKey); 302 303 // load the markup and watch for changes 304 markup = loadMarkupAndWatchForChanges(container, resourceStream, enforceReload); 305 } 306 else 307 { 308 markup = onMarkupNotFound(cacheKey, container, Markup.NO_MARKUP); 309 } 310 } 311 312 // NO_MARKUP should only be used inside the Cache. 313 if (markup == Markup.NO_MARKUP) 314 { 315 markup = null; 316 } 317 318 return markup; 319 } 320 321 /** 322 * Will be called if the markup was not in the cache yet and could not be found either. 323 * <p> 324 * Subclasses may change the default implementation. E.g. they might choose not to update the 325 * cache to enforce reloading of any markup not found. This might be useful in very dynamic 326 * environments. Additionally a non-caching IResourceStreamLocator should be used. 327 * 328 * @param cacheKey 329 * @param container 330 * @param markup 331 * Markup.NO_MARKUP 332 * @return Same as parameter "markup" 333 * @see org.apache.wicket.settings.ResourceSettings#setResourceStreamLocator(org.apache.wicket.core.util.resource.locator.IResourceStreamLocator) 334 */ 335 protected Markup onMarkupNotFound(final String cacheKey, final MarkupContainer container, 336 final Markup markup) 337 { 338 if (log.isDebugEnabled()) 339 { 340 log.debug("Markup not found: " + cacheKey); 341 } 342 343 // If cacheKey == null then caching is disabled for the component 344 if (cacheKey != null) 345 { 346 // flag markup as non-existent 347 markupKeyCache.put(cacheKey, cacheKey); 348 putIntoCache(cacheKey, container, markup); 349 } 350 351 return markup; 352 } 353 354 /** 355 * Put the markup into the cache if cacheKey is not null and the cache does not yet contain the 356 * cacheKey. Return the markup stored in the cache if cacheKey is present already. 357 * 358 * More sophisticated implementations may call a container method to e.g. cache it per container 359 * instance. 360 * 361 * @param locationString 362 * If {@code null} then ignore the cache 363 * @param container 364 * The container this markup is for. 365 * @param markup 366 * @return markup The markup provided, except if the cacheKey already existed in the cache, then 367 * the markup from the cache is provided. 368 */ 369 protected Markup putIntoCache(final String locationString, final MarkupContainer container, 370 Markup markup) 371 { 372 if (locationString != null) 373 { 374 if (markupCache.containsKey(locationString) == false) 375 { 376 // The default cache implementation is a ConcurrentHashMap. Thus neither the key nor 377 // the value can be null. 378 if (markup == null) 379 { 380 markup = Markup.NO_MARKUP; 381 } 382 383 markupCache.put(locationString, markup); 384 } 385 else 386 { 387 // We don't lock the cache while loading a markup. Thus it may 388 // happen that the very same markup gets loaded twice (the first 389 // markup being loaded, but not yet in the cache, and another 390 // request requesting the very same markup). Since markup 391 // loading in avg takes less than 100ms, it is not really an 392 // issue. For consistency reasons however, we should always use 393 // the markup loaded first which is why it gets returned. 394 markup = markupCache.get(locationString); 395 } 396 } 397 return markup; 398 } 399 400 /** 401 * Wicket's default implementation just uses the cacheKey to retrieve the markup from the cache. 402 * More sophisticated implementations may call a container method to e.g. ignore the cached 403 * markup under certain situations. 404 * 405 * @param cacheKey 406 * If null, than the cache will be ignored 407 * @param container 408 * @return null, if not found or to enforce reloading the markup 409 */ 410 protected Markup getMarkupFromCache(final String cacheKey, final MarkupContainer container) 411 { 412 if (cacheKey != null) 413 { 414 String locationString = markupKeyCache.get(cacheKey); 415 if (locationString != null) 416 { 417 return markupCache.get(locationString); 418 } 419 } 420 return null; 421 } 422 423 /** 424 * Loads markup from a resource stream. 425 * 426 * @param container 427 * The original requesting markup container 428 * @param markupResourceStream 429 * The markup resource stream to load 430 * @param enforceReload 431 * The cache will be ignored and all, including inherited markup files, will be 432 * reloaded. Whatever is in the cache, it will be ignored 433 * @return The markup. Markup.NO_MARKUP, if not found. 434 */ 435 private Markup loadMarkup(final MarkupContainer container, 436 final MarkupResourceStream markupResourceStream, final boolean enforceReload) 437 { 438 String cacheKey = markupResourceStream.getCacheKey(); 439 String locationString = markupResourceStream.locationAsString(); 440 if (locationString == null) 441 { 442 // set the cache key as location string, because location string 443 // couldn't be resolved. 444 locationString = cacheKey; 445 } 446 447 Markup markup = MarkupFactory.get().loadMarkup(container, markupResourceStream, 448 enforceReload); 449 if (markup != null) 450 { 451 if (cacheKey != null) 452 { 453 String temp = markup.locationAsString(); 454 if (temp != null) 455 { 456 locationString = temp; 457 } 458 459 // add the markup to the cache. 460 markupKeyCache.put(cacheKey, locationString); 461 return putIntoCache(locationString, container, markup); 462 } 463 return markup; 464 } 465 466 // In case the markup could not be loaded (without exception) then .. 467 if (cacheKey != null) 468 { 469 removeMarkup(cacheKey); 470 } 471 472 return Markup.NO_MARKUP; 473 } 474 475 /** 476 * Load markup from an IResourceStream and add an {@link IChangeListener}to the 477 * {@link ModificationWatcher} so that if the resource changes, we can remove it from the cache 478 * automatically and subsequently reload when needed. 479 * 480 * @param container 481 * The original requesting markup container 482 * @param markupResourceStream 483 * The markup stream to load and begin to watch 484 * @param enforceReload 485 * The cache will be ignored and all, including inherited markup files, will be 486 * reloaded. Whatever is in the cache, it will be ignored 487 * @return The markup in the stream 488 */ 489 private Markup loadMarkupAndWatchForChanges(final MarkupContainer container, 490 final MarkupResourceStream markupResourceStream, final boolean enforceReload) 491 { 492 // @TODO the following code sequence looks very much like in loadMarkup. Can it be 493 // optimized? 494 final String cacheKey = markupResourceStream.getCacheKey(); 495 if (cacheKey != null) 496 { 497 if (enforceReload == false) 498 { 499 // get the location String 500 String locationString = markupResourceStream.locationAsString(); 501 if (locationString == null) 502 { 503 // set the cache key as location string, because location string 504 // couldn't be resolved. 505 locationString = cacheKey; 506 } 507 Markup markup = markupCache.get(locationString); 508 if (markup != null) 509 { 510 markupKeyCache.put(cacheKey, locationString); 511 return markup; 512 } 513 } 514 515 // Watch file in the future 516 final IModificationWatcher watcher = application.getResourceSettings() 517 .getResourceWatcher(true); 518 if (watcher != null) 519 { 520 watcher.add(markupResourceStream, new IChangeListener<IModifiable>() 521 { 522 @Override 523 public void onChange(IModifiable modifiable) 524 { 525 if (log.isDebugEnabled()) 526 { 527 log.debug("Remove markup from watcher: " + markupResourceStream); 528 } 529 530 // Remove the markup from the cache. It will be reloaded 531 // next time when the markup is requested. 532 watcher.remove(markupResourceStream); 533 removeMarkup(cacheKey); 534 } 535 }); 536 } 537 } 538 539 if (log.isDebugEnabled()) 540 { 541 log.debug("Loading markup from " + markupResourceStream); 542 } 543 return loadMarkup(container, markupResourceStream, enforceReload); 544 } 545 546 /** 547 * Get the markup cache key provider to be used 548 * 549 * @param container 550 * The MarkupContainer requesting the markup resource stream 551 * @return IMarkupResourceStreamProvider 552 */ 553 public IMarkupCacheKeyProvider getMarkupCacheKeyProvider(final MarkupContainer container) 554 { 555 if (container instanceof IMarkupCacheKeyProvider) 556 { 557 return (IMarkupCacheKeyProvider)container; 558 } 559 560 if (markupCacheKeyProvider == null) 561 { 562 markupCacheKeyProvider = new DefaultMarkupCacheKeyProvider(); 563 } 564 return markupCacheKeyProvider; 565 } 566 567 /** 568 * Allows you to change the map implementation which will hold the cache data. By default it is 569 * a ConcurrentHashMap() in order to allow multiple thread to access the data in a secure way. 570 * 571 * @param <K> 572 * @param <V> 573 * @return new instance of cache implementation 574 */ 575 protected <K, V> ICache<K, V> newCacheImplementation() 576 { 577 return new DefaultCacheImplementation<K, V>(); 578 } 579 580 /** 581 * MarkupCache allows you to implement you own cache implementation. ICache is the interface the 582 * implementation must comply with. 583 * 584 * @param <K> 585 * The key type 586 * @param <V> 587 * The value type 588 */ 589 public interface ICache<K, V> 590 { 591 /** 592 * Clear the cache 593 */ 594 void clear(); 595 596 /** 597 * Remove an entry from the cache. 598 * 599 * @param key 600 * @return true, if found and removed 601 */ 602 boolean remove(K key); 603 604 /** 605 * Get the cache element associated with the key 606 * 607 * @param key 608 * @return cached object for key <code>key</code> or null if no matches 609 */ 610 V get(K key); 611 612 /** 613 * Get all the keys referencing cache entries 614 * 615 * @return collection of cached keys 616 */ 617 Collection<K> getKeys(); 618 619 /** 620 * Get all the values referencing cache entries 621 * 622 * @return collection of cached keys 623 */ 624 Collection<V> getValues(); 625 626 /** 627 * Check if key is in the cache 628 * 629 * @param key 630 * @return true if cache contains key <code>key</code> 631 */ 632 boolean containsKey(K key); 633 634 /** 635 * Get the number of cache entries 636 * 637 * @return number of cache entries 638 */ 639 int size(); 640 641 /** 642 * Put an entry into the cache 643 * 644 * @param key 645 * The reference key to find the element. Must not be null. 646 * @param value 647 * The element to be cached. Must not be null. 648 */ 649 void put(K key, V value); 650 651 /** 652 * Cleanup and shutdown 653 */ 654 void shutdown(); 655 } 656 657 /** 658 * @param <K> 659 * @param <V> 660 */ 661 public static class DefaultCacheImplementation<K, V> implements ICache<K, V> 662 { 663 // Neither key nor value are allowed to be null with ConcurrentHashMap 664 private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<K, V>(); 665 666 /** 667 * Construct. 668 */ 669 public DefaultCacheImplementation() 670 { 671 } 672 673 @Override 674 public void clear() 675 { 676 cache.clear(); 677 } 678 679 @Override 680 public boolean containsKey(final Object key) 681 { 682 if (key == null) 683 { 684 return false; 685 } 686 return cache.containsKey(key); 687 } 688 689 @Override 690 public V get(final Object key) 691 { 692 if (key == null) 693 { 694 return null; 695 } 696 return cache.get(key); 697 } 698 699 @Override 700 public Collection<K> getKeys() 701 { 702 return cache.keySet(); 703 } 704 705 @Override 706 public Collection<V> getValues() 707 { 708 return cache.values(); 709 } 710 711 @Override 712 public void put(K key, V value) 713 { 714 // Note that neither key nor value are allowed to be null with ConcurrentHashMap 715 cache.put(key, value); 716 } 717 718 @Override 719 public boolean remove(K key) 720 { 721 if (key == null) 722 { 723 return false; 724 } 725 return cache.remove(key) == null; 726 } 727 728 @Override 729 public int size() 730 { 731 return cache.size(); 732 } 733 734 @Override 735 public void shutdown() 736 { 737 clear(); 738 } 739 } 740}