/*
 * Helma License Notice
 *
 * The contents of this file are subject to the Helma License
 * Version 2.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://adele.helma.org/download/helma/license.txt
 *
 * Copyright 1998-2003 Helma Software. All Rights Reserved.
 *
 * $RCSfile$
 * $Author$
 * $Revision$
 * $Date$
 */

package helma.framework;

import helma.framework.core.Skin;
import helma.framework.core.Application;
import helma.util.*;
import helma.scripting.ScriptingException;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.security.*;
import java.util.*;

import org.apache.commons.codec.binary.Base64;
import org.apache.xmlrpc.XmlRpcResponseProcessor;

/**
 * A Transmitter for a response to the servlet client. Objects of this
 * class are directly exposed to JavaScript as global property res.
 */
public final class ResponseTrans extends Writer implements Serializable {

    static final long serialVersionUID = -8627370766119740844L;
    static final int INITIAL_BUFFER_SIZE = 2048;

    static final String newLine = System.getProperty("line.separator");

    //  MIME content type of the response.
    private String contentType = "text/html";

    // Charset (encoding) to use for the response.
    private String charset;

    // Used to allow or disable client side caching
    private boolean cacheable = true;

    // HTTP response code, defaults to 200 (OK).
    private int status = 200;

    // HTTP authentication realm
    private String realm;

    // the actual response
    private byte[] response = null;

    // contains the redirect URL
    private String redir = null;

    // the forward (internal redirect) URL
    private String forward = null;

    // the last-modified date, if it should be set in the response
    private long lastModified = -1;

    // flag to signal that resource has not been modified
    private boolean notModified = false;

    // Entity Tag for this response, used for conditional GETs
    private String etag = null;

    // cookies
    Map cookies;

    // the buffer used to build the response
    private transient StringBuffer buffer = null;

    // an idle StringBuffer waiting to be reused
    private transient StringBuffer cachedBuffer = null;

    // these are used to implement the _as_string variants for Hop templates.
    private transient Stack buffers;

    // the path used to tell where to look for skins
    private transient Object[] skinpath = null;

    // hashmap for skin caching
    private transient HashMap skincache;

    // buffer for debug messages - will be automatically appended to response
    private transient StringBuffer debugBuffer;

    // field for generic message to be displayed
    private transient String message;

    // field for error
    private transient Throwable error;

    // the res.data map of form and cookie data
    private transient Map values = new SystemMap();

    // the res.handlers map of macro handlers
    private transient Map handlers = new SystemMap();

    // the res.meta map for meta response data
    private transient Map meta = new SystemMap();

    // the request trans for this response
    private transient RequestTrans reqtrans;

    // the message digest used to generate composed digests for ETag headers
    private transient MessageDigest digest;

    // the skin current or last rendered skin
    private transient volatile Skin activeSkin;

    // the application
    Application app;


    /**
     * Creates a new ResponseTrans object.
     *
     * @param req the RequestTrans for this response
     */
    public ResponseTrans(Application app, RequestTrans req) {
        this.app = app;
        reqtrans = req;
    }

    /**
     *  Get a value from the responses map by key.
     */
    public Object get(String name) {
        try {
            return values.get(name);
        } catch (Exception x) {
            return null;
        }
    }

    /**
     *  Get the data map for this response transmitter.
     */
    public Map getResponseData() {
        return values;
    }

    /**
     *  Get the macro handlers map for this response transmitter.
     */
    public Map getMacroHandlers() {
        return handlers;
    }

    /**
     *  Get the meta info map for this response transmitter.
     */
    public Map getMetaData() {
        return meta;
    }

    /**
     * Returns the ServletResponse instance for this ResponseTrans.
     * Returns null for internal and XML-RPC requests.
     */
    public HttpServletResponse getServletResponse() {
        return reqtrans.getServletResponse();
    }

    /**
     * Reset the current response buffer.
     */
    public synchronized void resetBuffer() {
        if (buffer != null) {
            buffer.setLength(0);
        }
    }

