package jp;

import com.mortbay.HTTP.*;
import com.mortbay.Util.*;
import java.io.*;
import Tools.*;
import sdsi.*;

/**
 * This class represents an incoming HTTP response, whose result code
 * and headers have been extracted. It's very different than
 * com.mortbay.HTTP.HttpResponse, which is a response being constructed
 * to send outbound. This class doesn't (yet) have any useful
 * write operations, it's only a way to examine that incoming header.
 * It is being designed as part of a proxy, which reads the header,
 * makes a decision, then (sometimes) passes the <em>original stream</em>
 * on to the client, not this header.
 *
 * I imagine someday I could extend this to actually act like an
 * HttpResponse, so that the proxy could send it back out the same
 * way it does a fresh HttpResponse.
 */
public class IncomingResponse
	extends HttpHeader {

	/**
	 * the thing we can rewind to play back headers verbatim
	 */
	RecordingInputStream ris;

	/**
	 * the stream that we'll use to read out the headers and that
	 * unchunks or content-length limits the content.
	 */
	HttpInputStream his;

	/**
	 * the thing that can be played out, and has the entire verbatim
	 * stream (the headers followed by the content).
	 */
	InputStream replayStream;

	/**
	 * Stream that will check the stream contents against an expected
	 * hash value.
	 */
	DigestInputStream dis;

	/**
	 * A reference to a stream we need to remember to close later.
	 */
	InputStream closeThisStream;

	String responseLine;
	int code;
	String desc;

	/**
	 * becomes true once parse() runs successfully
	 */
	boolean parsed;

	/**
	 * if something goes wrong, it stays wrong. :v)
	 */
	ParseException priorException;

	/**
	 * Parse a stream into an IncomingResponse object, ready to return
	 * headers or the content stream.
	 */
	public IncomingResponse(InputStream is)
		throws ParseException {
		this(is, true);
	}

	/**
	 * Parse a stream into an IncomingResponse object, ready to return
	 * headers or the content stream.
	 */
	public IncomingResponse(InputStream is, boolean rewindable)
		throws ParseException {
		this(is, rewindable, true);
	}

	/**
	 * Parse a stream into an IncomingResponse object, ready to return
	 * headers or the content stream.
	 *
	 * @param rewindable - if false, some overhead is saved by not
	 * setting up a RecordingInputStream to capture the protocol line &
	 * headers for later replay. Generally, rewindable = true for
	 * proxies that need to inspect the stream but then replay it
	 * verbatim later; else it should be false.
	 *
	 * @param closeInputStream - if true, the <code>is</code> argument
	 * is closed when close() is called on this object.
	 */
	public IncomingResponse(InputStream is, boolean rewindable,
		boolean closeInputStream)
		throws ParseException {
		if (rewindable) {
			ris = new RecordingInputStream(is);
			his = new HttpInputStream(ris, false);
		} else {
			his = new HttpInputStream(is, false);
		}
		if (closeInputStream) {
			closeThisStream = is;
		}
		parse();
	}

	void parse()
		throws ParseException {
		if (parsed) {
			return;
		}
		if (priorException != null) {
			throw priorException;
		}

		try {
			if (ris!=null) {
				// start the recording at the beginning of this particular
				// response
				ris.startRecording();
			}

			// parse out the HTTP response line
			// Example: HTTP/1.1 200 OK
			while (true) {
				responseLine = his.readLine();
				if (responseLine==null
					|| !responseLine.startsWith("HTTP")) {
					// invalid response line
					throwParseException("Invalid response line");
				}
				try {
					int c0 = responseLine.indexOf(' ', 0);
					int c1 = responseLine.indexOf(' ', c0+1);
					code = Integer.parseInt(responseLine.substring(c0+1, c1));
					desc = responseLine.substring(c1+1);
				} catch (IndexOutOfBoundsException ex) {
					// invalid response line
					throwParseException("Invalid response line: "+responseLine);
				}
				if (code==100) {
					// Oh, that's just a Continue header. Go look for another
					// one.
					// But first, read the following blank line.
					// TODO: Continue is actually a response, and could
					// be followed by headers of its own. What should we
					// do with them?
					responseLine = his.readLine();
					if (responseLine.length()!=0
						&& responseLine.charAt(0)!='\n') {
						throwParseException(
							"Continue header followed by gunk I don't grok: "
								+responseLine);
					}
					// loop around to pick up a new response line
				} else {
					break;
				}
			}

			// parse the headers out of the stream
			read(his);
			if (ris!=null) {
				// not strictly necessary, since ris shouldn't get used again.
				ris.stopRecording();
			}
			parsed = true;
		} catch (Exception ex) {
			ex.printStackTrace();
			throwParseException(ex.toString());
		}
	}

	void throwParseException(String str)
		throws ParseException {			// not surprisingly :v)
		throw new ParseException(str);
	}

	void checkPriorException()
		throws ParseException {
		if (priorException!=null) {
			throw priorException;
		}
	}

	/**
	 * Return the response line of the response: the first line, with
	 * the numeric result code.
	 */
	public String getResponseLine()
		throws ParseException {
		checkPriorException();
		return responseLine;
	}

	/**
	 * Return the numeric result code of the response, e.g., 100 (OK).
	 */
	public int getCode()
		throws ParseException {
		checkPriorException();
		return code;
	}

	/**
	 * Return the string descriptor that follows the numeric result
	 * code.
	 */
	public String getDesc()
		throws ParseException {
		checkPriorException();
		return desc;
	}

	/**
	 * A proxy uses this method to pass this incoming response out to a
	 * calling client using an outgoing HttpResponse object.
	 * 
	 * If we had an exception parsing the input stream, then we play
	 * out the input verbatim.
	 * 
	 * Otherwise, we play out this object as a header block followed by
	 * the rest of the input stream. That allows any changes made to
	 * the headers to show up at the client.
	 */
	public void passAlongResponse(HttpResponse outgoingResponse)
		throws IOException {
		if (getReplayStream()==null) {
			throw new IOException("Input stream is broken in "+getClass());
		}

		outgoingResponse.writeInputStream(getReplayStream(), -1, true);
	}

	/**
	 * Return a stream containing the entire response: protocol line,
	 * headers, separator line, content.
	 */
	public InputStream getReplayStream() {
		if (replayStream==null && ris!=null) {
			// Construct a virtual stream that is made from the part
			// we already read, followed by the HttpInputStream where
			// we left it.
			replayStream = new ChainInputStream(ris.getRecordingAsStream(),
				getContentStream());
		}
		return replayStream;
	}

	/**
	 * Return a stream containing the content (everything past the
	 * headers).
	 */
	public InputStream getContentStream() {
		if (dis!=null) {
			return dis;
		} else {
			return his;
		}
	}

	/**
	 * Attach a message digest verifier to this stream, so that if,
	 * when the stream is fully read out, its digest does not match,
	 * the stream throws an IOException.
	 */
	public void attachDocumentVerifier(ObjectHash objHash) {
		dis = new DigestInputStream(his, objHash.getHash());
	}

	/**
	 * Close the content stream and the input stream that this
	 * object was parsed from.
	 */
	public void close()
		throws IOException {
		his.close();
		if (closeThisStream!=null) {
			closeThisStream.close();
		}
	}

	/**
	 * Write this response onto the specified OutputStream.
	 * This method only writes out the response line and the headers;
	 * it does not copy the content. Hmm, maybe it does not even write
	 * out the response line.
	 */
	public void write(OutputStream out)
		throws IOException {
		super.write(out);
	}
}
