Java tutorial
/* * Weblounge: Web Content Management System * Copyright (c) 2003 - 2011 The Weblounge Team * http://entwinemedia.com/weblounge * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package ch.entwine.weblounge.cache.impl; import static ch.entwine.weblounge.common.impl.request.Http11Constants.HEADER_IF_MODIFIED_SINCE; import static ch.entwine.weblounge.common.impl.request.Http11Constants.HEADER_IF_NONE_MATCH; import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; import ch.entwine.weblounge.cache.CacheListener; import ch.entwine.weblounge.cache.CacheService; import ch.entwine.weblounge.cache.StreamFilter; import ch.entwine.weblounge.cache.impl.handle.TaggedCacheHandle; import ch.entwine.weblounge.common.Times; import ch.entwine.weblounge.common.impl.util.config.ConfigurationUtils; import ch.entwine.weblounge.common.request.CacheHandle; import ch.entwine.weblounge.common.request.CacheTag; import ch.entwine.weblounge.common.request.WebloungeRequest; import ch.entwine.weblounge.common.request.WebloungeResponse; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Element; import net.sf.ehcache.Status; import net.sf.ehcache.config.CacheConfiguration; import net.sf.ehcache.config.Configuration; import net.sf.ehcache.config.ConfigurationFactory; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Dictionary; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.ServletResponse; import javax.servlet.ServletResponseWrapper; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Default implementation of the <code>CacheService</code> that is used to store * rendered pages and page elements so that they may be served out of the cache * upon the next request. * <p> * The service itself has no logic implemented besides configuring, starting and * stopping the cache. The actual caching is provided by the * <code>CacheManager</code>. */ public class CacheServiceImpl implements CacheService, ManagedService { /** Logging facility provided by log4j */ private static final Logger logger = LoggerFactory.getLogger(CacheServiceImpl.class); /** Path to cache configuration */ private static final String CACHE_MANAGER_CONFIG = "/ehcache/config.xml"; /** Name of the weblounge cache debug header */ private static final String CACHE_DEBUG_HEADER = "X-Cache-Debug"; /** Name of the weblounge cache header */ private static final String CACHE_KEY_HEADER = "X-Cache-Key"; /** Configuration key prefix for content repository configuration */ public static final String OPT_PREFIX = "cache"; /** Configuration key for the enabled state of the cache */ public static final String OPT_ENABLE = OPT_PREFIX + ".enable"; /** The default value for "enable" configuration property */ private static final boolean DEFAULT_ENABLE = true; /** Configuration key for the debugging option */ public static final String OPT_DEBUG = OPT_PREFIX + ".debug"; /** The default value for "debug" configuration property */ private static final boolean DEFAULT_DEBUG = true; /** Configuration key for the cache identifier */ public static final String OPT_ID = OPT_PREFIX + ".id"; /** Configuration key for the cache name */ public static final String OPT_NAME = OPT_PREFIX + ".name"; /** Configuration key indicating that a clear() operation is required */ public static final String OPT_CLEAR = OPT_PREFIX + ".clear"; /** Configuration key for the path to the cache's disk store */ public static final String OPT_DISKSTORE_PATH = OPT_PREFIX + ".diskStorePath"; /** Configuration key for the persistence of the cache */ public static final String OPT_DISK_PERSISTENT = OPT_PREFIX + ".diskPersistent"; /** The default value for "disk persistent" configuration property */ private static final boolean DEFAULT_DISK_PERSISTENT = false; /** Configuration key for the overflow to disk setting */ public static final String OPT_OVERFLOW_TO_DISK = OPT_PREFIX + ".overflowToDisk"; /** The default value for "overflow to disk" configuration property */ private static final boolean DEFAULT_OVERFLOW_TO_DISK = false; /** Configuration key for the statistics setting */ public static final String OPT_ENABLE_STATISTICS = OPT_PREFIX + ".statistics"; /** The default value for "statistics enabled" configuration property */ private static final boolean DEFAULT_STATISTICS_ENABLED = true; /** Configuration key for the maximum number of elements in memory */ public static final String OPT_MAX_ELEMENTS_IN_MEMORY = OPT_PREFIX + ".maxElementsInMemory"; /** The default value for "max elements in memory" configuration property */ private static final int DEFAULT_MAX_ELEMENTS_IN_MEMORY = 1000; /** Configuration key for the maximum number of elements on disk */ public static final String OPT_MAX_ELEMENTS_ON_DISK = OPT_PREFIX + ".maxElementsOnDisk"; /** The default value for "max elements on disk" configuration property */ private static final int DEFAULT_MAX_ELEMENTS_ON_DISK = 0; /** Configuration key for the time to idle setting */ public static final String OPT_TIME_TO_IDLE = OPT_PREFIX + ".timeToIdle"; /** The default value for "seconds to idle" configuration property */ private static final int DEFAULT_TIME_TO_IDLE = 0; /** Configuration key for the time to live setting */ public static final String OPT_TIME_TO_LIVE = OPT_PREFIX + ".timeToLive"; /** The default value for "seconds to live" configuration property */ private static final int DEFAULT_TIME_TO_LIVE = (int) (Times.MS_PER_DAY / 1000); /** Make the cache persistent between reboots? */ protected boolean diskPersistent = DEFAULT_DISK_PERSISTENT; /** Write overflow elements from memory to disk? */ protected boolean overflowToDisk = DEFAULT_OVERFLOW_TO_DISK; /** Maximum number of elements in memory */ protected int maxElementsInMemory = DEFAULT_MAX_ELEMENTS_IN_MEMORY; /** Maximum number of elements in memory */ protected int maxElementsOnDisk = DEFAULT_MAX_ELEMENTS_ON_DISK; /** Number of seconds for an element to live from its last access time */ protected int timeToIdle = DEFAULT_TIME_TO_IDLE; /** Number of seconds for an element to live from its creation date */ protected int timeToLive = DEFAULT_TIME_TO_LIVE; /** Whether cache statistics are enabled */ protected boolean statisticsEnabled = DEFAULT_STATISTICS_ENABLED; /** Identifier for the default cache */ private static final String DEFAULT_CACHE = "site"; /** The ehache cache manager */ protected CacheManager cacheManager = null; /** True if the cache is enabled */ protected boolean enabled = true; /** True if additional response headers are enabled */ protected boolean debug = false; /** The stream filter */ protected StreamFilter filter = null; /** Cache identifier */ protected String id = null; /** Cache name */ protected String name = null; /** Path to the local disk store */ protected String diskStorePath = null; /** * True to indicate that everything went fine with the setup of the disk store */ protected boolean diskStoreEnabled = true; /** Transactions that are currently being processed */ protected Map<String, CacheTransaction> transactions = null; /** List of registered cache listeners */ protected List<CacheListener> cacheListeners = null; /** * Creates a new cache with the given identifier and name. * * @param id * the cache identifier * @param name * the cache name * @param diskStorePath * the cache's disk store */ public CacheServiceImpl(String id, String name, String diskStorePath) { if (StringUtils.isBlank(id)) throw new IllegalArgumentException("Cache id cannot be blank"); if (StringUtils.isBlank(name)) throw new IllegalArgumentException("Cache name cannot be blank"); this.id = id; this.name = name; this.diskStorePath = diskStorePath; this.transactions = new HashMap<String, CacheTransaction>(); this.cacheListeners = new ArrayList<CacheListener>(); init(id, name, diskStorePath); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.cache.CacheService#addCacheListener(ch.entwine.weblounge.cache.CacheListener) */ public void addCacheListener(CacheListener listener) { if (!cacheListeners.contains(listener)) cacheListeners.add(listener); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.cache.CacheService#removeCacheListener(ch.entwine.weblounge.cache.CacheListener) */ public void removeCacheListener(CacheListener listener) { cacheListeners.remove(listener); } /** * Initializes the cache service with an identifier, a name and a path to the * local disk store (if applicable). * * @param id * the cache identifier * @param name * the cache name * @param diskStorePath * path to the local disk store */ private void init(String id, String name, String diskStorePath) { InputStream configInputStream = null; try { configInputStream = getClass().getClassLoader().getResourceAsStream(CACHE_MANAGER_CONFIG); Configuration cacheManagerConfig = ConfigurationFactory.parseConfiguration(configInputStream); cacheManagerConfig.getDiskStoreConfiguration().setPath(diskStorePath); cacheManager = new CacheManager(cacheManagerConfig); cacheManager.setName(id); } finally { IOUtils.closeQuietly(configInputStream); } // Check the path to the cache if (StringUtils.isNotBlank(diskStorePath)) { File file = new File(diskStorePath); try { if (!file.exists()) FileUtils.forceMkdir(file); if (!file.isDirectory()) throw new IOException(); if (!file.canWrite()) throw new IOException(); } catch (IOException e) { logger.warn("Unable to create disk store for cache '{}' at {}", id, diskStorePath); logger.warn("Persistent cache will be disabled for '{}'", id); diskPersistent = false; diskStoreEnabled = false; } } else { diskStoreEnabled = false; } // Configure the cache CacheConfiguration cacheConfig = new CacheConfiguration(); cacheConfig.setName(DEFAULT_CACHE); cacheConfig.setDiskPersistent(diskPersistent && diskStoreEnabled); cacheConfig.setOverflowToDisk(overflowToDisk && diskStoreEnabled); if (overflowToDisk && diskStoreEnabled) { cacheConfig.setDiskStorePath(diskStorePath); cacheConfig.setMaxElementsOnDisk(maxElementsOnDisk); } cacheConfig.setEternal(false); cacheConfig.setMaxElementsInMemory(maxElementsInMemory); cacheConfig.setStatistics(statisticsEnabled); cacheConfig.setTimeToIdleSeconds(timeToIdle); cacheConfig.setTimeToLiveSeconds(timeToLive); Cache cache = new Cache(cacheConfig); cacheManager.addCache(cache); if (overflowToDisk) logger.info("Cache extension for site '{}' created at {}", id, cacheManager.getDiskStorePath()); else logger.info("In-memory cache for site '{}' created", id); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.cache.CacheService#shutdown() */ public void shutdown() { if (cacheManager == null) return; for (String cacheName : cacheManager.getCacheNames()) { Cache cache = cacheManager.getCache(cacheName); cache.dispose(); } cacheManager.shutdown(); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.cache.CacheService#getIdentifier() */ public String getIdentifier() { return id; } /** * Configures the caching service. Available options are: * <ul> * <li><code>size</code> - the maximum cache size</li> * <li><code>filters</code> - the name of the output filters</li> * </ul> * * @see org.osgi.service.cm.ManagedService#updated(java.util.Dictionary) */ @SuppressWarnings("rawtypes") public void updated(Dictionary properties) throws ConfigurationException { if (properties == null) return; // Do we need to clear the cache? boolean clear = ConfigurationUtils.isTrue((String) properties.get(OPT_CLEAR), false); if (clear) { clear(); } // Enabled status enabled = ConfigurationUtils.isTrue((String) properties.get(OPT_ENABLE), DEFAULT_ENABLE); logger.debug("Cache is {}", diskPersistent ? "enabled" : "disabled"); debug = ConfigurationUtils.isTrue((String) properties.get(OPT_DEBUG), DEFAULT_DEBUG); logger.debug("Cache is {}", diskPersistent ? "enabled" : "disabled"); // Disk persistence diskPersistent = ConfigurationUtils.isTrue((String) properties.get(OPT_DISK_PERSISTENT), DEFAULT_DISK_PERSISTENT); logger.debug("Cache persistance between reboots is {}", diskPersistent ? "on" : "off"); // Statistics statisticsEnabled = ConfigurationUtils.isTrue((String) properties.get(OPT_ENABLE_STATISTICS), DEFAULT_STATISTICS_ENABLED); logger.debug("Cache statistics are {}", statisticsEnabled ? "enabled" : "disabled"); // Max elements in memory try { maxElementsInMemory = ConfigurationUtils.getValue((String) properties.get(OPT_MAX_ELEMENTS_IN_MEMORY), DEFAULT_MAX_ELEMENTS_IN_MEMORY); logger.debug("Cache will keep {} elements in memory", maxElementsInMemory > 0 ? "up to " + maxElementsInMemory : "all"); } catch (NumberFormatException e) { logger.warn("Value for cache setting '" + OPT_MAX_ELEMENTS_IN_MEMORY + "' is malformed: " + (String) properties.get(OPT_MAX_ELEMENTS_IN_MEMORY)); logger.warn("Cache setting '" + OPT_MAX_ELEMENTS_IN_MEMORY + "' set to default value of " + DEFAULT_MAX_ELEMENTS_IN_MEMORY); maxElementsInMemory = DEFAULT_MAX_ELEMENTS_IN_MEMORY; } // Max elements on disk try { maxElementsOnDisk = ConfigurationUtils.getValue((String) properties.get(OPT_MAX_ELEMENTS_ON_DISK), DEFAULT_MAX_ELEMENTS_ON_DISK); logger.debug("Cache will keep {} elements on disk", maxElementsOnDisk > 0 ? "up to " + maxElementsOnDisk : "all"); } catch (NumberFormatException e) { logger.warn("Value for cache setting '" + OPT_MAX_ELEMENTS_ON_DISK + "' is malformed: " + (String) properties.get(OPT_MAX_ELEMENTS_ON_DISK)); logger.warn("Cache setting '" + OPT_MAX_ELEMENTS_ON_DISK + "' set to default value of " + DEFAULT_MAX_ELEMENTS_ON_DISK); maxElementsOnDisk = DEFAULT_MAX_ELEMENTS_ON_DISK; } // Overflow to disk overflowToDisk = ConfigurationUtils.isTrue((String) properties.get(OPT_OVERFLOW_TO_DISK), DEFAULT_OVERFLOW_TO_DISK); // Time to idle try { timeToIdle = ConfigurationUtils.getValue((String) properties.get(OPT_TIME_TO_IDLE), DEFAULT_TIME_TO_IDLE); logger.debug("Cache time to idle is set to ", timeToIdle > 0 ? timeToIdle + "s" : "unlimited"); } catch (NumberFormatException e) { logger.warn("Value for cache setting '" + OPT_TIME_TO_IDLE + "' is malformed: " + (String) properties.get(OPT_TIME_TO_IDLE)); logger.warn("Cache setting '" + OPT_TIME_TO_IDLE + "' set to default value of " + DEFAULT_TIME_TO_IDLE); timeToIdle = DEFAULT_TIME_TO_IDLE; } // Time to live try { timeToLive = ConfigurationUtils.getValue((String) properties.get(OPT_TIME_TO_LIVE), DEFAULT_TIME_TO_LIVE); logger.debug("Cache time to live is set to ", timeToIdle > 0 ? timeToLive + "s" : "unlimited"); } catch (NumberFormatException e) { logger.warn("Value for cache setting '" + OPT_TIME_TO_LIVE + "' is malformed: " + (String) properties.get(OPT_TIME_TO_LIVE)); logger.warn("Cache setting '" + OPT_TIME_TO_LIVE + "' set to default value of " + DEFAULT_TIME_TO_LIVE); timeToLive = DEFAULT_TIME_TO_LIVE; } for (String cacheId : cacheManager.getCacheNames()) { Cache cache = cacheManager.getCache(cacheId); if (cache == null) continue; CacheConfiguration config = cache.getCacheConfiguration(); config.setOverflowToDisk(overflowToDisk && diskStoreEnabled); if (overflowToDisk && diskStoreEnabled) { config.setMaxElementsOnDisk(maxElementsOnDisk); } config.setDiskPersistent(diskPersistent && diskStoreEnabled); config.setStatistics(statisticsEnabled); config.setMaxElementsInMemory(maxElementsInMemory); config.setTimeToIdleSeconds(timeToIdle); config.setTimeToLiveSeconds(timeToLive); } } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.request.ResponseCache#resetStatistics() */ public void resetStatistics() { for (String cacheId : cacheManager.getCacheNames()) { Cache cache = cacheManager.getCache(cacheId); cache.setStatisticsEnabled(false); cache.setStatisticsEnabled(true); } } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.request.ResponseCache#clear() */ public void clear() { cacheManager.clearAll(); logger.info("Cache '{}' cleared", id); for (CacheListener listener : cacheListeners) { listener.cacheCleared(); } } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.request.ResponseCache#preload(ch.entwine.weblounge.common.request.CacheTag[]) */ public void preload(CacheTag[] tags) { if (tags == null || tags.length == 0) throw new IllegalArgumentException("Tags cannot be null or empty"); Cache cache = cacheManager.getCache(DEFAULT_CACHE); // Get the matching keys and load the elements into the cache Collection<Object> keys = getKeysForPrimaryTags(cache, tags); for (Object key : keys) { cache.load(key); } logger.info("Loaded first {} elements of cache '{}' into memory", keys.size(), id); } /** * Returns those keys from the given cache that contain at least all the tags * as defined in the <code>tags</code> array. * * @param cache * the cache * @param tags * the set of tags * @return the collection of matching keys */ private Collection<Object> getKeysForPrimaryTags(Cache cache, CacheTag[] tags) { // Create the parts of the key to look for List<String> keyParts = new ArrayList<String>(tags.length); for (CacheTag tag : tags) { StringBuffer b = new StringBuffer(tag.getName()).append("=").append(tag.getValue()); keyParts.add(b.toString()); } // Collect those keys that contain all relevant parts Collection<Object> keys = new ArrayList<Object>(); key: for (Object k : cache.getKeys()) { String key = k.toString(); for (String keyPart : keyParts) { if (!key.contains(keyPart)) continue key; } keys.add(k); } return keys; } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.request.ResponseCache#createCacheableResponse(javax.servlet.http.HttpServletRequest, * javax.servlet.http.HttpServletResponse) */ public HttpServletResponse createCacheableResponse(HttpServletRequest request, HttpServletResponse response) { return new CacheableHttpServletResponse(response); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.request.ResponseCache#startResponse(ch.entwine.weblounge.common.request.CacheTag[], * ch.entwine.weblounge.common.request.WebloungeRequest, * ch.entwine.weblounge.common.request.WebloungeResponse, long, long) */ public CacheHandle startResponse(CacheTag[] uniqueTags, WebloungeRequest request, WebloungeResponse response, long expirationTime, long revalidationTime) { CacheHandle hdl = new TaggedCacheHandle(uniqueTags, expirationTime, revalidationTime); return startResponse(hdl, request, response); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.request.ResponseCache#startResponse(ch.entwine.weblounge.common.request.CacheHandle, * ch.entwine.weblounge.common.request.WebloungeRequest, * ch.entwine.weblounge.common.request.WebloungeResponse) */ public CacheHandle startResponse(CacheHandle handle, WebloungeRequest request, WebloungeResponse response) { // Check whether the response has been properly wrapped CacheableHttpServletResponse cacheableResponse = unwrapResponse(response); if (cacheableResponse == null) { throw new IllegalStateException("Cached response is not properly wrapped"); } // While disabled, don't do lookups but return immediately if (!enabled) { cacheableResponse.startTransaction(handle, filter); return handle; } // Make sure the cache is still alive if (cacheManager.getStatus() != Status.STATUS_ALIVE) { logger.debug("Cache '{}' has unexpected status '{}'", request.getSite().getIdentifier(), cacheManager.getStatus()); return null; } // Load the cache Cache cache = cacheManager.getCache(DEFAULT_CACHE); if (cache == null) throw new IllegalStateException("No cache found for site '" + id + "'"); // Try to load the content from the cache Element element = cache.get(new CacheEntryKey(handle.getKey())); // Is the element already beyond its lifetime? if (element != null) { long expirationTime = element.getExpirationTime(); if (expirationTime < System.currentTimeMillis()) { logger.debug("Cache element {} of cache {} has expired", request, id); cache.remove(handle.getKey()); element = null; } } // If it exists, write the contents back to the response if (element != null && element.getValue() != null) { try { logger.debug("Answering {} from cache '{}'", request, id); writeCacheEntry(element, handle, request, response); return null; } catch (IOException e) { logger.debug("Error writing cached response to client"); return null; } } // Make sure that there are no two transactions producing the same content. // If there is a transaction already working on specific content, have // this transaction simply wait for the outcome. synchronized (transactions) { CacheTransaction tx = transactions.get(handle.getKey()); if (tx != null) { try { logger.debug("Waiting for cache transaction {} to be finished", request); while (transactions.containsKey(handle.getKey())) { transactions.wait(1000); // Was this a notify or a timeout? if (transactions.get(handle.getKey()) != null) { logger.debug("After waiting 1s, cache entry {} is still being worked on", handle.getKey()); response.setStatus(SC_SERVICE_UNAVAILABLE); return null; } } } catch (InterruptedException e) { // Done sleeping! } } // The cache might have been shut down in the meantime if (cacheManager.getStatus() == Status.STATUS_ALIVE) { element = cache.get(handle.getKey()); } else { logger.debug("Cache '{}' changed status to '{}'", request.getSite().getIdentifier(), cacheManager.getStatus()); } if (element == null) { tx = cacheableResponse.startTransaction(handle, filter); transactions.put(handle.getKey(), tx); logger.debug("Starting work on cached version of {}", request); } } // If we were waiting for an active cache transaction, let's try again if (element != null && element.getValue() != null) { try { logger.debug("Answering {} from cache '{}'", request, id); writeCacheEntry(element, handle, request, response); return null; } catch (IOException e) { logger.warn("Error writing cached response to client"); return null; } } // Apparently, we need to get it done ourselves return handle; } /** * Writes the cache element to the response, setting the cache headers * according to the settings found on the element. * * @param element * the cache contents * @param handle * the cache handle * @param request * the request * @param response * the response * @throws IOException * if writing the cache contents to the response fails */ private void writeCacheEntry(Element element, CacheHandle handle, WebloungeRequest request, WebloungeResponse response) throws IOException { CacheEntry entry = (CacheEntry) element.getValue(); // Check what the client has available locally String eTag = request.getHeader(HEADER_IF_NONE_MATCH); long clientCacheDate = 0; try { clientCacheDate = request.getDateHeader(HEADER_IF_MODIFIED_SINCE); } catch (IllegalArgumentException e) { logger.debug("The client provided a malformed '{}' date header: '{}'", HEADER_IF_MODIFIED_SINCE, request.getHeader(HEADER_IF_MODIFIED_SINCE)); } // Do we have a more recent version? boolean isModified = !entry.notModified(clientCacheDate) && !entry.matches(eTag); // Write the response headers if (isModified) { entry.getHeaders().apply(response); writeContentHeaders(response, entry); } writeCacheHeaders(response, entry, isModified); // Add the X-Cache-Key header if (debug || request.getHeader(CACHE_DEBUG_HEADER) != null) { StringBuffer cacheKeyHeader = new StringBuffer(name); cacheKeyHeader.append(" (").append(handle.getKey()).append(")"); response.addHeader(CACHE_KEY_HEADER, cacheKeyHeader.toString()); } // Check the headers first. Maybe we don't need to send anything but // a not-modified back if (isModified) { response.getOutputStream().write(entry.getContent()); } else { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } response.flushBuffer(); } /** * Writes the headers that are relevant for proper content handling based on * the cache entry. * * @param response * the response * @param entry * the cache entry */ private void writeContentHeaders(WebloungeResponse response, CacheEntry entry) { response.setContentType(entry.getContentType()); response.setCharacterEncoding(entry.getEncoding()); response.setContentLength(entry.getContent().length); } /** * Writes the headers that are relevant for proper caching based on the cache * entry. * * @param response * the response * @param entry * the cache entry * @param isModified * <code>true</code> if the client asked for the content only if the * content is more recent that what was cached locally */ private void writeCacheHeaders(WebloungeResponse response, CacheEntry entry, boolean isModified) { long expirationDate = System.currentTimeMillis() + entry.getClientRevalidationTime(); long revalidationTimeInSeconds = entry.getClientRevalidationTime() / 1000; // Send Cache directives, ETag and Last-Modified if (isModified) { response.setHeader("Cache-Control", "private, max-age=" + revalidationTimeInSeconds + ", must-revalidate"); response.setHeader("ETag", entry.getETag()); response.setDateHeader("Last-Modified", entry.getModificationDate()); } // Set the current date response.setDateHeader("Date", System.currentTimeMillis()); // This header must be set, otherwise it defaults to // "Thu, 01-Jan-1970 00:00:00 GMT" response.setDateHeader("Expires", expirationDate); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.request.ResponseCache#endResponse(ch.entwine.weblounge.common.request.WebloungeResponse) */ public boolean endResponse(WebloungeResponse response) { CacheableHttpServletResponse cacheableResponse = unwrapResponse(response); if (cacheableResponse == null) return false; // Discard any cached content while disabled if (!enabled) return true; // Make sure the cache is still available and active if (cacheManager.getStatus() != Status.STATUS_ALIVE) { logger.debug("Cache '{}' has unexpected status '{}'", cacheManager.getName(), cacheManager.getStatus()); return false; } // Load the cache Cache cache = cacheManager.getCache(DEFAULT_CACHE); if (cache == null) { logger.debug("Cache for {} was deactivated, response is not being cached", response); return false; } // Finish writing the element CacheTransaction tx = cacheableResponse.endOutput(); // Is the response ready to be cached? if (tx == null) { logger.debug("Response to {} was not associated with a transaction", response); return false; } // Important note: Do not return prior to this try block if there is a // transaction associated with the request. try { // Is the response ready to be cached? if (tx.isValid() && response.isValid() && response.getStatus() == HttpServletResponse.SC_OK) { logger.trace("Writing response for {} to the cache", response); CacheHandle cacheHdl = tx.getHandle(); String encoding = cacheableResponse.getCharacterEncoding(); CacheEntry entry = new CacheEntry(cacheHdl, tx.getContent(), encoding, tx.getHeaders()); Element element = new Element(new CacheEntryKey(cacheHdl), entry); element.setTimeToLive((int) (cacheHdl.getCacheExpirationTime() / 1000)); cache.put(element); // Write cache and content relevant headers writeCacheHeaders(response, entry, true); writeContentHeaders(response, entry); // Inform listeners for (CacheListener listener : cacheListeners) { listener.cacheEntryAdded(cacheHdl); } } else if (tx.isValid() && response.isValid()) { logger.trace("Skip caching of response for {}: {}", response, response.getStatus()); response.setDateHeader("Expires", System.currentTimeMillis() + tx.getHandle().getCacheExpirationTime()); } else { logger.debug("Response to {} was invalid and is not being cached", response); } return tx.isValid() && response.isValid(); } finally { // Mark the current transaction as finished and notify anybody who was // waiting for it to be finished synchronized (transactions) { transactions.remove(tx.getHandle().getKey()); logger.debug("Caching of {} finished", response); transactions.notifyAll(); } try { if (!response.isCommitted()) response.flushBuffer(); } catch (IOException e) { String message = e.getMessage(); // This is debug, as the client may have closed the connection logger.debug("Error flushing response: {}", message); } } } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.request.ResponseCache#invalidate(ch.entwine.weblounge.common.request.WebloungeResponse) */ public void invalidate(WebloungeResponse response) { CacheableHttpServletResponse cacheableResponse = unwrapResponse(response); if (cacheableResponse == null || cacheableResponse.getTransaction() == null) return; cacheableResponse.invalidate(); CacheTransaction tx = cacheableResponse.getTransaction(); invalidate(tx.getHandle()); logger.debug("Removed {} from cache '{}'", response, id); } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.request.ResponseCache#invalidate(ch.entwine.weblounge.common.request.CacheTag[], * boolean) */ public void invalidate(CacheTag[] tags, boolean partialMatches) { if (tags == null || tags.length == 0) throw new IllegalArgumentException("Tags cannot be null or empty"); // Load the cache Cache cache = cacheManager.getCache(DEFAULT_CACHE); // Inform listeners for (CacheListener listener : cacheListeners) { listener.cacheSetInvalidated(tags); } // Remove the objects matched by the tags long removed = 0; for (Object key : getKeysForTags(cache, tags, partialMatches)) { if (cache.remove(key)) removed++; } logger.debug("Removed {} elements from cache '{}'", removed, id); } /** * Returns those keys from the given cache that contain all or any of the tags * as defined in the <code>tags</code> array. * * @param cache * the cache * @param tags * the set of tags * @param partialMatches * <code>true</code> to include partial matches, where only one of * the tag matches instead of all * @return the collection of matching keys */ private Collection<Object> getKeysForTags(Cache cache, CacheTag[] tags, boolean partialMatches) { // Create the parts of the key to look for List<String> keyParts = new ArrayList<String>(tags.length); for (CacheTag tag : tags) { StringBuffer b = new StringBuffer(tag.getName()).append("=").append(tag.getValue()); keyParts.add(b.toString()); } // Collect those keys that contain all relevant parts Collection<Object> cacheKeys = new ArrayList<Object>(); key: for (Object k : cache.getKeys()) { String key = ((CacheEntryKey) k).tags; for (String keyPart : keyParts) { if (!key.contains(keyPart) && !partialMatches) { continue key; } else if (key.contains(keyPart)) { cacheKeys.add(k); continue key; } } } return cacheKeys; } /** * {@inheritDoc} * * @see ch.entwine.weblounge.common.request.ResponseCache#invalidate(ch.entwine.weblounge.common.request.CacheHandle) */ public void invalidate(CacheHandle handle) { if (handle == null) throw new IllegalArgumentException("Handle cannot be null"); // Make sure the cache is still available and active if (cacheManager.getStatus() != Status.STATUS_ALIVE) { logger.debug("Cache '{}' has unexpected status '{}'", cacheManager.getName(), cacheManager.getStatus()); return; } // Load the cache Cache cache = cacheManager.getCache(DEFAULT_CACHE); if (cache == null) { logger.debug("Cache for {} was deactivated, response is not being invalidated"); return; } cache.remove(handle.getKey()); // Mark the current transaction as finished and notify anybody that was // waiting for it to be finished synchronized (transactions) { transactions.remove(handle.getKey()); transactions.notifyAll(); } logger.debug("Removed {} from cache '{}'", handle.getKey(), id); // Inform listeners for (CacheListener listener : cacheListeners) { listener.cacheEntryRemoved(handle); } } /** * Extracts the <code>CacheableServletResponse</code> from its wrapper(s). * * @param response * the original response * @return the wrapped <code>CacheableServletResponse</code> or * <code>null</code> if the response is not cacheable */ private static CacheableHttpServletResponse unwrapResponse(ServletResponse response) { while (response != null) { if (response instanceof CacheableHttpServletResponse) return (CacheableHttpServletResponse) response; if (!(response instanceof ServletResponseWrapper)) break; response = ((ServletResponseWrapper) response).getResponse(); } return null; } /** * {@inheritDoc} * * @see java.lang.Object#toString() */ @Override public String toString() { return name; } }