// JigsawHttpServletReponse.java
// $Id: JigsawHttpServletResponse.java,v 1.31 1999/04/12 09:56:55 ylafon Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.jigsaw.servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.net.*;

import org.w3c.www.mime.*;
import org.w3c.www.http.*;
import org.w3c.jigsaw.http.*;

/**
 * @author Alexandre Rafalovitch <alex@access.com.au>
 * @author Anselm Baird-Smith <abaird@w3.org>
 * @author Benoît Mahé (bmahe@w3.org)
 * @author Roland Mainz (Roland.Mainz@informatik.med.uni-giessen.de)
 */

public class JigsawHttpServletResponse implements HttpServletResponse {

    private final static int STATE_INITIAL        = 0;
    private final static int STATE_HEADERS_DONE   = 1;
    private final static int STATE_ALL_DONE       = 2;
    private final static int STREAM_STATE_INITIAL = 0;
    private final static int STREAM_WRITER_USED   = 1;
    private final static int OUTPUT_STREAM_USED   = 2;

    private int stream_state = STREAM_STATE_INITIAL;

    private JigsawServletOutputStream output = null;
    private PrintWriter               writer = null;
 
    private MimeTypeFormatException setContentTypeException = null;

    // servlet has set a fixed content length or not, 
    // and cut (see flushStream) any data which are too much here...
    private final static int CALC_CONTENT_LENGTH = -1; 
    private              int fixedContentLength  = CALC_CONTENT_LENGTH;

    /**
     * Our temp stream.
     */
    protected ByteArrayOutputStream out = null;

    protected JigsawHttpServletRequest jrequest = null;

    protected void setServletRequest(JigsawHttpServletRequest jrequest) {
	this.jrequest = jrequest;
    }

    public static final 
	String INCLUDED = "org.w3c.jigsaw.servlet.included";

    int   state = STATE_INITIAL;
    Reply reply = null;
    Request request = null;
    
    /**
     * Sets the content length for this response. 
     * @param len - the content length 
     */
    public void setContentLength(int i) {
	fixedContentLength = i;
	reply.setContentLength(i);
    }

    /**
     * Sets the content type for this response. This type may later be 
     * implicitly modified by addition of properties such as the MIME
     * charset=<value> if the service finds it necessary, and the appropriate
     * media type property has not been set.
     * <p>This response property may only be assigned one time. If a writer 
     * is to be used to write a text response, this method must be
     * called before the method getWriter. If an output stream will be used 
     * to write a response, this method must be called before the
     * output stream is used to write response data. 
     * @param spec - the content's MIME type 
     * @see JigsawHttpServletResponse#getOutputStream
     * @see JigsawHttpServletResponse#getWriter
     */    
    public void setContentType(String spec) {
	try {
	    MimeType type= new MimeType(spec);
	    reply.setContentType(type);
	    setContentTypeException = null;
	} catch(MimeTypeFormatException ex) {
	    //store exception
	    setContentTypeException = ex;
	}
    }

    protected boolean isStreamObtained() {
	return (stream_state != STREAM_STATE_INITIAL);
    }

    protected Reply getReply() {
	return reply;
    }

    /**
     * Returns an output stream for writing binary response data.
     * @return A ServletOutputStream
     * @exception IOException if an I/O exception has occurred 
     * @exception IllegalStateException if getWriter has been called on this 
     * same request. 
     * @see JigsawHttpServletResponse#getWriter
     */    
    public synchronized ServletOutputStream getOutputStream()
	throws IOException
    {
	if (stream_state == STREAM_WRITER_USED)
	    throw new IllegalStateException("Writer used");
	stream_state = OUTPUT_STREAM_USED;
	return getJigsawOutputStream();
    }