    /**
     * Reset the response object to its initial empty state.
     */
    public synchronized void reset() {
        if (buffer != null) {
            buffer.setLength(0);
        }

        buffers = null;
        response = null;
        cacheable = true;
        redir = forward = message = null;
        error = null;
        etag = realm = charset = null;
        contentType =  "text/html";
        values.clear();
        handlers.clear();
        meta.clear();
        lastModified = -1;
        notModified = false;
        skinpath = null;
        skincache = null;
        cookies = null;

        if (digest != null) {
            digest.reset();
        }
    }

    /**
     * This is called before a skin is rendered as string
     * (renderSkinAsString) to redirect the output to a new
     * string buffer.
     * @param buf the StringBuffer to use, or null
     * @return the new StringBuffer instance
     */
    public synchronized StringBuffer pushBuffer(StringBuffer buf) {
        if (buffers == null) {
            buffers = new Stack();
        }

        if (buffer != null) {
            buffers.push(buffer);
        }

        if (buf != null) {
            buffer = buf;
        } else if (cachedBuffer != null) {
            buffer = cachedBuffer;
            cachedBuffer = null;
        } else {
            buffer = new StringBuffer(64);
        }
        return buffer;
    }

    /**
     * Returns the content of the current string buffer and switches back to the previos one.
     */
    public synchronized String popString() {
        StringBuffer buf = popBuffer();
        String str = buf.toString();
        // store stringbuffer for later reuse
        buf.setLength(0);
        cachedBuffer = buf;
        return str;
    }

    public synchronized StringBuffer popBuffer() {
        if (buffer == null) {
            throw new RuntimeException("Can't pop string buffer: buffer is null");
        } else if (buffers == null) {
            throw new RuntimeException("Can't pop string buffer: buffer stack is empty");
        }
        // get local reference
        StringBuffer buf = buffer;
        // restore the previous buffer, which may be null
        buffer = buffers.empty() ? null : (StringBuffer) buffers.pop();
        return buf;
    }

    /**
     *  Get the response buffer, creating it if it doesn't exist
     */
    public synchronized StringBuffer getBuffer() {
        if (buffer == null) {
            buffer = new StringBuffer(INITIAL_BUFFER_SIZE);
        }

        return buffer;
    }

    /**
     * Append a string to the response unchanged.
     */
    public synchronized void write(String str) {
        if (str != null) {
            if (buffer == null) {
                buffer = new StringBuffer(Math.max(str.length() + 100, INITIAL_BUFFER_SIZE));
            }
            buffer.append(str);
        }
    }

    /**
     * Appends a objct to the response unchanged.
     * The object is first converted to a string.
     */
    public void write(Object what) {
        if (what != null) {
            write(what.toString());
        }
    }

    /**
     *  Appends a part from a char array to the response buffer.
     *
     * @param chars
     * @param offset
     * @param length
     */
    public synchronized void write(char[] chars, int offset, int length) {
        if (buffer == null) {
            buffer = new StringBuffer(Math.max(length + 100, INITIAL_BUFFER_SIZE));
        }
        buffer.append(chars, offset, length);
    }

    /**
     *  Appends a char array to the response buffer.
     *
     * @param chars
     */
    public void write(char chars[]) {
        write(chars, 0, chars.length);
    }


    /**
     * Appends a signle character to the response buffer.
     * @param c
     */
    public synchronized void write(int c) {
        if (buffer == null) {
            buffer = new StringBuffer(INITIAL_BUFFER_SIZE);
        }
        buffer.append((char) c);
    }

    /**
     * Appends a part from a string to the response buffer.
     * @param str
     * @param offset
     * @param length
     */
    public void write(String str, int offset, int length) {
        char cbuf[]  = new char[length];
        str.getChars(offset, (offset + length), cbuf, 0);
        write(cbuf, 0, length);
    }

