/*
* 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.jsp.util.CmsJspStandardContextBean;
import org.opencms.main.CmsIllegalArgumentException;
import org.opencms.main.CmsLog;
import org.opencms.main.OpenCms;
import org.opencms.util.CmsDateUtil;
import org.opencms.util.CmsRequestUtil;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.apache.commons.logging.Log;
/**
* Wrapper class for a HttpServletResponse, required in order to process JSPs from the OpenCms VFS.
*
* This class wraps the standard HttpServletResponse so that it's output can be delivered to
* the CmsFlexCache.
*
* @since 6.0.0
*/
public class CmsFlexResponse extends HttpServletResponseWrapper {
/**
* Wrapped implementation of the ServletOutputStream.
*
* This implementation writes to an internal buffer and optionally to another
* output stream at the same time.
*
* It should be fully transparent to the standard ServletOutputStream.
*/
private static class CmsServletOutputStream extends ServletOutputStream {
/** The optional output stream to write to. */
private ServletOutputStream m_servletStream;
/** The internal stream buffer. */
private ByteArrayOutputStream m_stream;
/**
* Constructor that must be used if the stream should write
* only to a buffer.
*/
public CmsServletOutputStream() {
m_servletStream = null;
clear();
}
/**
* Constructor that must be used if the stream should write
* to a buffer and to another stream at the same time.
*
* @param servletStream The stream to write to
*/
public CmsServletOutputStream(ServletOutputStream servletStream) {
m_servletStream = servletStream;
clear();
}
/**
* Clears the buffer by initializing the buffer with a new stream.
*/
public void clear() {
m_stream = new java.io.ByteArrayOutputStream(1024);
}
/**
* @see java.io.OutputStream#close()
*/
@Override
public void close() throws IOException {
if (m_stream != null) {
m_stream.close();
}
if (m_servletStream != null) {
m_servletStream.close();
}
super.close();
}
/**
* @see java.io.OutputStream#flush()
*/
@Override
public void flush() throws IOException {
if (LOG.isDebugEnabled()) {
LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_FLUSHED_1, m_servletStream));
}
if (m_servletStream != null) {
m_servletStream.flush();
}
}
/**
* Provides access to the bytes cached in the buffer.
*
* @return the cached bytes from the buffer
*/
public byte[] getBytes() {
return m_stream.toByteArray();
}
/**
* @see java.io.OutputStream#write(byte[], int, int)
*/
@Override
public void write(byte[] b, int off, int len) throws IOException {
m_stream.write(b, off, len);
if (m_servletStream != null) {
m_servletStream.write(b, off, len);
}
}
/**
* @see java.io.OutputStream#write(int)
*/
@Override
public void write(int b) throws IOException {
m_stream.write(b);
if (m_servletStream != null) {
m_servletStream.write(b);
}
}
/**
* @see javax.servlet.ServletOutputStream#isReady()
*/
@Override
public boolean isReady() {
return null != m_stream;
}
/**
* @see javax.servlet.ServletOutputStream#setWriteListener(javax.servlet.WriteListener)
*/
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
/** The cache delimiter char. */
public static final char FLEX_CACHE_DELIMITER = (char)0;
/** Prefix for permanent redirect targets. */
public static final String PREFIX_PERMANENT_REDIRECT = "permanent-redirect:";
/** Static string to indicate a header is "set" in the header maps. */
public static final String SET_HEADER = "[setHeader]";
/** The log object for this class. */
protected static final Log LOG = CmsLog.getLog(CmsFlexResponse.class);
/** Map to save response headers belonging to a single include call in .*/
private Map> m_bufferHeaders;
/** String to hold a buffered redirect target. */
private String m_bufferRedirect;
/** Byte array used for "cached leafs" optimization. */
private byte[] m_cacheBytes;
/** The cached entry that is constructed from this response. */
private CmsFlexCacheEntry m_cachedEntry;
/** Indicates if caching is required, will always be true if m_writeOnlyToBuffer is true. */
private boolean m_cachingRequired;
/** The CmsFlexController for this response. */
private CmsFlexController m_controller;
/** The encoding to use for the response. */
private String m_encoding;
/** Map to save all response headers (including sub-elements) in. */
private Map> m_headers;
/** A list of include calls that origin from this page, i.e. these are sub elements of this element. */
private List m_includeList;
/** A list of attributes that belong to the include calls. */
private List> m_includeListAttributes;
/** A list of parameters that belong to the include calls. */
private List> m_includeListParameters;
/** Indicates if this element is currently in include mode, i.e. processing a sub-element. */
private boolean m_includeMode;
/** A list of results from the inclusions, needed because of JSP buffering. */
private List m_includeResults;
/** Flag to indicate if this is the top level element or an included sub - element. */
private boolean m_isTopElement;
/** The CmsFlexCacheKey for this response. */
private CmsFlexCacheKey m_key;
/** A special wrapper class for a ServletOutputStream. */
private CmsFlexResponse.CmsServletOutputStream m_out;
/** Indicates that parent stream is writing only in the buffer. */
private boolean m_parentWritesOnlyToBuffer;
/** Flag which indicates whether a permanent redirect has been buffered. */
private boolean m_redirectPermanent;
/** The wrapped ServletResponse. */
private HttpServletResponse m_res;
/** Indicates if this response is suspended (probably because of a redirect). */
private boolean m_suspended;
/** State bit indicating whether content type has been set, type may only be set once according to spec. */
private boolean m_typeSet;
/** Indicates that the OutputStream m_out should write ONLY in the buffer. */
private boolean m_writeOnlyToBuffer;
/** A print writer that writes in the m_out stream. */
private java.io.PrintWriter m_writer;
/**
* Constructor for the CmsFlexResponse,
* this variation one is usually used to wrap responses for further include calls in OpenCms.
*
* @param res the CmsFlexResponse to wrap
* @param controller the controller to use
*/
public CmsFlexResponse(HttpServletResponse res, CmsFlexController controller) {
super(res);
m_res = res;
m_controller = controller;
m_encoding = controller.getCurrentResponse().getEncoding();
m_isTopElement = controller.getCurrentResponse().isTopElement();
m_parentWritesOnlyToBuffer = controller.getCurrentResponse().hasIncludeList() && !controller.isForwardMode();
setOnlyBuffering(m_parentWritesOnlyToBuffer);
m_headers = new HashMap>(16);
m_bufferHeaders = new HashMap>(8);
}
/**
* Constructor for the CmsFlexResponse,
* this variation is usually used for the "top" response.
*
* @param res the HttpServletResponse to wrap
* @param controller the controller to use
* @param streaming indicates if streaming should be enabled or not
* @param isTopElement indicates if this is the top element of an include cascade
*/
public CmsFlexResponse(
HttpServletResponse res,
CmsFlexController controller,
boolean streaming,
boolean isTopElement) {
super(res);
m_res = res;
m_controller = controller;
m_encoding = controller.getCmsObject().getRequestContext().getEncoding();
m_isTopElement = isTopElement;
m_parentWritesOnlyToBuffer = !streaming && !controller.isForwardMode();
setOnlyBuffering(m_parentWritesOnlyToBuffer);
m_headers = new HashMap>(16);
m_bufferHeaders = new HashMap>(8);
}
/**
* Process the headers stored in the provided map and add them to the response.
*
* @param headers the headers to add
* @param res the response to add the headers to
*/
public static void processHeaders(Map> headers, HttpServletResponse res) {
if (headers != null) {
Iterator>> i = headers.entrySet().iterator();
while (i.hasNext()) {
Map.Entry> entry = i.next();
String key = entry.getKey();
List l = entry.getValue();
for (int j = 0; j < l.size(); j++) {
if ((j == 0) && ((l.get(0)).startsWith(SET_HEADER))) {
String s = l.get(0);
res.setHeader(key, s.substring(SET_HEADER.length()));
} else {
res.addHeader(key, l.get(j));
}
}
}
}
}
/**
* Method overloaded from the standard HttpServletRequest API.
*
* Cookies must be set directly as a header, otherwise they might not be set
* in the super class.
*
* @see javax.servlet.http.HttpServletResponseWrapper#addCookie(javax.servlet.http.Cookie)
*/
@Override
public void addCookie(Cookie cookie) {
if (cookie == null) {
throw new CmsIllegalArgumentException(Messages.get().container(Messages.ERR_ADD_COOKIE_0));
}
StringBuffer header = new StringBuffer(128);
// name and value
header.append(cookie.getName());
header.append('=');
header.append(cookie.getValue());
// add version 1 / RFC 2109 specific information
if (cookie.getVersion() == 1) {
header.append("; Version=1");
// comment
if (cookie.getComment() != null) {
header.append("; Comment=");
header.append(cookie.getComment());
}
}
// domain
if (cookie.getDomain() != null) {
header.append("; Domain=");
header.append(cookie.getDomain());
}
// max-age / expires
if (cookie.getMaxAge() >= 0) {
if (cookie.getVersion() == 0) {
// old Netscape format
header.append("; Expires=");
long time;
if (cookie.getMaxAge() == 0) {
time = 10000L;
} else {
time = System.currentTimeMillis() + (cookie.getMaxAge() * 1000L);
}
header.append(CmsDateUtil.getOldCookieDate(time));
} else {
// new RFC 2109 format
header.append("; Max-Age=");
header.append(cookie.getMaxAge());
}
}
// path
if (cookie.getPath() != null) {
header.append("; Path=");
header.append(cookie.getPath());
}
// secure
if (cookie.getSecure()) {
header.append("; Secure");
}
addHeader("Set-Cookie", header.toString());
}
/**
* Method overload from the standard HttpServletRequest API.
*
* @see javax.servlet.http.HttpServletResponse#addDateHeader(java.lang.String, long)
*/
@Override
public void addDateHeader(String name, long date) {
addHeader(name, CmsDateUtil.getHeaderDate(date));
}
/**
* Method overload from the standard HttpServletRequest API.
*
* @see javax.servlet.http.HttpServletResponse#addHeader(java.lang.String, java.lang.String)
*/
@Override
public void addHeader(String name, String value) {
if (isSuspended()) {
return;
}
if (CmsRequestUtil.HEADER_CONTENT_TYPE.equalsIgnoreCase(name)) {
setContentType(value);
return;
}
if (m_cachingRequired && !m_includeMode) {
addHeaderList(m_bufferHeaders, name, value);
if (LOG.isDebugEnabled()) {
LOG.debug(
Messages.get().getBundle().key(
Messages.LOG_FLEXRESPONSE_ADDING_HEADER_TO_ELEMENT_BUFFER_2,
name,
value));
}
}
if (m_writeOnlyToBuffer) {
addHeaderList(m_headers, name, value);
if (LOG.isDebugEnabled()) {
LOG.debug(
Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_ADDING_HEADER_TO_HEADERS_2, name, value));
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug(
Messages.get().getBundle().key(
Messages.LOG_FLEXRESPONSE_ADDING_HEADER_TO_PARENT_RESPONSE_2,
name,
value));
}
m_res.addHeader(name, value);
}
}
/**
* Method overload from the standard HttpServletRequest API.
*
* @see javax.servlet.http.HttpServletResponse#addIntHeader(java.lang.String, int)
*/
@Override
public void addIntHeader(String name, int value) {
addHeader(name, String.valueOf(value));
}
/**
* Adds an inclusion target to the list of include results.
*
* Should be used only in inclusion-scenarios
* like the JSP cms:include tag processing.
*
* @param target the include target name to add
* @param parameterMap the map of parameters given with the include command
* @param attributeMap the map of attributes given with the include command
*/
public void addToIncludeList(String target, Map parameterMap, Map attributeMap) {
if (m_includeList == null) {
m_includeList = new ArrayList(10);
m_includeListParameters = new ArrayList>(10);
m_includeListAttributes = new ArrayList>(10);
}
// never cache some request attributes, e.g. the Flex controller
m_controller.removeUncacheableAttributes(attributeMap);
// only cache a copy of the JSP standard context bean
CmsJspStandardContextBean bean = (CmsJspStandardContextBean)attributeMap.get(
CmsJspStandardContextBean.ATTRIBUTE_NAME);
if (bean != null) {
attributeMap.put(CmsJspStandardContextBean.ATTRIBUTE_NAME, bean.createCopy());
}
m_includeListAttributes.add(attributeMap);
m_includeListParameters.add(parameterMap);
m_includeList.add(target);
}
/**
* @see javax.servlet.ServletResponseWrapper#flushBuffer()
*/
@Override
public void flushBuffer() throws IOException {
if (OpenCms.getSystemInfo().getServletContainerSettings().isPreventResponseFlush()) {
// Websphere does not allow to set headers afterwards, so we have to prevent this call
return;
}
super.flushBuffer();
}
/**
* Returns the value of the encoding used for this response.
*
* @return the value of the encoding used for this response
*/
public String getEncoding() {
return m_encoding;
}
/**
* Provides access to the header cache of the top wrapper.
*
* @return the Map of cached headers
*/
public Map> getHeaders() {
return m_headers;
}
/**
* Method overload from the standard HttpServletRequest API.
*
* @see javax.servlet.ServletResponse#getOutputStream()
*/
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (m_out == null) {
initStream();
}
return m_out;
}
/**
* Method overload from the standard HttpServletRequest API.
*
* @see javax.servlet.ServletResponse#getWriter()
*/
@Override
public PrintWriter getWriter() throws IOException {
if (m_writer == null) {
initStream();
}
return m_writer;
}
/**
* Returns the bytes that have been written on the current writers output stream.
*
* @return the bytes that have been written on the current writers output stream
*/
public byte[] getWriterBytes() {
if (isSuspended()) {
// No output whatsoever if the response is suspended
return new byte[0];
}
if (m_cacheBytes != null) {
// Optimization for cached "leaf" nodes, here I re-use the array from the cache
return m_cacheBytes;
}
if (m_out == null) {
// No output was written so far, just return an empty array
return new byte[0];
}
if (m_writer != null) {
// Flush the writer in case something was written on it
m_writer.flush();
}
return m_out.getBytes();
}
/**
* This flag indicates if the response is suspended or not.
*
* A suspended response must not write further output to any stream or
* process a cache entry for itself.
*
* Currently, a response is only suspended if it is redirected.
*
* @return true if the response is suspended, false otherwise
*/
public boolean isSuspended() {
return m_suspended;
}
/**
* Returns true
if this response has been constructed for the
* top level element of this request, false
if it was
* constructed for an included sub-element.
*
* @return true
if this response has been constructed for the
* top level element of this request, false
if it was
* constructed for an included sub-element.
*/
public boolean isTopElement() {
return m_isTopElement;
}
/**
* Method overload from the standard HttpServletRequest API.
*
* @see javax.servlet.http.HttpServletResponse#sendRedirect(java.lang.String)
*
* @throws IllegalArgumentException In case of a malformed location string
*/
@Override
public void sendRedirect(String location) throws IOException {
sendRedirect(location, false);
}
/**
* Internal redirect method used to handle both temporary and permanent redirects.
*
* @param location the redirect target
* @param permanent true for a permanent redirect, false for a temporary one
*
* @throws IOException if IO operations on the response fail
*/
public void sendRedirect(String location, boolean permanent) throws IOException {
// Ignore any redirects after the first one
if (isSuspended() && (!location.equals(m_bufferRedirect))) {
return;
}
if (LOG.isDebugEnabled()) {
LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_SENDREDIRECT_1, location));
}
if (m_cachingRequired && !m_includeMode) {
m_bufferRedirect = location;
m_redirectPermanent = permanent;
}
if (!m_cachingRequired) {
// If caching is required a cached entry will be constructed first and redirect will
// be called after this is completed and stored in the cache
if (LOG.isDebugEnabled()) {
LOG.debug(
Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_TOPRESPONSE_SENDREDIRECT_1, location));
}
if (LOG.isWarnEnabled()) {
if (m_controller.getResponseStackSize() > 2) {
// sendRedirect in a stacked response scenario, this may cause issues in some app servers
LOG.warn(
Messages.get().getBundle().key(
Messages.LOG_FLEXRESPONSE_REDIRECTWARNING_3,
m_controller.getCmsResource().getRootPath(),
m_controller.getCurrentRequest().getElementUri(),
location));
}
}
try {
// Checking for possible illegal characters (for example, XSS exploits) before sending the redirect
// The constructor is key here. That method will throw an URISyntaxException if the URL
// format is not according to standards (e.g. contains illegal characters, like spaces, < or >, etc).
@SuppressWarnings("unused")
URI validURI = new URI(location);
} catch (URISyntaxException e) {
// Deliberately NOT passing the original exception, since the URISyntaxException contains the full path,
// which may include the XSS attempt
LOG.error(Messages.get().getBundle().key(Messages.ERR_FLEXRESPONSE_URI_SYNTAX_EXCEPTION_0), e);
throw new IllegalArgumentException("Illegal or malformed characters found in path");
}
// use top response for redirect
HttpServletResponse topRes = m_controller.getTopResponse();
// add all headers found to make sure cookies can be set before redirect
processHeaders(getHeaders(), topRes);
if (permanent) {
topRes.setHeader(CmsRequestUtil.HEADER_LOCATION, location);
topRes.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
} else {
topRes.sendRedirect(location);
}
}
m_controller.suspendFlexResponse();
}
/**
* Method overload from the standard HttpServletRequest API.
*
* @see javax.servlet.ServletResponse#setContentType(java.lang.String)
*/
@Override
public void setContentType(String type) {
if (LOG.isDebugEnabled()) {
LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_SETTING_CONTENTTYPE_1, type));
}
// only if this is the "Top-Level" element, do set the content type
// otherwise an included JSP could reset the type with some unwanted defaults
if (!m_typeSet && m_isTopElement) {
// type must be set only once, otherwise some Servlet containers (not Tomcat) generate errors
m_typeSet = true;
super.setContentType(type);
return;
}
}
/**
* Method overload from the standard HttpServletRequest API.
*
* @see javax.servlet.http.HttpServletResponse#setDateHeader(java.lang.String, long)
*/
@Override
public void setDateHeader(String name, long date) {
setHeader(name, CmsDateUtil.getHeaderDate(date));
}
/**
* Method overload from the standard HttpServletRequest API.
*
* @see javax.servlet.http.HttpServletResponse#setHeader(java.lang.String, java.lang.String)
*/
@Override
public void setHeader(String name, String value) {
if (isSuspended()) {
return;
}
if (CmsRequestUtil.HEADER_CONTENT_TYPE.equalsIgnoreCase(name)) {
setContentType(value);
return;
}
if (m_cachingRequired && !m_includeMode) {
setHeaderList(m_bufferHeaders, name, value);
if (LOG.isDebugEnabled()) {
LOG.debug(
Messages.get().getBundle().key(
Messages.LOG_FLEXRESPONSE_SETTING_HEADER_IN_ELEMENT_BUFFER_2,
name,
value));
}
}
if (m_writeOnlyToBuffer) {
setHeaderList(m_headers, name, value);
if (LOG.isDebugEnabled()) {
LOG.debug(
Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_SETTING_HEADER_IN_HEADERS_2, name, value));
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug(
Messages.get().getBundle().key(
Messages.LOG_FLEXRESPONSE_SETTING_HEADER_IN_PARENT_RESPONSE_2,
name,
value));
}
m_res.setHeader(name, value);
}
}
/**
* Method overload from the standard HttpServletRequest API.
*
* @see javax.servlet.http.HttpServletResponse#setIntHeader(java.lang.String, int)
*/
@Override
public void setIntHeader(String name, int value) {
setHeader(name, "" + value);
}
/**
* Sets buffering status of the response.
*
* This must be done before the first output is written.
* Buffering is needed to process elements that can not be written
* directly to the output stream because their sub - elements have to
* be processed separately. Which is so far true only for JSP pages.
*
* If buffering is on, nothing is written to the output stream
* even if streaming for this response is enabled.
*
* @param value the value to set
*/
public void setOnlyBuffering(boolean value) {
m_writeOnlyToBuffer = value && !m_controller.isForwardMode();
if (m_writeOnlyToBuffer) {
setCmsCachingRequired(true);
}
}
/**
* Adds some bytes to the list of include results.
*
* Should be used only in inclusion-scenarios
* like the JSP cms:include tag processing.
*
* @param result the byte array to add
*/
void addToIncludeResults(byte[] result) {
if (m_includeResults == null) {
m_includeResults = new ArrayList(10);
}
m_includeResults.add(result);
}
/**
* Returns the cache key for to this response.
*
* @return the cache key for to this response
*/
CmsFlexCacheKey getCmsCacheKey() {
return m_key;
}
/**
* Is used to check if the response has an include list,
* which indicates a) it is probably processing a JSP element
* and b) it can never be streamed and always must be buffered.
*
* @return true if this response has an include list, false otherwise
*/
boolean hasIncludeList() {
return m_includeList != null;
}
/**
* Generates a CmsFlexCacheEntry from the current response using the
* stored include results.
*
* In case the results were written only to the buffer until now,
* they are now re-written on the output stream, with all included
* elements.
*
* @throws IOException in case something goes wrong while writing to the output stream
*
* @return the generated cache entry
*/
CmsFlexCacheEntry processCacheEntry() throws IOException {
if (isSuspended() && (m_bufferRedirect == null)) {
// an included element redirected this response, no cache entry must be produced
return null;
}
if (m_cachingRequired) {
// cache entry must only be calculated if it's actually needed (always true if we write only to buffer)
m_cachedEntry = new CmsFlexCacheEntry();
if (m_bufferRedirect != null) {
// only set et cached redirect target
m_cachedEntry.setRedirect(m_bufferRedirect, m_redirectPermanent);
} else {
// add cached headers
m_cachedEntry.addHeaders(m_bufferHeaders);
// add cached output
if (m_includeList != null) {
// probably JSP: we must analyze out stream for includes calls
// also, m_writeOnlyToBuffer must be "true" or m_includeList can not be != null
processIncludeList();
} else {
// output is delivered directly, no include call parsing required
m_cachedEntry.add(getWriterBytes());
}
}
// update the "last modified" date for the cache entry
m_cachedEntry.complete();
}
// in case the output was only buffered we have to re-write it to the "right" stream
if (m_writeOnlyToBuffer) {
// since we are processing a cache entry caching is not required
m_cachingRequired = false;
if (m_bufferRedirect != null) {
// send buffered redirect, will trigger redirect of top response
sendRedirect(m_bufferRedirect, m_redirectPermanent);
} else {
// process the output
if (m_parentWritesOnlyToBuffer) {
// write results back to own stream, headers are already in buffer
if (m_out != null) {
try {
m_out.clear();
} catch (Exception e) {
if (LOG.isDebugEnabled()) {
LOG.debug(
Messages.get().getBundle().key(
Messages.LOG_FLEXRESPONSE_ERROR_FLUSHING_OUTPUT_STREAM_1,
e));
}
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug(
Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_ERROR_OUTPUT_STREAM_NULL_0));
}
}
writeCachedResultToStream(this);
} else {
// we can use the parent stream
processHeaders(m_headers, m_res);
writeCachedResultToStream(m_res);
}
}
}
return m_cachedEntry;
}
/**
* Sets the cache key for this response from
* a pre-calculated cache key.
*
* @param value the cache key to set
*/
void setCmsCacheKey(CmsFlexCacheKey value) {
m_key = value;
}
/**
* Sets the cache key for this response, which is calculated
* from the provided parameters.
*
* @param resourcename the target resource for which to create the cache key
* @param cacheDirectives the cache directives of the resource (value of the property "cache")
* @param online indicates if this resource is online or offline
*
* @return the generated cache key
*
* @throws CmsFlexCacheException in case the value String had a parse error
*/
CmsFlexCacheKey setCmsCacheKey(String resourcename, String cacheDirectives, boolean online)
throws CmsFlexCacheException {
m_key = new CmsFlexCacheKey(resourcename, cacheDirectives, online);
if (m_key.hadParseError()) {
// We throw the exception here to make sure this response has a valid key (cache=never)
throw new CmsFlexCacheException(
Messages.get().container(
Messages.LOG_FLEXRESPONSE_PARSE_ERROR_IN_CACHE_KEY_2,
cacheDirectives,
resourcename));
}
return m_key;
}
/**
* Set caching status for this response.
*
* Will always be set to "true"
if setOnlyBuffering() is set to "true"
.
* Currently this is an optimization for non - JSP elements that
* are known not to be cachable.
*
* @param value the value to set
*/
void setCmsCachingRequired(boolean value) {
m_cachingRequired = (value || m_writeOnlyToBuffer) && !m_controller.isForwardMode();
}
/**
* This flag indicates to the response if it is in "include mode" or not.
*
* This is important in case a cache entry is constructed,
* since the cache entry must not consist of output or headers of the
* included elements.
*
* @param value the value to set
*/
void setCmsIncludeMode(boolean value) {
m_includeMode = value;
}
/**
* Sets the suspended status of the response, and also sets
* the suspend status of all responses wrapping this response.
*
* A suspended response must not write further output to any stream or
* process a cache entry for itself.
*
* @param value the value to set
*/
void setSuspended(boolean value) {
m_suspended = value;
}
/**
* Writes some bytes to the current output stream,
* this method should be called from CmsFlexCacheEntry.service() only.
*
* @param bytes an array of bytes
* @param useArray indicates that the byte array should be used directly
*
* @throws IOException in case something goes wrong while writing to the stream
*/
void writeToOutputStream(byte[] bytes, boolean useArray) throws IOException {
if (isSuspended()) {
return;
}
if (m_writeOnlyToBuffer) {
if (useArray) {
// This cached entry has no sub-elements (it a "leaf") and so we can just use it's bytes
m_cacheBytes = bytes;
} else {
if (m_out == null) {
initStream();
}
// In this case the buffer will not write to the servlet stream, but to it's internal buffer only
m_out.write(bytes);
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_ERROR_WRITING_TO_OUTPUT_STREAM_0));
}
// The request is not buffered, so we can write directly to it's parents output stream
m_res.getOutputStream().write(bytes);
m_res.getOutputStream().flush();
}
}
/**
* Helper method to add a value in the internal header list.
*
* @param headers the headers to look up the value in
* @param name the name to look up
* @param value the value to set
*/
private void addHeaderList(Map> headers, String name, String value) {
List values = headers.get(name);
if (values == null) {
values = new ArrayList();
headers.put(name, values);
}
values.add(value);
}
/**
* Initializes the current responses output stream
* and the corresponding print writer.
*
* @throws IOException in case something goes wrong while initializing
*/
private void initStream() throws IOException {
if (m_out == null) {
if (!m_writeOnlyToBuffer) {
// we can use the parents output stream
if (m_cachingRequired || (m_controller.getResponseStackSize() > 1)) {
// we are allowed to cache our results (probably to construct a new cache entry)
m_out = new CmsFlexResponse.CmsServletOutputStream(m_res.getOutputStream());
} else {
// we are not allowed to cache so we just use the parents output stream
m_out = (CmsFlexResponse.CmsServletOutputStream)m_res.getOutputStream();
}
} else {
// construct a "buffer only" output stream
m_out = new CmsFlexResponse.CmsServletOutputStream();
}
}
if (m_writer == null) {
// create a PrintWriter that uses the encoding required for the request context
m_writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(m_out, m_encoding)), false);
}
}
/**
* This method is needed to process pages that can NOT be analyzed
* directly during delivering (like JSP) because they write to
* their own buffer.
*
* In this case, we don't actually write output of include calls to the stream.
* Where there are include calls we write a {@link #FLEX_CACHE_DELIMITER}
char on the stream
* to indicate that at this point the output of the include must be placed later.
* The include targets (resource names) are then saved in the m_includeList.
*
* This method must be called after the complete page has been processed.
* It will contain the output of the page only (no includes),
* with {@link #FLEX_CACHE_DELIMITER}
chars were the include calls should be placed.
* What we do here is analyze the output and cut it in parts
* of byte[]
arrays which then are saved in the resulting cache entry.
* For the includes, we just save the name of the resource in
* the cache entry.
*
* If caching is disabled this method is just not called.
*/
private void processIncludeList() {
byte[] result = getWriterBytes();
if (!hasIncludeList()) {
// no include list, so no includes and we just use the bytes as they are in one block
m_cachedEntry.add(result);
} else {
// process the include list
int max = result.length;
int pos = 0;
int last = 0;
int size = 0;
int count = 0;
// work through result and split this with include list calls
int i = 0;
while ((i < m_includeList.size()) && (pos < max)) {
// look for the first FLEX_CACHE_DELIMITER char
while ((pos < max) && (result[pos] != FLEX_CACHE_DELIMITER)) {
pos++;
}
if ((pos < max) && (result[pos] == FLEX_CACHE_DELIMITER)) {
count++;
// a byte value of C_FLEX_CACHE_DELIMITER in our (String) output list indicates
// that the next include call must be placed here
size = pos - last;
if (size > 0) {
// if not (it might be 0) there would be 2 include calls back 2 back
byte[] piece = new byte[size];
System.arraycopy(result, last, piece, 0, size);
// add the byte array to the cache entry
m_cachedEntry.add(piece);
piece = null;
}
last = ++pos;
// add an include call to the cache entry
m_cachedEntry.add(
m_includeList.get(i),
m_includeListParameters.get(i),
m_includeListAttributes.get(i));
i++;
}
}
if (pos < max) {
// there is content behind the last include call
size = max - pos;
byte[] piece = new byte[size];
System.arraycopy(result, pos, piece, 0, size);
m_cachedEntry.add(piece);
piece = null;
}
if (i >= m_includeList.size()) {
// clear the include list if all include calls are handled
m_includeList = null;
m_includeListParameters = null;
m_includeListAttributes = null;
} else {
// if something is left, remove the processed entries
m_includeList = m_includeList.subList(count, m_includeList.size());
m_includeListParameters = m_includeListParameters.subList(count, m_includeListParameters.size());
m_includeListAttributes = m_includeListAttributes.subList(count, m_includeListAttributes.size());
}
}
}
/**
* Helper method to set a value in the internal header list.
*
* @param headers the headers to set the value in
* @param name the name to set
* @param value the value to set
*/
private void setHeaderList(Map> headers, String name, String value) {
List values = new ArrayList();
values.add(SET_HEADER + value);
headers.put(name, values);
}
/**
* This delivers cached sub-elements back to the stream.
* Needed to overcome JSP buffering.
*
* @param res the response to write the cached results to
*
* @throws IOException in case something goes wrong writing to the responses output stream
*/
private void writeCachedResultToStream(HttpServletResponse res) throws IOException {
List elements = m_cachedEntry.elements();
int count = 0;
if (elements != null) {
for (int i = 0; i < elements.size(); i++) {
Object o = elements.get(i);
if (o instanceof byte[]) {
res.getOutputStream().write((byte[])o);
} else {
if ((m_includeResults != null) && (m_includeResults.size() > count)) {
// make sure that we don't run behind end of list (should never happen, though)
res.getOutputStream().write(m_includeResults.get(count));
count++;
}
// skip next entry, which is the parameter map for this include call
i++;
// skip next entry, which is the attribute map for this include call
i++;
}
}
}
}
}