// HttpReplyMessage.java
// $Id: HttpReplyMessage.java,v 1.18 1998/07/02 14:29:29 ylafon Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.www.http;

import java.io.*;
import java.net.*;

import org.w3c.www.mime.*;

public class HttpReplyMessage extends HttpEntityMessage {
    // HTTP Reply message well-known headers
    public static int H_ACCEPT_RANGES       = 46;
    public static int H_AGE                 = 47;
    public static int H_LOCATION            = 48;
    public static int H_PROXY_AUTHENTICATE  = 49;
    public static int H_PUBLIC              = 50;
    public static int H_RETRY_AFTER         = 51;
    public static int H_SERVER              = 52;
    public static int H_VARY                = 53;
    public static int H_WARNING             = 54;
    public static int H_WWW_AUTHENTICATE    = 55;


    static {
	registerHeader("Accept-Ranges"
		       , "org.w3c.www.http.HttpTokenList"
		       , H_ACCEPT_RANGES);
	registerHeader("Age"
		       , "org.w3c.www.http.HttpInteger"
		       , H_AGE);
	registerHeader("Location"
		       , "org.w3c.www.http.HttpString"
		       , H_LOCATION);
	registerHeader("Proxy-Authenticate"
		       , "org.w3c.www.http.HttpChallenge"
		       , H_PROXY_AUTHENTICATE);
	registerHeader("Public"
		       , "org.w3c.www.http.HttpTokenList"
		       , H_PUBLIC);
	// String as it can be a number or a date...
	registerHeader("Retry-After"
		       , "org.w3c.www.http.HttpString"
		       , H_RETRY_AFTER);
	registerHeader("Server"
		       , "org.w3c.www.http.HttpString"
		       , H_SERVER);
	registerHeader("Trailer"
		       , "org.w3c.www.http.HttpTokenList"
		       , H_TRAILER);
	registerHeader("Vary"
		       , "org.w3c.www.http.HttpTokenList"
		       , H_VARY);
	registerHeader("Warning"
		       , "org.w3c.www.http.HttpWarningList"
		       , H_WARNING);
	registerHeader("WWW-authenticate"
		       , "org.w3c.www.http.HttpChallenge"
		       , H_WWW_AUTHENTICATE);
    }

    /**
     * The status associated with this reply.
     */
    protected int status = -1;
    /**
     * The reason phrase.
     */
    protected String reason = null; 

    /**
     * Emit the status line before emiting the actual reply headers.
     * @param out The output stream to emit the reply to.
     */

    protected void startEmit(OutputStream out, int what) 
	 throws IOException
    {
	if ((what & EMIT_HEADERS) != EMIT_HEADERS)
	    return ;
	if ( major < 1 )
	    return;
	HttpBuffer buf = new HttpBuffer();
	buf.append(HTTP.byteArrayVersion);
	buf.append(' ');
	buf.appendInt(status);
	buf.append(' ');
	buf.append((reason != null) ? reason : "Unknowed Status Code");
	buf.append('\r');
	buf.append('\n');
	buf.emit(out);
    }

    public void dump(OutputStream out) {
	// Dump the reply status line first, and then the headers
	try {
	    startEmit(out, EMIT_HEADERS);
	} catch (Exception ex) {
	}
	super.dump(out);
    }

    /**
     * MimeHeaderHolder implementation - Begining of reply parsing.
     * If we can determine that this reply version number is less then
     * 1.0, then we skip the header parsing by returning <strong>true</strong>
     * to the MIME parser.
     * <p>Otherwise, we parse the status line, and return <strong>false
     * </strong> to make the MIME parser continue.
     * @return A boolean <strong>true</strong> if the MIME parser should stop
     * parsing, <strong>false</strong> otherwise.
     * @exception IOException If some IO error occured while reading the 
     * stream.
     */

