| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.wicket.markup; |
| |
| import java.util.Collection; |
| import java.util.Iterator; |
| import java.util.concurrent.ConcurrentHashMap; |
| |
| import org.apache.wicket.Application; |
| import org.apache.wicket.MarkupContainer; |
| import org.apache.wicket.WicketRuntimeException; |
| import org.apache.wicket.util.lang.Args; |
| import org.apache.wicket.util.listener.IChangeListener; |
| import org.apache.wicket.util.watch.IModifiable; |
| import org.apache.wicket.util.watch.IModificationWatcher; |
| import org.apache.wicket.util.watch.ModificationWatcher; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * This is Wicket's default IMarkupCache implementation. It will load the markup and cache it for |
| * fast retrieval. |
| * <p> |
| * If the application is in development mode and a markup file changes, it'll automatically be |
| * removed from the cache and reloaded when needed. |
| * <p> |
| * MarkupCache is registered with {@link MarkupFactory} which in turn is registered with |
| * {@link org.apache.wicket.settings.MarkupSettings} and thus can be replaced with a sub-classed version. |
| * |
| * @see org.apache.wicket.settings.MarkupSettings |
| * @see MarkupFactory |
| * |
| * @author Jonathan Locke |
| * @author Juergen Donnerstag |
| */ |
| public class MarkupCache implements IMarkupCache |
| { |
| /** Log for reporting. */ |
| private static final Logger log = LoggerFactory.getLogger(MarkupCache.class); |
| |
| /** The actual cache: location => Markup */ |
| private final ICache<String, Markup> markupCache; |
| |
| /** |
| * Add extra indirection to the cache: key => location |
| * <p> |
| * Since ConcurrentHashMap does not allow to store null values, we are using Markup.NO_MARKUP |
| * instead. |
| */ |
| private final ICache<String, String> markupKeyCache; |
| |
| /** The markup cache key provider used by MarkupCache */ |
| private IMarkupCacheKeyProvider markupCacheKeyProvider; |
| |
| /** |
| * Note that you can not use Application.get() since removeMarkup() will be called from a |
| * ModificationWatcher thread which has no associated Application. |
| */ |
| private final Application application; |
| |
| /** |
| * A convenient helper to get the markup cache registered with the application. |
| * |
| * @see Application#getMarkupSettings() |
| * @see MarkupFactory#getMarkupCache() |
| * |
| * @return The markup cache registered with the {@link Application} |
| */ |
| public static IMarkupCache get() |
| { |
| return Application.get().getMarkupSettings().getMarkupFactory().getMarkupCache(); |
| } |
| |
| /** |
| * Constructor. |
| */ |
| protected MarkupCache() |
| { |
| application = Application.get(); |
| |
| markupCache = newCacheImplementation(); |
| if (markupCache == null) |
| { |
| throw new WicketRuntimeException("The map used to cache markup must not be null"); |
| } |
| |
| markupKeyCache = newCacheImplementation(); |
| } |
| |
| @Override |
| public void clear() |
| { |
| markupCache.clear(); |
| markupKeyCache.clear(); |
| } |
| |
| @Override |
| public void shutdown() |
| { |
| markupCache.shutdown(); |
| markupKeyCache.shutdown(); |
| } |
| |
| /** |
| * Note that this method will be called from a "cleanup" thread which might not have a thread |
| * local application. |
| */ |
| @Override |
| public final IMarkupFragment removeMarkup(final String cacheKey) |
| { |
| Args.notNull(cacheKey, "cacheKey"); |
| |
| if (log.isDebugEnabled()) |
| { |
| log.debug("Removing from cache: " + cacheKey); |
| } |
| |
| // Remove the markup from the cache |
| String locationString = markupKeyCache.get(cacheKey); |
| IMarkupFragment markup = (locationString != null ? markupCache.get(locationString) : null); |
| if (markup == null) |
| { |
| return null; |
| } |
| |
| // Found an entry: actual markup or Markup.NO_MARKUP. Null values are not possible |
| // because of ConcurrentHashMap. |
| markupCache.remove(locationString); |
| |
| if (log.isDebugEnabled()) |
| { |
| log.debug("Removed from cache: " + locationString); |
| } |
| |
| // If a base markup file has been removed from the cache then |
| // the derived markup should be removed as well. |
| removeMarkupWhereBaseMarkupIsNoLongerInTheCache(); |
| |
| // And now remove all watcher entries associated with markup |
| // resources no longer in the cache. |
| |
| // Note that you can not use Application.get() since removeMarkup() will be called from a |
| // ModificationWatcher thread which has no associated Application. |
| |
| IModificationWatcher watcher = application.getResourceSettings().getResourceWatcher(false); |
| if (watcher != null) |
| { |
| Iterator<IModifiable> iter = watcher.getEntries().iterator(); |
| while (iter.hasNext()) |
| { |
| IModifiable modifiable = iter.next(); |
| if (modifiable instanceof MarkupResourceStream) |
| { |
| if (!isMarkupCached((MarkupResourceStream)modifiable)) |
| { |
| iter.remove(); |
| |
| if (log.isDebugEnabled()) |
| { |
| log.debug("Removed from watcher: " + modifiable); |
| } |
| } |
| } |
| } |
| } |
| |
| return markup; |
| } |
| |
| private void removeMarkupWhereBaseMarkupIsNoLongerInTheCache() |
| { |
| // Repeat until all dependent resources have been removed (count == 0) |
| int count = 1; |
| while (count > 0) |
| { |
| // Reset prior to next round |
| count = 0; |
| |
| // Iterate though all entries of the cache |
| Iterator<Markup> iter = markupCache.getValues().iterator(); |
| while (iter.hasNext()) |
| { |
| Markup markup = iter.next(); |
| |
| if ((markup != null) && (markup != Markup.NO_MARKUP)) |
| { |
| // Check if the markup associated with key has a base markup. And if yes, test |
| // if that is cached. If the base markup has been removed, than remove the |
| // derived markup as well. |
| |
| MarkupResourceStream resourceStream = markup.getMarkupResourceStream(); |
| if (resourceStream != null) |
| { |
| resourceStream = resourceStream.getBaseMarkupResourceStream(); |
| } |
| |
| // Is the base markup available in the cache? |
| if ((resourceStream != null) && !isMarkupCached(resourceStream)) |
| { |
| iter.remove(); |
| count++; |
| |
| if (log.isDebugEnabled()) |
| { |
| log.debug("Removed derived markup from cache: " + |
| markup.getMarkupResourceStream()); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * @param resourceStream |
| * @return True if the markup is cached |
| */ |
| private boolean isMarkupCached(final MarkupResourceStream resourceStream) |
| { |
| if (resourceStream != null) |
| { |
| String key = resourceStream.getCacheKey(); |
| if (key != null) |
| { |
| String locationString = markupKeyCache.get(key); |
| if ((locationString != null) && (markupCache.get(locationString) != null)) |
| { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public final int size() |
| { |
| return markupCache.size(); |
| } |
| |
| /** |
| * Get a unmodifiable map which contains the cached data. The map key is of type String and the |
| * value is of type Markup. |
| * <p> |
| * May be used to debug or iterate the cache content. |
| * |
| * @return cache implementation |
| */ |
| public final ICache<String, Markup> getMarkupCache() |
| { |
| return markupCache; |
| } |
| |
| @Override |
| public final Markup getMarkup(final MarkupContainer container, final Class<?> clazz, |
| final boolean enforceReload) |
| { |
| Class<?> containerClass = MarkupFactory.get().getContainerClass(container, clazz); |
| |
| // Get the cache key to be associated with the markup resource stream. |
| // If the cacheKey returned == null, than caching is disabled for the resource stream. |
| final String cacheKey = getMarkupCacheKeyProvider(container).getCacheKey(container, |
| containerClass); |
| |
| // Is the markup already in the cache? |
| Markup markup = null; |
| if ((enforceReload == false) && (cacheKey != null)) |
| { |
| markup = getMarkupFromCache(cacheKey, container); |
| } |
| |
| // If markup not found in cache or cache disabled, than ... |
| if (markup == null) |
| { |
| if (log.isDebugEnabled()) |
| { |
| log.debug("Load markup: cacheKey=" + cacheKey); |
| } |
| |
| // Get the markup resource stream for the container |
| final MarkupResourceStream resourceStream = MarkupFactory.get() |
| .getMarkupResourceStream(container, containerClass); |
| |
| // Found markup? |
| if (resourceStream != null) |
| { |
| resourceStream.setCacheKey(cacheKey); |
| |
| // load the markup and watch for changes |
| markup = loadMarkupAndWatchForChanges(container, resourceStream, enforceReload); |
| } |
| else |
| { |
| markup = onMarkupNotFound(cacheKey, container, Markup.NO_MARKUP); |
| } |
| } |
| |
| // NO_MARKUP should only be used inside the Cache. |
| if (markup == Markup.NO_MARKUP) |
| { |
| markup = null; |
| } |
| |
| return markup; |
| } |
| |
| /** |
| * Will be called if the markup was not in the cache yet and could not be found either. |
| * <p> |
| * Subclasses may change the default implementation. E.g. they might choose not to update the |
| * cache to enforce reloading of any markup not found. This might be useful in very dynamic |
| * environments. Additionally a non-caching IResourceStreamLocator should be used. |
| * |
| * @param cacheKey |
| * @param container |
| * @param markup |
| * Markup.NO_MARKUP |
| * @return Same as parameter "markup" |
| * @see org.apache.wicket.settings.ResourceSettings#setResourceStreamLocator(org.apache.wicket.core.util.resource.locator.IResourceStreamLocator) |
| */ |
| protected Markup onMarkupNotFound(final String cacheKey, final MarkupContainer container, |
| final Markup markup) |
| { |
| if (log.isDebugEnabled()) |
| { |
| log.debug("Markup not found: " + cacheKey); |
| } |
| |
| // If cacheKey == null then caching is disabled for the component |
| if (cacheKey != null) |
| { |
| // flag markup as non-existent |
| markupKeyCache.put(cacheKey, cacheKey); |
| putIntoCache(cacheKey, container, markup); |
| } |
| |
| return markup; |
| } |
| |
| /** |
| * Put the markup into the cache if cacheKey is not null and the cache does not yet contain the |
| * cacheKey. Return the markup stored in the cache if cacheKey is present already. |
| * |
| * More sophisticated implementations may call a container method to e.g. cache it per container |
| * instance. |
| * |
| * @param locationString |
| * If {@code null} then ignore the cache |
| * @param container |
| * The container this markup is for. |
| * @param markup |
| * @return markup The markup provided, except if the cacheKey already existed in the cache, then |
| * the markup from the cache is provided. |
| */ |
| protected Markup putIntoCache(final String locationString, final MarkupContainer container, |
| Markup markup) |
| { |
| if (locationString != null) |
| { |
| if (markupCache.containsKey(locationString) == false) |
| { |
| // The default cache implementation is a ConcurrentHashMap. Thus neither the key nor |
| // the value can be null. |
| if (markup == null) |
| { |
| markup = Markup.NO_MARKUP; |
| } |
| |
| markupCache.put(locationString, markup); |
| } |
| else |
| { |
| // We don't lock the cache while loading a markup. Thus it may |
| // happen that the very same markup gets loaded twice (the first |
| // markup being loaded, but not yet in the cache, and another |
| // request requesting the very same markup). Since markup |
| // loading in avg takes less than 100ms, it is not really an |
| // issue. For consistency reasons however, we should always use |
| // the markup loaded first which is why it gets returned. |
| markup = markupCache.get(locationString); |
| } |
| } |
| return markup; |
| } |
| |
| /** |
| * Wicket's default implementation just uses the cacheKey to retrieve the markup from the cache. |
| * More sophisticated implementations may call a container method to e.g. ignore the cached |
| * markup under certain situations. |
| * |
| * @param cacheKey |
| * If null, than the cache will be ignored |
| * @param container |
| * @return null, if not found or to enforce reloading the markup |
| */ |
| protected Markup getMarkupFromCache(final String cacheKey, final MarkupContainer container) |
| { |
| if (cacheKey != null) |
| { |
| String locationString = markupKeyCache.get(cacheKey); |
| if (locationString != null) |
| { |
| return markupCache.get(locationString); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Loads markup from a resource stream. |
| * |
| * @param container |
| * The original requesting markup container |
| * @param markupResourceStream |
| * The markup resource stream to load |
| * @param enforceReload |
| * The cache will be ignored and all, including inherited markup files, will be |
| * reloaded. Whatever is in the cache, it will be ignored |
| * @return The markup. Markup.NO_MARKUP, if not found. |
| */ |
| private Markup loadMarkup(final MarkupContainer container, |
| final MarkupResourceStream markupResourceStream, final boolean enforceReload) |
| { |
| String cacheKey = markupResourceStream.getCacheKey(); |
| String locationString = markupResourceStream.locationAsString(); |
| if (locationString == null) |
| { |
| // set the cache key as location string, because location string |
| // couldn't be resolved. |
| locationString = cacheKey; |
| } |
| |
| Markup markup = MarkupFactory.get().loadMarkup(container, markupResourceStream, |
| enforceReload); |
| if (markup != null) |
| { |
| if (cacheKey != null) |
| { |
| String temp = markup.locationAsString(); |
| if (temp != null) |
| { |
| locationString = temp; |
| } |
| |
| // add the markup to the cache. |
| markupKeyCache.put(cacheKey, locationString); |
| return putIntoCache(locationString, container, markup); |
| } |
| return markup; |
| } |
| |
| // In case the markup could not be loaded (without exception) then .. |
| if (cacheKey != null) |
| { |
| removeMarkup(cacheKey); |
| } |
| |
| return Markup.NO_MARKUP; |
| } |
| |
| /** |
| * Load markup from an IResourceStream and add an {@link IChangeListener}to the |
| * {@link ModificationWatcher} so that if the resource changes, we can remove it from the cache |
| * automatically and subsequently reload when needed. |
| * |
| * @param container |
| * The original requesting markup container |
| * @param markupResourceStream |
| * The markup stream to load and begin to watch |
| * @param enforceReload |
| * The cache will be ignored and all, including inherited markup files, will be |
| * reloaded. Whatever is in the cache, it will be ignored |
| * @return The markup in the stream |
| */ |
| private Markup loadMarkupAndWatchForChanges(final MarkupContainer container, |
| final MarkupResourceStream markupResourceStream, final boolean enforceReload) |
| { |
| // @TODO the following code sequence looks very much like in loadMarkup. Can it be |
| // optimized? |
| final String cacheKey = markupResourceStream.getCacheKey(); |
| if (cacheKey != null) |
| { |
| if (enforceReload == false) |
| { |
| // get the location String |
| String locationString = markupResourceStream.locationAsString(); |
| if (locationString == null) |
| { |
| // set the cache key as location string, because location string |
| // couldn't be resolved. |
| locationString = cacheKey; |
| } |
| Markup markup = markupCache.get(locationString); |
| if (markup != null) |
| { |
| markupKeyCache.put(cacheKey, locationString); |
| return markup; |
| } |
| } |
| |
| // Watch file in the future |
| final IModificationWatcher watcher = application.getResourceSettings() |
| .getResourceWatcher(true); |
| if (watcher != null) |
| { |
| watcher.add(markupResourceStream, new IChangeListener<IModifiable>() |
| { |
| @Override |
| public void onChange(IModifiable modifiable) |
| { |
| if (log.isDebugEnabled()) |
| { |
| log.debug("Remove markup from watcher: " + markupResourceStream); |
| } |
| |
| // Remove the markup from the cache. It will be reloaded |
| // next time when the markup is requested. |
| watcher.remove(markupResourceStream); |
| removeMarkup(cacheKey); |
| } |
| }); |
| } |
| } |
| |
| if (log.isDebugEnabled()) |
| { |
| log.debug("Loading markup from " + markupResourceStream); |
| } |
| return loadMarkup(container, markupResourceStream, enforceReload); |
| } |
| |
| /** |
| * Get the markup cache key provider to be used |
| * |
| * @param container |
| * The MarkupContainer requesting the markup resource stream |
| * @return IMarkupResourceStreamProvider |
| */ |
| public IMarkupCacheKeyProvider getMarkupCacheKeyProvider(final MarkupContainer container) |
| { |
| if (container instanceof IMarkupCacheKeyProvider) |
| { |
| return (IMarkupCacheKeyProvider)container; |
| } |
| |
| if (markupCacheKeyProvider == null) |
| { |
| markupCacheKeyProvider = new DefaultMarkupCacheKeyProvider(); |
| } |
| return markupCacheKeyProvider; |
| } |
| |
| /** |
| * Allows you to change the map implementation which will hold the cache data. By default it is |
| * a ConcurrentHashMap() in order to allow multiple thread to access the data in a secure way. |
| * |
| * @param <K> |
| * @param <V> |
| * @return new instance of cache implementation |
| */ |
| protected <K, V> ICache<K, V> newCacheImplementation() |
| { |
| return new DefaultCacheImplementation<K, V>(); |
| } |
| |
| /** |
| * MarkupCache allows you to implement you own cache implementation. ICache is the interface the |
| * implementation must comply with. |
| * |
| * @param <K> |
| * The key type |
| * @param <V> |
| * The value type |
| */ |
| public interface ICache<K, V> |
| { |
| /** |
| * Clear the cache |
| */ |
| void clear(); |
| |
| /** |
| * Remove an entry from the cache. |
| * |
| * @param key |
| * @return true, if found and removed |
| */ |
| boolean remove(K key); |
| |
| /** |
| * Get the cache element associated with the key |
| * |
| * @param key |
| * @return cached object for key <code>key</code> or null if no matches |
| */ |
| V get(K key); |
| |
| /** |
| * Get all the keys referencing cache entries |
| * |
| * @return collection of cached keys |
| */ |
| Collection<K> getKeys(); |
| |
| /** |
| * Get all the values referencing cache entries |
| * |
| * @return collection of cached keys |
| */ |
| Collection<V> getValues(); |
| |
| /** |
| * Check if key is in the cache |
| * |
| * @param key |
| * @return true if cache contains key <code>key</code> |
| */ |
| boolean containsKey(K key); |
| |
| /** |
| * Get the number of cache entries |
| * |
| * @return number of cache entries |
| */ |
| int size(); |
| |
| /** |
| * Put an entry into the cache |
| * |
| * @param key |
| * The reference key to find the element. Must not be null. |
| * @param value |
| * The element to be cached. Must not be null. |
| */ |
| void put(K key, V value); |
| |
| /** |
| * Cleanup and shutdown |
| */ |
| void shutdown(); |
| } |
| |
| /** |
| * @param <K> |
| * @param <V> |
| */ |
| public static class DefaultCacheImplementation<K, V> implements ICache<K, V> |
| { |
| // Neither key nor value are allowed to be null with ConcurrentHashMap |
| private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<K, V>(); |
| |
| /** |
| * Construct. |
| */ |
| public DefaultCacheImplementation() |
| { |
| } |
| |
| @Override |
| public void clear() |
| { |
| cache.clear(); |
| } |
| |
| @Override |
| public boolean containsKey(final Object key) |
| { |
| if (key == null) |
| { |
| return false; |
| } |
| return cache.containsKey(key); |
| } |
| |
| @Override |
| public V get(final Object key) |
| { |
| if (key == null) |
| { |
| return null; |
| } |
| return cache.get(key); |
| } |
| |
| @Override |
| public Collection<K> getKeys() |
| { |
| return cache.keySet(); |
| } |
| |
| @Override |
| public Collection<V> getValues() |
| { |
| return cache.values(); |
| } |
| |
| @Override |
| public void put(K key, V value) |
| { |
| // Note that neither key nor value are allowed to be null with ConcurrentHashMap |
| cache.put(key, value); |
| } |
| |
| @Override |
| public boolean remove(K key) |
| { |
| if (key == null) |
| { |
| return false; |
| } |
| return cache.remove(key) == null; |
| } |
| |
| @Override |
| public int size() |
| { |
| return cache.size(); |
| } |
| |
| @Override |
| public void shutdown() |
| { |
| clear(); |
| } |
| } |
| } |