Package org.apache.wicket.markup

Source Code of org.apache.wicket.markup.MarkupCache$DefaultCacheImplementation

/*
* 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()
        {
          @Override
          public void onChange()
          {
            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();
    }
  }
}
TOP

Related Classes of org.apache.wicket.markup.MarkupCache$DefaultCacheImplementation

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.