    public boolean notifyBeginParsing(MimeParser parser) 
	 throws HttpParserException, IOException
    {
	if ( major <= 0 ) 
	    return true;
	// Append the whole reply line in some buffer:
	HttpBuffer buf = new HttpBuffer();
	int        ch  = parser.read();
	int        len = 0;

	// Skip leading CRLF, a present to Netscape
	while((ch == '\r') || (ch =='\n'))
	    ch = parser.read();
    loop:
	while (true) {
	    switch(ch) {
	      case -1:
		  throw new HttpParserException("End Of File");
	      case '\r':
		if ((ch = parser.read()) != '\n')
		    parser.unread(ch);
		break loop;
	      case '\n':
		break loop;
	      default:
		buf.append(ch);
		len++;
	    }
	    // That's a guard against very poor HTTP
	    if ( len > 16*1024 )
		throw new HttpParserException("Invalid HTTP");
	    ch = parser.read();
	}
	// Parse the bufer into HTTP version and status code
	byte       line[] = buf.getByteCopy();
	ParseState ps     = new ParseState();
	ps.ioff      = 0;
	ps.bufend    = line.length;
	ps.separator = (byte) ' ';
	if ( HttpParser.nextItem(line, ps) < 0 )
	    throw new RuntimeException("Bad reply: invalid status line ["
				       + new String(line, 0, 0, line.length)
				       + "]");
	// Parse the reply version:
	if ((line.length >= 4) && line[4] == (byte) '/' ) {
	    // A present to broken NCSA servers around
	    ParseState item = new ParseState();
	    item.ioff   = ps.start+5;
	    item.bufend = ps.end;
	    this.major = (short) HttpParser.parseInt(line, item);
	    item.prepare();
	    item.ioff++;
	    this.minor = (short) HttpParser.parseInt(line, item);
	} else {
	    this.major = 1;
	    this.minor = 0;
	}
	// Parse the status code:
	ps.prepare();
	this.status = HttpParser.parseInt(line, ps);
	// The rest of the sentence if the reason phrase:
	ps.prepare();
	HttpParser.skipSpaces(line, ps);
	this.reason = new String(line, 0, ps.ioff, line.length-ps.ioff);
	return false;
    }

    /**
     * Get the standard HTTP reason phrase for the given status code.
     * @param status The given status code.
     * @return A String giving the standard reason phrase, or
     * <strong>null</strong> if the status doesn't match any knowned error.
     */

    public String getStandardReason(int status) {
	int category = status / 100;
	int catcode  = status % 100;
	switch(category) {
	  case 1:
	      if ((catcode >= 0) && (catcode < msg_100.length))
		  return HTTP.msg_100[catcode];
	      break;
	  case 2:
	      if ((catcode >= 0) && (catcode < msg_200.length))
		  return HTTP.msg_200[catcode];
	      break;
	  case 3:
	      if ((catcode >= 0) && (catcode < msg_300.length))
		  return HTTP.msg_300[catcode];
	      break;
	  case 4:
	      if ((catcode >= 0) && (catcode < msg_400.length))
		  return HTTP.msg_400[catcode];
	      break;
	  case 5:
	      if ((catcode >= 0) && (catcode < msg_500.length))
		  return HTTP.msg_500[catcode];
	      break;
	}
	return null;
    }

    /**
     * Get this reply status code.
     * @return An integer, giving the reply status code.
     */

    public int getStatus() {
	return status;
    }

    /**
     * Set this reply status code.
     * This will also set the reply reason, to the default HTTP/1.1 reason
     * phrase.
     * @param status The status code for this reply.
     */

    public void setStatus(int status) {
	if ((status != this.status) || (reason == null))
	    this.reason = getStandardReason(status);
	this.status = status;
    }

    /**
     * Get the reason phrase for this reply.
     * @return A String encoded reason phrase.
     */

    public String getReason() {
	return reason;
    }

    /**
     * Set the reason phrase of this reply.
     * @param reason The reason phrase for this reply.
     */

    public void setReason(String reason) {
	this.reason = reason;
    }

    /**
     * Get the <code>private</code> directive of the cache control header.
     * @return A list of fields (potentially empty) encoded as an array of
     * String (with 0 length if empty), or <strong>null</strong> if
     * undefined.
     */

    public String[] getPrivate() {
	HttpCacheControl cc = getCacheControl();
	return (cc == null) ? null : cc.getPrivate();
    }

    /**
     * Check the <code>public</code> directive of the cache control header.
     * @return A boolean <strong>true</strong> if set, <strong>false</strong>
     * otherwise.
     */

    public boolean checkPublic() {
	HttpCacheControl cc = getCacheControl();
	return (cc == null) ? false : cc.checkPublic();
    }

    /**
     * Set the <code>public</code> cache control directive.
     * @param onoff Set it on or off.
     */

    public void setPublic(boolean onoff) {
	HttpCacheControl cc = getCacheControl();
	if (onoff && (cc == null)) 
	    setCacheControl(cc = new HttpCacheControl(true));
	else
	    return;
	cc.setPublic(onoff);
    }