    /**
     * Write object to response buffer and append a platform dependent newline sequence.
     */
    public synchronized void writeln(Object what) {
        if (what != null) {
            write(what.toString());
        } else if (buffer == null) {
            // if what is null, buffer may still be uninitialized
            buffer = new StringBuffer(INITIAL_BUFFER_SIZE);
        }
        buffer.append(newLine);
    }

    /**
     * Writes a platform dependent newline sequence to response buffer.
     */
    public synchronized void writeln() {
        // buffer may still be uninitialized
        if (buffer == null) {
            buffer = new StringBuffer(INITIAL_BUFFER_SIZE);
        }
        buffer.append(newLine);
    }

    /**
     *  Insert string somewhere in the response buffer. Caller has to make sure
     *  that buffer exists and its length is larger than offset. str may be null, in which
     *  case nothing happens.
     */
    public void debug(Object message) {
        if (debugBuffer == null) {
            debugBuffer = new StringBuffer();
        }

        String str = (message == null) ? "null" : message.toString();

        debugBuffer.append("<div class=\"helma-debug-line\">");
        debugBuffer.append(str);
        debugBuffer.append("</div>");
    }

    /**
     * Replace special characters with entities, including <, > and ", thus allowing
     * no HTML tags.
     */
    public synchronized void encode(Object what) {
        if (what != null) {
            String str = what.toString();

            if (buffer == null) {
                buffer = new StringBuffer(Math.max(str.length() + 100, INITIAL_BUFFER_SIZE));
            }

            HtmlEncoder.encodeAll(str, buffer);
        }
    }

    /**
     * Replace special characters with entities but pass through HTML tags
     */
    public synchronized void format(Object what) {
        if (what != null) {
            String str = what.toString();

            if (buffer == null) {
                buffer = new StringBuffer(Math.max(str.length() + 100, INITIAL_BUFFER_SIZE));
            }

            HtmlEncoder.encode(str, buffer);
        }
    }

    /**
     * Replace special characters with entities, including <, > and ", thus allowing
     * no HTML tags.
     */
    public synchronized void encodeXml(Object what) {
        if (what != null) {
            String str = what.toString();

            if (buffer == null) {
                buffer = new StringBuffer(Math.max(str.length() + 100, INITIAL_BUFFER_SIZE));
            }

            HtmlEncoder.encodeXml(str, buffer);
        }
    }

    /**
     * Encode HTML entities, but leave newlines alone. This is for the content of textarea forms.
     */
    public synchronized void encodeForm(Object what) {
        if (what != null) {
            String str = what.toString();

            if (buffer == null) {
                buffer = new StringBuffer(Math.max(str.length() + 100, INITIAL_BUFFER_SIZE));
            }

            HtmlEncoder.encodeAll(str, buffer, false);
        }
    }

    /**
     *
     *
     * @param url ...
     *
     * @throws RedirectException ...
     */
    public void redirect(String url) throws RedirectException {
        // remove newline chars to prevent response splitting attack
        redir = url == null ?
                null : url.replaceAll("[\r\n]", "");
        throw new RedirectException(redir);
    }

    /**
     *
     *
     * @return ...
     */
    public String getRedirect() {
        return redir;
    }

    /**
     *
     *
     * @param url ...
     *
     * @throws RedirectException ...
     */
    public void forward(String url) throws RedirectException {
        // remove newline chars to prevent response splitting attack
        forward = url == null ?
                null : url.replaceAll("[\r\n]", "");
        throw new RedirectException(forward);
    }

    /**
     *
     *
     * @return ...
     */
    public String getForward() {
        return forward;
    }

    /**
     *  Allow to directly set the byte array for the response. Calling this more than once will
     *  overwrite the previous output.
     * @param bytes an arbitrary byte array
     */
    public void writeBinary(byte[] bytes) {
        response = bytes;
    }

    /**
     * Proxy to HttpServletResponse.addHeader()
     * @param name the header name
     * @param value the header value
     */
    public void addHeader(String name, String value) {
        HttpServletResponse res = getServletResponse();
        if (res != null)
            res.addHeader(name, value);
    }

