You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@commons.apache.org by og...@apache.org on 2002/12/09 10:16:17 UTC

cvs commit: jakarta-commons/httpclient/src/test/org/apache/commons/httpclient TestGetMethodLocal.java TestHttpClientLocalHost.java TestMethodsLocalHost.java TestMethodsNoHost.java

oglueck     2002/12/09 01:16:17

  Modified:    httpclient/src/java/org/apache/commons/httpclient
                        AutoCloseInputStream.java ChunkedInputStream.java
                        ContentLengthInputStream.java HttpConnection.java
                        HttpMethodBase.java
                        MultiThreadedHttpConnectionManager.java
                        SimpleHttpConnectionManager.java
               httpclient/src/java/org/apache/commons/httpclient/methods
                        GetMethod.java HeadMethod.java PostMethod.java
               httpclient/src/test/org/apache/commons/httpclient
                        TestGetMethodLocal.java
                        TestHttpClientLocalHost.java
                        TestMethodsLocalHost.java TestMethodsNoHost.java
  Added:       httpclient/src/java/org/apache/commons/httpclient
                        ResponseConsumedWatcher.java
  Log:
  Handle closing of streams properly to avoid response parse failures.
  
  Contributed by: Eric Johnson.
  
  Revision  Changes    Path
  1.3       +91 -23    jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/AutoCloseInputStream.java
  
  Index: AutoCloseInputStream.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/AutoCloseInputStream.java,v
  retrieving revision 1.2
  retrieving revision 1.3
  diff -u -r1.2 -r1.3
  --- AutoCloseInputStream.java	9 Sep 2002 05:43:39 -0000	1.2
  +++ AutoCloseInputStream.java	9 Dec 2002 09:16:16 -0000	1.3
  @@ -67,44 +67,58 @@
   import java.io.InputStream;
   
   /**
  - * Closes a HttpConnection as soon as the end of the stream is reached.
  + * Closes an underlying stream as soon as the end of the stream is reached, and
  + * notifies a client when it has done so.
  + *
    * @author Ortwin Gl�ck
  + * @author Eric Johnson
    *
    * @since 2.0
    */
   
   class AutoCloseInputStream extends FilterInputStream {
  -    /** the connection the input stream comes from */
  -    private HttpConnection conn;
  +
  +    // assume that the underlying stream is open until we get an EOF indication.
  +    private boolean streamOpen = true;
  +
  +    private boolean selfClosed = false;
  +
  +    /** The watcher is notified when the contents of the stream have been exhausted */
  +    private ResponseConsumedWatcher watcher = null;
   
       /**
        * Create a new auto closing stream for the provided connection
  -     * 
  +     *
        * @param in the input stream to read from
  -     * @param conn the connection to close when done reading
  +     * @param watcher   To be notified when the contents of the stream have been
  +     *  consumed.
        */
  -    public AutoCloseInputStream(InputStream in, HttpConnection conn) {
  +    public AutoCloseInputStream(InputStream in, ResponseConsumedWatcher watcher) {
           super(in);
  -        this.conn = conn;
  +        this.watcher = watcher;
       }
   
       /**
        * Reads the next byte of data from the input stream.
  -     * 
  +     *
        * @throws IOException when there is an error reading
        * @return the character read, or -1 for EOF
        */
       public int read() throws IOException {
  -        int l = super.read();
  -        if (l == -1) {
  -            conn.close();
  +        int l = -1;
  +
  +        if (isReadAllowed()) {
  +            // underlying stream not closed, go ahead and read.
  +            l = super.read();
  +            checkClose(l);
           }
  +
           return l;
       }
   
       /**
        * Reads up to <code>len</code> bytes of data from the stream.
  -     * 
  +     *
        * @param b a <code>byte</code> array to read data into
        * @param off an offset within the array to store data
        * @param len the maximum number of bytes to read
  @@ -112,26 +126,80 @@
        * @throws IOException if there are errors reading
        */
       public int read(byte[] b, int off, int len) throws IOException {
  -        int l = super.read(b,  off,  len);
  -        if (l == -1) {
  -            conn.close();
  +        int l = -1;
  +
  +        if ( isReadAllowed() ) {
  +            l = super.read(b,  off,  len);
  +            checkClose(l);
           }
  +
           return l;
       }
   
       /**
        * Reads some number of bytes from the input stream and stores them into the
        * buffer array b.
  -     * 
  +     *
        * @param b a <code>byte</code> array to read data into
        * @return the number of bytes read or -1 for EOF
        * @throws IOException if there are errors reading
        */
       public int read(byte[] b) throws IOException {
  -        int l = super.read(b);
  -        if (l == -1) {
  -            conn.close();
  +        int l = -1;
  +
  +        if ( isReadAllowed() ) {
  +            l = super.read(b);
  +            checkClose(l);
           }
           return l;
       }
  -}
  \ No newline at end of file
  +
  +    /**
  +     * Close the stream, and also close the underlying stream if it is not
  +     * already closed.
  +     */
  +    public void close() throws IOException {
  +        if (!selfClosed) {
  +            selfClosed = true;
  +            notifyWatcher();
  +        }
  +    }
  +
  +    /**
  +     * Close the underlying stream should the end of the stream arrive.
  +     *
  +     * @param readResult    The result of the read operation to check.
  +     */
  +    private void checkClose(int readResult) throws IOException {
  +        if (readResult == -1) {
  +            notifyWatcher();
  +        }
  +    }
  +
  +    /**
  +     * See whether a read of the underlying stream should be allowed, and if
  +     * not, check to see whether our stream has already been closed!
  +     *
  +     * @return <code>true</code> if it is still OK to read from the stream.
  +     */
  +    private boolean isReadAllowed() throws IOException {
  +        if (!streamOpen && selfClosed) {
  +            throw new IOException("Attempted read on closed stream.");
  +        }
  +        return streamOpen;
  +    }
  +
  +    /**
  +     * Notify the watcher that the contents have been consumed.
  +     */
  +    private void notifyWatcher() throws IOException {
  +        if (streamOpen) {
  +            super.close();
  +            streamOpen = false;
  +
  +            if (watcher != null)
  +                watcher.responseConsumed();
  +        }
  +    }
  +}
  +
  
  
  
  1.7       +52 -6     jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ChunkedInputStream.java
  
  Index: ChunkedInputStream.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ChunkedInputStream.java,v
  retrieving revision 1.6
  retrieving revision 1.7
  diff -u -r1.6 -r1.7
  --- ChunkedInputStream.java	16 Oct 2002 13:14:11 -0000	1.6
  +++ ChunkedInputStream.java	9 Dec 2002 09:16:16 -0000	1.7
  @@ -63,17 +63,22 @@
   package org.apache.commons.httpclient;
   
   import java.io.*;
  -import java.util.*;
   
   /**
    * <p>Transparently coalesces chunks of a HTTP stream that uses Transfer-Encoding
    * chunked.</p>
    *
  + * <p>Note that this class NEVER closes the underlying stream, even when close gets
  + * called.  Instead, it will read until the "end" of its chunking on close, which
  + * allows for the seamless invocation of subsequent HTTP 1.1 calls, while not
  + * requiring the client to remember to read the entire contents of the response.</p>
  + *
    * @see ResponseInputStream
    *
    * @author Ortwin Gl�ck
    * @author Sean C. Sullivan
    * @author Martin Elwin
  + * @author Eric Johnson
    *
    * @since 2.0
    *
  @@ -83,6 +88,7 @@
       private InputStream in;
       private int chunkSize, pos;
       private boolean eof = false;
  +    private boolean closed = false;
       private static final String HTTP_ENC = "US-ASCII";
       private HttpMethod method;
   
  @@ -96,7 +102,7 @@
        * @throws java.lang.NullPointerException
        *
        */
  -    public ChunkedInputStream(final InputStream in, final HttpMethod method) throws IOException {
  +    public ChunkedInputStream(InputStream in, HttpMethod method) throws IOException {
         if (null == in) {
           throw new NullPointerException("InputStream parameter");
         }
  @@ -124,6 +130,9 @@
        * @throws IOException
        */
       public int read() throws IOException {
  +
  +        if (closed)
  +            throw new IOException("Attempted read from closed stream.");
           if (eof) return -1;
           if (pos >= chunkSize) {
               nextChunk();
  @@ -134,6 +143,10 @@
       }
   
       public int read(byte[] b, int off, int len) throws java.io.IOException {
  +
  +        if (closed)
  +            throw new IOException("Attempted read from closed stream.");
  +
           if (eof) return -1;
           if (pos >= chunkSize) {
               nextChunk();
  @@ -274,7 +287,40 @@
           return (buf.toString());
       }
   
  +    /**
  +     * Upon close, this reads the remainder of the chunked message,
  +     * leaving the underlying socket at a position to start reading the
  +     * next response without scanning.
  +     */
       public void close() throws IOException {
  -        in.close();
  +        if (!closed) {
  +            try {
  +                if (!eof) {
  +                    exhaustInputStream(this);
  +                }
  +            }
  +            finally {
  +                eof = true;
  +                closed = true;
  +            }
  +        }
  +    }
  +
  +    /**
  +     * Exhaust an input stream, reading until EOF has been encountered.
  +     *
  +     * <p>Note that this function is intended as a non-public utility.
  +     * This is a little weird, but it seemed silly to make a utility
  +     * class for this one function, so instead it is just static and
  +     * shared that way.</p>
  +     *
  +     * @param inStream The {@link InputStream} to exhaust.
  +     */
  +    static void exhaustInputStream(InputStream inStream) throws IOException {
  +        // read and discard the remainder of the message
  +        byte buffer[] = new byte[1024];
  +        while ( inStream.read(buffer) >= 0) {
  +            ;
  +        }
       }
   }
  
  
  
  1.3       +47 -6     jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ContentLengthInputStream.java
  
  Index: ContentLengthInputStream.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ContentLengthInputStream.java,v
  retrieving revision 1.2
  retrieving revision 1.3
  diff -u -r1.2 -r1.3
  --- ContentLengthInputStream.java	19 Sep 2002 10:15:08 -0000	1.2
  +++ ContentLengthInputStream.java	9 Dec 2002 09:16:16 -0000	1.3
  @@ -68,6 +68,7 @@
    * Cuts the wrapped InputStream off after a specified number of bytes.
    *
    * @author Ortwin Gl�ck
  + * @author Eric Johnson
    * @since 2.0
    */
   
  @@ -75,6 +76,8 @@
       private int contentLength;
       private int pos = 0;
   
  +    private boolean closed = false;
  +
       /**
        * Creates a new length limited stream
        *
  @@ -87,15 +90,53 @@
           this.contentLength = contentLength;
       }
   
  +    /**
  +     * Reads until the end of the known length of content.
  +     *
  +     * <p>Does not close the underlying socket input, but instead leaves it
  +     * primed to parse the next response.</p>
  +     */
  +    public void close() throws IOException {
  +        if (!closed) {
  +            try {
  +                ChunkedInputStream.exhaustInputStream(this);
  +            } finally {
  +                // close after above so that we don't throw an exception trying
  +                // to read after closed!
  +                closed = true;
  +            }
  +        }
  +    }
  +
       public int read() throws java.io.IOException {
  -        if (pos >= contentLength) return -1;
  +        if (closed)
  +            throw new IOException("Attempted read from closed stream.");
  +
  +        if (pos >= contentLength)
  +            return -1;
           pos++;
           return super.read();
       }
   
  -
  +    /**
  +     * Does standard {@link InputStream#read(byte[], int, int)} behavior, but
  +     * also notifies the watcher when the contents have been consumed.
  +     *
  +     * @param b     The byte array to fill.
  +     * @param off   Start filling at this position.
  +     * @param len   The number of bytes to attempt to read.
  +     * @return The number of bytes read, or -1 if the end of content has been
  +     *  reached.
  +     *
  +     * @throws java.io.IOException Should an error occur on the wrapped stream.
  +     */
       public int read(byte[] b, int off, int len) throws java.io.IOException {
  -        if (pos >= contentLength) return -1;
  +        if (closed)
  +            throw new IOException("Attempted read from closed stream.");
  +
  +        if (pos >= contentLength)
  +            return -1;
  +
           if (pos + len > contentLength) {
               len = contentLength - pos;
           }
  
  
  
  1.27      +40 -4     jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java
  
  Index: HttpConnection.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java,v
  retrieving revision 1.26
  retrieving revision 1.27
  diff -u -r1.26 -r1.27
  --- HttpConnection.java	3 Dec 2002 05:46:15 -0000	1.26
  +++ HttpConnection.java	9 Dec 2002 09:16:16 -0000	1.27
  @@ -306,6 +306,37 @@
           return (!(null == _proxyHost || 0 >= _proxyPort));
       }
   
  +    /**
  +     * Set the state to keep track of the last response for the last request.
  +     *
  +     * <p>The connection managers use this to ensure that previous requests are
  +     * properly closed before a new request is attempted.  That way, a GET
  +     * request need not be read in its entirety before a new request is issued.
  +     * Instead, this stream can be closed as appropriate.</p>
  +     *
  +     * @param inStream  The stream associated with an HttpMethod.
  +     */
  +    public void setLastResponseInputStream(InputStream inStream) {
  +        _lastResponseInput = inStream;
  +    }
  +
  +    /**
  +     * Returns the stream used to read the last response's body.
  +     *
  +     * <p>Clients will generally not need to call this function unless
  +     * using HttpConnection directly, instead of calling {@link HttpClient#executeMethod}.
  +     * For those clients, call this function, and if it returns a non-null stream,
  +     * close the stream before attempting to execute a method.  Note that
  +     * calling "close" on the stream returned by this function <i>may</i> close
  +     * the connection if the previous response contained a "Connection: close" header. </p>
  +     *
  +     * @return An {@link InputStream} corresponding to the body of the last
  +     *  response.
  +     */
  +    public InputStream getLastResponseInputStream() {
  +        return _lastResponseInput;
  +    }
  +
       // --------------------------------------------------- Other Public Methods
   
       /**
  @@ -776,6 +807,9 @@
       protected void closeSocketAndStreams() {
           log.trace("enter HttpConnection.closeSockedAndStreams()");
   
  +        // no longer care about previous responses...
  +        _lastResponseInput = null;
  +
           if (null != _input) {
               try {
                   _input.close();
  @@ -891,6 +925,8 @@
       private InputStream _input = null;
       /** My OutputStream. */
       private OutputStream _output = null;
  +    /** An {@link InputStream} for the response to an individual request. */
  +    private InputStream _lastResponseInput = null;
       /** Whether or not I am connected. */
       private boolean _open = false;
       /** Whether or not I am/should connect via SSL. */
  
  
  
  1.85      +210 -194  jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodBase.java
  
  Index: HttpMethodBase.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodBase.java,v
  retrieving revision 1.84
  retrieving revision 1.85
  diff -u -r1.84 -r1.85
  --- HttpMethodBase.java	8 Dec 2002 18:51:06 -0000	1.84
  +++ HttpMethodBase.java	9 Dec 2002 09:16:16 -0000	1.85
  @@ -158,6 +158,7 @@
    * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
    * @author <a href="mailto:dims@apache.org">Davanum Srinivas</a>
    * @author Ortwin Gl�ck
  + * @author Eric Johnson
    */
   public abstract class HttpMethodBase implements HttpMethod {
       //~ Static variables/initializers ������������������������������������������
  @@ -235,12 +236,19 @@
       /** Whether or not I have been executed. */
       private boolean used = false;
   
  +    /** How many times did this transparently handle a recoverable exception? */
  +    private int recoverableExceptionCount = 0;
  +
       /**
        * The maximum number of attempts to attempt recovery from an
        * HttpRecoverableException.
        */
       private int maxRetries = 3;
   
  +    private boolean inExecute = false;
  +
  +    private boolean doneWithConnection = false;
  +
       /** Default content encoding chatset */
       protected static final String DEFAULT_CHARSET = "ISO-8859-1";
   
  @@ -568,6 +576,8 @@
                   while ((len = is.read(buffer)) > 0) {
                       os.write(buffer, 0, len);
                   }
  +                is.close();
  +                os.close();
                   responseBody = os.toByteArray();
                   setResponseStream(null);
                   log.debug("buffering response body");
  @@ -592,9 +602,9 @@
               return responseStream;
           }
           if (responseBody != null) {
  -            responseStream = new ByteArrayInputStream(responseBody);
  +            InputStream byteResponseStream = new ByteArrayInputStream(responseBody);
               log.debug("re-creating response stream from byte array");
  -            return responseStream;
  +            return byteResponseStream;
           }
           return null;
       }
  @@ -719,25 +729,7 @@
           setRequestHeader(header);
       }
   
  -
  -    /**
  -     * Close the provided HTTP connection, if:
  -     *  http 1.0 and not using the 'connect' method, or
  -     *  http 1.1 and the Connection: close header is sent
  -     *
  -     * @param connection the HTTP connection to process
  -     * Add the specified request header. If a header of the same name already
  -     * exists, the new value will be appended onto the the existing value
  -     * list.  A <i>header</i> value of <code>null</code> will be ignored. Note
  -     * that header-name matching is case insensitive.
  -     */
  -    private void closeConnection(HttpConnection connection) {
  -        if (shouldCloseConnection()) {
  -            connection.close();
  -        }
  -    }
  -
  -    private boolean shouldCloseConnection() {
  +    protected boolean shouldCloseConnection() {
           if (!http11) {
               if (getName().equals(ConnectMethod.NAME) &&
                       (statusLine.getStatusCode() == HttpStatus.SC_OK)) {
  @@ -759,13 +751,59 @@
           return false;
       }
   
  -    private void wrapResponseStream( HttpConnection connection ) {
  +    private boolean isRetryNeeded(int statusCode, HttpState state, HttpConnection conn) {
  +        switch (statusCode) {
  +            case HttpStatus.SC_UNAUTHORIZED:
  +            case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
  +                log.debug("Authorization required");
  +                if (doAuthentication) { //process authentication response
  +                    //if the authentication is successful, return the statusCode
  +                    //otherwise, drop through the switch and try again.
  +                    if (processAuthenticationResponse(state)) {
  +                        return false;
  +                    }
  +                } else { //let the client handle the authenticaiton
  +                    return false;
  +                }
  +                break;
  +
  +            case HttpStatus.SC_MOVED_TEMPORARILY:
  +            case HttpStatus.SC_MOVED_PERMANENTLY:
  +            case HttpStatus.SC_TEMPORARY_REDIRECT:
  +                log.debug("Redirect required");
  +
  +                if (! processRedirectResponse(conn)) {
  +                    return false;
  +                }
  +                break;
  +
  +            default:
  +                // neither an unauthorized nor a redirect response
  +                return false;
  +        } //end of switch
  +
  +        return true;
  +    }
  +
  +    private void checkExecuteConditions(HttpState state, HttpConnection conn)
  +    throws HttpException {
   
  -        if ( responseStream != null ) {
  -            this.responseConnection = connection;
  -            this.responseStream = new ResponseAutoReleaseInputStream(responseStream);
  +        if (null == state) {
  +            throw new NullPointerException("HttpState parameter");
  +        }
  +        if (null == conn) {
  +            throw new NullPointerException("HttpConnection parameter");
  +        }
  +        if (hasBeenUsed()) {
  +            throw new HttpException("Already used, but not recycled.");
  +        }
  +        if (!validate()) {
  +            throw new HttpException("Not valid");
           }
   
  +        if (inExecute) {
  +            throw new IllegalStateException("Execute invoked recursively, or exited abnormally.");
  +        }
       }
   
       /**
  @@ -795,104 +833,83 @@
       throws HttpException, IOException, NullPointerException {
           log.trace("enter HttpMethodBase.execute(HttpState, HttpConnection)");
   
  -        //TODO: This method is too large
  -        //check some error conditions
  -        if (null == state) {
  -            throw new NullPointerException("HttpState parameter");
  -        }
  -        if (null == conn) {
  -            throw new NullPointerException("HttpConnection parameter");
  -        }
  -        if (hasBeenUsed()) {
  -            throw new HttpException("Already used, but not recycled.");
  -        }
  -        if (!validate()) {
  -            throw new HttpException("Not valid");
  -        }
  -
  -        //pre-emptively add the authorization header, if required.
  -        Authenticator.authenticate(this, state);
  -        if (conn.isProxied()) {
  -            Authenticator.authenticateProxy(this, state);
  -        }
  +        checkExecuteConditions(state, conn);
  +        inExecute = true;
   
  -        //Set visited = new HashSet();
  -        realms = new HashSet();
  -        proxyRealms = new HashSet();
  -        int forwardCount = 0; //protect from an infinite loop
  +        try {
  +            //TODO: This method is too large
   
  -        while (forwardCount++ < maxForwards) {
  -            if (log.isDebugEnabled()) {
  -                log.debug("Execute loop try " + forwardCount);
  -            }
  +            //pre-emptively add the authorization header, if required.
  +            Authenticator.authenticate(this, state);
  +            if (conn.isProxied()) {
  +                Authenticator.authenticateProxy(this, state);
  +            }
  +
  +            //Set visited = new HashSet();
  +            realms = new HashSet();
  +            proxyRealms = new HashSet();
  +            int forwardCount = 0; //protect from an infinite loop
  +
  +            while (forwardCount++ < maxForwards) {
  +                // on every retry, reset this state information.
  +                responseConnection = conn;
  +                conn.setLastResponseInputStream(null);
   
  -            //write the request and read the response, will retry
  -            processRequest(state, conn);
  +                if (log.isDebugEnabled()) {
  +                    log.debug("Execute loop try " + forwardCount);
  +                }
   
  -            //if SC_CONTINUE write the request body
  -            writeRemainingRequestBody(state, conn);
  +                //write the request and read the response, will retry
  +                processRequest(state, conn);
   
  -            int statusCode = statusLine.getStatusCode();
  -            switch (statusCode) {
  -                case HttpStatus.SC_UNAUTHORIZED:
  -                case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
  -                    log.debug("Authorization required");
  -                    if (doAuthentication) { //process authentication response
  -                        //if the authentication is successful, return the statusCode
  -                        //otherwise, drop through the switch and try again.
  -                        if (processAuthenticationResponse(state, conn)) {
  -                            wrapResponseStream(conn);
  -                            return statusCode;
  -                        }
  -                    } else { //let the client handle the authenticaiton
  -                        wrapResponseStream(conn);
  -                        return statusCode;
  -                    }
  -                    break;
  +                //if SC_CONTINUE write the request body
  +                writeRemainingRequestBody(state, conn);
   
  -                case HttpStatus.SC_MOVED_TEMPORARILY:
  -                case HttpStatus.SC_MOVED_PERMANENTLY:
  -                case HttpStatus.SC_TEMPORARY_REDIRECT:
  -                    log.debug("Redirect required");
  -
  -                    if (! processRedirectResponse(state, conn)) {
  -                        wrapResponseStream(conn);
  -                        return statusCode;
  -                    }
  +                if (!isRetryNeeded(statusLine.getStatusCode(), state, conn)) {
  +                    // nope, no retry needed, exit loop.
                       break;
  +                }
  +                /*
  +                   Revisiting may be desired. We do not know about the server's internal state.
   
  -                default:
  -                    // neither an unauthorized nor a redirect response
  -                    wrapResponseStream(conn);
  +                //check to see if we have visited this url before
  +                if (visited.contains(generateVisitedKey(conn))) {
  +                    log.error("Link " + generateVisitedKey(conn) + "' revisited");
                       return statusCode;
  -            } //end of switch
  +                }
  +                visited.add(generateVisitedKey(conn));
  +                */
   
  -/*
  -    Revisiting may be desired. We do not know about the server's internal state.
  +                // retry - close previous stream.  Caution - this causes
  +                // responseBodyConsumed to be called, which may also close the
  +                // connection.
  +                if (responseStream != null) {
  +                    responseStream.close();
  +                }
   
  -            //check to see if we have visited this url before
  -            if (visited.contains(generateVisitedKey(conn))) {
  -                log.error("Link " + generateVisitedKey(conn) + "' revisited");
  -                return statusCode;
  -            }
  -            visited.add(generateVisitedKey(conn));
  -*/
  +            } //end of retry loop
   
  -            //close connection if required
  -            closeConnection(conn);
  -            if (conn.isOpen()) {
  -                //throw away body to position the stream after the response
  -                getResponseBodyAsString();
  +            if (forwardCount >= maxForwards) {
  +                log.error("Narrowly avoided an infinite loop in execute");
  +                throw new HttpRecoverableException("Maximum redirects ("+ maxForwards +") exceeded");
  +            }
  +        }
  +        finally {
  +            inExecute = false;
  +            // If the response has been fully processed, return the connection
  +            // to the pool.  Use this flag, rather than other tests (like
  +            // responseStream == null), as subclasses, might reset the stream,
  +            // for example, reading the entire response into a file and then
  +            // setting the file as the stream.
  +            if (doneWithConnection) {
  +                ensureConnectionRelease();
               }
  -        } //end of loop
  -
  -        wrapResponseStream(conn);
  +        }
   
  -        log.error("Narrowly avoided an infinite loop in execute");
  -        throw new HttpRecoverableException("Maximum redirects ("+ maxForwards +") exceeded");
  +        return statusLine.getStatusCode();
       }
   
  -    private boolean processRedirectResponse(HttpState state, HttpConnection conn) {
  +    private boolean processRedirectResponse(HttpConnection conn) {
   
           if (!getFollowRedirects()) {
               log.info("Redirect requested but followRedirects is "
  @@ -967,8 +984,8 @@
        * Check for a valid redirect given the current conn and new url.
        * Redirect to a different protocol, host or port are checked for validity.
        *
  -     * @param conn The existing HttpConnection
  -     * @param url The new URL to redirect to
  +     * @param currentUrl The current URL (redirecting from)
  +     * @param redirectUrl The new URL to redirect to
        * @throws HttpException if the redirect is invalid
        * @since 2.0
        */
  @@ -1056,6 +1073,9 @@
           http11 = true;
           bodySent = false;
           responseBody = null;
  +        recoverableExceptionCount = 0;
  +        inExecute = false;
  +        doneWithConnection = false;
       }
   
       /**
  @@ -1065,10 +1085,13 @@
        */
       public void releaseConnection() {
   
  -        if ( responseConnection != null ) {
  -            responseConnection.releaseConnection();
  -            this.responseConnection = null;
  -            this.responseStream = null;
  +        if (responseStream != null) {
  +            try {
  +                // FYI - this may indirectly invoke responseBodyConsumed.
  +                responseStream.close();
  +            } catch (IOException e) {
  +                // attempting cleanup, don't care about exception.
  +            }
           }
   
       }
  @@ -1603,9 +1626,10 @@
        * Read the response body from the given {@link HttpConnection}.
        *
        * <p>
  -     * The current implementation simply consumes the expected response body
  -     * (according to the values of the <tt>Content-Length</tt> and
  -     * <tt>Transfer-Encoding</tt> headers, if any).
  +     * The current implementation wraps the socket level stream with
  +     * an appropriate stream for the type of response (chunked, content-length,
  +     * or auto-close).  If there is no response body, the connection associated
  +     * with the request will be returned to the connection manager.
        * </p>
        *
        * <p>
  @@ -1626,7 +1650,17 @@
           log.trace(
               "enter HttpMethodBase.readResponseBody(HttpState, HttpConnection)");
   
  -        setResponseStream(_readResponseBody(state, conn));
  +        // assume we are not done with the connection if we get a stream
  +        doneWithConnection = false;
  +        InputStream stream = _readResponseBody(conn);
  +        if (stream == null) {
  +            // done using the connection!
  +            responseBodyConsumed();
  +        }
  +        else {
  +            conn.setLastResponseInputStream(stream);
  +            setResponseStream(stream);
  +        }
       }
   
       /**
  @@ -1641,11 +1675,10 @@
        * @see #readResponse
        * @see #processResponseBody
        *
  -     * @param state the client state
        * @param conn the {@link HttpConnection} to read the response from
        * @return InputStream to read the response body from
        */
  -    private InputStream _readResponseBody(HttpState state, HttpConnection conn)
  +    private InputStream _readResponseBody(HttpConnection conn)
       throws IOException {
           log.trace("enter HttpMethodBase.readResponseBody(HttpState, HttpConnection)");
   
  @@ -1697,12 +1730,12 @@
                   && !getName().equals(ConnectMethod.NAME)){
               result = is;
           }
  -        if (result == null) {
  -            return null;
  -        }
   
  -        if (shouldCloseConnection()) {
  -            result = new AutoCloseInputStream(result, conn);
  +        // if there is a result - ALWAYS wrap it in an observer which will
  +        // close the underlying stream as soon as it is consumed, and notify
  +        // the watcher that the stream has been consumed.
  +        if (result != null) {
  +            result = new AutoCloseInputStream(result, m_responseWatcher);
           }
   
           return result;
  @@ -2100,29 +2133,14 @@
       }
   
       /**
  -     * Generates a key used for idenifying visited URLs.
  -     *
  -     * @param conn DOCUMENT ME!
  -     *
  -     * @return DOCUMENT ME!
  -     */
  -    private String generateVisitedKey(HttpConnection conn) {
  -        return conn.getHost() + ":" + conn.getPort() + "|"
  -               + generateRequestLine(conn, getName(), getPath(),
  -                                     getQueryString(), getHttpVersion());
  -    }
  -
  -    /**
        * process a response that requires authentication
        *
        * @param state the current state
  -     * @param connection the connection for communication
        *
        * @return true if the request has completed process, false if more
        *         attempts are needed
        */
  -    private boolean processAuthenticationResponse(HttpState state,
  -                                                  HttpConnection connection) {
  +    private boolean processAuthenticationResponse(HttpState state) {
           log.trace("enter HttpMethodBase.processAuthenticationResponse("
               + "HttpState, HttpConnection)");
   
  @@ -2210,8 +2228,8 @@
        * @throws IOException when an I/O error occurs communicating with the
        *         server
        *
  -     * @see writeRequest(HttpState,HttpConnection)
  -     * @see readResponse(HttpState,HttpConnection)
  +     * @see #writeRequest(HttpState,HttpConnection)
  +     * @see #readResponse(HttpState,HttpConnection)
        */
       private void processRequest(HttpState state, HttpConnection connection)
       throws HttpException, IOException {
  @@ -2238,6 +2256,9 @@
                       log.debug("Closing the connection.");
                   }
   
  +                // update the recoverable exception count.
  +                recoverableExceptionCount++;
  +
                   connection.close();
                   log.info("Recoverable exception caught when writing request");
                   if (retryCount == maxRetries) {
  @@ -2335,64 +2356,59 @@
       }
   
       /**
  -     * Releases this connection from its connectionManager when the response has
  -     * been read.
  +     * Returns the number of "recoverable" exceptions thrown and handled, to
  +     * allow for monitoring the quality of the connection.
  +     *
  +     * @return The number of recoverable exceptions handled by the method.
        */
  -    private class ResponseAutoReleaseInputStream extends InputStream {
  +    public int getRecoverableExceptionCount() {
  +        return recoverableExceptionCount;
  +    }
   
  -        private InputStream is;
  +    /**
  +     * A response has been consumed.
  +     *
  +     * <p>The default behavior for this class is to check to see if the connection
  +     * should be closed, and close if need be, and to ensure that the connection
  +     * is returned to the connection manager - if and only if we are not still
  +     * inside the execute call.</p>
  +     *
  +     */
  +    protected void responseBodyConsumed() {
   
  -        public ResponseAutoReleaseInputStream(InputStream is) {
  -            this.is = is;
  -        }
  +        // make sure this is the initial invocation of the notification,
  +        // ignore subsequent ones.
  +        responseStream = null;
  +        responseConnection.setLastResponseInputStream(null);
   
  -        /**
  -         * @see java.io.InputStream#close()
  -         */
  -        public void close() throws IOException {
  -            is.close();
  -            releaseConnection();
  +        if (shouldCloseConnection()) {
  +            responseConnection.close();
           }
   
  -        /**
  -         * @see java.io.InputStream#read()
  -         */
  -        public int read() throws IOException {
  -            int b = is.read();
  -
  -            if ( b == -1 ) {
  -                releaseConnection();
  -            }
  -
  -            return b;
  +        doneWithConnection = true;
  +        if (!inExecute) {
  +            ensureConnectionRelease();
           }
  +    }
   
  -        /**
  -         * @see java.io.InputStream#read(byte, int, int)
  -         */
  -        public int read(byte[] array, int off, int len) throws IOException {
  -            int b = is.read(array, off, len);
  -
  -            if ( b == -1 ) {
  -                releaseConnection();
  -            }
  -
  -            return b;
  +    /**
  +     * Insure that the connection is released back to the pool.
  +     */
  +    private void ensureConnectionRelease() {
  +        if ( responseConnection != null ) {
  +            responseConnection.releaseConnection();
  +            responseConnection = null;
           }
  +    }
   
  -        /**
  -         * @see java.io.InputStream#read(byte)
  -         */
  -        public int read(byte[] array) throws IOException {
  -            int b = is.read(array);
  -
  -            if ( b == -1 ) {
  -                releaseConnection();
  -            }
  -
  -            return b;
  +    /**
  +     * This exists so that the public interface to this class need not include
  +     * either the responseConsumed or the responseBodyConsumed methods.
  +     */
  +    private ResponseConsumedWatcher m_responseWatcher = new ResponseConsumedWatcher() {
  +        public void responseConsumed() {
  +            responseBodyConsumed();
           }
  -
  -    }
  +    };
   
   }
  
  
  
  1.2       +59 -56    jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/MultiThreadedHttpConnectionManager.java
  
  Index: MultiThreadedHttpConnectionManager.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/MultiThreadedHttpConnectionManager.java,v
  retrieving revision 1.1
  retrieving revision 1.2
  diff -u -r1.1 -r1.2
  --- MultiThreadedHttpConnectionManager.java	3 Dec 2002 05:46:15 -0000	1.1
  +++ MultiThreadedHttpConnectionManager.java	9 Dec 2002 09:16:16 -0000	1.2
  @@ -77,11 +77,12 @@
    * Manages a set of HttpConnections for various host:ports.
    *
    * @author <a href="mailto:becke@u.washington.edu">Michael Becke</a>
  - * 
  + * @author Eric Johnson
  + *
    * @since 2.0
    */
   public class MultiThreadedHttpConnectionManager implements HttpConnectionManager {
  -    
  +
       // -------------------------------------------------------- Class Variables
       /** Log object for this class. */
       private static final Log log = LogFactory.getLog(MultiThreadedHttpConnectionManager.class);
  @@ -100,12 +101,12 @@
        * No-args constructor
        */
       public MultiThreadedHttpConnectionManager() {
  -        
  +
           this.referenceToHostPort = Collections.synchronizedMap( new HashMap() );
           this.referenceQueue = new ReferenceQueue();
  -        
  +
           new ReferenceQueueThread().start();
  -        
  +
       }
   
       /**
  @@ -132,23 +133,23 @@
        */
       public HttpConnection getConnection(HostConfiguration hostConfiguration)
       throws MalformedURLException {
  -        
  +
           while( true ) {
               try {
                   return getConnection(hostConfiguration, 0);
               } catch ( HttpException e ) {
  -                log.debug( 
  -                    "Unexpected exception while waiting for connection", 
  -                    e 
  +                log.debug(
  +                    "Unexpected exception while waiting for connection",
  +                    e
                   );
               };
           }
  -        
  +
       }
   
       /**
        * Return the port provided if not -1 (default), return 443 if the
  -     * protocol is HTTPS, otherwise 80. 
  +     * protocol is HTTPS, otherwise 80.
        *
        * This functionality is a URLUtil and may be better off in URIUtils
        *
  @@ -171,11 +172,11 @@
           }
           return portForProtocol;
       }
  -    
  +
       /**
        * @see HttpConnectionManager#getConnection(HostConfiguration, long)
        */
  -    public HttpConnection getConnection(HostConfiguration hostConfiguration, long timeout) 
  +    public HttpConnection getConnection(HostConfiguration hostConfiguration, long timeout)
       throws HttpException, MalformedURLException {
           log.trace("enter HttpConnectionManager.getConnection(HostConfiguration, long)");
   
  @@ -187,7 +188,7 @@
           String protocol = hostConfiguration.getProtocol();
           String host = hostConfiguration.getHost();
           int port = MultiThreadedHttpConnectionManager.getPort(
  -            protocol, 
  +            protocol,
               hostConfiguration.getPort()
           );
           String hostAndPort = host + ":" + port;
  @@ -199,8 +200,8 @@
   
           // Look for a list of connections for the given host:port
           HostConnectionPool connectionPool = getConnectionPool(hostAndPort);
  -        
  -        HttpConnection conn = getConnection( 
  +
  +        HttpConnection conn = getConnection(
               connectionPool,
               host,
               port,
  @@ -217,7 +218,7 @@
        * Gets a connection or waits if one is not available.  A connection is
        * available if one exists that is not being used or if fewer than
        * maxConnections have been created in the connectionPool.
  -     * 
  +     *
        * @param connectionPool
        * @param host
        * @param port
  @@ -226,9 +227,9 @@
        * @param proxyPort
        * @param timeout the number of milliseconds to wait for a connection, 0 to
        * wait indefinitely
  -     * 
  +     *
        * @return HttpConnection an available connection
  -     * 
  +     *
        * @throws HttpException if a connection does not available in 'timeout'
        * milliseconds
        */
  @@ -241,15 +242,15 @@
           int proxyPort,
           long timeout
       ) throws HttpException {
  -        
  +
           HttpConnection connection = null;
  -        
  +
           synchronized( connectionPool ) {
  -            
  -            // keep trying until a connection is available, should happen at 
  +
  +            // keep trying until a connection is available, should happen at
               // most twice
               while ( connection == null ) {
  -        
  +
                   if (connectionPool.freeConnections.size() > 0) {
                       connection = (HttpConnection)connectionPool.freeConnections.removeFirst();
                   } else {
  @@ -257,9 +258,9 @@
                       if (connectionPool.numConnections < maxConnections) {
                           // Create a new connection
                           connection = new HttpConnection(
  -                            proxyHost, 
  -                            proxyPort, 
  -                            host, 
  +                            proxyHost,
  +                            proxyPort,
  +                            host,
                               port,
                               useHttps
                           );
  @@ -271,14 +272,14 @@
                               new WeakReference( connection, referenceQueue ),
                               host + ":" + port
                           );
  -                        
  +
                       } else {
  -    
  +
                           TimeoutThread threadTimeout = new TimeoutThread();
                           threadTimeout.setTimeout(timeout);
                           threadTimeout.setWakeupThread(Thread.currentThread());
                           threadTimeout.start();
  -    
  +
                           try {
                               log.debug(
                                   "HttpConnectionManager.getConnection:  waiting for "
  @@ -291,14 +292,14 @@
                           } catch (InterruptedException e) {
                               throw new HttpException("Timeout waiting for connection.");
                           }
  -    
  +
                       }
                   }
               }
  -            
  +
           }
  -        
  -        return connection;        
  +
  +        return connection;
       }
   
       /**
  @@ -322,7 +323,7 @@
           }
           return listConnections;
       }
  -    
  +
       /**
        * Get the number of connections in use for the key
        *
  @@ -334,21 +335,23 @@
   
           HostConnectionPool connectionPool = getConnectionPool(hostAndPort);
           synchronized( connectionPool ) {
  -            return connectionPool.numConnections;   
  +            return connectionPool.numConnections;
           }
   
       }
  -    
  +
       /**
        * Make the given HttpConnection available for use by other requests.
        * If another thread is blocked in getConnection() waiting for a connection
        * for this host:port, they will be woken up.
  -     * 
  +     *
        * @param conn - The HttpConnection to make available.
        */
       public void releaseConnection(HttpConnection conn) {
           log.trace("enter HttpConnectionManager.releaseConnection(HttpConnection)");
   
  +        // make sure that the response has been read.
  +        SimpleHttpConnectionManager.finishLastResponse(conn);
           String host = conn.getHost();
           int port = conn.getPort();
           String key = host + ":" + port;
  @@ -369,38 +372,38 @@
               listConnections.notify();
           }
       }
  -    
  +
       /**
        * A simple struct-link class to combine the connection list and the count
        * of created connections.
        */
       private class HostConnectionPool {
  -        
  +
           public LinkedList freeConnections = new LinkedList();
           public int numConnections = 0;
  -           
  +
       }
   
       /**
        * A thread for listening for HttpConnections reclaimed by the garbage
        * collector.
  -	 */
  +     */
       private class ReferenceQueueThread extends Thread {
  -    
  +
           public ReferenceQueueThread() {
  -            setDaemon(true);   
  +            setDaemon(true);
           }
  -    
  +
           /**
            * @see java.lang.Runnable#run()
            */
           public void run() {
  -            
  +
               while(true) {
  -                
  +
                   try {
                       Reference ref = referenceQueue.remove();
  -                    
  +
                       if ( ref != null ) {
                           String hostPort = (String)referenceToHostPort.get(ref);
                           referenceToHostPort.remove(ref);
  @@ -413,9 +416,9 @@
                   } catch (InterruptedException e) {
                       log.debug("ReferenceQueueThread interrupted", e);
                   }
  -                
  +
               }
  -            
  +
           }
   
       }
  @@ -454,7 +457,7 @@
           }
   
           public void run() {
  -	    log.trace("TimeoutThread.run()");
  +        log.trace("TimeoutThread.run()");
               if(timeout == 0){
                   return;
               }
  @@ -466,10 +469,10 @@
                   sleep(timeout);
                   thrdWakeup.interrupt();
               }catch(InterruptedException e){
  -	        log.debug("InterruptedException caught as expected");
  +            log.debug("InterruptedException caught as expected");
                   // This is expected
               }
           }
       }
  -    
  +
   }
  
  
  
  1.2       +43 -16    jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/SimpleHttpConnectionManager.java
  
  Index: SimpleHttpConnectionManager.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/SimpleHttpConnectionManager.java,v
  retrieving revision 1.1
  retrieving revision 1.2
  diff -u -r1.1 -r1.2
  --- SimpleHttpConnectionManager.java	3 Dec 2002 05:46:15 -0000	1.1
  +++ SimpleHttpConnectionManager.java	9 Dec 2002 09:16:16 -0000	1.2
  @@ -62,14 +62,17 @@
   package org.apache.commons.httpclient;
   
   import java.net.MalformedURLException;
  +import java.io.InputStream;
  +import java.io.IOException;
   
   
   /**
    * A connection manager that provides access to a single HttpConnection.  This
    * manager makes no attempt to provide exclusive access to the contained
    * HttpConnection.
  - * 
  + *
    * @author <a href="mailto:becke@u.washington.edu">Michael Becke</a>
  + * @author Eric Johnson
    *
    * @since 2.0
    */
  @@ -103,27 +106,27 @@
           String host = hostConfiguration.getHost();
           int port = hostConfiguration.getPort();
           boolean isSecure = protocol.equalsIgnoreCase("HTTPS");
  -        
  +
           if ( httpConnection == null ) {
  -            
  +
               if ( hostConfiguration.isProxySet() ) {
                   httpConnection = new HttpConnection(
                       hostConfiguration.getProxyHost(),
                       hostConfiguration.getProxyPort(),
  -                    host, 
  -                    port, 
  +                    host,
  +                    port,
                       isSecure
                   );
  -            } else {            
  +            } else {
                   httpConnection = new HttpConnection(host, port, isSecure);
               }
  -            
  +
           } else {
  -            
  +
               // make sure the host and proxy are correct for this connection
               // close it and set the values if they are not
  -            if ( 
  -                !hostConfiguration.hostEquals(httpConnection) 
  +            if (
  +                !hostConfiguration.hostEquals(httpConnection)
                   || !hostConfiguration.proxyEquals(httpConnection)
               ) {
                   if ( httpConnection.isOpen() ) {
  @@ -137,18 +140,42 @@
                   httpConnection.setProxyHost(hostConfiguration.getProxyHost());
                   httpConnection.setProxyPort(hostConfiguration.getProxyPort());
   
  -            } 
  -            
  +            }
  +            else {
  +                finishLastResponse(httpConnection);
  +            }
           }
   
           return httpConnection;
  -        
  +
       }
   
       /**
        * @see org.apache.commons.httpclient.HttpConnectionManager#releaseConnection(org.apache.commons.httpclient.HttpConnection)
        */
       public void releaseConnection(HttpConnection conn) {
  +        if (conn != httpConnection)
  +            throw new IllegalStateException("Unexpected close on a different connection.");
  +
  +        finishLastResponse(httpConnection);
       }
   
  +    /**
  +     * Since the same connection is about to be reused, make sure the
  +     * previous request was completely processed, and if not
  +     * consume it now.
  +     */
  +    static void finishLastResponse(HttpConnection conn) {
  +        InputStream lastResponse = conn.getLastResponseInputStream();
  +        if ( lastResponse != null) {
  +            conn.setLastResponseInputStream(null);
  +            try {
  +                lastResponse.close();
  +            }
  +            catch (IOException ioe) {
  +                // badness - close to force reconnect.
  +                conn.close();
  +            }
  +        }
  +    }
   }
  
  
  
  1.1                  jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ResponseConsumedWatcher.java
  
  Index: ResponseConsumedWatcher.java
  ===================================================================
  /*
   * $Header: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ResponseConsumedWatcher.java,v 1.1 2002/12/09 09:16:16 oglueck Exp $
   * $Revision: 1.1 $
   * $Date: 2002/12/09 09:16:16 $
   * ====================================================================
   *
   * The Apache Software License, Version 1.1
   *
   * Copyright (c) 1999-2002 The Apache Software Foundation.  All rights
   * reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions
   * are met:
   *
   * 1. Redistributions of source code must retain the above copyright
   *    notice, this list of conditions and the following disclaimer.
   *
   * 2. Redistributions in binary form must reproduce the above copyright
   *    notice, this list of conditions and the following disclaimer in
   *    the documentation and/or other materials provided with the
   *    distribution.
   *
   * 3. The end-user documentation included with the redistribution, if
   *    any, must include the following acknowlegement:
   *       "This product includes software developed by the
   *        Apache Software Foundation (http://www.apache.org/)."
   *    Alternately, this acknowlegement may appear in the software itself,
   *    if and wherever such third-party acknowlegements normally appear.
   *
   * 4. The names "The Jakarta Project", "HttpClient", and "Apache Software
   *    Foundation" must not be used to endorse or promote products derived
   *    from this software without prior written permission. For written
   *    permission, please contact apache@apache.org.
   *
   * 5. Products derived from this software may not be called "Apache"
   *    nor may "Apache" appear in their names without prior written
   *    permission of the Apache Group.
   *
   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
   * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
   * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
   * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
   * SUCH DAMAGE.
   * ====================================================================
   *
   * This software consists of voluntary contributions made by many
   * individuals on behalf of the Apache Software Foundation.  For more
   * information on the Apache Software Foundation, please see
   * <http://www.apache.org/>.
   *
   * [Additional notices, if required by prior licensing conditions]
   *
   */
  
  package org.apache.commons.httpclient;
  
  /**
   * When a response stream has been consumed, various parts of the HttpClient
   * implementation need to respond appropriately.
   *
   * <p>When one of the three types of {@link java.io.InputStream}, one of
   * AutoCloseInputStream (package), {@link ContentLengthInputStream}, or
   * {@link ChunkedInputStream} finishes with its content, either because
   * all content has been consumed, or because it was explicitly closed,
   * it notifies its corresponding method via this interface.</p>
   *
   * @see ContentLengthInputStream
   * @see ChunkedInputStream
   * @author Eric Johnson
   */
  interface ResponseConsumedWatcher {
  
      /**
       * A response has been consumed.
       */
      void responseConsumed();
  }
  
  
  
  1.19      +4 -3      jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/GetMethod.java
  
  Index: GetMethod.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/GetMethod.java,v
  retrieving revision 1.18
  retrieving revision 1.19
  diff -u -r1.18 -r1.19
  --- GetMethod.java	3 Sep 2002 01:36:26 -0000	1.18
  +++ GetMethod.java	9 Dec 2002 09:16:17 -0000	1.19
  @@ -372,6 +372,7 @@
               while ((len = in.read(buffer)) > 0) {
                   out.write(buffer, 0, len);
               }
  +            in.close();
               out.close();
               setResponseStream(new FileInputStream(createTempFile()));
           }
  
  
  
  1.12      +6 -5      jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/HeadMethod.java
  
  Index: HeadMethod.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/HeadMethod.java,v
  retrieving revision 1.11
  retrieving revision 1.12
  diff -u -r1.11 -r1.12
  --- HeadMethod.java	1 Sep 2002 01:27:37 -0000	1.11
  +++ HeadMethod.java	9 Dec 2002 09:16:17 -0000	1.12
  @@ -146,8 +146,9 @@
           log.trace(
               "enter HeadMethod.readResponseBody(HttpState, HttpConnection)");
   
  -        // despite the possible presence of a content-length header, 
  +        // despite the possible presence of a content-length header,
           // HEAD returns no response body
  +        responseBodyConsumed();
           return;
       }
  -}
  \ No newline at end of file
  +}
  
  
  
  1.28      +5 -3      jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/PostMethod.java
  
  Index: PostMethod.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/PostMethod.java,v
  retrieving revision 1.27
  retrieving revision 1.28
  diff -u -r1.27 -r1.28
  --- PostMethod.java	12 Nov 2002 09:58:23 -0000	1.27
  +++ PostMethod.java	9 Dec 2002 09:16:17 -0000	1.28
  @@ -736,6 +736,8 @@
               outstream = new ChunkedOutputStream(outstream);
               }
           if (this.requestContentLength >= 0) {
  +            // don't need a watcher here - we're reading from something local,
  +            // not server-side.
               instream = new ContentLengthInputStream(instream, this.requestContentLength);
           }
   
  
  
  
  1.4       +39 -4     jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestGetMethodLocal.java
  
  Index: TestGetMethodLocal.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestGetMethodLocal.java,v
  retrieving revision 1.3
  retrieving revision 1.4
  diff -u -r1.3 -r1.4
  --- TestGetMethodLocal.java	4 Feb 2002 15:26:43 -0000	1.3
  +++ TestGetMethodLocal.java	9 Dec 2002 09:16:17 -0000	1.4
  @@ -89,6 +89,7 @@
       // -------------------------------------------------------------- Constants
   
       private static final String host = System.getProperty("httpclient.test.localHost","127.0.0.1");
  +    private static final String webAppContext = System.getProperty("httpclient.test.webappContext");
       private static final int port;
       static {
           String portString = System.getProperty("httpclient.test.localPort","8080");
  @@ -223,6 +224,40 @@
           }
           assertEquals(404,method.getStatusCode());
   
  +    }
  +
  +    /**
  +     * The intent of this test is to allow for the incomplete parsing of a GET
  +     * response, and to make it particularly tricky, the GET response issues
  +     * a Connection: close".
  +     *
  +     * <p>This wants to insure that a recoverable exception is not unexpectedly
  +     * triggered.</p>
  +     */
  +    public void testGetResponseNotReadAutoRecover() {
  +
  +        HttpClient client = new HttpClient();
  +        client.startSession(host, port);
  +
  +        try {
  +            // issue a GET with a connection: close, and don't parse the body.
  +            String path = "/" + webAppContext + "/body";
  +            GetMethod method1 = new GetMethod(path);
  +            method1.addRequestHeader("Connection", "close");
  +            client.executeMethod(method1);
  +            assertEquals(0, method1.getRecoverableExceptionCount() );
  +
  +            // issue another GET.
  +            GetMethod method2 = new GetMethod(path);
  +            client.executeMethod(method2);
  +            assertEquals(0, method2.getRecoverableExceptionCount() );
  +
  +            client.endSession();
  +        }
  +        catch (IOException ioe) {
  +
  +            fail("Problem executing method : " + ioe.toString() );
  +        }
       }
   
   }
  
  
  
  1.5       +13 -7     jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestHttpClientLocalHost.java
  
  Index: TestHttpClientLocalHost.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestHttpClientLocalHost.java,v
  retrieving revision 1.4
  retrieving revision 1.5
  diff -u -r1.4 -r1.5
  --- TestHttpClientLocalHost.java	3 Dec 2002 05:46:16 -0000	1.4
  +++ TestHttpClientLocalHost.java	9 Dec 2002 09:16:17 -0000	1.5
  @@ -84,6 +84,10 @@
       // -------------------------------------------------------------- Constants
   
   
  +    private static final String host = "127.0.0.1";
  +    private static final int port = 8080;
  +    private static final String webAppContext = System.getProperty("httpclient.test.webappContext");
  +
       // ------------------------------------------------------------ Constructor
   
   
  @@ -100,12 +104,12 @@
       }
   
       private HttpClient client = null;
  +    private String  getPath = null;
       private GetMethod getSlash = null;
  -    private GetMethod getSlash2 = null;
   
       public void setUp() {
  +        getPath =  "/" + webAppContext + "/body";
           client = new HttpClient();
  -        getSlash = new GetMethod("/");
       }
   
       public void tearDown() {
  @@ -115,6 +119,7 @@
   
       public void testExecuteMethod() throws Exception {
           client.startSession(host, port);
  +        GetMethod getSlash = new GetMethod(getPath);
           assertEquals(200, client.executeMethod(getSlash));
           String data = getSlash.getResponseBodyAsString();
           assertTrue(null != data);
  @@ -125,13 +130,14 @@
   
       public void testExecuteMultipleMethods() throws Exception {
           client.startSession(host, port);
  +        getSlash = new GetMethod(getPath);
           for(int i=0;i<10;i++) {
               assertEquals(200, client.executeMethod(getSlash));
               String data = getSlash.getResponseBodyAsString();
               assertTrue(null != data);
               assertTrue(data.length() > 0);
               getSlash.recycle();
  -            getSlash.setPath("/");
  +            getSlash.setPath(getPath);
           }
           client.endSession();
       }
  
  
  
  1.5       +8 -6      jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestMethodsLocalHost.java
  
  Index: TestMethodsLocalHost.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestMethodsLocalHost.java,v
  retrieving revision 1.4
  retrieving revision 1.5
  diff -u -r1.4 -r1.5
  --- TestMethodsLocalHost.java	1 Nov 2002 09:51:05 -0000	1.4
  +++ TestMethodsLocalHost.java	9 Dec 2002 09:16:17 -0000	1.5
  @@ -88,6 +88,7 @@
       // -------------------------------------------------------------- Constants
   
   
  +    private static final String webAppContext = System.getProperty("httpclient.test.webappContext");
       private static final String host = "127.0.0.1";
       private static final int port = 8080;
   
  @@ -208,7 +209,8 @@
               fail("Unable to execute method : " + t.toString());
           }
   
  -        HeadMethod method = new HeadMethod("/");
  +        String path = "/" + webAppContext + "/body";
  +        HeadMethod method = new HeadMethod(path);
   
           try {
               client.executeMethod(method);
  @@ -220,7 +222,7 @@
           assertEquals(200, method.getStatusCode());
   
           method.recycle();
  -        method.setPath("/index.html");
  +        method.setPath(path);
   
           try {
               client.executeMethod(method);
  
  
  
  1.12      +23 -6     jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestMethodsNoHost.java
  
  Index: TestMethodsNoHost.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestMethodsNoHost.java,v
  retrieving revision 1.11
  retrieving revision 1.12
  diff -u -r1.11 -r1.12
  --- TestMethodsNoHost.java	31 Oct 2002 07:45:35 -0000	1.11
  +++ TestMethodsNoHost.java	9 Dec 2002 09:16:17 -0000	1.12
  @@ -71,6 +71,7 @@
   import junit.framework.TestSuite;
   import org.apache.commons.httpclient.methods.GetMethod;
   import org.apache.commons.httpclient.methods.PostMethod;
  +import org.apache.commons.httpclient.methods.HeadMethod;
   
   /**
    * @author Rodney Waldhoff
  @@ -276,7 +277,8 @@
           HttpMethodBase method = new GetMethod("/");
           method.execute(new HttpState(), conn);
           String responseBody = method.getResponseBodyAsString();
  -        conn.close();
  +        // verify that the connection was closed.
  +        conn.assertNotOpen();
           assertEquals("1234567890123", responseBody);
       }
   
  @@ -300,7 +302,22 @@
           while ((c = response.read()) != -1) {
              assertEquals((int) 'A', c);
           }
  -        assertTrue(!conn.isOpen());
  +        conn.assertNotOpen();
  +
  +        // note - this test is here because the HEAD method handler overrides the
  +        // standard behavior for reading a response body.
  +        HeadMethod headMethod = new HeadMethod("/");
  +
  +        conn.addResponse(headers, "");
  +
  +        try {
  +            headMethod.execute(new HttpState(), conn);
  +            conn.assertNotOpen();
  +
  +        } catch (Throwable t) {
  +            t.printStackTrace();
  +            fail("Unable to execute method : " + t.toString());
  +        }
       }
   
       public void testSetGetQueryString1() {
  
  
  

--
To unsubscribe, e-mail:   <ma...@jakarta.apache.org>
For additional commands, e-mail: <ma...@jakarta.apache.org>