// Request.java
// $Id: Request.java,v 1.21 1997/02/05 21:45:00 abaird Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.jigsaw.http ;

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

import w3c.www.mime.* ;
import w3c.www.http.* ;
import w3c.util.*;
import w3c.jigsaw.resources.*;

/**
 * this class extends HttpRequestMessage to cope with HTTP request.
 * One subtely here: note how each field acessor <em>never</em> throws an
 * exception, but rather is provided with a default value: this is in the hope
 * that sometime, HTTP will not require all the parsing it requires right now.
 */

public class Request extends HttpRequestMessage implements HTTP {
    /**
     * The URL that means <strong>*</strong> for an <code>OPTIONS</code>
     * method.
     */
    public static URL THE_SERVER = null;
    
    static {
	try {
	    THE_SERVER = new URL("http://your.url.unknown");
	} catch (Exception ex) {
	    ex.printStackTrace();
	}
    }

    protected Client      client  = null;
    protected MimeParser  parser  = null;
    protected InputStream in      = null;
    protected boolean     keepcon = true;
    boolean is_proxy = false;

    /**
     * Fix the target URL of the request, this is the only good time to do so.
     */

    public void notifyEndParsing(MimeParser parser)
	throws HttpParserException, IOException
    {
	super.notifyEndParsing(parser);
	String target = getTarget();
	String url    = null;

	// Get rid of the nasty cases (who deserve a place in hell ?)
	if ( target.equals("*") ) {
	    setURL(THE_SERVER);
	    return;
	}
	// Is this a full http URL:
	int colon = target.indexOf(':');
	if ((colon != -1) && target.substring(0, colon).equals("http")) {
	    // Good we have a full URL:
	    try {
		setURL(new URL(target));
	    } catch (Exception ex) {
		throw new HttpParserException("Invalid URL ["+url+"]");
	    }
	} else {
	    try {
		// Do we have a valid host header ?
		String host = getHost();
		if ( host == null ) {
		    // If this claims to be 1.1, tell him he's wrong:
		    if ((major == 1) && (minor >= 1))
			throw new HttpParserException("No Host Header");
		    httpd server = getClient().getServer();
		    setURL(new URL("http"
				   , server.getHost(), server.getPort()
				   , target));
		} else {
		    int ic = host.indexOf(':');
		    if ( ic < 0 ) {
			setURL(new URL("http", host, target));
		    } else {
			setURL(new URL("http"
				       , host.substring(0, ic)
				       , Integer.parseInt(host.substring(ic+1))
				       , target));
		    }
		}
	    } catch (Exception ex) {
		throw new HttpParserException("Invalid URL ["+url+"]");
	    }
	}
    }

    // FIXME
    // This guy should also check that the (optional) request stream has been
    // exhausted.

    public boolean canKeepConnection() {
	// HTTP/0.9 doesn't know about keeping connections alive:
	if (( ! keepcon) || (major < 1))
	    return false;
	if ( minor >= 1 ) 
	    // HTTP/1.1 keeps connections alive by default
	    return hasConnection("close") ? false : true;
	// For HTTP/1.0 check the [proxy] connection header:
	if ( is_proxy )
	    return hasProxyConnection("keep-alive");
	else
	    return hasConnection("keep-alive");
    }

    private HTTPResource target_resource = null;
    protected void setTargetResource(HTTPResource resource) {
	target_resource = resource;
    }

    /**
     * Get this request target resource.
     * @return An instance of HTTPResource, or <strong>null</strong> if
     * not found.
     */

    public HTTPResource getTargetResource() {
	return target_resource;
    }

    public void setProxy(boolean onoff) {
	is_proxy = onoff;
    }

    public boolean isProxy() {
	return is_proxy;
    }

    public String getURLPath() {
	return url.getFile();
    }

    public void setURLPath(String path) {
	try {
	    url = new URL(url, path);
	} catch (Exception ex) {
	    ex.printStackTrace();
	}
    }

    public boolean hasContentLength() {
	return hasHeader(H_CONTENT_LENGTH);
    }

    public boolean hasContentType() {
	return hasHeader(H_CONTENT_TYPE);
    }

    public boolean hasAccept() {
	return hasHeader(H_ACCEPT);
    }

    public boolean hasAcceptEncoding() {
	return hasHeader(H_ACCEPT_ENCODING);
    }