    /**
     * Proxy to HttpServletResponse.addDateHeader()
     * @param name the header name
     * @param value the header value
     */
    public void addDateHeader(String name, Date value) {
        HttpServletResponse res = getServletResponse();
        if (res != null)
            res.addDateHeader(name, value.getTime());
    }

    /**
     * Proxy to HttpServletResponse.setHeader()
     * @param name the header name
     * @param value the header value
     */
    public void setHeader(String name, String value) {
        HttpServletResponse res = getServletResponse();
        if (res != null)
            res.setHeader(name, value);
    }

    /**
     * Proxy to HttpServletResponse.setDateHeader()
     * @param name the header name
     * @param value the header value
     */
    public void setDateHeader(String name, Date value) {
        HttpServletResponse res = getServletResponse();
        if (res != null)
            res.setDateHeader(name, value.getTime());
    }

    /**
     * Write a vanilla error report. Callers should make sure the ResponeTrans is
     * new or has been reset.
     *
     * @param throwable the error
     */
    public void reportError(Throwable throwable) {
        if (throwable == null) {
            // just to be safe
            reportError("Unspecified error");
            return;
        }
        if (reqtrans.isXmlRpc()) {
            writeXmlRpcError(new RuntimeException(throwable));
        } else {
            status = 500;
            if (!"true".equalsIgnoreCase(app.getProperty("suppressErrorPage"))) {
                write("<html><body>");
                write("<h2>Error in application " + app.getName() + "</h2><p>");
                encode(getErrorMessage(throwable));
                writeln("</p>");
                if (app.debug()) {
                    if (throwable instanceof ScriptingException) {
                        ScriptingException scriptx = (ScriptingException) throwable;
                        writeln("<h4>Script Stack</h4>");
                        writeln("<pre>" + scriptx.getScriptStackTrace() + "</pre>");
                        writeln("<h4>Java Stack</h4>");
                        writeln("<pre>" + scriptx.getJavaStackTrace() + "</pre>");
                    } else {
                        writeln("<h4>Java Stack</h4>");
                        writeln("<pre>");
                        throwable.printStackTrace(new PrintWriter(this));
                        writeln("</pre>");
                    }
                }
                writeln("</body></html>");
            }
        }
    }

    /**
     * Write a vanilla error report. Callers should make sure the ResponeTrans is
     * new or has been reset.
     * @param errorMessage the error message
     */
    public void reportError(String errorMessage) {
        if (reqtrans.isXmlRpc()) {
            writeXmlRpcError(new RuntimeException(errorMessage));
        } else {
            status = 500;
            if (!"true".equalsIgnoreCase(app.getProperty("suppressErrorPage"))) {
                write("<html><body><h2>");
                write("Error in application ");
                write(app.getName());
                write("</h2><p>");
                encode(errorMessage);
                writeln("</p></body></html>");
            }
        }
    }

    public void writeXmlRpcResponse(Object result) {
        try {
            reset();
            contentType = "text/xml";
            if (charset == null) {
                charset = "UTF-8";
            }
            XmlRpcResponseProcessor xresproc = new XmlRpcResponseProcessor();
            writeBinary(xresproc.encodeResponse(result, charset));
        } catch (Exception x) {
            writeXmlRpcError(x);
        }
    }

    public void writeXmlRpcError(Exception x) {
        contentType = "text/xml";
        if (charset == null) {
            charset = "UTF-8";
        }
        XmlRpcResponseProcessor xresproc = new XmlRpcResponseProcessor();
        writeBinary(xresproc.encodeException(x, charset));
    }

    public void flush() {
        // does nothing!
    }

    /**
     * This has to be called after writing to this response has finished and before it is shipped back to the
     * web server. Transforms the string buffer into a byte array for transmission.
     */
    public void close() throws UnsupportedEncodingException {
        close(null);
    }

