org.apache.wicket.markup.MarkupCache.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wicket.markup.MarkupCache.java

Source

/*
 * 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 {@link Application#getMarkupSettings()}
     * @see {@link 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();
        }
    }
}