    protected ServletOutputStream getJigsawOutputStream()
	throws IOException
    {
	if ( output != null )
	    return output;

	// any exception during setContentType ?    
	if( setContentTypeException != null ) {
	    // "wrap" the exception from setContentType in an IOException
	    throw new IOException("Illegal Contenttype: "+
				  setContentTypeException.toString());
	}

	if( request.hasState(INCLUDED) ) {
	    out = new ByteArrayOutputStream();
	    output = new JigsawServletOutputStream(this, 
						   new DataOutputStream(out));
	} else {
	    output = new JigsawServletOutputStream(this, reply);
	}
	return output;
    }

    /**
     * Sets the status code and message for this response. If the field had
     * already been set, the new value overwrites the previous one. The message
     * is sent as the body of an HTML page, which is returned to the user to
     * describe the problem. The page is sent with a default HTML header; the
     * message is enclosed in simple body tags (<body></body>).
     * @param i - the status code 
     * @param reason - the status message
     * @deprecated since jsdk2.1
     */
    public void setStatus(int i, String reason) {
	reply.setStatus(i);
	reply.setReason(reason);
    }
    
    /**
     * Sets the status code for this response. This method is used to set the
     * return status code when there is no error (for example, for the status
     * codes SC_OK or SC_MOVED_TEMPORARILY). If there is an error, the 
     * sendError method should be used instead.
     * @param i - the status code 
     * @see JigsawHttpServletResponse#sendError
     */
    public void setStatus(int i) {
	setStatus(i, reply.getStandardReason(i));
    }
    
    /**
     * Adds a field to the response header with the given name and value. If
     * the field had already been set, the new value overwrites the previous
     * one. The containsHeader method can be used to test for the presence of a
     * header before setting its value.
     * @param name - the name of the header field 
     * @param value - the header field's value 
     * @see JigsawHttpServletResponse#containsHeader
     */
    public void setHeader(String name, String value) {
	reply.setValue(name, value);
    }
    
    /**
     * Adds a field to the response header with the given name and integer
     * value. If the field had already been set, the new value overwrites the
     * previous one. The containsHeader method can be used to test for the
     * presence of a header before setting its value.
     * @param name - the name of the header field 
     * @param value - the header field's integer value 
     * @see JigsawHttpServletResponse#containsHeader
     */
    public void setIntHeader(String name, int value) {
	setHeader(name, String.valueOf(value));
    }
    
    /**
     * Adds a field to the response header with the given name and date-valued
     * field. The date is specified in terms of milliseconds since the epoch. 
     * If the date field had already been set, the new value overwrites the
     * previous one. The containsHeader method can be used to test for the
     * presence of a header before setting its value.
     * @param name - the name of the header field 
     * @param value - the header field's date value 
     * @see JigsawHttpServletResponse#containsHeader 
     */
    public void setDateHeader(String name, long date) {
	setHeader(name, String.valueOf(date));
    }
    
    public void unsetHeader(String name) {
	setHeader(name, null);
    }
    
    /**
     * Sends an error response to the client using the specified status code
     * and descriptive message. If setStatus has previously been called, it is
     * reset to the error status code. The message is sent as the body of an
     * HTML page, which is returned to the user to describe the problem. The
     * page is sent with a default HTML header; the message is enclosed in
     * simple body tags (<body></body>).
     * @param sc - the status code 
     * @param msg - the detail message 
     * @exception IOException If an I/O error has occurred.
     */
    public void sendError(int i, String msg) 
	throws IOException
    {
	setStatus(i);
	reply.setContent(msg);
	state = STATE_ALL_DONE;
    }

    /**
     * Sends an error response to the client using the specified status 
     * code and a default message. 
     * @param sc - the status code 
     * @exception IOException If an I/O error has occurred.
     */
    public void sendError(int i)
        throws IOException
    {
	setStatus(i);
	reply.setContent(reply.getStandardReason(i));
	state = STATE_ALL_DONE;
    }
    
    /**
     * Sends a temporary redirect response to the client using the specified
     *  redirect location URL. The URL must be absolute (for example, 
     * https://hostname/path/file.html). Relative URLs are not permitted here. 
     * @param url - the redirect location URL 
     * @exception IOException If an I/O error has occurred. 
     */    
    public void sendRedirect(String url)
        throws IOException
    {
	URL loc = null;
	try {
	    loc = new URL(request.getURL(), url);
	    setStatus(SC_MOVED_TEMPORARILY);
	    reply.setLocation(loc);
	    state = STATE_ALL_DONE;
	} catch (Exception ex) {
	    ex.printStackTrace();
	}
    }