    public boolean hasAcceptLanguage() {
	return hasHeader(H_ACCEPT_LANGUAGE);
    }

    public boolean hasAuthorization() {
	return hasHeader(H_AUTHORIZATION);
    }

    public boolean hasProxyAuthorization() {
	return hasHeader(H_PROXY_AUTHORIZATION);
    }

    public String getQueryString() {
	return (String) getState("query");
    }

    public boolean hasQueryString() {
	return hasState("query");
    }

    protected boolean internal = false;
    public boolean isInternal() {
	return internal;
    }

    public void setInternal(boolean onoff) {
	this.internal = onoff;
    }

    protected Request original = null;
    public Request getOriginal() {
	return original == null ? this : original ;
    }

    /**
     * Clone this request, in order to launch an internal request.
     * This method can be used to run a request in some given context, defined
     * by an original request. It will preserve all the original information
     * (such as authentication, etc), and will provide a <em>clone</em> of
     * the original request.
     * <p>The original request and its clone differ in the following way:
     * <ul>
     * <li>The clone is marked as <em>internal</em>, which can be tested
     * by the <code>isInternal</code> method.
     * <li>The clone will keep a pointer to the first request that was 
     * cloned. This original request can be accessed by the <code>getOriginal
     * </code> method.
     * </ul>
     * <p>To run an internal request, the caller can then use the <code>
     * w3c.jigsaw.http.httpd</code> <code>perform</code> method.
     * @return A fresh Request instance, marked as internal.
     */

    public HttpMessage getClone() {
	Request cl = (Request) super.getClone();
	cl.internal = true;
	if ( cl.original == null )
	    cl.original = this;
	return cl;
    }

    /**
     * Get this reply entity body.
     * The reply entity body is returned as an InputStream, that the caller
     * has to read to actually get the bytes of the content.
     * @return An InputStream instance. If the reply has no body, the returned
     * input stream will just return <strong>-1</strong> on first read.
     */

    public InputStream getInputStream()
	throws IOException
    {
	if ( in != null )
	    return in;
	// Find out which method is used to the length:
	int len = getContentLength();
	if ( len >= 0 ) {
	    in = new ContentLengthInputStream(parser.getInputStream(), len);
	} else {
	    // No content length, try out chunked encoding:
	    String te[] = getTransferEncoding() ;
	    if ( te != null ) {
		for (int i = 0 ; i < te.length ; i++) {
		    if (te[i].equals("chunked")) 
			in = new ChunkedInputStream(parser.getInputStream());
		}
	    }
	}
	// Handle broken HTTP/1.0 request
	// It is mandatory for 1.1 requests to have been handled above.
	if ((major == 1) && (minor == 0) && (in == null)) {
	    String m = getMethod();
	    if (m.equals("POST") || m.equals("PUT")) {
		keepcon = false;
		in      = parser.getInputStream();
	    }
	}
	return in;
    }

    /**
     * Unescape a HTTP escaped string
     * @param s The string to be unescaped
     * @return the unescaped string.
     */

    public static String unescape (String s) {
	StringBuffer sbuf = new StringBuffer () ;
	int l  = s.length() ;
	int ch = -1 ;
	for (int i = 0 ; i < l ; i++) {
	    switch (ch = s.charAt(i)) {
	      case '%':
		ch = s.charAt (++i) ;
		int hb = (Character.isDigit ((char) ch) 
			  ? ch - '0'
			  : 10+Character.toLowerCase ((char) ch)-'a') & 0xF ;
		ch = s.charAt (++i) ;
		int lb = (Character.isDigit ((char) ch)
			  ? ch - '0'
			  : 10+Character.toLowerCase ((char) ch)-'a') & 0xF ;
		sbuf.append ((char) ((hb << 4) | lb)) ;
		break ;
	      case '+':
		sbuf.append (' ') ;
		break ;
	      default:
		sbuf.append ((char) ch) ;
	    }
	}
	return sbuf.toString() ;
    }
    
    /**
     * Make an empty Reply object matching this request version.
     * @param status The status of the reply.
     */

    public Reply makeReply(int status) {
	return new Reply(client, getMajorVersion(), getMinorVersion(), status);
    }

    /**
     * Get the client of this request.
     */

    public Client getClient() {
	return client ;
    }

    public Request (Client client, MimeParser parser) {
	super (parser);
	this.parser = parser;
	this.client = client ;
    }
}
