/*
* 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.common.impl.request;
import ch.entwine.weblounge.common.content.page.HTMLHeadElement;
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.ResponseCache;
import ch.entwine.weblounge.common.request.WebloungeRequest;
import ch.entwine.weblounge.common.request.WebloungeResponse;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* Default implementation of the <code>WebloungeResponse</code>.
*/
public class WebloungeResponseImpl extends HttpServletResponseWrapper implements WebloungeResponse {
/** Flag for invalidated responses that should not be cached */
private boolean isValid = true;
/** True if an error has been reported */
private boolean hasError = false;
/** Flag to indicate whether the buffered response has been submitted */
private boolean submitted = false;
/** Response status */
private int responseStatus = SC_OK;
/** Associated HTTP request object */
private WeakReference<WebloungeRequest> request = null;
/** The cache service */
private WeakReference<ResponseCache> cache = null;
/** The response's cache handle */
private WeakReference<CacheHandle> cacheHandle = null;
private List<HTMLHeadElement> htmlHeaders = null;
/** Whether the getOuputStream has already been called */
private boolean osCalled = false;
/** Holds the special writer that copies the output to the buffer first */
private PrintWriter out = null;
/** A buffer that can hold the output stream prior to writing it back */
private CachedOutputStream os = null;
/** The response's content modification date */
private Date modificationDate = new Date(0);
/** Default encoding */
private static final String DEFAULT_ENCODING = "utf-8";
/**
* Creates a new <code>HttpServletResponse</code> wrapper around the original
* response object.
*
* @param response
* the response
*/
public WebloungeResponseImpl(HttpServletResponse response) {
super(response);
}
/**
* {@inheritDoc}
*
* @see javax.servlet.ServletResponseWrapper#getOutputStream()
*/
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (out != null)
throw new IllegalStateException("A writer has already been allocated");
osCalled = true;
String contentType = getContentType();
if (contentType != null && contentType.startsWith("text")) {
os = new CachedOutputStream();
return os;
} else {
return super.getOutputStream();
}
}
/**
* Returns the modified writer that enables the caching of the response.
*
* @return a PrintWriter object that can return character data to the client
* @throws IOException
* if the writer could not be allocated
* @see javax.servlet.ServletResponse#getWriter()
* @see ch.entwine.weblounge.OldCacheManager.cache.CacheManager
*/
@Override
public PrintWriter getWriter() throws IOException {
// Check whether there's already a writer allocated
if (out != null)
return out;
// Check whether getOutputStream() has already been called
if (osCalled)
throw new IllegalStateException("An output stream has already been allocated");
// Get the character encoding
String encoding = getCharacterEncoding();
if (encoding == null) {
encoding = DEFAULT_ENCODING;
setCharacterEncoding(encoding);
}
// Install the writer
try {
String contentType = getContentType();
if (contentType != null && contentType.startsWith("text")) {
os = new CachedOutputStream();
out = new PrintWriter(new OutputStreamWriter(os, encoding));
} else {
out = new PrintWriter(new OutputStreamWriter(super.getOutputStream(), encoding));
}
} catch (UnsupportedEncodingException e) {
throw new IOException(e.getMessage());
}
// Return the new writer
return out;
}
/**
* {@inheritDoc}
*
* @see javax.servlet.ServletResponseWrapper#flushBuffer()
*/
@Override
public void flushBuffer() throws IOException {
if (isCommitted())
return;
if (!submitted)
submitResponseBuffer();
// Send the response back to the client
super.flushBuffer();
}
/**
* {@inheritDoc}
*
* @see javax.servlet.http.HttpServletResponseWrapper#sendRedirect(java.lang.String)
*/
@Override
public void sendRedirect(String location) throws IOException {
super.sendRedirect(location);
responseStatus = HttpServletResponse.SC_MOVED_PERMANENTLY;
}
/**
* {@inheritDoc}
*
* @see javax.servlet.http.HttpServletResponseWrapper#sendError(int,
* java.lang.String)
*/
@Override
public void sendError(int error, String msg) throws IOException {
hasError = true;
responseStatus = error;
super.sendError(error, msg);
}
/**
* {@inheritDoc}
*
* @see javax.servlet.http.HttpServletResponseWrapper#sendError(int)
*/
@Override
public void sendError(int error) throws IOException {
sendError(error, null);
}
/**
* Returns <code>true</code> if an error code has been sent back to the
* client.
*
* @return <code>true</code> if an error code has been sent to the client
*/
public boolean hasError() {
return hasError;
}
/**
* {@inheritDoc}
*
* @see javax.servlet.http.HttpServletResponseWrapper#setStatus(int)
*/
@Override
public void setStatus(int sc) {
super.setStatus(sc);
responseStatus = sc;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#getStatus()
*/
public int getStatus() {
return responseStatus;
}
/**
* Sets the associated request object.
*
* @param request
* the request
*/
public void setRequest(WebloungeRequest request) {
this.request = new WeakReference<WebloungeRequest>(request);
}
/**
* Sets the service that is used to cache responses to clients.
*
* @param cache
* the cache
*/
public void setResponseCache(ResponseCache cache) {
this.cache = new WeakReference<ResponseCache>(cache);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#addTag(java.lang.String,
* java.lang.String)
*/
public boolean addTag(String name, String value) {
boolean result = false;
if (cacheHandle != null && cacheHandle.get() != null) {
result = cacheHandle.get().addTag(name, value);
}
return result;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#addTags(java.util.Collection)
*/
public boolean addTags(Collection<CacheTag> tags) {
boolean result = false;
if (cacheHandle != null && cacheHandle.get() != null) {
result = cacheHandle.get().addTags(tags);
}
return result;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#addTag(ch.entwine.weblounge.common.content.Tag)
*/
public boolean addTag(CacheTag tag) {
boolean result = false;
if (cacheHandle != null && cacheHandle.get() != null) {
result = cacheHandle.get().addTag(tag);
}
return result;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#clearTags()
*/
public void clearTags() {
if (cacheHandle != null && cacheHandle.get() != null) {
cacheHandle.get().clearTags();
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#containsTag(ch.entwine.weblounge.common.content.Tag)
*/
public boolean containsTag(CacheTag tag) {
if (cacheHandle != null && cacheHandle.get() != null) {
return cacheHandle.get().containsTag(tag);
}
return false;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#containsTag(java.lang.String)
*/
public boolean containsTag(String name) {
if (cacheHandle != null && cacheHandle.get() != null) {
return cacheHandle.get().containsTag(name);
}
return false;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#containsTag(java.lang.String,
* java.lang.String)
*/
public boolean containsTag(String name, String value) {
if (cacheHandle != null && cacheHandle.get() != null) {
return cacheHandle.get().containsTag(name, value);
}
return false;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#isTagged()
*/
public boolean isTagged() {
if (cacheHandle != null && cacheHandle.get() != null) {
return cacheHandle.get().isTagged();
}
return false;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#removeTag(ch.entwine.weblounge.common.content.Tag)
*/
public boolean removeTag(CacheTag tag) {
if (cacheHandle != null && cacheHandle.get() != null) {
return cacheHandle.get().removeTag(tag);
}
return false;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#removeTags(java.lang.String)
*/
public boolean removeTags(String name) {
if (cacheHandle != null && cacheHandle.get() != null) {
return cacheHandle.get().removeTags(name);
}
return false;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#removeTag(java.lang.String,
* java.lang.String)
*/
public boolean removeTag(String name, String value) {
if (cacheHandle != null && cacheHandle.get() != null) {
return cacheHandle.get().removeTag(name, value);
}
return false;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#tags()
*/
public Iterator<CacheTag> tags() {
if (cacheHandle != null && cacheHandle.get() != null) {
return cacheHandle.get().tags();
}
return new ArrayList<CacheTag>().iterator();
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Taggable#getTags()
*/
public CacheTag[] getTags() {
if (cacheHandle != null && cacheHandle.get() != null) {
return cacheHandle.get().getTags();
}
return new CacheTag[] {};
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#startResponse(ch.entwine.weblounge.common.request.CacheTag[],
* long, long)
*/
public boolean startResponse(CacheTag[] tags, long expirationTime,
long revalidationTime) throws IllegalStateException {
if (!isValid || cache == null)
return false;
ResponseCache cache = this.cache.get();
if (cache == null)
return false;
if (cacheHandle != null)
throw new IllegalStateException("The response is already being cached");
// Is the response in the cache?
CacheHandle hdl = cache.startResponse(tags, request.get(), this, expirationTime, revalidationTime);
if (hdl == null)
return true;
// It's not, meaning we need to do the processing ourselves
cacheHandle = new WeakReference<CacheHandle>(hdl);
return false;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#endResponse()
*/
public void endResponse() throws IllegalStateException {
try {
// Make sure to flush any print writer so its content is
// written to the cached output stream
if (out != null)
out.flush();
// Copy the response buffer to the cached response
if (!submitted)
submitResponseBuffer();
// See if there is an active cache transaction
if (cache == null)
return;
ResponseCache cache = this.cache.get();
if (cache == null)
return;
if (cacheHandle == null || cacheHandle.get() == null)
return;
// End the response and have the output sent back to the client
cache.endResponse(this);
} catch (IOException e) {
// The client closed the connection
} finally {
cacheHandle = null;
}
}
/**
* Submits the buffered response to the wrapped response's output stream. This
* method returns gracefully if the response has already been submitted.
*
* @throws IOException
* if submitting fails
*/
private void submitResponseBuffer() throws IOException {
// Has content been added?
if (os == null)
return;
// Has the content been submitted already?
if (submitted)
return;
// Is there an output stream that we can copy to?
OutputStream clientOS = super.getOutputStream();
if (clientOS == null)
return;
// Check if there are HTML header includes
String response = new String(os.getContent(), DEFAULT_ENCODING);
StringBuffer headersHTML = new StringBuffer();
if (htmlHeaders != null) {
for (HTMLHeadElement e : htmlHeaders) {
String header = ConfigurationUtils.processTemplate(e.toHtml(), request.get().getSite(), request.get().getEnvironment());
headersHTML.append(header).append('\n');
}
}
// Replace the marker with the actual headers
int headersPlaceholderLocation = response.indexOf(HTML_HEADER_MARKER);
if (headersPlaceholderLocation >= 1) {
StringBuffer updatedResponse = new StringBuffer(response.substring(0, headersPlaceholderLocation));
updatedResponse.append(headersHTML.toString());
updatedResponse.append(response.substring(headersPlaceholderLocation + HTML_HEADER_MARKER.length()));
response = updatedResponse.toString();
}
setContentLength(response.getBytes().length);
IOUtils.write(response, clientOS, DEFAULT_ENCODING);
clientOS.flush();
os = null;
submitted = true;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#setModificationDate(Date)
*/
@Override
public Date setModificationDate(Date modificationDate) {
if (modificationDate == null)
return this.modificationDate;
this.modificationDate = modificationDate.after(this.modificationDate) ? modificationDate : this.modificationDate;
if (cacheHandle == null)
return this.modificationDate;
CacheHandle hdl = cacheHandle.get();
if (hdl == null)
return this.modificationDate;
return hdl.setModificationDate(modificationDate);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#getModificationDate()
*/
@Override
public Date getModificationDate() {
if (cacheHandle == null)
return modificationDate.getTime() > 0 ? modificationDate : new Date();
CacheHandle hdl = cacheHandle.get();
if (hdl == null)
return modificationDate.getTime() > 0 ? modificationDate : new Date();
return hdl.getModificationDate();
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#setClientRevalidationTime(long)
*/
public void setClientRevalidationTime(long revalidationTime) {
if (cacheHandle == null)
return;
CacheHandle hdl = cacheHandle.get();
if (hdl == null)
return;
hdl.setClientRevalidationTime(Math.min(revalidationTime, hdl.getClientRevalidationTime()));
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#getClientRevalidationTime()
*/
public long getClientRevalidationTime() {
if (cacheHandle == null)
return 0;
CacheHandle hdl = cacheHandle.get();
if (hdl == null)
return 0;
return hdl.getClientRevalidationTime();
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#setCacheExpirationTime(long)
*/
public void setCacheExpirationTime(long expirationTime) {
if (cacheHandle == null)
return;
CacheHandle hdl = cacheHandle.get();
if (hdl == null)
return;
hdl.setCacheExpirationTime(Math.min(expirationTime, hdl.getCacheExpirationTime()));
// The recheck time can't be longer than the valid time
setClientRevalidationTime(expirationTime);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#isCached()
*/
public boolean isCached() {
return cache != null && cache.get() != null;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#getCacheExpirationTime()
*/
public long getCacheExpirationTime() {
if (cacheHandle == null)
return 0;
CacheHandle hdl = cacheHandle.get();
if (hdl == null)
return 0;
return hdl.getCacheExpirationTime();
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#invalidate()
*/
public void invalidate() {
isValid = false;
if (cache != null) {
ResponseCache c = cache.get();
if (c != null) {
c.invalidate(this);
}
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#isValid()
*/
public boolean isValid() {
return isValid;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#addHTMLHeader(ch.entwine.weblounge.common.content.page.HTMLHeadElement)
*/
@Override
public void addHTMLHeader(HTMLHeadElement header) {
if (htmlHeaders == null)
htmlHeaders = new ArrayList<HTMLHeadElement>();
if (!htmlHeaders.contains(header))
htmlHeaders.add(header);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.request.WebloungeResponse#getHTMLHeaders()
*/
@Override
public HTMLHeadElement[] getHTMLHeaders() {
if (htmlHeaders == null)
return new HTMLHeadElement[] {};
return htmlHeaders.toArray(new HTMLHeadElement[htmlHeaders.size()]);
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
if (request == null || request.get() == null)
return super.toString();
return request.get().toString();
}
}