/* * @(#)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 HTTPClient; import java.io.InputStream; import java.io.SequenceInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InterruptedIOException; import java.io.EOFException; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.ProtocolException; import java.util.Date; import java.util.Vector; import java.util.Hashtable; import java.util.StringTokenizer; import java.util.NoSuchElementException; /** * 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; 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; idxIf data is not null then that is used; else if the * is is not null that is used; else the entity is empty. * If the input stream is used then cont_len 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 *
  • 1xx - Informational (new in HTTP/1.1) *
  • 2xx - Success *
  • 3xx - Redirection *
  • 4xx - Client Error *
  • 5xx - Server Error * * * @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. * *

    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 getData(). * * @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. * *

    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 all 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.write(Log.RESP, "Resp: (" + 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 HttpOutputStream * and the module is therefore not able to retry the request itself. * This flag is false by default. * *

    If a module sets this flag then it must also reset() the * the HttpOutputStream so it may be reused by the application. * It should then also use this HttpOutputStream 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 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 } Log.write(Log.RESP, "Resp: 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 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 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. * *

    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 (buf_pos == 0) Log.write(Log.RESP, "Resp: Reading Response headers " + inp_stream.hashCode()); else Log.write(Log.RESP, "Resp: 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.write(Log.RESP, "Resp: (" + inp_stream.hashCode() + ")", eof); throw eof; } for (int idx=0; idx or . The lines are * stored in the hdrs buffers. Continued lines are merged * and stored as one line. * *

    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.isEnabled(Log.RESP)) Log.write(Log.RESP, "Resp: 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 getData() * to first force all data and trailers to be read. Then the trailers * parsed into the Trailers 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; } Log.write(Log.RESP, "Resp: Reading Response trailers " + inp_stream.hashCode()); try { if (!trailers_read) { if (resp_inp_stream != null) resp_inp_stream.readAll(timeout); } if (trailers_read) { Log.write(Log.RESP, "Resp: 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 ": " * 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 con and req * 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; } }