// JigsawHttpServletReponse.java
// $Id: JigsawHttpServletResponse.java,v 1.21 1998/06/12 11:56:33 bmahe 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)
 */

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;
 
    protected JigsawHttpServletRequest jrequest = null;

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

    public static final 
	String REMOVE_HEADER = "org.w3c.jigsaw.servlet.removeHeader";
   
    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) {
	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 getOutputStream, getWriter
     */    
    public void setContentType(String spec) {
	try {
	    MimeType type= new MimeType(spec);
	    reply.setContentType(type);
	} catch(MimeTypeFormatException ex) {
	    // FIXME what should I do?
	}
    }

    /**
     * Returns an output stream for writing binary response data.
     * @return 
     * @exception IOException if an I/O exception has occurred 
     * @exception IllegalStateException if getWriter has been called on this 
     * same request. 
     * @see 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;
      
	if ( request.hasState(REMOVE_HEADER) )
	    output = (new JigsawServletOutputStream
		      (this
		       , new DataOutputStream(reply.getOutputStream(false))));
	else
	    output = (new JigsawServletOutputStream
		      (this
		       , new DataOutputStream(reply.getOutputStream())));
	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
     */
    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 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 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 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 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);
    }

    public 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. 
     * @see sendRedirect, 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 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) {
	if (! jrequest.isRequestedSessionIdFromCookie()) {
	    url = url+"?"+jrequest.getCookieName()+"="+
		jrequest.getSession(true).getId();
	}
	return 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 "ISO-8859-1";
    }

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

    protected synchronized void closeStream() {
	try {
	    if (stream_state == OUTPUT_STREAM_USED) {
		output.flush();
		output.close();
	    } else if (stream_state == STREAM_WRITER_USED) {
		writer.flush();
		writer.close();
	    }
	} catch (IOException ex) {}
    }

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

}
