blob: 3caa46f1de23279ee98ba8cf05d75b3a7cc8d745 [file] [log] [blame]
/*
* 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();
}
}
}