    /**
     * This has to be called after writing to this response has finished and before it is shipped back to the
     * web server. Transforms the string buffer into a byte array for transmission.
     * @param defaultCharset the charset to use if no explicit charset has been set on the response
     * @throws UnsupportedEncodingException if the charset is not a valid encoding name
     */
    public synchronized void close(String defaultCharset) throws UnsupportedEncodingException {
        // if the response was already written and committed by the application
        // there's no point in closing the response buffer
        HttpServletResponse res = reqtrans.getServletResponse();
        if (res != null && res.isCommitted()) {
            // response was committed using HttpServletResponse directly. We need
            // set response to null and notify waiters in order to let attached
            // requests know they can't reuse this response.
            response = null;
            notifyAll();
            return;
        }

        boolean encodingError = false;

        // only close if the response hasn't been closed yet, and if no
        // response was generated using writeBinary().
        if (response == null) {
            // only use default charset if not explicitly set for this response.
            if (charset == null) {
                charset = defaultCharset;
            }
            // if charset is not set, use western encoding
            if (charset == null) {
                charset = "UTF-8";
            }

            // if debug buffer exists, append it to main buffer
            if (contentType != null &&
                    contentType.startsWith("text/html") &&
                    debugBuffer != null) {
                debugBuffer.append("</div>");
                if (buffer == null) {
                    buffer = debugBuffer;
                } else {
                    buffer.append(debugBuffer);
                }
            }

            // get the buffer's bytes in the specified encoding
            if (buffer != null) {
                try {
                    response = buffer.toString().getBytes(charset);
                } catch (UnsupportedEncodingException uee) {
                    encodingError = true;
                    response = buffer.toString().getBytes();
                }

                // make sure this is done only once, even with more requsts attached
                buffer = null;
            } else {
                response = new byte[0];
            }
        }

        boolean autoETags = "true".equals(app.getProperty("autoETags", "true"));
        // if etag is not set, calc MD5 digest and check it, but only if
        // not a redirect or error
        if (autoETags &&
                etag == null &&
                lastModified == -1 &&
                status == 200 &&
                redir == null) {
            try {
                digest = MessageDigest.getInstance("MD5");
                // if (contentType != null)
                //     digest.update (contentType.getBytes());
                byte[] b = this.digest.digest(this.response);
                this.etag = "\"" + new String(Base64.encodeBase64(b)) + "\""; //$NON-NLS-1$ //$NON-NLS-2$
                // only set response to 304 not modified if no cookies were set
                if (reqtrans.hasETag(etag) && countCookies() == 0) {
                    response = new byte[0];
                    notModified = true;
                }
            } catch (Exception e) {
                // Etag creation failed for some reason.
                app.logError("Error creating ETag: " + e);
            }
        }

        notifyAll();

        // if there was a problem with the encoding, let the app know
        if (encodingError) {
            throw new UnsupportedEncodingException(charset);
        }
    }

    /**
     * If we just attached to evaluation we call this instead of close because only the primary thread
     * is responsible for closing the result
     */
    public synchronized void waitForClose() {
        try {
            if (response == null) {
                wait(10000L);
            }
        } catch (InterruptedException ix) {
            // Ignore
        }
    }

    /**
     * Get the body content for this response as byte array, encoded using the
     * response's charset.
     *
     * @return the response body
     */
    public byte[] getContent() {
        return response;
    }

    /**
     * Get the number of bytes of the response body.
     *
     * @return the length of the response body
     */
    public int getContentLength() {
        if (response != null) {
            return response.length;
        }

        return 0;
    }

    /**
     * Get the response's MIME content type
     *
     * @return the MIME type for this response
     */
    public String getContentType() {
        if (charset != null) {
            return contentType + "; charset=" + charset;
        }

        return contentType;
    }


    /**
     * Set the response's MIME content type
     *
     * @param contentType MIME type for this response
     */
    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    /**
     * Set the Last-Modified header for this response
     *
     * @param modified the Last-Modified header in milliseconds
     */
    public void setLastModified(long modified) {
        // date headers don't do milliseconds, round to seconds
        lastModified = (modified / 1000) * 1000;
        if (reqtrans.getIfModifiedSince() == lastModified) {
            notModified = true;
            throw new RedirectException(null);
        }
    }