    /**
     * Checks whether the response message header has a field with the
     * specified name. 
     * @param name - the header field name 
     * @return true if the response message header has a field with the 
     * specified name; false otherwise
     */
    public boolean containsHeader(String header) {
	return reply.hasHeader(header);
    }

    /**
     * Adds the specified cookie to the response. It can be called multiple 
     * times to set more than one cookie. 
     * @param cookie - the Cookie to return to the client 
     */
    public void addCookie(Cookie cookie) {
	HttpSetCookieList clist = reply.getSetCookie();
	if (clist == null) {
	    HttpSetCookie cookies [] = new HttpSetCookie[1];
	    cookies[0] = convertCookie(cookie);
	    clist = new HttpSetCookieList(cookies);
	} else {
	    clist.addSetCookie(convertCookie(cookie));
	}
	reply.setSetCookie(clist);
    }

    private HttpSetCookie convertCookie(Cookie cookie) {
	HttpSetCookie scookie = new HttpSetCookie(true, 
						  cookie.getName(),
						  cookie.getValue());
	scookie.setComment(cookie.getComment());
	scookie.setDomain(cookie.getDomain());
	scookie.setMaxAge(cookie.getMaxAge());
	scookie.setPath(cookie.getPath());
	scookie.setSecurity(cookie.getSecure());
	scookie.setVersion(cookie.getVersion());
	return scookie;
    }

    /**
     * Encodes the specified URL for use in the sendRedirect method or, if 
     * encoding is not needed, returns the URL unchanged. The implementation 
     * of this method should include the logic to determine whether the 
     * session ID needs to be encoded in the URL.
     * Because the rules for making this determination differ from those used
     * to decide whether to encode a normal link, this method is seperate from
     * the encodeUrl method.
     * <p>All URLs sent to the HttpServletResponse.sendRedirect method should
     * be run through this method. Otherwise, URL rewriting canont be used 
     * with browsers which do not support cookies. 
     * @param url - the url to be encoded. 
     * @return the encoded URL if encoding is needed; the unchanged URL 
     * otherwise. 
     * @deprecated since jsdk2.1
     * @see JigsawHttpServletResponse#sendRedirect
     * @see JigsawHttpServletResponse#encodeUrl
     */
    public String encodeRedirectUrl(String url) {
	try {
	    URL redirect = new URL(url);
	    URL requested = new URL(jrequest.getRequestURI());
	    if ( redirect.getHost().equals(requested.getHost()) &&
		 redirect.getPort() == requested.getPort())
		return encodeUrl(url);
	} catch (MalformedURLException ex) {
	    //error so return url.
	    return url;
	}
	return url;
    }

        /**
     * Encodes the specified URL for use in the sendRedirect method or, if 
     * encoding is not needed, returns the URL unchanged. The implementation 
     * of this method should include the logic to determine whether the 
     * session ID needs to be encoded in the URL.
     * Because the rules for making this determination differ from those used
     * to decide whether to encode a normal link, this method is seperate from
     * the encodeUrl method.
     * <p>All URLs sent to the HttpServletResponse.sendRedirect method should
     * be run through this method. Otherwise, URL rewriting canont be used 
     * with browsers which do not support cookies. 
     * @param url - the url to be encoded. 
     * @return the encoded URL if encoding is needed; the unchanged URL 
     * otherwise. 
     * @see JigsawHttpServletResponse#sendRedirect
     * @see JigsawHttpServletResponse#encodeUrl
     */
    public String encodeRedirectURL(String url) {
	return encodeRedirectUrl(url);
    }

