// ResponseTrans.java
// Copyright (c) Hannes Wallnöfer 1998-2000

package helma.framework;

import java.io.*;
import java.util.*;
import helma.objectmodel.*;
import helma.util.*;

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

    /**
     * Set the MIME content type of the response.
     */
    public String contentType = "text/html";

    /**
     * Set the charset of the response.
     */
    public String charset;

    /**
     * used to allow or disable client side caching
     */
    public boolean cache = true;

    /**
     * Used for HTTP response code, if 0 code 200 OK will be used.
     */
    public int status = 0;

    // name of the skin to be rendered  after completion, if any
    public transient String skin = null;

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

    // contains the redirect URL
    String redir = null;

    // cookies
    String cookieKeys[];
    String cookieValues[];
    int cookieDays[];
    int nCookies = 0;

    // the buffer used to build the response
    private transient StringBuffer buffer = null;
    // these are used to implement the _as_string variants for Hop templates.
    private transient Stack buffers;

    static final long serialVersionUID = -8627370766119740844L;

    /**
     * the buffers used to build the single body parts -
     * transient, response must be constructed before this is serialized
     */
    public transient String title, head, body, message, error;

    /**
     *  JavaScript object to make the values Map accessible to
     *  script code as res.data
     */
    public transient Object data;

    // the map of form and cookie data
    private transient Map values;


    public ResponseTrans () {
	super ();
	title = head = body = message = error = null;
	values = new HashMap ();
    }


    /**
     *  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;
    }

    /**
     * Reset the response object to its initial empty state.
     */
    public void reset () {
	if (buffer != null)
	    buffer.setLength (0);
	response = null;
	redir = null;
	skin = null;
	title = head = body = message = error = null;
	values.clear ();
    }


    /**
     * This is called before a template is rendered as string (xxx_as_string) to redirect the output
     * to a new string buffer.
     */
    public void pushStringBuffer () {
	if (buffers == null)
	    buffers = new Stack();
	if (buffer != null)
	    buffers.push (buffer);
	buffer = new StringBuffer (128);
    }

    /**
     * Returns the content of the current string buffer and switches back to the previos one.
     */
    public String popStringBuffer () {
	StringBuffer b = buffer;
	buffer = buffers.empty() ? null : (StringBuffer) buffers.pop ();
	return b.toString ();
    }

    /**
     * Append a string to the response unchanged.
     */
    public void write (Object what) {
	if (what != null) {
	    if (buffer == null)
	        buffer = new StringBuffer (512);
	    buffer.append (what.toString ());
	}
    }

    /**
     * Utility function that appends a <br> to whatever is written
     */
    public void writeln (Object what) {
	if (buffer == null)
	    buffer = new StringBuffer (512);
	if (what != null)
	    buffer.append (what.toString ());
	buffer.append ("<br>\r\n");
    }

    /**
     * Replace special characters with entities, including <, > and ", thus allowing
     * no HTML tags.
     */
    public void encode (Object what) {
	if (what != null) {
	    if (buffer == null)
	        buffer = new StringBuffer (512);
	    HtmlEncoder.encodeAll (what.toString (), buffer);
	}
    }

    /**
     * Replace special characters with entities but leave <, > and ", allowing HTML tags
     *  in the response.
     */
    public void format (Object what) {
	if (what != null) {
	    if (buffer == null)
	        buffer = new StringBuffer (512);
	    HtmlEncoder.encode (what.toString (), buffer);
	}
    }


    /**
     * Replace special characters with entities, including <, > and ", thus allowing
     * no HTML tags.
     */
    public void encodeXml (Object what) {
	if (what != null) {
	    if (buffer == null)
	        buffer = new StringBuffer (512);
	    HtmlEncoder.encodeXml (what.toString (), buffer);
	}
    }


    /**
     * Encode HTML entities, but leave newlines alone. This is for the content of textarea forms.
     */
    public void encodeForm (Object what) {
	if (what != null) {
	    if (buffer == null)
	        buffer = new StringBuffer (512);
	    HtmlEncoder.encodeAll (what.toString (), buffer, false);
	}
    }


    public void append (String what) {
	if (what != null) {
	    if (buffer == null)
	        buffer = new StringBuffer (512);
	    buffer.append (what);
	}
    }

    public void redirect (String url) throws RedirectException {
	redir = url;
	throw new RedirectException (url);
    }

    public String getRedirect () {
	return redir;
    }

    /**
     *  Allow to directly set the byte array for the response. Calling this more than once will
     *  overwrite the previous output. We take a generic object as parameter to be able to
     * generate a better error message, but it must be byte[].
     */
    public void writeBinary (byte[] what) {
	response = what;
    }


    /**
     * 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 char array to minimize size.
     */
    public synchronized void close (String cset) throws UnsupportedEncodingException {
	// only use default charset if not explicitly set for this response.
	if (charset == null)
	    charset = cset;

	boolean error = false;
	if (response == null) {
	    if (buffer != null) {
	        try {
	            response = buffer.toString ().getBytes (charset);
	        } catch (UnsupportedEncodingException uee) {
	            error = true;
	            response = buffer.toString ().getBytes ();
	        }
	        buffer = null; // make sure this is done only once, even with more requsts attached
	    } else {
	        response = new byte[0];
	    }
	}
	notifyAll ();
	// if there was a problem with the encoding, let the app know
	if (error)
	    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) {}
    }

    public byte[] getContent () {
	return response == null ? new byte[0] : response;
    }

    public int getContentLength () {
	if (response != null)
	    return response.length;
	return 0;
    }

    public String getContentType () {
    	return contentType;
    }

    public synchronized void setCookie (String key, String value) {
	setCookie (key, value, -1);
    }

    public synchronized void setCookie (String key, String value, int days) {
	if (nCookies == 0) {
	    cookieKeys = new String [3];
	    cookieValues = new String [3];
	    cookieDays = new int [3];
	}
	if (nCookies == cookieKeys.length) {
	    String nk[] = new String [nCookies+3];
	    System.arraycopy (cookieKeys, 0, nk, 0, nCookies);
	    String nv[] = new String [nCookies+3];
	    System.arraycopy (cookieValues, 0, nv, 0, nCookies);
	    int nd[] = new int [nCookies+3];
	    System.arraycopy (cookieDays, 0, nd, 0, nCookies);
	    cookieKeys = nk;
	    cookieValues = nv;
	    cookieDays = nd;
	}
	cookieKeys [nCookies] = key;
	cookieValues [nCookies] = value;
	cookieDays [nCookies] = days;
	nCookies += 1;
    }

    public void resetCookies () {
	nCookies = 0;
    }

    public int countCookies () {
	return nCookies;
    }

    public int getDaysAt (int i) {
	return cookieDays[i];
    }

    public String getKeyAt (int i) {
	return cookieKeys[i];
    }

    public String getValueAt (int i) {
	return cookieValues[i];
    }

    public void readExternal (ObjectInput s) throws ClassNotFoundException, IOException {
	contentType = (String) s.readObject ();
	response = (byte[]) s.readObject ();
	redir = (String) s.readObject ();
	cookieKeys = (String[]) s.readObject ();
	cookieValues = (String[]) s.readObject ();
	cookieDays = (int[]) s.readObject ();
	nCookies = s.readInt ();
	cache = s.readBoolean ();
	status = s.readInt ();
    }

    public void writeExternal (ObjectOutput s) throws IOException {
	s.writeObject (contentType);
	s.writeObject (response);
	s.writeObject (redir);
	s.writeObject (cookieKeys);
	s.writeObject (cookieValues);
	s.writeObject (cookieDays);
	s.writeInt (nCookies);
	s.writeBoolean (cache);
	s.writeInt (status);
    }

}