    /**
     * Get the value of the Last-Modified header for this response.
     *
     * @return the Last-Modified header in milliseconds
     */
    public long getLastModified() {
        return lastModified;
    }

    /**
     * Set the ETag header value for this response.
     *
     * @param value the ETag header value
     */
    public void setETag(String value) {
        etag = (value == null) ? null : ("\"" + value + "\"");
        if (etag != null && reqtrans.hasETag(etag)) {
            notModified = true;
            throw new RedirectException(null);
        }
    }

    /**
     * Get the ETag header value for this response.
     *
     * @return the ETag header value
     */
    public String getETag() {
        return etag;
    }

    /**
     * Check if this response should generate a Not-Modified response.
     *
     * @return true if the the response wasn't modified since the client last saw it.
     */
    public boolean getNotModified() {
        return notModified;
    }

    /**
     * Add a dependency to this response.
     *
     * @param what an item this response's output depends on.
     */
    public void dependsOn(Object what) {
        if (digest == null) {
            try {
                digest = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException nsa) {
                // MD5 should always be available
            }
        }

        if (what == null) {
            digest.update(new byte[0]);
        } else if (what instanceof Date) {
            digest.update(Long.toBinaryString(((Date) what).getTime()).getBytes());
        } else if (what instanceof byte[]) {
            digest.update((byte[]) what);
        } else {
            String str = what.toString();

            if (str != null) {
                digest.update(str.getBytes());
            } else {
                digest.update(new byte[0]);
            }
        }
    }

    /**
     * Digest all dependencies to a checksum to see if the response has changed.
     */
    public void digestDependencies() {
        if (digest == null) {
            return;
        }

        // add the application checksum as dependency to make ETag
        // generation sensitive to changes in the app
        byte[] b = digest.digest(Long.toBinaryString((this.app.getChecksum())).getBytes());

        setETag(new String(Base64.encodeBase64(b)));
    }

    /**
     * Set the path in which to look for skins. This may contain file locations and
     * HopObjects.
     *
     * @param arr the skin path
     */
    public void setSkinpath(Object[] arr) {
        this.skinpath = arr;
        skincache = null;
    }

    /**
     * Get the path in which to look for skins. This may contain file locations and
     * HopObjects.
     *
     * @return the skin path
     */
    public Object[] getSkinpath() {
        if (skinpath == null) {
            skinpath = new Object[0];
        }

        return skinpath;
    }

    /**
     * Look up a cached skin.
     *
     * @param id the skin key
     * @return the skin, or null if no skin is cached for the given key
     */
    public Skin getCachedSkin(Object id) {
        if (skincache == null) {
            return null;
        }

        return (Skin) skincache.get(id);
    }

    /**
     * Cache a skin for the length of this response.
     *
     * @param id the skin key
     * @param skin the skin to cache
     */
    public void cacheSkin(Object id, Skin skin) {
        if (skincache == null) {
            skincache = new HashMap();
        }

        skincache.put(id, skin);
    }

    /**
     * Set the skin currently being rendered, returning the previously active skin.
     * @param skin the new active skin
     * @return the previously active skin
     */
    public Skin switchActiveSkin(Skin skin) {
        Skin previousSkin = activeSkin;
        activeSkin = skin;
        return previousSkin;
    }

    /**
     * Return the skin currently being rendered, or none.
     * @return the currently active skin
     */
    public Skin getActiveSkin() {
        return activeSkin;
    }