    /**
     * Check the <code>proxy-revalidate</code> directive of the cache control
     * header.
     * @return A boolean <strong>true</strong> if set, <strong>false</strong>
     * otherwise.
     */

    public boolean checkProxyRevalidate() {
	HttpCacheControl cc = getCacheControl();
	return (cc == null) ? false : cc.checkProxyRevalidate();
    }

    /**
     * Set the <code>proxy-revalidate</code> cache control directive.
     * @param onoff Set it on or off.
     */

    public void setProxyRevalidate(boolean onoff) {
	HttpCacheControl cc = getCacheControl();
	if (onoff && (cc == null)) 
	    setCacheControl(cc = new HttpCacheControl(true));
	else
	    return;
	cc.setProxyRevalidate(onoff);
    }
     
    /**
     * Check the <code>must-revalidate</code> directive of the cache control
     * header.
     * @return A boolean <strong>true</strong> if set, <strong>false</strong>
     * otherwise.
     */

    public boolean checkMustRevalidate() {
	HttpCacheControl cc = getCacheControl();
	return (cc == null) ? false : cc.checkMustRevalidate();
    }

    /**
     * Set the <code>must-revalidate</code> cache control directive.
     * @param onoff Set it on or off.
     */

    public void setMustRevalidate(boolean onoff) {
	HttpCacheControl cc = getCacheControl();
	if (onoff && (cc == null)) 
	    setCacheControl(cc = new HttpCacheControl(true));
	else
	    return;
	cc.setMustRevalidate(onoff);
    }
     
    /**
     * Get the list of accepted ranges.
     * @return The list of units in which range requests are accepted, encoded
     * as an array of String, or <strong>null</strong> if undefined.
     */

    public String[] getAcceptRanges() {
	HeaderValue value = getHeaderValue(H_ACCEPT_RANGES);
	return (value != null) ? (String[]) value.getValue() : null;
    }

    /**
     * Set the list of units in which range requests are accepted.
     * @param units The list of units, encoded as a String array, or
     * <strong>null</strong> to reset the value.
     */

    public void setAcceptRanges(String units[]) {
	setHeaderValue(H_ACCEPT_RANGES
		       , ((units == null) ? null : new HttpTokenList(units)));
    }

    /**
     * Get the age of the attached entity.
     * @return An integer giving the age as a number of seconds, or 
     * <strong>-1</strong> if undefined.
     */

    public int getAge() {
	HeaderValue value = getHeaderValue(H_AGE);
	return (value != null) ? ((Integer) value.getValue()).intValue() :-1;
    }

    /**
     * Set the age of the attached entity.
     * @param age The age of the attached entity as a number of seconds,
     * or <strong>null</strong> to reset the value.
     */

    public void setAge(int age) {
	setHeaderValue(H_AGE
		       , ((age == -1) ? null : new HttpInteger(true, age)));
    }

    /**
     * Get the location of the reply.
     * The location header field keeps track of where to relocate clients
     * if needed.
     * @return The location encoded as a String.
     */

    public String getLocation() {
	HeaderValue value = getHeaderValue(H_LOCATION);
	return (value != null) ? (String) value.getValue() : null;
    }

    /**
     * Set the location value of the reply.
     * @param location The location, or <strong>null</strong> to reset the 
     * value.
     */

    public void setLocation(URL location) {
	setHeaderValue(H_LOCATION
		       , ((location == null)
			  ? null
			  : new HttpString(true, location.toExternalForm())));
    }

    /**
     * Get the proxy authentication challenge from this reply.
     * @return An instance of HttpChallenge, or <strong>null</strong> 
     * if undefined.
     */

    public HttpChallenge getProxyAuthenticate() {
	HeaderValue value = getHeaderValue(H_PROXY_AUTHENTICATE);
	return (value == null) ? null : (HttpChallenge) value.getValue();
    }


    /**
     * Set thye proxy authentication challenge on this reply.
     * @param challenge The challenge to set, or <strong>null</strong>
     * to reset the value.
     */

    public void setProxyAuthenticate(HttpChallenge challenge) {
	setHeaderValue(H_PROXY_AUTHENTICATE, challenge);
    }

    /**
     * Get the list of publicly allowed methods on queried resource.
     * @return The list of methods, encoded as a String array, or <strong>
     * null</strong> if undefined.
     */

    public String[] getPublic() {
	HeaderValue value = getHeaderValue(H_PUBLIC);
	return (value != null) ? (String[]) value.getValue() : null;
    }

