/*
* This library is part of OpenCms -
* the Open Source Content Management System
*
* Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
*
* This library 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.1 of the License, or (at your option) any later version.
*
* This library 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.
*
* For further information about Alkacon Software GmbH & Co. KG, please see the
* company website: http://www.alkacon.com
*
* For further information about OpenCms, please see the
* project website: http://www.opencms.org
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.opencms.flex;
import org.opencms.cache.I_CmsLruCacheObject;
import org.opencms.file.CmsResource;
import org.opencms.i18n.CmsMessageContainer;
import org.opencms.jsp.util.CmsJspStandardContextBean;
import org.opencms.main.CmsLog;
import org.opencms.monitor.CmsMemoryMonitor;
import org.opencms.monitor.I_CmsMemoryMonitorable;
import org.opencms.util.CmsCollectionsGenericWrapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.ServletException;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.logging.Log;
/**
* Contains the contents of a cached resource.
*
* It is basically a list of pre-generated output,
* include() calls to other resources (with request parameters) and http headers that this
* resource requires to be set.
*
* A CmsFlexCacheEntry might also describe a redirect-call, but in this case
* nothing else will be cached.
*
* The pre-generated output is saved in byte[]
arrays.
* The include() calls are saved as Strings of the included resource name,
* the parameters for the calls are saved in a HashMap.
* The headers are saved in a HashMap.
* In case of a redirect, the redirect target is cached in a String.
*
* The CmsFlexCacheEntry can also have an expire date value, which indicates the time
* that his entry will become invalid and should thus be cleared from the cache.
*
* @since 6.0.0
*
* @see org.opencms.cache.I_CmsLruCacheObject
*/
public class CmsFlexCacheEntry implements I_CmsLruCacheObject, I_CmsMemoryMonitorable {
/** Initial size for lists. */
public static final int INITIAL_CAPACITY_LISTS = 10;
/** The log object for this class. */
private static final Log LOG = CmsLog.getLog(CmsFlexCacheEntry.class);
/** The CacheEntry's size in bytes. */
private int m_byteSize;
/** Indicates if this cache entry is completed. */
private boolean m_completed;
/** The "expires" date for this Flex cache entry. */
private long m_dateExpires;
/** The "last modified" date for this Flex cache entry. */
private long m_dateLastModified;
/** The list of items for this resource. */
private List m_elements;
/** A Map of cached headers for this resource. */
private Map> m_headers;
/** Pointer to the next cache entry in the LRU cache. */
private I_CmsLruCacheObject m_next;
/** Pointer to the previous cache entry in the LRU cache. */
private I_CmsLruCacheObject m_previous;
/** Flag which indicates whether a cached redirect is permanent. */
private boolean m_redirectPermanent;
/** A redirection target (if redirection is set). */
private String m_redirectTarget;
/** The key under which this cache entry is stored in the variation map. */
private String m_variationKey;
/** The variation map where this cache entry is stored. */
private Map m_variationMap;
/**
* Constructor for class CmsFlexCacheEntry.
*
* The way to use this class is to first use this empty constructor
* and later add data with the various add methods.
*/
public CmsFlexCacheEntry() {
m_elements = new ArrayList(INITIAL_CAPACITY_LISTS);
m_dateExpires = CmsResource.DATE_EXPIRED_DEFAULT;
m_dateLastModified = -1;
// base memory footprint of this object with all referenced objects
m_byteSize = 1024;
setNextLruObject(null);
setPreviousLruObject(null);
}
/**
* Adds an array of bytes to this cache entry,
* this will usually be the result of some kind of output - stream.
*
* @param bytes the output to save in the cache
*/
public void add(byte[] bytes) {
if (m_completed) {
return;
}
if (m_redirectTarget == null) {
// Add only if not already redirected
m_elements.add(bytes);
m_byteSize += CmsMemoryMonitor.getMemorySize(bytes);
}
}
/**
* Add an include - call target resource to this cache entry.
*
* @param resource a name of a resource in the OpenCms VFS
* @param parameters a map of parameters specific to this include call
* @param attrs a map of request attributes specific to this include call
*/
public void add(String resource, Map parameters, Map attrs) {
if (m_completed) {
return;
}
if (m_redirectTarget == null) {
// Add only if not already redirected
m_elements.add(resource);
m_byteSize += CmsMemoryMonitor.getMemorySize(resource);
if (parameters == null) {
parameters = Collections.emptyMap();
}
m_elements.add(parameters);
m_byteSize += CmsMemoryMonitor.getValueSize(parameters);
if (attrs == null) {
attrs = Collections.emptyMap();
}
m_elements.add(attrs);
m_byteSize += CmsMemoryMonitor.getValueSize(attrs);
}
}
/**
* Add a map of headers to this cache entry,
* which are usually collected in the class CmsFlexResponse first.
*
* @param headers the map of headers to add to the entry
*/
public void addHeaders(Map> headers) {
if (m_completed) {
return;
}
m_headers = headers;
Iterator allHeaders = m_headers.keySet().iterator();
while (allHeaders.hasNext()) {
m_byteSize += CmsMemoryMonitor.getMemorySize(allHeaders.next());
}
}
/**
* @see org.opencms.cache.I_CmsLruCacheObject#addToLruCache()
*/
public void addToLruCache() {
// do nothing here...
if (LOG.isDebugEnabled()) {
LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEENTRY_ADDED_ENTRY_1, this));
}
}
/**
* Completes this cache entry.
*
* A completed cache entry is made "unmodifiable",
* so that no further data can be added and existing data can not be changed.
*
* This is to prevent the (unlikely) case that some user-written class
* tries to make changes to a cache entry.
*/
public void complete() {
m_completed = true;
// Prevent changing of the cached lists
if (m_headers != null) {
m_headers = Collections.unmodifiableMap(m_headers);
}
if (m_elements != null) {
m_elements = Collections.unmodifiableList(m_elements);
}
if (LOG.isDebugEnabled()) {
LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEENTRY_ENTRY_COMPLETED_1, toString()));
}
}
/**
* Returns the list of data entries of this cache entry.
*
* Data entries are byte arrays representing some kind of output
* or Strings representing include calls to other resources.
*
* @return the list of data elements of this cache entry
*/
public List elements() {
return m_elements;
}
/**
* Returns the expiration date of this cache entry,
* this is set to the time when the entry becomes invalid.
*
* @return the expiration date value for this resource
*/
public long getDateExpires() {
return m_dateExpires;
}
/**
* Returns the "last modified" date for this Flex cache entry.
*
* @return the "last modified" date for this Flex cache entry
*/
public long getDateLastModified() {
return m_dateLastModified;
}
/**
* @see org.opencms.cache.I_CmsLruCacheObject#getLruCacheCosts()
*/
public int getLruCacheCosts() {
return m_byteSize;
}
/**
* @see org.opencms.monitor.I_CmsMemoryMonitorable#getMemorySize()
*/
public int getMemorySize() {
return getLruCacheCosts();
}
/**
* @see org.opencms.cache.I_CmsLruCacheObject#getNextLruObject()
*/
public I_CmsLruCacheObject getNextLruObject() {
return m_next;
}
/**
* @see org.opencms.cache.I_CmsLruCacheObject#getPreviousLruObject()
*/
public I_CmsLruCacheObject getPreviousLruObject() {
return m_previous;
}
/**
* @see org.opencms.cache.I_CmsLruCacheObject#getValue()
*/
public Object getValue() {
return m_elements;
}
/**
* @see org.opencms.cache.I_CmsLruCacheObject#removeFromLruCache()
*/
public void removeFromLruCache() {
if ((m_variationMap != null) && (m_variationKey != null)) {
m_variationMap.remove(m_variationKey);
}
if (LOG.isDebugEnabled()) {
LOG.debug(
Messages.get().getBundle().key(
Messages.LOG_FLEXCACHEENTRY_REMOVED_ENTRY_FOR_VARIATION_1,
m_variationKey));
}
}
/**
* Processing method for this cached entry.
*
* If this method is called, it delivers the contents of
* the cached entry to the given request / response.
* This includes calls to all included resources.
*
* @param req the request from the client
* @param res the server response
*
* @throws CmsFlexCacheException is thrown when problems writing to the response output-stream occur
* @throws ServletException might be thrown from call to RequestDispatcher.include()
* @throws IOException might be thrown from call to RequestDispatcher.include() or from Response.sendRedirect()
*/
public void service(CmsFlexRequest req, CmsFlexResponse res)
throws CmsFlexCacheException, ServletException, IOException {
if (!m_completed) {
return;
}
if (m_redirectTarget != null) {
res.setOnlyBuffering(false);
res.setCmsCachingRequired(false);
// redirect the response, no further output required
res.sendRedirect(m_redirectTarget, m_redirectPermanent);
} else {
// process cached headers first
CmsFlexResponse.processHeaders(m_headers, res);
// check if this cache entry is a "leaf" (i.e. no further includes)
boolean hasNoSubElements = (m_elements.size() == 1);
// write output to stream and process all included elements
for (int i = 0; i < m_elements.size(); i++) {
Object o = m_elements.get(i);
if (o instanceof String) {
// handle cached parameters
i++;
Map paramMap = CmsCollectionsGenericWrapper.map(m_elements.get(i));
Map oldParamMap = null;
if (paramMap.size() > 0) {
oldParamMap = req.getParameterMap();
req.addParameterMap(paramMap);
}
// handle cached attributes
i++;
Map attrMap = CmsCollectionsGenericWrapper.map(m_elements.get(i));
Map oldAttrMap = null;
if (attrMap.size() > 0) {
oldAttrMap = req.getAttributeMap();
// to avoid issues with multi threading, try to clone the attribute instances
req.addAttributeMap(cloneAttributes(attrMap));
//req.addAttributeMap(attrMap);
}
// do the include call
req.getRequestDispatcher((String)o).include(req, res);
// reset parameters if necessary
if (oldParamMap != null) {
req.setParameterMap(oldParamMap);
}
// reset attributes if necessary
if (oldAttrMap != null) {
req.setAttributeMap(oldAttrMap);
}
} else {
try {
res.writeToOutputStream((byte[])o, hasNoSubElements);
} catch (IOException e) {
CmsMessageContainer message = Messages.get().container(
Messages.LOG_FLEXCACHEKEY_NOT_FOUND_1,
getClass().getName());
if (LOG.isDebugEnabled()) {
LOG.debug(message.key());
}
throw new CmsFlexCacheException(message, e);
}
}
}
}
}
/**
* Sets the expiration date of this Flex cache entry exactly to the
* given time.
*
* @param dateExpires the time to expire this cache entry
*/
public void setDateExpires(long dateExpires) {
m_dateExpires = dateExpires;
if (LOG.isDebugEnabled()) {
long now = System.currentTimeMillis();
LOG.debug(
Messages.get().getBundle().key(
Messages.LOG_FLEXCACHEENTRY_SET_EXPIRATION_DATE_3,
new Long(m_dateExpires),
new Long(now),
new Long(m_dateExpires - now)));
}
}
/**
* Sets an expiration date for this cache entry to the next timeout,
* which indicates the time this entry becomes invalid.
*
* The timeout parameter represents the minute - interval in which the cache entry
* is to be cleared.
* The interval always starts at 0.00h.
* A value of 60 would indicate that this entry will reach it's expiration date at the beginning of the next
* full hour, a timeout of 20 would indicate that the entry is invalidated at x.00, x.20 and x.40 of every hour etc.
*
* @param timeout the timeout value to be set
*/
public void setDateExpiresToNextTimeout(long timeout) {
if ((timeout < 0) || !m_completed) {
return;
}
long now = System.currentTimeMillis();
long daytime = now % 86400000;
long timeoutMinutes = timeout * 60000;
setDateExpires((now - (daytime % timeoutMinutes)) + timeoutMinutes);
}
/**
* Sets the "last modified" date for this Flex cache entry with the given value.
*
* @param dateLastModified the value to set for the "last modified" date
*/
public void setDateLastModified(long dateLastModified) {
m_dateLastModified = dateLastModified;
}
/**
* Sets the "last modified" date for this Flex cache entry by using the last passed timeout value.
*
* If a cache entry uses the timeout feature, it becomes invalid every time the timeout interval
* passes. Thus the "last modified" date is the time the last timeout passed.
*
* @param timeout the timeout value to use to calculate the date last modified
*/
public void setDateLastModifiedToPreviousTimeout(long timeout) {
long now = System.currentTimeMillis();
long daytime = now % 86400000;
long timeoutMinutes = timeout * 60000;
setDateLastModified(now - (daytime % timeoutMinutes));
}
/**
* @see org.opencms.cache.I_CmsLruCacheObject#setNextLruObject(org.opencms.cache.I_CmsLruCacheObject)
*/
public void setNextLruObject(I_CmsLruCacheObject theNextEntry) {
m_next = theNextEntry;
}
/**
* @see org.opencms.cache.I_CmsLruCacheObject#setPreviousLruObject(org.opencms.cache.I_CmsLruCacheObject)
*/
public void setPreviousLruObject(I_CmsLruCacheObject thePreviousEntry) {
m_previous = thePreviousEntry;
}
/**
* Set a redirect target for this cache entry.
*
* Important:
* When a redirect target is set, all saved data is thrown away,
* and new data will not be saved in the cache entry.
* This is so since with a redirect nothing will be displayed
* in the browser anyway, so there is no point in saving the data.
*
* @param target The redirect target (must be a valid URL).
* @param permanent true if this is a permanent redirect
*/
public void setRedirect(String target, boolean permanent) {
if (m_completed || (target == null)) {
return;
}
m_redirectTarget = target;
m_redirectPermanent = permanent;
m_byteSize = 512 + CmsMemoryMonitor.getMemorySize(target);
// If we have a redirect we don't need any other output or headers
m_elements = null;
m_headers = null;
}
/**
* Stores a backward reference to the map and key where this cache entry is stored.
*
* This is required for the FlexCache.
*
* @param theVariationKey the variation key
* @param theVariationMap the variation map
*/
public void setVariationData(String theVariationKey, Map theVariationMap) {
m_variationKey = theVariationKey;
m_variationMap = theVariationMap;
}
/**
* @see java.lang.Object#toString()
*
* @return a basic String representation of this CmsFlexCache entry
*/
@Override
public String toString() {
String str = null;
if (m_redirectTarget == null) {
str = "CmsFlexCacheEntry [" + m_elements.size() + " Elements/" + getLruCacheCosts() + " bytes]\n";
Iterator i = m_elements.iterator();
int count = 0;
while (i.hasNext()) {
count++;
Object o = i.next();
if (o instanceof String) {
str += "" + count + " - \n";
} else if (o instanceof byte[]) {
str += "" + count + " - \n";
} else {
str += "";
}
}
} else {
str = "CmsFlexCacheEntry [Redirect to target=" + m_redirectTarget + "]";
}
return str;
}
/**
* Clones the attribute instances if possible.
*
* @param attrs the attributes
*
* @return a new map instance with the cloned attributes
*/
private Map cloneAttributes(Map attrs) {
Map result = new HashMap();
for (Entry entry : attrs.entrySet()) {
if (entry.getValue() instanceof CmsJspStandardContextBean) {
result.put(entry.getKey(), ((CmsJspStandardContextBean)entry.getValue()).createCopy());
} else if (entry.getValue() instanceof Cloneable) {
Object clone = null;
try {
clone = ObjectUtils.clone(entry.getValue());
} catch (Exception e) {
LOG.info(e.getMessage(), e);
}
result.put(entry.getKey(), clone != null ? clone : entry.getValue());
} else {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}
}