    /**
     * Set a cookie.
     *
     * @param key the cookie key
     * @param value the cookie value
     * @param days the cookie's lifespan in days
     * @param path the URL path to apply the cookie to
     * @param domain the domain to apply the cookie to
     * @param isHttpOnly marks the cookie as HttpOnly
     * @param isSecure limits the cookie for use with secure protocols
     */
    public void setCookie(String key, String value, int days, String path, String domain, boolean isHttpOnly, boolean isSecure) {
        CookieTrans c = null;

        if (cookies == null) {
            cookies = new HashMap();
        } else {
            c = (CookieTrans) cookies.get(key);
        }

        // remove newline chars to prevent response splitting attack
        if (value != null) {
            value = value.replaceAll("[\r\n]", "");
        }

        if (c == null) {
            c = new CookieTrans(key, value);
            cookies.put(key, c);
        } else {
            c.setValue(value);
        }

        c.setDays(days);
        c.setPath(path);
        c.setDomain(domain);

        if (!"false".equalsIgnoreCase(app.getProperty("cookies.httpOnly"))) {
            c.setHttpOnly(true);
        }

        if ("true".equalsIgnoreCase(app.getProperty("cookies.secure"))) {
            c.setSecure(true);
        }
    }

    /**
     * Reset all previously set cookies.
     */
    public void resetCookies() {
        if (cookies != null) {
            cookies.clear();
        }
    }

    /**
     * Get the number of cookies set in this response.
     *
     * @return the number of cookies
     */
    public int countCookies() {
        if (cookies != null) {
            return cookies.size();
        }

        return 0;
    }

    /**
     * Get the cookies set in this response.
     *
     * @return the cookies
     */
    public CookieTrans[] getCookies() {
        if (cookies == null) {
            return new CookieTrans[0];
        }

        CookieTrans[] c = new CookieTrans[cookies.size()];
        cookies.values().toArray(c);
        return c;
    }

    /**
     * Get the message to display to the user, if any.
     * @return the message
     */
    public String getMessage() {
        return message;
    }

    /**
     * Set a message to display to the user.
     * @param message the message
     */
    public void setMessage(String message) {
        this.message = message;
    }

    /**
     * Get the error message to display to the user, if any.
     * @return the error message
     */
    public Throwable getError() {
        return error;
    }

    /**
     * Set a message to display to the user.
     * @param error the error message
     */
    public void setError(Throwable error) {
        this.error = error;
    }

    public String getErrorMessage() {
        if (error == null)
            return null;
        return getErrorMessage(error);
    }

    private static String getErrorMessage(Throwable t) {
        String msg = t.getMessage();
        if (msg == null || msg.length() == 0)
            msg = t.toString();
        if (msg == null || msg.length() == 0)
            return "Unspecified Error: " + t.getClass().getName();
        return msg;
    }

    /**
     * Get debug messages to append to the response, if any.
     * @return the response's debug buffer
     */
    public StringBuffer getDebugBuffer() {
        return debugBuffer;
    }

    /**
     * Set debug messages to append to the response.
     * @param debugBuffer the response's debug buffer
     */
    public void setDebugBuffer(StringBuffer debugBuffer) {
        this.debugBuffer = debugBuffer;
    }

    /**
     * Get the charset/encoding for this response
     * @return the charset name
     */
    public String getCharset() {
        return charset;
    }

    /**
     * Set the charset/encoding for this response
     * @param charset the charset name
     */
    public void setCharset(String charset) {
        this.charset = charset;
    }

    /**
     * Returns true if this response may be cached by the client
     * @return true if the response may be cached
     */
    public boolean isCacheable() {
        return cacheable;
    }

    /**
     * Set the cacheability of this response
     * @param cache true if the response may be cached
     */
    public void setCacheable(boolean cache) {
        this.cacheable = cache;
    }

    /**
     * Get the HTTP response status code
     * @return the HTTP response code
     */
    public int getStatus() {
        return status;
    }

    /**
     * Set the HTTP response status code
     * @param status the HTTP response code
     */
    public void setStatus(int status) {
        this.status = status;
    }

    /**
     * Get the HTTP authentication realm
     * @return the name of the authentication realm
     */
    public String getRealm() {
        return realm;
    }

    /**
     * Set the HTTP authentication realm
     * @param realm the name of the authentication realm
     */
    public void setRealm(String realm) {
        this.realm = realm;
    }
}