    /**
     * Set the list of allowed method on queried resource.
     * @param mth The list of public methods, encoded as a String array, or
     * <strong>null</strong> to reset the value.
     */

    public void setPublic(String mth[]) {
	setHeaderValue(H_PUBLIC
		       , ((mth == null) 
			  ? null
			  : new HttpTokenList(mth)));
    }

    /**
     * Get the description of the server that generated this reply.
     * @return A String giving the description, or <strong>null</strong>
     * if undefined.
     */

    public String getServer() {
	HeaderValue value = getHeaderValue(H_SERVER);
	return (value != null) ? (String) value.getValue() : null;
    }

    /**
     * Set the description of the server.
     * @param server The String decribing the server, or <strong>null</strong>
     * to reset the value.
     */

    public void setServer(String server) {
	setHeaderValue(H_SERVER
		       , ((server == null) 
			  ? null 
			  : new HttpString(true, server)));
    }

    /**
     * set the retry after as a delay
     * @param an integer representing the delay, in seconds
     */

    public void setRetryAfter(int delay) {
	setHeaderValue(H_RETRY_AFTER,
		       ((delay == -1) ? null :
			new HttpString(true, 
				       ((new Integer(delay)).toString()))));
    }

    /**
     * set the retry after as a date
     * @param long the date as a long
     */
    
    public void setRetryAfter(long date) {
	if (date == -1) {
	    setHeaderValue(H_RETRY_AFTER, null);
	} else {
	    HttpDate d = new HttpDate(true, date);
	    setHeaderValue(H_RETRY_AFTER,
			   new HttpString(true, d.toExternalForm()));
	}
    }

    /**
     * get the RetryAfter as a date
     * @return  A long giving the date as the number of milliseconds since the
     * Java epoch, or <strong>-1</strong> if undefined.
     */
    
    // FIXME Implementation

    /**
     * Get the vary header value.
     * @return A list of field-names on which the negotiated resource vary,
     * or a list containing only <code>*</code> (if varies on all headers),
     * or <strong>null</strong> if undefined.
     */

    public String[] getVary() {
	HeaderValue value = getHeaderValue(H_VARY);
	return (value != null) ? (String[]) value.getValue() : null;
    }

    /**
     * Set the vary header value.
     * @param varies The list of headers on which this resource varies, or
     * <strong>null</strong> to reset the value.
     */

    public void setVary(String varies[]) {
	setHeaderValue(H_VARY
		       , ((varies == null) 
			  ? null 
			  : new HttpTokenList(varies)));
    }

    /**
     * Get the list of warnings attached to this reply.
     * @return An array of HttpWarning, or <strong>null</strong> if
     * undefined.
     */

    public HttpWarning[] getWarning() {
	HeaderValue value = getHeaderValue(H_WARNING);
	return (value != null) ? (HttpWarning[]) value.getValue() : null;
    }

    /**
     * Set the warning list attached to this reply.
     * @param warnings An array of warnings to attach to the given reply,
     * or <strong>null</strong> to reset the value.
     */

    public void setWarning(HttpWarning warnings[]) {
	setHeaderValue(H_WARNING
		       , ((warnings == null)
			  ? null
			  : new HttpWarningList(warnings)));
    }

    /**
     * Add a warning to this reply message.
     * @param warning The warning to add.
     */

    public void addWarning(HttpWarning warning) {
	HttpWarningList wl = (HttpWarningList) getHeaderValue(H_WARNING);
	if ( wl == null ) {
	    wl = new HttpWarningList(warning);
	    setHeaderValue(H_WARNING, wl);
	} else {
	    wl.addWarning(warning);
	}
    }

    /**
     * Get the challenge attached to this reply.
     * @return An instance of HttpChallenge, or <strong>null</strong> if
     * undefined.
     */

    public HttpChallenge getWWWAuthenticate() {
	HeaderValue value = getHeaderValue(H_WWW_AUTHENTICATE);
	return (value != null) ? (HttpChallenge) value.getValue() : null;
    }

    /**
     * Attach a challenge to this reply.
     * @param challenge The challenge to be attached to the reply, or
     * <strong>null</strong> to reset the value.
     */

    public void setWWWAuthenticate(HttpChallenge challenge) {
	setHeaderValue(H_WWW_AUTHENTICATE, challenge);
    }


    public HttpReplyMessage(MimeParser parser) {
	super(parser);
    }

    public HttpReplyMessage() {
	super();
    }


}