    /**
     * Encodes the specified URL by including the session ID in it, or, if 
     * encoding is not needed, returns the URL unchanged. The implementation of
     * this method should include the logic to determine whether the session ID
     * needs to be encoded in the URL. For example, if the browser supports
     * cookies, or session tracking is turned off, URL encoding is unnecessary.
     * <p>All URLs emitted by a Servlet should be run through this method. 
     * Otherwise, URL rewriting cannot be used with browsers which do not 
     * support cookies.
     * @param url - the url to be encoded. 
     * @return the encoded URL if encoding is needed; the unchanged URL 
     * otherwise. 
     * @deprecated since jsdk2.1
     */
    public String encodeUrl(String url) {
	if (! jrequest.isRequestedSessionIdFromCookie()) {
	    url = url + ((url.indexOf("?") != -1) ? "&" : "?")+
		jrequest.getCookieName()+"="+
		jrequest.getSession(true).getId();
	}
	return url;
    }

    /**
     * Encodes the specified URL by including the session ID in it, or, if 
     * encoding is not needed, returns the URL unchanged. The implementation of
     * this method should include the logic to determine whether the session ID
     * needs to be encoded in the URL. For example, if the browser supports
     * cookies, or session tracking is turned off, URL encoding is unnecessary.
     * <p>All URLs emitted by a Servlet should be run through this method. 
     * Otherwise, URL rewriting cannot be used with browsers which do not 
     * support cookies.
     * @param url - the url to be encoded. 
     * @return the encoded URL if encoding is needed; the unchanged URL 
     * otherwise. 
     */
    public String encodeURL(String url) {
	return encodeUrl(url);
    }

    /**
     * Return the Charset parameter of content type
     * @return A String instance
     */
    public String getCharacterEncoding() {
	org.w3c.www.mime.MimeType type = reply.getContentType();
	if ((type != null) && (type.hasParameter("charset"))) {
	    return type.getParameterValue("charset");
	}
	return System.getProperty("file.encoding");
    }

    /**
     * Returns a print writer for writing formatted text responses. 
     * The MIME type of the response will be modified, if necessary, to
     * reflect the character encoding used, through the charset=... property. 
     * This means that the content type must be set before calling this 
     * method. 
     * @exception UnsupportedEncodingException if no such encoding can be 
     * provided 
     * @exception IllegalStateException if getOutputStream has been called 
     * on this same request.
     * @exception IOException on other errors. 
     * @see JigsawHttpServletResponse#getOutputStream
     * @see JigsawHttpServletResponse#setContentType 
     */    
    public synchronized PrintWriter getWriter() 
	throws IOException, UnsupportedEncodingException
    {
	if (stream_state == OUTPUT_STREAM_USED)
	    throw new IllegalStateException("Output stream used");
	stream_state = STREAM_WRITER_USED;
      
	if (writer == null) {
	    writer = new PrintWriter(
			 new OutputStreamWriter(getJigsawOutputStream(), 
						getCharacterEncoding()));
	}
	return writer;
    }

    /**
     * Flush the output stream.
     * @param close Close the stream if true.
     * @exception IOException if an IO error occurs.
     */
    protected synchronized void flushStream(boolean close) 
	throws IOException
    {
	int writeLength;

	if (stream_state == OUTPUT_STREAM_USED) {
	    output.flush();
	} else if (stream_state == STREAM_WRITER_USED) {
	    writer.flush();
	}

	if (request.hasState(INCLUDED)) {
	    if (out == null)
		return;

	    if( fixedContentLength != CALC_CONTENT_LENGTH ) {
		writeLength = (out.size() < fixedContentLength) 
		    ? (out.size())
		    : (fixedContentLength);
	    } else {
		writeLength = out.size();
	    }
	    reply.setContentLength(writeLength);
	    
	    OutputStream rout = reply.getOutputStream(false);
	    byte content[] = out.toByteArray();
	    if (close)
		out.close();
	    else 
		out.reset();
	    rout.write(content);
	    rout.flush();
	}
    }

    JigsawHttpServletResponse(Request request, Reply reply) {
	this.request = request;
	this.reply   = reply;
    }

}
