/*
* @(#)Response.java 0.3-3 06/05/2001
*
* This file is part of the HTTPClient package
* Copyright (C) 1996-2001 Ronald Tschal�r
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free
* Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307, USA
*
* For questions, suggestions, bug-reports, enhancement-requests etc.
* I may be contacted at:
*
* ronald@innovation.ch
*
* The HTTPClient's home page is located at:
*
* http://www.innovation.ch/java/HTTPClient/
*/
package org.exoplatform.common.http.client;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.SequenceInputStream;
import java.io.UnsupportedEncodingException;
import java.net.ProtocolException;
import java.net.URL;
import java.util.Date;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.Vector;
/**
* This class represents an intermediate response. It's used internally by the
* modules. When all modules have handled the response then the HTTPResponse
* fills in its fields with the data from this class.
* @version 0.3-3 06/05/2001
* @author Ronald Tschal�r
*/
public final class Response implements RoResponse, GlobalConstants, Cloneable
{
/** This contains a list of headers which may only have a single value */
private static final Hashtable singleValueHeaders;
/** our http connection */
private HTTPConnection connection;
/** our stream demux */
private StreamDemultiplexor stream_handler;
/** the HTTPResponse we're coupled with */
HTTPResponse http_resp;
/** the timeout for read operations */
int timeout = 0;
/**
* our input stream (usually from the stream demux). Push input streams onto
* this if necessary.
*/
public InputStream inp_stream;
/** our response input stream from the stream demux */
private RespInputStream resp_inp_stream = null;
/** the method used in the request */
private String method;
/** the resource in the request (for debugging purposes) */
String resource;
/** was a proxy used for the request? */
private boolean used_proxy;
/** did the request contain an entity? */
private boolean sent_entity;
/** the status code returned. */
int StatusCode = 0;
/** the reason line associated with the status code. */
String ReasonLine;
/** the HTTP version of the response. */
String Version;
/** the final URI of the document. */
URI EffectiveURI = null;
/** any headers which were received and do not fit in the above list. */
CIHashtable Headers = new CIHashtable();
/** any trailers which were received and do not fit in the above list. */
CIHashtable Trailers = new CIHashtable();
/**
* the message length of the response if either there is no data (in which
* case ContentLength=0) or if the message length is controlled by a
* Content-Length header. If neither of these, then it's -1
*/
int ContentLength = -1;
/** this indicates how the length of the entity body is determined */
int cd_type = CD_HDRS;
/** the data (body) returned. */
byte[] Data = null;
/** signals if we in the process of reading the headers */
boolean reading_headers = false;
/** signals if we have got and parsed the headers yet */
boolean got_headers = false;
/** signals if we have got and parsed the trailers yet */
boolean got_trailers = false;
/** remembers any exception received while reading/parsing headers */
private IOException exception = null;
/** should this response be handled further? */
boolean final_resp = false;
/** should the request be retried by the application? */
boolean retry = false;
private static final Log log = ExoLogger.getLogger("exo.ws.commons.Response");
static
{
/*
* This static initializer creates a hashtable of header names that should
* only have at most a single value in a server response. Other headers that
* may have multiple values (ie Set-Cookie) will have their values combined
* into one header, with individual values being separated by commas.
*/
String[] singleValueHeaderNames =
{"age", "location", "content-base", "content-length", "content-location", "content-md5", "content-range",
"content-type", "date", "etag", "expires", "proxy-authenticate", "retry-after",};
singleValueHeaders = new Hashtable(singleValueHeaderNames.length);
for (int idx = 0; idx < singleValueHeaderNames.length; idx++)
singleValueHeaders.put(singleValueHeaderNames[idx], singleValueHeaderNames[idx]);
}
// Constructors
/**
* Creates a new Response and registers it with the stream-demultiplexor.
*/
Response(Request request, boolean used_proxy, StreamDemultiplexor stream_handler) throws IOException
{
this.connection = request.getConnection();
this.method = request.getMethod();
this.resource = request.getRequestURI();
this.used_proxy = used_proxy;
this.stream_handler = stream_handler;
sent_entity = (request.getData() != null) ? true : false;
stream_handler.register(this, request);
resp_inp_stream = stream_handler.getStream(this);
inp_stream = resp_inp_stream;
}
/**
* Creates a new Response that reads from the given stream. This is used for
* the CONNECT subrequest which is used in establishing an SSL tunnel through
* a proxy.
* @param request the subrequest
* @param is the input stream from which to read the headers and data.
*/
Response(Request request, InputStream is) throws IOException
{
this.connection = request.getConnection();
this.method = request.getMethod();
this.resource = request.getRequestURI();
used_proxy = false;
stream_handler = null;
sent_entity = (request.getData() != null) ? true : false;
inp_stream = is;
}
/**
* Create a new response with the given info. This is used when creating a
* response in a requestHandler().
* <P>
* If <var>data</var> is not null then that is used; else if the <var>is</var>
* is not null that is used; else the entity is empty. If the input stream is
* used then <var>cont_len</var> specifies the length of the data that can be
* read from it, or -1 if unknown.
* @param version the response version (such as "HTTP/1.1")
* @param status the status code
* @param reason the reason line
* @param headers the response headers
* @param data the response entity
* @param is the response entity as an InputStream
* @param cont_len the length of the data in the InputStream
*/
public Response(String version, int status, String reason, NVPair[] headers, byte[] data, InputStream is,
int cont_len)
{
this.Version = version;
this.StatusCode = status;
this.ReasonLine = reason;
if (headers != null)
for (int idx = 0; idx < headers.length; idx++)
setHeader(headers[idx].getName(), headers[idx].getValue());
if (data != null)
this.Data = data;
else if (is == null)
this.Data = new byte[0];
else
{
this.inp_stream = is;
ContentLength = cont_len;
}
got_headers = true;
got_trailers = true;
}
// Methods
/**
* give the status code for this request. These are grouped as follows:
* <UL>
* <LI> 1xx - Informational (new in HTTP/1.1) <LI> 2xx - Success <LI> 3xx -
* Redirection <LI> 4xx - Client Error <LI> 5xx - Server Error
* </UL>
* @exception IOException If any exception occurs on the socket.
*/
public final int getStatusCode() throws IOException
{
if (!got_headers)
getHeaders(true);
return StatusCode;
}
/**
* give the reason line associated with the status code.
* @exception IOException If any exception occurs on the socket.
*/
public final String getReasonLine() throws IOException
{
if (!got_headers)
getHeaders(true);
return ReasonLine;
}
/**
* get the HTTP version used for the response.
* @exception IOException If any exception occurs on the socket.
*/
public final String getVersion() throws IOException
{
if (!got_headers)
getHeaders(true);
return Version;
}
/**
* Wait for either a '100 Continue' or an error.
* @return the return status.
*/
int getContinue() throws IOException
{
getHeaders(false);
return StatusCode;
}
/**
* get the final URI of the document. This is set if the original request was
* deferred via the "moved" (301, 302, or 303) return status.
* @return the new URI, or null if not redirected
* @exception IOException If any exception occurs on the socket.
*/
public final URI getEffectiveURI() throws IOException
{
if (!got_headers)
getHeaders(true);
return EffectiveURI;
}
/**
* set the final URI of the document. This is only for internal use.
*/
public void setEffectiveURI(URI final_uri)
{
EffectiveURI = final_uri;
}
/**
* get the final URL of the document. This is set if the original request was
* deferred via the "moved" (301, 302, or 303) return status.
* @exception IOException If any exception occurs on the socket.
* @deprecated use getEffectiveURI() instead
* @see #getEffectiveURI
*/
public final URL getEffectiveURL() throws IOException
{
return getEffectiveURI().toURL();
}
/**
* set the final URL of the document. This is only for internal use.
* @deprecated use setEffectiveURI() instead
* @see #setEffectiveURI
*/
public void setEffectiveURL(URL final_url)
{
try
{
setEffectiveURI(new URI(final_url));
}
catch (ParseException pe)
{
throw new Error(pe.toString());
} // shouldn't happen
}
/**
* retrieves the field for a given header.
* @param hdr the header name.
* @return the value for the header, or null if non-existent.
* @exception IOException If any exception occurs on the socket.
*/
public String getHeader(String hdr) throws IOException
{
if (!got_headers)
getHeaders(true);
return (String)Headers.get(hdr.trim());
}
/**
* retrieves the field for a given header. The value is parsed as an int.
* @param hdr the header name.
* @return the value for the header if the header exists
* @exception NumberFormatException if the header's value is not a number or
* if the header does not exist.
* @exception IOException if any exception occurs on the socket.
*/
public int getHeaderAsInt(String hdr) throws IOException, NumberFormatException
{
String val = getHeader(hdr);
if (val == null)
throw new NumberFormatException("null");
return Integer.parseInt(val);
}
/**
* retrieves the field for a given header. The value is parsed as a date; if
* this fails it is parsed as a long representing the number of seconds since
* 12:00 AM, Jan 1st, 1970. If this also fails an IllegalArgumentException is
* thrown.
* <P>
* Note: When sending dates use Util.httpDate().
* @param hdr the header name.
* @return the value for the header, or null if non-existent.
* @exception IOException If any exception occurs on the socket.
* @exception IllegalArgumentException If the header cannot be parsed as a
* date or time.
*/
public Date getHeaderAsDate(String hdr) throws IOException, IllegalArgumentException
{
String raw_date = getHeader(hdr);
if (raw_date == null)
return null;
// asctime() format is missing an explicit GMT specifier
if (raw_date.toUpperCase().indexOf("GMT") == -1 && raw_date.indexOf(' ') > 0)
raw_date += " GMT";
Date date;
try
{
date = Util.parseHttpDate(raw_date);
}
catch (IllegalArgumentException iae)
{
long time;
try
{
time = Long.parseLong(raw_date);
}
catch (NumberFormatException nfe)
{
throw iae;
}
if (time < 0)
time = 0;
date = new Date(time * 1000L);
}
return date;
}
/**
* Set a header field in the list of headers. If the header already exists it
* will be overwritten; otherwise the header will be added to the list. This
* is used by some modules when they process the header so that higher level
* stuff doesn't get confused when the headers and data don't match.
* @param header The name of header field to set.
* @param value The value to set the field to.
*/
public void setHeader(String header, String value)
{
Headers.put(header.trim(), value.trim());
}
/**
* Removes a header field from the list of headers. This is used by some
* modules when they process the header so that higher level stuff doesn't get
* confused when the headers and data don't match.
* @param header The name of header field to remove.
*/
public void deleteHeader(String header)
{
Headers.remove(header.trim());
}
/**
* Retrieves the field for a given trailer. Note that this should not be
* invoked until all the response data has been read. If invoked before, it
* will force the data to be read via <code>getData()</code>.
* @param trailer the trailer name.
* @return the value for the trailer, or null if non-existent.
* @exception IOException If any exception occurs on the socket.
*/
public String getTrailer(String trailer) throws IOException
{
if (!got_trailers)
getTrailers();
return (String)Trailers.get(trailer.trim());
}
/**
* Retrieves the field for a given tailer. The value is parsed as an int.
* @param trailer the tailer name.
* @return the value for the trailer if the trailer exists
* @exception NumberFormatException if the trailer's value is not a number or
* if the trailer does not exist.
* @exception IOException if any exception occurs on the socket.
*/
public int getTrailerAsInt(String trailer) throws IOException, NumberFormatException
{
String val = getTrailer(trailer);
if (val == null)
throw new NumberFormatException("null");
return Integer.parseInt(val);
}
/**
* Retrieves the field for a given trailer. The value is parsed as a date; if
* this fails it is parsed as a long representing the number of seconds since
* 12:00 AM, Jan 1st, 1970. If this also fails an IllegalArgumentException is
* thrown.
* <P>
* Note: When sending dates use Util.httpDate().
* @param trailer the trailer name.
* @return the value for the trailer, or null if non-existent.
* @exception IllegalArgumentException if the trailer's value is neither a
* legal date nor a number.
* @exception IOException if any exception occurs on the socket.
* @exception IllegalArgumentException If the header cannot be parsed as a
* date or time.
*/
public Date getTrailerAsDate(String trailer) throws IOException, IllegalArgumentException
{
String raw_date = getTrailer(trailer);
if (raw_date == null)
return null;
// asctime() format is missing an explicit GMT specifier
if (raw_date.toUpperCase().indexOf("GMT") == -1 && raw_date.indexOf(' ') > 0)
raw_date += " GMT";
Date date;
try
{
date = Util.parseHttpDate(raw_date);
}
catch (IllegalArgumentException iae)
{
// some servers erroneously send a number, so let's try that
long time;
try
{
time = Long.parseLong(raw_date);
}
catch (NumberFormatException nfe)
{
throw iae;
} // give up
if (time < 0)
time = 0;
date = new Date(time * 1000L);
}
return date;
}
/**
* Set a trailer field in the list of trailers. If the trailer already exists
* it will be overwritten; otherwise the trailer will be added to the list.
* This is used by some modules when they process the trailer so that higher
* level stuff doesn't get confused when the trailer and data don't match.
* @param trailer The name of trailer field to set.
* @param value The value to set the field to.
*/
public void setTrailer(String trailer, String value)
{
Trailers.put(trailer.trim(), value.trim());
}
/**
* Removes a trailer field from the list of trailers. This is used by some
* modules when they process the trailer so that higher level stuff doesn't
* get confused when the trailers and data don't match.
* @param trailer The name of trailer field to remove.
*/
public void deleteTrailer(String trailer)
{
Trailers.remove(trailer.trim());
}
/**
* Reads all the response data into a byte array. Note that this method won't
* return until <em>all</em> the data has been received (so for instance don't
* invoke this method if the server is doing a server push). If
* getInputStream() had been previously called then this method only returns
* any unread data remaining on the stream and then closes it.
* @see #getInputStream()
* @return an array containing the data (body) returned. If no data was
* returned then it's set to a zero-length array.
* @exception IOException If any io exception occured while reading the data
*/
public synchronized byte[] getData() throws IOException
{
if (!got_headers)
getHeaders(true);
if (Data == null)
{
try
{
readResponseData(inp_stream);
}
catch (InterruptedIOException ie) // don't intercept
{
throw ie;
}
catch (IOException ioe)
{
log.error("Response: (" + inp_stream.hashCode() + ")", ioe);
try
{
inp_stream.close();
}
catch (Exception e)
{
}
throw ioe;
}
inp_stream.close();
}
return Data;
}
/**
* Gets an input stream from which the returned data can be read. Note that if
* getData() had been previously called it will actually return a
* ByteArrayInputStream created from that data.
* @see #getData()
* @return the InputStream.
* @exception IOException If any exception occurs on the socket.
*/
public synchronized InputStream getInputStream() throws IOException
{
if (!got_headers)
getHeaders(true);
if (Data == null)
return inp_stream;
else
return new ByteArrayInputStream(Data);
}
/**
* Some responses such as those from a HEAD or with certain status codes don't
* have an entity. This is detected by the client and can be queried here.
* Note that this won't try to do a read() on the input stream (it will
* however cause the headers to be read and parsed if not already done).
* @return true if the response has an entity, false otherwise
* @since V0.3-1
*/
public synchronized boolean hasEntity() throws IOException
{
if (!got_headers)
getHeaders(true);
return (cd_type != CD_0);
}
/**
* Should the request be retried by the application? This can be used by
* modules to signal to the application that it should retry the request. It's
* used when the request used an <var>HttpOutputStream</var> and the module is
* therefore not able to retry the request itself. This flag is
* <var>false</var> by default.
* <P>
* If a module sets this flag then it must also reset() the the
* <var>HttpOutputStream</var> so it may be reused by the application. It
* should then also use this <var>HttpOutputStream</var> to recognize the
* retried request in the requestHandler().
* @param flag indicates whether the application should retry the request.
*/
public void setRetryRequest(boolean flag)
{
retry = flag;
}
/**
* @return true if the request should be retried.
*/
public boolean retryRequest()
{
return retry;
}
// Helper Methods
/**
* Gets and parses the headers. Sets up Data if no data will be received.
* @param skip_cont if true skips over '100 Continue' status codes.
* @exception IOException If any exception occurs while reading the headers.
*/
private synchronized void getHeaders(boolean skip_cont) throws IOException
{
if (got_headers)
return;
if (exception != null)
{
exception.fillInStackTrace();
throw exception;
}
reading_headers = true;
try
{
do
{
Headers.clear(); // clear any headers from 100 Continue
String headers = readResponseHeaders(inp_stream);
parseResponseHeaders(headers);
}
while ((StatusCode == 100 && skip_cont) || // Continue
(StatusCode > 101 && StatusCode < 200)); // Unknown
}
catch (IOException ioe)
{
if (!(ioe instanceof InterruptedIOException))
exception = ioe;
if (ioe instanceof ProtocolException) // thrown internally
{
cd_type = CD_CLOSE;
if (stream_handler != null)
stream_handler.markForClose(this);
}
throw ioe;
}
finally
{
reading_headers = false;
}
if (StatusCode == 100)
return;
// parse the Content-Length header
int cont_len = -1;
String cl_hdr = (String)Headers.get("Content-Length");
if (cl_hdr != null)
{
try
{
cont_len = Integer.parseInt(cl_hdr);
if (cont_len < 0)
throw new NumberFormatException();
}
catch (NumberFormatException nfe)
{
throw new ProtocolException("Invalid Content-length header" + " received: " + cl_hdr);
}
}
// parse the Transfer-Encoding header
boolean te_chunked = false, te_is_identity = true, ct_mpbr = false;
Vector te_hdr = null;
try
{
te_hdr = Util.parseHeader((String)Headers.get("Transfer-Encoding"));
}
catch (ParseException pe)
{
}
if (te_hdr != null)
{
te_chunked = ((HttpHeaderElement)te_hdr.lastElement()).getName().equalsIgnoreCase("chunked");
for (int idx = 0; idx < te_hdr.size(); idx++)
if (((HttpHeaderElement)te_hdr.elementAt(idx)).getName().equalsIgnoreCase("identity"))
te_hdr.removeElementAt(idx--);
else
te_is_identity = false;
}
// parse Content-Type header
try
{
String hdr;
if ((hdr = (String)Headers.get("Content-Type")) != null)
{
Vector phdr = Util.parseHeader(hdr);
ct_mpbr =
phdr.contains(new HttpHeaderElement("multipart/byteranges"))
|| phdr.contains(new HttpHeaderElement("multipart/x-byteranges"));
}
}
catch (ParseException pe)
{
}
// now determine content-delimiter
if (StatusCode < 200 || StatusCode == 204 || StatusCode == 205 || StatusCode == 304)
{
cd_type = CD_0;
}
else if (te_chunked)
{
cd_type = CD_CHUNKED;
te_hdr.removeElementAt(te_hdr.size() - 1);
if (te_hdr.size() > 0)
setHeader("Transfer-Encoding", Util.assembleHeader(te_hdr));
else
deleteHeader("Transfer-Encoding");
}
else if (cont_len != -1 && te_is_identity)
cd_type = CD_CONTLEN;
else if (ct_mpbr && te_is_identity)
cd_type = CD_MP_BR;
else if (!method.equals("HEAD"))
{
cd_type = CD_CLOSE;
if (stream_handler != null)
stream_handler.markForClose(this);
if (Version.equals("HTTP/0.9"))
{
inp_stream = new SequenceInputStream(new ByteArrayInputStream(Data), inp_stream);
Data = null;
}
}
if (cd_type == CD_CONTLEN)
ContentLength = cont_len;
else
deleteHeader("Content-Length"); // Content-Length is not valid in this
// case
/*
* We treat HEAD specially down here because the above code needs to know
* whether to remove the Content-length header or not.
*/
if (method.equals("HEAD"))
cd_type = CD_0;
if (cd_type == CD_0)
{
ContentLength = 0;
Data = new byte[0];
inp_stream.close(); // we will not receive any more data
}
if (log.isDebugEnabled())
{
log.debug("Response entity delimiter: "
+ (cd_type == CD_0 ? "No Entity" : cd_type == CD_CLOSE ? "Close" : cd_type == CD_CONTLEN ? "Content-Length"
: cd_type == CD_CHUNKED ? "Chunked" : cd_type == CD_MP_BR ? "Multipart" : "???") + " ("
+ inp_stream.hashCode() + ")");
}
// remove erroneous connection tokens
if (connection.ServerProtocolVersion >= HTTP_1_1)
deleteHeader("Proxy-Connection");
else
// HTTP/1.0
{
if (connection.getProxyHost() != null)
deleteHeader("Connection");
else
deleteHeader("Proxy-Connection");
Vector pco;
try
{
pco = Util.parseHeader((String)Headers.get("Connection"));
}
catch (ParseException pe)
{
pco = null;
}
if (pco != null)
{
for (int idx = 0; idx < pco.size(); idx++)
{
String name = ((HttpHeaderElement)pco.elementAt(idx)).getName();
if (!name.equalsIgnoreCase("keep-alive"))
{
pco.removeElementAt(idx);
deleteHeader(name);
idx--;
}
}
if (pco.size() > 0)
setHeader("Connection", Util.assembleHeader(pco));
else
deleteHeader("Connection");
}
try
{
pco = Util.parseHeader((String)Headers.get("Proxy-Connection"));
}
catch (ParseException pe)
{
pco = null;
}
if (pco != null)
{
for (int idx = 0; idx < pco.size(); idx++)
{
String name = ((HttpHeaderElement)pco.elementAt(idx)).getName();
if (!name.equalsIgnoreCase("keep-alive"))
{
pco.removeElementAt(idx);
deleteHeader(name);
idx--;
}
}
if (pco.size() > 0)
setHeader("Proxy-Connection", Util.assembleHeader(pco));
else
deleteHeader("Proxy-Connection");
}
}
// this must be set before we invoke handleFirstRequest()
got_headers = true;
// special handling if this is the first response received
if (isFirstResponse)
{
if (!connection.handleFirstRequest(req, this))
{
// got a buggy server - need to redo the request
Response resp;
try
{
resp = connection.sendRequest(req, timeout);
}
catch (ModuleException me)
{
throw new IOException(me.toString());
}
resp.getVersion();
this.StatusCode = resp.StatusCode;
this.ReasonLine = resp.ReasonLine;
this.Version = resp.Version;
this.EffectiveURI = resp.EffectiveURI;
this.ContentLength = resp.ContentLength;
this.Headers = resp.Headers;
this.inp_stream = resp.inp_stream;
this.Data = resp.Data;
req = null;
}
}
}
/*
* these are external to readResponseHeaders() because we need to be able to
* restart after an InterruptedIOException
*/
private byte[] buf = new byte[7];
private int buf_pos = 0;
private StringBuffer hdrs = new StringBuffer(400);
private boolean reading_lines = false;
private boolean bol = true;
private boolean got_cr = false;
/**
* Reads the response headers received, folding continued lines.
* <P>
* Some of the code is a bit convoluted because we have to be able restart
* after an InterruptedIOException.
* @inp the input stream from which to read the response
* @return a (newline separated) list of headers
* @exception IOException if any read on the input stream fails
*/
private String readResponseHeaders(InputStream inp) throws IOException
{
if (log.isDebugEnabled())
{
if (buf_pos == 0)
log.debug("Reading Response headers " + inp_stream.hashCode());
else
log.debug("Resuming reading Response headers " + inp_stream.hashCode());
}
// read 7 bytes to see type of response
if (!reading_lines)
{
try
{
// Skip any leading white space to accomodate buggy responses
if (buf_pos == 0)
{
int c;
do
{
if ((c = inp.read()) == -1)
throw new EOFException("Encountered premature EOF " + "while reading Version");
}
while (Character.isWhitespace((char)c));
buf[0] = (byte)c;
buf_pos = 1;
}
// Now read first seven bytes (the version string)
while (buf_pos < buf.length)
{
int got = inp.read(buf, buf_pos, buf.length - buf_pos);
if (got == -1)
throw new EOFException("Encountered premature EOF " + "while reading Version");
buf_pos += got;
}
}
catch (EOFException eof)
{
log.error("Response: (" + inp_stream.hashCode() + ")", eof);
throw eof;
}
for (int idx = 0; idx < buf.length; idx++)
hdrs.append((char)buf[idx]);
reading_lines = true;
}
if (hdrs.toString().startsWith("HTTP/") || // It's x.x
hdrs.toString().startsWith("HTTP ")) // NCSA bug
readLines(inp);
// reset variables for next round
buf_pos = 0;
reading_lines = false;
bol = true;
got_cr = false;
String tmp = hdrs.toString();
hdrs.setLength(0);
return tmp;
}
boolean trailers_read = false;
/**
* This is called by the StreamDemultiplexor to read all the trailers of a
* chunked encoded entity.
* @param inp the raw input stream to read from
* @exception IOException if any IOException is thrown by the stream
*/
void readTrailers(InputStream inp) throws IOException
{
try
{
readLines(inp);
trailers_read = true;
}
catch (IOException ioe)
{
if (!(ioe instanceof InterruptedIOException))
exception = ioe;
throw ioe;
}
}
/**
* This reads a set of lines up to and including the first empty line. A line
* is terminated by either a <CR><LF> or <LF>. The lines are stored in the
* <var>hdrs</var> buffers. Continued lines are merged and stored as one line.
* <P>
* This method is restartable after an InterruptedIOException.
* @param inp the input stream to read from
* @exception IOException if any IOException is thrown by the stream
*/
private void readLines(InputStream inp) throws IOException
{
/*
* This loop is a merge of readLine() from DataInputStream and the necessary
* header logic to merge continued lines and terminate after an empty line.
* The reason this is explicit is because of the need to handle
* InterruptedIOExceptions.
*/
loop : while (true)
{
int b = inp.read();
switch (b)
{
case -1 :
throw new EOFException("Encountered premature EOF while reading headers:\n" + hdrs);
case '\r' :
got_cr = true;
break;
case '\n' :
if (bol)
break loop; // all headers read
hdrs.append('\n');
bol = true;
got_cr = false;
break;
case ' ' :
case '\t' :
if (bol) // a continued line
{
// replace previous \n with SP
hdrs.setCharAt(hdrs.length() - 1, ' ');
bol = false;
break;
}
default :
if (got_cr)
{
hdrs.append('\r');
got_cr = false;
}
hdrs.append((char)(b & 0xFF));
bol = false;
break;
}
}
}
/**
* Parses the headers received into a new Response structure.
* @param headers a (newline separated) list of headers
* @exception ProtocolException if any part of the headers do not conform
*/
private void parseResponseHeaders(String headers) throws ProtocolException
{
String sts_line = null;
StringTokenizer lines = new StringTokenizer(headers, "\r\n"), elem;
if (log.isDebugEnabled())
log.debug("Parsing Response headers from Request " + "\"" + method + " " + resource + "\": ("
+ inp_stream.hashCode() + ")\n\n" + headers);
// Detect and handle HTTP/0.9 responses
if (!headers.regionMatches(true, 0, "HTTP/", 0, 5) && !headers.regionMatches(true, 0, "HTTP ", 0, 5)) // NCSA bug
{
Version = "HTTP/0.9";
StatusCode = 200;
ReasonLine = "OK";
try
{
Data = headers.getBytes("8859_1");
}
catch (UnsupportedEncodingException uee)
{
throw new Error(uee.toString());
}
return;
}
// get the status line
try
{
sts_line = lines.nextToken();
elem = new StringTokenizer(sts_line, " \t");
Version = elem.nextToken();
StatusCode = Integer.valueOf(elem.nextToken()).intValue();
if (Version.equalsIgnoreCase("HTTP")) // NCSA bug
Version = "HTTP/1.0";
}
catch (NoSuchElementException e)
{
throw new ProtocolException("Invalid HTTP status line received: " + sts_line);
}
try
{
ReasonLine = elem.nextToken("").trim();
}
catch (NoSuchElementException e)
{
ReasonLine = "";
}
/*
* If the status code shows an error and we're sending (or have sent) an
* entity and it's length is delimited by a Content-length header, then we
* must close the the connection (if indeed it hasn't already been done) -
* RFC-2616, Section 8.2.2 .
*/
if (StatusCode >= 300 && sent_entity)
{
if (stream_handler != null)
stream_handler.markForClose(this);
}
// get the rest of the headers
parseHeaderFields(lines, Headers);
/*
* make sure the connection isn't closed prematurely if we have trailer
* fields
*/
if (Headers.get("Trailer") != null && resp_inp_stream != null)
resp_inp_stream.dontTruncate();
// Mark the end of the connection if it's not to be kept alive
int vers;
if (Version.equalsIgnoreCase("HTTP/0.9") || Version.equalsIgnoreCase("HTTP/1.0"))
vers = 0;
else
vers = 1;
try
{
String con = (String)Headers.get("Connection"), pcon = (String)Headers.get("Proxy-Connection");
// parse connection header
if ((vers == 1 && con != null && Util.hasToken(con, "close"))
|| (vers == 0 && !((!used_proxy && con != null && Util.hasToken(con, "keep-alive")) || (used_proxy
&& pcon != null && Util.hasToken(pcon, "keep-alive")))))
if (stream_handler != null)
stream_handler.markForClose(this);
}
catch (ParseException pe)
{
}
}
/**
* If the trailers have not been read it calls <code>getData()</code> to first
* force all data and trailers to be read. Then the trailers parsed into the
* <var>Trailers</var> hashtable.
* @exception IOException if any exception occured during reading of the
* response
*/
private synchronized void getTrailers() throws IOException
{
if (got_trailers)
return;
if (exception != null)
{
exception.fillInStackTrace();
throw exception;
}
if (log.isDebugEnabled())
log.debug("Reading Response trailers " + inp_stream.hashCode());
try
{
if (!trailers_read)
{
if (resp_inp_stream != null)
resp_inp_stream.readAll(timeout);
}
if (trailers_read)
{
if (log.isDebugEnabled())
log.debug("Parsing Response trailers from " + "Request \"" + method + " " + resource + "\": ("
+ inp_stream.hashCode() + ")\n\n" + hdrs);
parseHeaderFields(new StringTokenizer(hdrs.toString(), "\r\n"), Trailers);
}
}
finally
{
got_trailers = true;
}
}
/**
* Parses the given lines as header fields of the form "<name>: <value>" into
* the given list.
* @param lines the header or trailer lines, one header field per line
* @param list the Hashtable to store the parsed fields in
* @exception ProtocolException if any part of the headers do not conform
*/
private void parseHeaderFields(StringTokenizer lines, CIHashtable list) throws ProtocolException
{
while (lines.hasMoreTokens())
{
String hdr = lines.nextToken();
int sep = hdr.indexOf(':');
/*
* Once again we have to deal with broken servers and try to wing it here.
* If no ':' is found, try using the first space:
*/
if (sep == -1)
sep = hdr.indexOf(' ');
if (sep == -1)
{
throw new ProtocolException("Invalid HTTP header received: " + hdr);
}
String hdr_name = hdr.substring(0, sep).trim();
String hdr_value = hdr.substring(sep + 1).trim();
// Can header have multiple values?
if (!singleValueHeaders.containsKey(hdr_name.toLowerCase()))
{
String old_value = (String)list.get(hdr_name);
if (old_value == null)
list.put(hdr_name, hdr_value);
else
list.put(hdr_name, old_value + ", " + hdr_value);
}
else
// No multiple values--just replace/put latest header value
list.put(hdr_name, hdr_value);
}
}
/**
* Reads the response data received. Does not return until either
* Content-Length bytes have been read or EOF is reached.
* @inp the input stream from which to read the data
* @exception IOException if any read on the input stream fails
*/
private void readResponseData(InputStream inp) throws IOException
{
if (ContentLength == 0)
return;
if (Data == null)
Data = new byte[0];
// read response data
int off = Data.length;
try
{
// check Content-length header in case CE-Module removed it
if (getHeader("Content-Length") != null)
{
int rcvd = 0;
Data = new byte[ContentLength];
do
{
off += rcvd;
rcvd = inp.read(Data, off, ContentLength - off);
}
while (rcvd != -1 && off + rcvd < ContentLength);
/*
* Don't do this! If we do, then getData() won't work after a
* getInputStream() because we'll never get all the expected data.
* Instead, let the underlying RespInputStream throw the EOF. if (rcvd
* == -1) // premature EOF { throw new EOFException("Encountered
* premature EOF while " + "reading headers: received " + off + " bytes
* instead of the expected " + ContentLength + " bytes"); }
*/
}
else
{
int inc = 1000, rcvd = 0;
do
{
off += rcvd;
Data = Util.resizeArray(Data, off + inc);
}
while ((rcvd = inp.read(Data, off, inc)) != -1);
Data = Util.resizeArray(Data, off);
}
}
catch (IOException ioe)
{
Data = Util.resizeArray(Data, off);
throw ioe;
}
finally
{
try
{
inp.close();
}
catch (IOException ioe)
{
}
}
}
Request req = null;
boolean isFirstResponse = false;
/**
* This marks this response as belonging to the first request made over an
* HTTPConnection. The <var>con</var> and <var>req</var> parameters are needed
* in case we have to do a resend of the request - this is to handle buggy
* servers which barf upon receiving a request marked as HTTP/1.1 .
* @param con The HTTPConnection used
* @param req The Request sent
*/
void markAsFirstResponse(Request req)
{
this.req = req;
isFirstResponse = true;
}
/**
* @return a clone of this request object
*/
public Object clone()
{
Response cl;
try
{
cl = (Response)super.clone();
}
catch (CloneNotSupportedException cnse)
{
throw new InternalError(cnse.toString()); /* shouldn't happen */
}
cl.Headers = (CIHashtable)Headers.clone();
cl.Trailers = (CIHashtable)Trailers.clone();
return cl;
}
}