/* * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/HttpMethodBase.java,v 1.222 2005/01/14 21:16:40 olegk Exp $ * $Revision: 539441 $ * $Date: 2007-05-18 14:56:55 +0200 (Fri, 18 May 2007) $ * * ==================================================================== * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ==================================================================== * * 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/>. * */ package org.apache.commons.httpclient; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.util.Collection; import org.apache.commons.httpclient.auth.AuthState; import org.apache.commons.httpclient.cookie.CookiePolicy; import org.apache.commons.httpclient.cookie.CookieSpec; import org.apache.commons.httpclient.cookie.CookieVersionSupport; import org.apache.commons.httpclient.cookie.MalformedCookieException; import org.apache.commons.httpclient.params.HttpMethodParams; import org.apache.commons.httpclient.protocol.Protocol; import org.apache.commons.httpclient.util.EncodingUtil; import org.apache.commons.httpclient.util.ExceptionUtil; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * An abstract base implementation of HttpMethod. * <p> * At minimum, subclasses will need to override: * <ul> * <li>{@link #getName} to return the approriate name for this method * </li> * </ul> * </p> * * <p> * When a method requires additional request headers, subclasses will typically * want to override: * <ul> * <li>{@link #addRequestHeaders addRequestHeaders(HttpState,HttpConnection)} * to write those headers * </li> * </ul> * </p> * * <p> * When a method expects specific response headers, subclasses may want to * override: * <ul> * <li>{@link #processResponseHeaders processResponseHeaders(HttpState,HttpConnection)} * to handle those headers * </li> * </ul> * </p> * * * @author <a href="mailto:remm@apache.org">Remy Maucherat</a> * @author Rodney Waldhoff * @author Sean C. Sullivan * @author <a href="mailto:dion@apache.org">dIon Gillard</a> * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a> * @author <a href="mailto:dims@apache.org">Davanum Srinivas</a> * @author Ortwin Glueck * @author Eric Johnson * @author Michael Becke * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a> * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> * @author <a href="mailto:ggregory@seagullsw.com">Gary Gregory</a> * @author Christian Kohlschuetter * * @version $Revision: 539441 $ $Date: 2007-05-18 14:56:55 +0200 (Fri, 18 May 2007) $ */ public abstract class HttpMethodBase implements HttpMethod { // -------------------------------------------------------------- Constants /** Log object for this class. */ private static final Log LOG = LogFactory.getLog(HttpMethodBase.class); // ----------------------------------------------------- Instance variables /** Request headers, if any. */ private HeaderGroup requestHeaders = new HeaderGroup(); /** The Status-Line from the response. */ protected StatusLine statusLine = null; /** Response headers, if any. */ private HeaderGroup responseHeaders = new HeaderGroup(); /** Response trailer headers, if any. */ private HeaderGroup responseTrailerHeaders = new HeaderGroup(); /** Path of the HTTP method. */ private String path = null; /** Query string of the HTTP method, if any. */ private String queryString = null; /** The response body of the HTTP method, assuming it has not be * intercepted by a sub-class. */ private InputStream responseStream = null; /** The connection that the response stream was read from. */ private HttpConnection responseConnection = null; /** Buffer for the response */ private byte[] responseBody = null; /** True if the HTTP method should automatically follow HTTP redirects.*/ private boolean followRedirects = false; /** True if the HTTP method should automatically handle * HTTP authentication challenges. */ private boolean doAuthentication = true; /** HTTP protocol parameters. */ private HttpMethodParams params = new HttpMethodParams(); /** Host authentication state */ private AuthState hostAuthState = new AuthState(); /** Proxy authentication state */ private AuthState proxyAuthState = new AuthState(); /** True if this method has already been executed. */ private boolean used = false; /** Count of how many times did this HTTP method transparently handle * a recoverable exception. */ private int recoverableExceptionCount = 0; /** the host for this HTTP method, can be null */ private HttpHost httphost = null; /** * Handles method retries * * @deprecated no loner used */ private MethodRetryHandler methodRetryHandler; /** True if the connection must be closed when no longer needed */ private boolean connectionCloseForced = false; /** Number of milliseconds to wait for 100-contunue response. */ private static final int RESPONSE_WAIT_TIME_MS = 3000; /** HTTP protocol version used for execution of this method. */ protected HttpVersion effectiveVersion = null; /** Whether the execution of this method has been aborted */ private volatile boolean aborted = false; /** Whether the HTTP request has been transmitted to the target * server it its entirety */ private boolean requestSent = false; /** Actual cookie policy */ private CookieSpec cookiespec = null; /** Default initial size of the response buffer if content length is unknown. */ private static final int DEFAULT_INITIAL_BUFFER_SIZE = 4*1024; // 4 kB // ----------------------------------------------------------- Constructors /** * No-arg constructor. */ public HttpMethodBase() { } /** * Constructor specifying a URI. * It is responsibility of the caller to ensure that URI elements * (path & query parameters) are properly encoded (URL safe). * * @param uri either an absolute or relative URI. The URI is expected * to be URL-encoded * * @throws IllegalArgumentException when URI is invalid * @throws IllegalStateException when protocol of the absolute URI is not recognised */ public HttpMethodBase(String uri) throws IllegalArgumentException, IllegalStateException { try { // create a URI and allow for null/empty uri values if (uri == null || uri.equals("")) { uri = "/"; } String charset = getParams().getUriCharset(); setURI(new URI(uri, true, charset)); } catch (URIException e) { throw new IllegalArgumentException("Invalid uri '" + uri + "': " + e.getMessage() ); } } // ------------------------------------------- Property Setters and Getters /** * Obtains the name of the HTTP method as used in the HTTP request line, * for example <tt>"GET"</tt> or <tt>"POST"</tt>. * * @return the name of this method */ public abstract String getName(); /** * Returns the URI of the HTTP method * * @return The URI * * @throws URIException If the URI cannot be created. * * @see org.apache.commons.httpclient.HttpMethod#getURI() */ public URI getURI() throws URIException { StringBuffer buffer = new StringBuffer(); if (this.httphost != null) { buffer.append(this.httphost.getProtocol().getScheme()); buffer.append("://"); buffer.append(this.httphost.getHostName()); int port = this.httphost.getPort(); if (port != -1 && port != this.httphost.getProtocol().getDefaultPort()) { buffer.append(":"); buffer.append(port); } } buffer.append(this.path); if (this.queryString != null) { buffer.append('?'); buffer.append(this.queryString); } String charset = getParams().getUriCharset(); return new URI(buffer.toString(), true, charset); } /** * Sets the URI for this method. * * @param uri URI to be set * * @throws URIException if a URI cannot be set * * @since 3.0 */ public void setURI(URI uri) throws URIException { // only set the host if specified by the URI if (uri.isAbsoluteURI()) { this.httphost = new HttpHost(uri); } // set the path, defaulting to root setPath( uri.getPath() == null ? "/" : uri.getEscapedPath() ); setQueryString(uri.getEscapedQuery()); } /** * Sets whether or not the HTTP method should automatically follow HTTP redirects * (status code 302, etc.) * * @param followRedirects <tt>true</tt> if the method will automatically follow redirects, * <tt>false</tt> otherwise. */ public void setFollowRedirects(boolean followRedirects) { this.followRedirects = followRedirects; } /** * Returns <tt>true</tt> if the HTTP method should automatically follow HTTP redirects * (status code 302, etc.), <tt>false</tt> otherwise. * * @return <tt>true</tt> if the method will automatically follow HTTP redirects, * <tt>false</tt> otherwise. */ public boolean getFollowRedirects() { return this.followRedirects; } /** Sets whether version 1.1 of the HTTP protocol should be used per default. * * @param http11 <tt>true</tt> to use HTTP/1.1, <tt>false</tt> to use 1.0 * * @deprecated Use {@link HttpMethodParams#setVersion(HttpVersion)} */ public void setHttp11(boolean http11) { if (http11) { this.params.setVersion(HttpVersion.HTTP_1_1); } else { this.params.setVersion(HttpVersion.HTTP_1_0); } } /** * Returns <tt>true</tt> if the HTTP method should automatically handle HTTP * authentication challenges (status code 401, etc.), <tt>false</tt> otherwise * * @return <tt>true</tt> if authentication challenges will be processed * automatically, <tt>false</tt> otherwise. * * @since 2.0 */ public boolean getDoAuthentication() { return doAuthentication; } /** * Sets whether or not the HTTP method should automatically handle HTTP * authentication challenges (status code 401, etc.) * * @param doAuthentication <tt>true</tt> to process authentication challenges * authomatically, <tt>false</tt> otherwise. * * @since 2.0 */ public void setDoAuthentication(boolean doAuthentication) { this.doAuthentication = doAuthentication; } // ---------------------------------------------- Protected Utility Methods /** * Returns <tt>true</tt> if version 1.1 of the HTTP protocol should be * used per default, <tt>false</tt> if version 1.0 should be used. * * @return <tt>true</tt> to use HTTP/1.1, <tt>false</tt> to use 1.0 * * @deprecated Use {@link HttpMethodParams#getVersion()} */ public boolean isHttp11() { return this.params.getVersion().equals(HttpVersion.HTTP_1_1); } /** * Sets the path of the HTTP method. * It is responsibility of the caller to ensure that the path is * properly encoded (URL safe). * * @param path the path of the HTTP method. The path is expected * to be URL-encoded */ public void setPath(String path) { this.path = path; } /** * Adds the specified request header, NOT overwriting any previous value. * Note that header-name matching is case insensitive. * * @param header the header to add to the request */ public void addRequestHeader(Header header) { LOG.trace("HttpMethodBase.addRequestHeader(Header)"); if (header == null) { LOG.debug("null header value ignored"); } else { getRequestHeaderGroup().addHeader(header); } } /** * Use this method internally to add footers. * * @param footer The footer to add. */ public void addResponseFooter(Header footer) { getResponseTrailerHeaderGroup().addHeader(footer); } /** * Gets the path of this HTTP method. * Calling this method <em>after</em> the request has been executed will * return the <em>actual</em> path, following any redirects automatically * handled by this HTTP method. * * @return the path to request or "/" if the path is blank. */ public String getPath() { return (path == null || path.equals("")) ? "/" : path; } /** * Sets the query string of this HTTP method. The caller must ensure that the string * is properly URL encoded. The query string should not start with the question * mark character. * * @param queryString the query string * * @see EncodingUtil#formUrlEncode(NameValuePair[], String) */ public void setQueryString(String queryString) { this.queryString = queryString; } /** * Sets the query string of this HTTP method. The pairs are encoded as UTF-8 characters. * To use a different charset the parameters can be encoded manually using EncodingUtil * and set as a single String. * * @param params an array of {@link NameValuePair}s to add as query string * parameters. The name/value pairs will be automcatically * URL encoded * * @see EncodingUtil#formUrlEncode(NameValuePair[], String) * @see #setQueryString(String) */ public void setQueryString(NameValuePair[] params) { LOG.trace("enter HttpMethodBase.setQueryString(NameValuePair[])"); queryString = EncodingUtil.formUrlEncode(params, "UTF-8"); } /** * Gets the query string of this HTTP method. * * @return The query string */ public String getQueryString() { return queryString; } /** * Set the specified request header, overwriting any previous value. Note * that header-name matching is case-insensitive. * * @param headerName the header's name * @param headerValue the header's value */ public void setRequestHeader(String headerName, String headerValue) { Header header = new Header(headerName, headerValue); setRequestHeader(header); } /** * Sets the specified request header, overwriting any previous value. * Note that header-name matching is case insensitive. * * @param header the header */ public void setRequestHeader(Header header) { Header[] headers = getRequestHeaderGroup().getHeaders(header.getName()); for (int i = 0; i < headers.length; i++) { getRequestHeaderGroup().removeHeader(headers[i]); } getRequestHeaderGroup().addHeader(header); } /** * Returns the specified request header. Note that header-name matching is * case insensitive. <tt>null</tt> will be returned if either * <i>headerName</i> is <tt>null</tt> or there is no matching header for * <i>headerName</i>. * * @param headerName The name of the header to be returned. * * @return The specified request header. * * @since 3.0 */ public Header getRequestHeader(String headerName) { if (headerName == null) { return null; } else { return getRequestHeaderGroup().getCondensedHeader(headerName); } } /** * Returns an array of the requests headers that the HTTP method currently has * * @return an array of my request headers. */ public Header[] getRequestHeaders() { return getRequestHeaderGroup().getAllHeaders(); } /** * @see org.apache.commons.httpclient.HttpMethod#getRequestHeaders(java.lang.String) */ public Header[] getRequestHeaders(String headerName) { return getRequestHeaderGroup().getHeaders(headerName); } /** * Gets the {@link HeaderGroup header group} storing the request headers. * * @return a HeaderGroup * * @since 2.0beta1 */ protected HeaderGroup getRequestHeaderGroup() { return requestHeaders; } /** * Gets the {@link HeaderGroup header group} storing the response trailer headers * as per RFC 2616 section 3.6.1. * * @return a HeaderGroup * * @since 2.0beta1 */ protected HeaderGroup getResponseTrailerHeaderGroup() { return responseTrailerHeaders; } /** * Gets the {@link HeaderGroup header group} storing the response headers. * * @return a HeaderGroup * * @since 2.0beta1 */ protected HeaderGroup getResponseHeaderGroup() { return responseHeaders; } /** * @see org.apache.commons.httpclient.HttpMethod#getResponseHeaders(java.lang.String) * * @since 3.0 */ public Header[] getResponseHeaders(String headerName) { return getResponseHeaderGroup().getHeaders(headerName); } /** * Returns the response status code. * * @return the status code associated with the latest response. */ public int getStatusCode() { return statusLine.getStatusCode(); } /** * Provides access to the response status line. * * @return the status line object from the latest response. * @since 2.0 */ public StatusLine getStatusLine() { return statusLine; } /** * Checks if response data is available. * @return <tt>true</tt> if response data is available, <tt>false</tt> otherwise. */ private boolean responseAvailable() { return (responseBody != null) || (responseStream != null); } /** * Returns an array of the response headers that the HTTP method currently has * in the order in which they were read. * * @return an array of response headers. */ public Header[] getResponseHeaders() { return getResponseHeaderGroup().getAllHeaders(); } /** * Gets the response header associated with the given name. Header name * matching is case insensitive. <tt>null</tt> will be returned if either * <i>headerName</i> is <tt>null</tt> or there is no matching header for * <i>headerName</i>. * * @param headerName the header name to match * * @return the matching header */ public Header getResponseHeader(String headerName) { if (headerName == null) { return null; } else { return getResponseHeaderGroup().getCondensedHeader(headerName); } } /** * Return the length (in bytes) of the response body, as specified in a * <tt>Content-Length</tt> header. * * <p> * Return <tt>-1</tt> when the content-length is unknown. * </p> * * @return content length, if <tt>Content-Length</tt> header is available. * <tt>0</tt> indicates that the request has no body. * If <tt>Content-Length</tt> header is not present, the method * returns <tt>-1</tt>. */ public long getResponseContentLength() { Header[] headers = getResponseHeaderGroup().getHeaders("Content-Length"); if (headers.length == 0) { return -1; } if (headers.length > 1) { LOG.warn("Multiple content-length headers detected"); } for (int i = headers.length - 1; i >= 0; i--) { Header header = headers[i]; try { return Long.parseLong(header.getValue()); } catch (NumberFormatException e) { if (LOG.isWarnEnabled()) { LOG.warn("Invalid content-length value: " + e.getMessage()); } } // See if we can have better luck with another header, if present } return -1; } /** * Returns the response body of the HTTP method, if any, as an array of bytes. * If response body is not available or cannot be read, returns <tt>null</tt>. * Buffers the response and this method can be called several times yielding * the same result each time. * * Note: This will cause the entire response body to be buffered in memory. A * malicious server may easily exhaust all the VM memory. It is strongly * recommended, to use getResponseAsStream if the content length of the response * is unknown or resonably large. * * @return The response body. * * @throws IOException If an I/O (transport) problem occurs while obtaining the * response body. */ public byte[] getResponseBody() throws IOException { if (this.responseBody == null) { InputStream instream = getResponseBodyAsStream(); if (instream != null) { long contentLength = getResponseContentLength(); if (contentLength > Integer.MAX_VALUE) { //guard below cast from overflow throw new IOException("Content too large to be buffered: "+ contentLength +" bytes"); } int limit = getParams().getIntParameter(HttpMethodParams.BUFFER_WARN_TRIGGER_LIMIT, 1024*1024); if ((contentLength == -1) || (contentLength > limit)) { LOG.warn("Going to buffer response body of large or unknown size. " +"Using getResponseBodyAsStream instead is recommended."); } LOG.debug("Buffering response body"); ByteArrayOutputStream outstream = new ByteArrayOutputStream( contentLength > 0 ? (int) contentLength : DEFAULT_INITIAL_BUFFER_SIZE); byte[] buffer = new byte[4096]; int len; while ((len = instream.read(buffer)) > 0) { outstream.write(buffer, 0, len); } outstream.close(); setResponseStream(null); this.responseBody = outstream.toByteArray(); } } return this.responseBody; } /** * Returns the response body of the HTTP method, if any, as an array of bytes. * If response body is not available or cannot be read, returns <tt>null</tt>. * Buffers the response and this method can be called several times yielding * the same result each time. * * Note: This will cause the entire response body to be buffered in memory. This method is * safe if the content length of the response is unknown, because the amount of memory used * is limited.<p> * * If the response is large this method involves lots of array copying and many object * allocations, which makes it unsuitable for high-performance / low-footprint applications. * Those applications should use {@link #getResponseBodyAsStream()}. * * @param maxlen the maximum content length to accept (number of bytes). * @return The response body. * * @throws IOException If an I/O (transport) problem occurs while obtaining the * response body. */ public byte[] getResponseBody(int maxlen) throws IOException { if (maxlen < 0) throw new IllegalArgumentException("maxlen must be positive"); if (this.responseBody == null) { InputStream instream = getResponseBodyAsStream(); if (instream != null) { // we might already know that the content is larger long contentLength = getResponseContentLength(); if ((contentLength != -1) && (contentLength > maxlen)) { throw new HttpContentTooLargeException( "Content-Length is " + contentLength, maxlen); } LOG.debug("Buffering response body"); ByteArrayOutputStream rawdata = new ByteArrayOutputStream( contentLength > 0 ? (int) contentLength : DEFAULT_INITIAL_BUFFER_SIZE); byte[] buffer = new byte[2048]; int pos = 0; int len; do { len = instream.read(buffer, 0, Math.min(buffer.length, maxlen-pos)); if (len == -1) break; rawdata.write(buffer, 0, len); pos += len; } while (pos < maxlen); setResponseStream(null); // check if there is even more data if (pos == maxlen) { if (instream.read() != -1) throw new HttpContentTooLargeException( "Content-Length not known but larger than " + maxlen, maxlen); } this.responseBody = rawdata.toByteArray(); } } return this.responseBody; } /** * Returns the response body of the HTTP method, if any, as an {@link InputStream}. * If response body is not available, returns <tt>null</tt>. If the response has been * buffered this method returns a new stream object on every call. If the response * has not been buffered the returned stream can only be read once. * * @return The response body or <code>null</code>. * * @throws IOException If an I/O (transport) problem occurs while obtaining the * response body. */ public InputStream getResponseBodyAsStream() throws IOException { if (responseStream != null) { return responseStream; } if (responseBody != null) { InputStream byteResponseStream = new ByteArrayInputStream(responseBody); LOG.debug("re-creating response stream from byte array"); return byteResponseStream; } return null; } /** * Returns the response body of the HTTP method, if any, as a {@link String}. * If response body is not available or cannot be read, returns <tt>null</tt> * The string conversion on the data is done using the character encoding specified * in <tt>Content-Type</tt> header. Buffers the response and this method can be * called several times yielding the same result each time. * * Note: This will cause the entire response body to be buffered in memory. A * malicious server may easily exhaust all the VM memory. It is strongly * recommended, to use getResponseAsStream if the content length of the response * is unknown or resonably large. * * @return The response body or <code>null</code>. * * @throws IOException If an I/O (transport) problem occurs while obtaining the * response body. */ public String getResponseBodyAsString() throws IOException { byte[] rawdata = null; if (responseAvailable()) { rawdata = getResponseBody(); } if (rawdata != null) { return EncodingUtil.getString(rawdata, getResponseCharSet()); } else { return null; } } /** * Returns the response body of the HTTP method, if any, as a {@link String}. * If response body is not available or cannot be read, returns <tt>null</tt> * The string conversion on the data is done using the character encoding specified * in <tt>Content-Type</tt> header. Buffers the response and this method can be * called several times yielding the same result each time.</p> * * Note: This will cause the entire response body to be buffered in memory. This method is * safe if the content length of the response is unknown, because the amount of memory used * is limited.<p> * * If the response is large this method involves lots of array copying and many object * allocations, which makes it unsuitable for high-performance / low-footprint applications. * Those applications should use {@link #getResponseBodyAsStream()}. * * @param maxlen the maximum content length to accept (number of bytes). Note that, * depending on the encoding, this is not equal to the number of characters. * @return The response body or <code>null</code>. * * @throws IOException If an I/O (transport) problem occurs while obtaining the * response body. */ public String getResponseBodyAsString(int maxlen) throws IOException { if (maxlen < 0) throw new IllegalArgumentException("maxlen must be positive"); byte[] rawdata = null; if (responseAvailable()) { rawdata = getResponseBody(maxlen); } if (rawdata != null) { return EncodingUtil.getString(rawdata, getResponseCharSet()); } else { return null; } } /** * Returns an array of the response footers that the HTTP method currently has * in the order in which they were read. * * @return an array of footers */ public Header[] getResponseFooters() { return getResponseTrailerHeaderGroup().getAllHeaders(); } /** * Gets the response footer associated with the given name. * Footer name matching is case insensitive. * <tt>null</tt> will be returned if either <i>footerName</i> is * <tt>null</tt> or there is no matching footer for <i>footerName</i> * or there are no footers available. If there are multiple footers * with the same name, there values will be combined with the ',' separator * as specified by RFC2616. * * @param footerName the footer name to match * @return the matching footer */ public Header getResponseFooter(String footerName) { if (footerName == null) { return null; } else { return getResponseTrailerHeaderGroup().getCondensedHeader(footerName); } } /** * Sets the response stream. * @param responseStream The new response stream. */ protected void setResponseStream(InputStream responseStream) { this.responseStream = responseStream; } /** * Returns a stream from which the body of the current response may be read. * If the method has not yet been executed, if <code>responseBodyConsumed</code> * has been called, or if the stream returned by a previous call has been closed, * <code>null</code> will be returned. * * @return the current response stream */ protected InputStream getResponseStream() { return responseStream; } /** * Returns the status text (or "reason phrase") associated with the latest * response. * * @return The status text. */ public String getStatusText() { return statusLine.getReasonPhrase(); } /** * Defines how strictly HttpClient follows the HTTP protocol specification * (RFC 2616 and other relevant RFCs). In the strict mode HttpClient precisely * implements the requirements of the specification, whereas in non-strict mode * it attempts to mimic the exact behaviour of commonly used HTTP agents, * which many HTTP servers expect. * * @param strictMode <tt>true</tt> for strict mode, <tt>false</tt> otherwise * * @deprecated Use {@link org.apache.commons.httpclient.params.HttpParams#setParameter(String, Object)} * to exercise a more granular control over HTTP protocol strictness. */ public void setStrictMode(boolean strictMode) { if (strictMode) { this.params.makeStrict(); } else { this.params.makeLenient(); } } /** * @deprecated Use {@link org.apache.commons.httpclient.params.HttpParams#setParameter(String, Object)} * to exercise a more granular control over HTTP protocol strictness. * * @return <tt>false</tt> */ public boolean isStrictMode() { return false; } /** * Adds the specified request header, NOT overwriting any previous value. * Note that header-name matching is case insensitive. * * @param headerName the header's name * @param headerValue the header's value */ public void addRequestHeader(String headerName, String headerValue) { addRequestHeader(new Header(headerName, headerValue)); } /** * Tests if the connection should be force-closed when no longer needed. * * @return <code>true</code> if the connection must be closed */ protected boolean isConnectionCloseForced() { return this.connectionCloseForced; } /** * Sets whether or not the connection should be force-closed when no longer * needed. This value should only be set to <code>true</code> in abnormal * circumstances, such as HTTP protocol violations. * * @param b <code>true</code> if the connection must be closed, <code>false</code> * otherwise. */ protected void setConnectionCloseForced(boolean b) { if (LOG.isDebugEnabled()) { LOG.debug("Force-close connection: " + b); } this.connectionCloseForced = b; } /** * Tests if the connection should be closed after the method has been executed. * The connection will be left open when using HTTP/1.1 or if <tt>Connection: * keep-alive</tt> header was sent. * * @param conn the connection in question * * @return boolean true if we should close the connection. */ protected boolean shouldCloseConnection(HttpConnection conn) { // Connection must be closed due to an abnormal circumstance if (isConnectionCloseForced()) { LOG.debug("Should force-close connection."); return true; } Header connectionHeader = null; // In case being connected via a proxy server if (!conn.isTransparent()) { // Check for 'proxy-connection' directive connectionHeader = responseHeaders.getFirstHeader("proxy-connection"); } // In all cases Check for 'connection' directive // some non-complaint proxy servers send it instread of // expected 'proxy-connection' directive if (connectionHeader == null) { connectionHeader = responseHeaders.getFirstHeader("connection"); } // In case the response does not contain any explict connection // directives, check whether the request does if (connectionHeader == null) { connectionHeader = requestHeaders.getFirstHeader("connection"); } if (connectionHeader != null) { if (connectionHeader.getValue().equalsIgnoreCase("close")) { if (LOG.isDebugEnabled()) { LOG.debug("Should close connection in response to directive: " + connectionHeader.getValue()); } return true; } else if (connectionHeader.getValue().equalsIgnoreCase("keep-alive")) { if (LOG.isDebugEnabled()) { LOG.debug("Should NOT close connection in response to directive: " + connectionHeader.getValue()); } return false; } else { if (LOG.isDebugEnabled()) { LOG.debug("Unknown directive: " + connectionHeader.toExternalForm()); } } } LOG.debug("Resorting to protocol version default close connection policy"); // missing or invalid connection header, do the default if (this.effectiveVersion.greaterEquals(HttpVersion.HTTP_1_1)) { if (LOG.isDebugEnabled()) { LOG.debug("Should NOT close connection, using " + this.effectiveVersion.toString()); } } else { if (LOG.isDebugEnabled()) { LOG.debug("Should close connection, using " + this.effectiveVersion.toString()); } } return this.effectiveVersion.lessEquals(HttpVersion.HTTP_1_0); } /** * Tests if the this method is ready to be executed. * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} to be used * @throws HttpException If the method is in invalid state. */ private void checkExecuteConditions(HttpState state, HttpConnection conn) throws HttpException { if (state == null) { throw new IllegalArgumentException("HttpState parameter may not be null"); } if (conn == null) { throw new IllegalArgumentException("HttpConnection parameter may not be null"); } if (this.aborted) { throw new IllegalStateException("Method has been aborted"); } if (!validate()) { throw new ProtocolException("HttpMethodBase object not valid"); } } /** * Executes this method using the specified <code>HttpConnection</code> and * <code>HttpState</code>. * * @param state {@link HttpState state} information to associate with this * request. Must be non-null. * @param conn the {@link HttpConnection connection} to used to execute * this HTTP method. Must be non-null. * * @return the integer status code if one was obtained, or <tt>-1</tt> * * @throws IOException if an I/O (transport) error occurs * @throws HttpException if a protocol exception occurs. */ public int execute(HttpState state, HttpConnection conn) throws HttpException, IOException { LOG.trace("enter HttpMethodBase.execute(HttpState, HttpConnection)"); // this is our connection now, assign it to a local variable so // that it can be released later this.responseConnection = conn; checkExecuteConditions(state, conn); this.statusLine = null; this.connectionCloseForced = false; conn.setLastResponseInputStream(null); // determine the effective protocol version if (this.effectiveVersion == null) { this.effectiveVersion = this.params.getVersion(); } writeRequest(state, conn); this.requestSent = true; readResponse(state, conn); // the method has successfully executed used = true; return statusLine.getStatusCode(); } /** * Aborts the execution of this method. * * @since 3.0 */ public void abort() { if (this.aborted) { return; } this.aborted = true; HttpConnection conn = this.responseConnection; if (conn != null) { conn.close(); } } /** * Returns <tt>true</tt> if the HTTP method has been already {@link #execute executed}, * but not {@link #recycle recycled}. * * @return <tt>true</tt> if the method has been executed, <tt>false</tt> otherwise */ public boolean hasBeenUsed() { return used; } /** * Recycles the HTTP method so that it can be used again. * Note that all of the instance variables will be reset * once this method has been called. This method will also * release the connection being used by this HTTP method. * * @see #releaseConnection() * * @deprecated no longer supported and will be removed in the future * version of HttpClient */ public void recycle() { LOG.trace("enter HttpMethodBase.recycle()"); releaseConnection(); path = null; followRedirects = false; doAuthentication = true; queryString = null; getRequestHeaderGroup().clear(); getResponseHeaderGroup().clear(); getResponseTrailerHeaderGroup().clear(); statusLine = null; effectiveVersion = null; aborted = false; used = false; params = new HttpMethodParams(); responseBody = null; recoverableExceptionCount = 0; connectionCloseForced = false; hostAuthState.invalidate(); proxyAuthState.invalidate(); cookiespec = null; requestSent = false; } /** * Releases the connection being used by this HTTP method. In particular the * connection is used to read the response(if there is one) and will be held * until the response has been read. If the connection can be reused by other * HTTP methods it is NOT closed at this point. * * @since 2.0 */ public void releaseConnection() { try { if (this.responseStream != null) { try { // FYI - this may indirectly invoke responseBodyConsumed. this.responseStream.close(); } catch (IOException ignore) { } } } finally { ensureConnectionRelease(); } } /** * Remove the request header associated with the given name. Note that * header-name matching is case insensitive. * * @param headerName the header name */ public void removeRequestHeader(String headerName) { Header[] headers = getRequestHeaderGroup().getHeaders(headerName); for (int i = 0; i < headers.length; i++) { getRequestHeaderGroup().removeHeader(headers[i]); } } /** * Removes the given request header. * * @param header the header */ public void removeRequestHeader(final Header header) { if (header == null) { return; } getRequestHeaderGroup().removeHeader(header); } // ---------------------------------------------------------------- Queries /** * Returns <tt>true</tt> the method is ready to execute, <tt>false</tt> otherwise. * * @return This implementation always returns <tt>true</tt>. */ public boolean validate() { return true; } /** * Returns the actual cookie policy * * @param state HTTP state. TODO: to be removed in the future * * @return cookie spec */ private CookieSpec getCookieSpec(final HttpState state) { if (this.cookiespec == null) { int i = state.getCookiePolicy(); if (i == -1) { this.cookiespec = CookiePolicy.getCookieSpec(this.params.getCookiePolicy()); } else { this.cookiespec = CookiePolicy.getSpecByPolicy(i); } this.cookiespec.setValidDateFormats( (Collection)this.params.getParameter(HttpMethodParams.DATE_PATTERNS)); } return this.cookiespec; } /** * Generates <tt>Cookie</tt> request headers for those {@link Cookie cookie}s * that match the given host, port and path. * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. */ protected void addCookieRequestHeader(HttpState state, HttpConnection conn) throws IOException, HttpException { LOG.trace("enter HttpMethodBase.addCookieRequestHeader(HttpState, " + "HttpConnection)"); Header[] cookieheaders = getRequestHeaderGroup().getHeaders("Cookie"); for (int i = 0; i < cookieheaders.length; i++) { Header cookieheader = cookieheaders[i]; if (cookieheader.isAutogenerated()) { getRequestHeaderGroup().removeHeader(cookieheader); } } CookieSpec matcher = getCookieSpec(state); String host = this.params.getVirtualHost(); if (host == null) { host = conn.getHost(); } Cookie[] cookies = matcher.match(host, conn.getPort(), getPath(), conn.isSecure(), state.getCookies()); if ((cookies != null) && (cookies.length > 0)) { if (getParams().isParameterTrue(HttpMethodParams.SINGLE_COOKIE_HEADER)) { // In strict mode put all cookies on the same header String s = matcher.formatCookies(cookies); getRequestHeaderGroup().addHeader(new Header("Cookie", s, true)); } else { // In non-strict mode put each cookie on a separate header for (int i = 0; i < cookies.length; i++) { String s = matcher.formatCookie(cookies[i]); getRequestHeaderGroup().addHeader(new Header("Cookie", s, true)); } } if (matcher instanceof CookieVersionSupport) { CookieVersionSupport versupport = (CookieVersionSupport) matcher; int ver = versupport.getVersion(); boolean needVersionHeader = false; for (int i = 0; i < cookies.length; i++) { if (ver != cookies[i].getVersion()) { needVersionHeader = true; } } if (needVersionHeader) { // Advertise cookie version support getRequestHeaderGroup().addHeader(versupport.getVersionHeader()); } } } } /** * Generates <tt>Host</tt> request header, as long as no <tt>Host</tt> request * header already exists. * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. */ protected void addHostRequestHeader(HttpState state, HttpConnection conn) throws IOException, HttpException { LOG.trace("enter HttpMethodBase.addHostRequestHeader(HttpState, " + "HttpConnection)"); // Per 19.6.1.1 of RFC 2616, it is legal for HTTP/1.0 based // applications to send the Host request-header. // TODO: Add the ability to disable the sending of this header for // HTTP/1.0 requests. String host = this.params.getVirtualHost(); if (host != null) { LOG.debug("Using virtual host name: " + host); } else { host = conn.getHost(); } int port = conn.getPort(); // Note: RFC 2616 uses the term "internet host name" for what goes on the // host line. It would seem to imply that host should be blank if the // host is a number instead of an name. Based on the behavior of web // browsers, and the fact that RFC 2616 never defines the phrase "internet // host name", and the bad behavior of HttpClient that follows if we // send blank, I interpret this as a small misstatement in the RFC, where // they meant to say "internet host". So IP numbers get sent as host // entries too. -- Eric Johnson 12/13/2002 if (LOG.isDebugEnabled()) { LOG.debug("Adding Host request header"); } //appends the port only if not using the default port for the protocol if (conn.getProtocol().getDefaultPort() != port) { host += (":" + port); } setRequestHeader("Host", host); } /** * Generates <tt>Proxy-Connection: Keep-Alive</tt> request header when * communicating via a proxy server. * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. */ protected void addProxyConnectionHeader(HttpState state, HttpConnection conn) throws IOException, HttpException { LOG.trace("enter HttpMethodBase.addProxyConnectionHeader(" + "HttpState, HttpConnection)"); if (!conn.isTransparent()) { if (getRequestHeader("Proxy-Connection") == null) { addRequestHeader("Proxy-Connection", "Keep-Alive"); } } } /** * Generates all the required request {@link Header header}s * to be submitted via the given {@link HttpConnection connection}. * * <p> * This implementation adds <tt>User-Agent</tt>, <tt>Host</tt>, * <tt>Cookie</tt>, <tt>Authorization</tt>, <tt>Proxy-Authorization</tt> * and <tt>Proxy-Connection</tt> headers, when appropriate. * </p> * * <p> * Subclasses may want to override this method to to add additional * headers, and may choose to invoke this implementation (via * <tt>super</tt>) to add the "standard" headers. * </p> * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. * * @see #writeRequestHeaders */ protected void addRequestHeaders(HttpState state, HttpConnection conn) throws IOException, HttpException { LOG.trace("enter HttpMethodBase.addRequestHeaders(HttpState, " + "HttpConnection)"); addUserAgentRequestHeader(state, conn); addHostRequestHeader(state, conn); addCookieRequestHeader(state, conn); addProxyConnectionHeader(state, conn); } /** * Generates default <tt>User-Agent</tt> request header, as long as no * <tt>User-Agent</tt> request header already exists. * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. */ protected void addUserAgentRequestHeader(HttpState state, HttpConnection conn) throws IOException, HttpException { LOG.trace("enter HttpMethodBase.addUserAgentRequestHeaders(HttpState, " + "HttpConnection)"); if (getRequestHeader("User-Agent") == null) { String agent = (String)getParams().getParameter(HttpMethodParams.USER_AGENT); if (agent == null) { agent = "Jakarta Commons-HttpClient"; } setRequestHeader("User-Agent", agent); } } /** * Throws an {@link IllegalStateException} if the HTTP method has been already * {@link #execute executed}, but not {@link #recycle recycled}. * * @throws IllegalStateException if the method has been used and not * recycled */ protected void checkNotUsed() throws IllegalStateException { if (used) { throw new IllegalStateException("Already used."); } } /** * Throws an {@link IllegalStateException} if the HTTP method has not been * {@link #execute executed} since last {@link #recycle recycle}. * * * @throws IllegalStateException if not used */ protected void checkUsed() throws IllegalStateException { if (!used) { throw new IllegalStateException("Not Used."); } } // ------------------------------------------------- Static Utility Methods /** * Generates HTTP request line according to the specified attributes. * * @param connection the {@link HttpConnection connection} used to execute * this HTTP method * @param name the method name generate a request for * @param requestPath the path string for the request * @param query the query string for the request * @param version the protocol version to use (e.g. HTTP/1.0) * * @return HTTP request line */ protected static String generateRequestLine(HttpConnection connection, String name, String requestPath, String query, String version) { LOG.trace("enter HttpMethodBase.generateRequestLine(HttpConnection, " + "String, String, String, String)"); StringBuffer buf = new StringBuffer(); // Append method name buf.append(name); buf.append(" "); // Absolute or relative URL? if (!connection.isTransparent()) { Protocol protocol = connection.getProtocol(); buf.append(protocol.getScheme().toLowerCase()); buf.append("://"); buf.append(connection.getHost()); if ((connection.getPort() != -1) && (connection.getPort() != protocol.getDefaultPort()) ) { buf.append(":"); buf.append(connection.getPort()); } } // Append path, if any if (requestPath == null) { buf.append("/"); } else { if (!connection.isTransparent() && !requestPath.startsWith("/")) { buf.append("/"); } buf.append(requestPath); } // Append query, if any if (query != null) { if (query.indexOf("?") != 0) { buf.append("?"); } buf.append(query); } // Append protocol buf.append(" "); buf.append(version); buf.append("\r\n"); return buf.toString(); } /** * This method is invoked immediately after * {@link #readResponseBody(HttpState,HttpConnection)} and can be overridden by * sub-classes in order to provide custom body processing. * * <p> * This implementation does nothing. * </p> * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @see #readResponse * @see #readResponseBody */ protected void processResponseBody(HttpState state, HttpConnection conn) { } /** * This method is invoked immediately after * {@link #readResponseHeaders(HttpState,HttpConnection)} and can be overridden by * sub-classes in order to provide custom response headers processing. * <p> * This implementation will handle the <tt>Set-Cookie</tt> and * <tt>Set-Cookie2</tt> headers, if any, adding the relevant cookies to * the given {@link HttpState}. * </p> * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @see #readResponse * @see #readResponseHeaders */ protected void processResponseHeaders(HttpState state, HttpConnection conn) { LOG.trace("enter HttpMethodBase.processResponseHeaders(HttpState, " + "HttpConnection)"); CookieSpec parser = getCookieSpec(state); // process set-cookie headers Header[] headers = getResponseHeaderGroup().getHeaders("set-cookie"); processCookieHeaders(parser, headers, state, conn); // see if the cookie spec supports cookie versioning. if (parser instanceof CookieVersionSupport) { CookieVersionSupport versupport = (CookieVersionSupport) parser; if (versupport.getVersion() > 0) { // process set-cookie2 headers. // Cookie2 will replace equivalent Cookie instances headers = getResponseHeaderGroup().getHeaders("set-cookie2"); processCookieHeaders(parser, headers, state, conn); } } } /** * This method processes the specified cookie headers. It is invoked from * within {@link #processResponseHeaders(HttpState,HttpConnection)} * * @param headers cookie {@link Header}s to be processed * @param state the {@link HttpState state} information associated with * this HTTP method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method */ protected void processCookieHeaders( final CookieSpec parser, final Header[] headers, final HttpState state, final HttpConnection conn) { LOG.trace("enter HttpMethodBase.processCookieHeaders(Header[], HttpState, " + "HttpConnection)"); String host = this.params.getVirtualHost(); if (host == null) { host = conn.getHost(); } for (int i = 0; i < headers.length; i++) { Header header = headers[i]; Cookie[] cookies = null; try { cookies = parser.parse( host, conn.getPort(), getPath(), conn.isSecure(), header); } catch (MalformedCookieException e) { if (LOG.isWarnEnabled()) { LOG.warn("Invalid cookie header: \"" + header.getValue() + "\". " + e.getMessage()); } } if (cookies != null) { for (int j = 0; j < cookies.length; j++) { Cookie cookie = cookies[j]; try { parser.validate( host, conn.getPort(), getPath(), conn.isSecure(), cookie); state.addCookie(cookie); if (LOG.isDebugEnabled()) { LOG.debug("Cookie accepted: \"" + parser.formatCookie(cookie) + "\""); } } catch (MalformedCookieException e) { if (LOG.isWarnEnabled()) { LOG.warn("Cookie rejected: \"" + parser.formatCookie(cookie) + "\". " + e.getMessage()); } } } } } } /** * This method is invoked immediately after * {@link #readStatusLine(HttpState,HttpConnection)} and can be overridden by * sub-classes in order to provide custom response status line processing. * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @see #readResponse * @see #readStatusLine */ protected void processStatusLine(HttpState state, HttpConnection conn) { } /** * Reads the response from the given {@link HttpConnection connection}. * * <p> * The response is processed as the following sequence of actions: * * <ol> * <li> * {@link #readStatusLine(HttpState,HttpConnection)} is * invoked to read the request line. * </li> * <li> * {@link #processStatusLine(HttpState,HttpConnection)} * is invoked, allowing the method to process the status line if * desired. * </li> * <li> * {@link #readResponseHeaders(HttpState,HttpConnection)} is invoked to read * the associated headers. * </li> * <li> * {@link #processResponseHeaders(HttpState,HttpConnection)} is invoked, allowing * the method to process the headers if desired. * </li> * <li> * {@link #readResponseBody(HttpState,HttpConnection)} is * invoked to read the associated body (if any). * </li> * <li> * {@link #processResponseBody(HttpState,HttpConnection)} is invoked, allowing the * method to process the response body if desired. * </li> * </ol> * * Subclasses may want to override one or more of the above methods to to * customize the processing. (Or they may choose to override this method * if dramatically different processing is required.) * </p> * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. */ protected void readResponse(HttpState state, HttpConnection conn) throws IOException, HttpException { LOG.trace( "enter HttpMethodBase.readResponse(HttpState, HttpConnection)"); // Status line & line may have already been received // if 'expect - continue' handshake has been used while (this.statusLine == null) { readStatusLine(state, conn); processStatusLine(state, conn); readResponseHeaders(state, conn); processResponseHeaders(state, conn); int status = this.statusLine.getStatusCode(); if ((status >= 100) && (status < 200)) { if (LOG.isInfoEnabled()) { LOG.info("Discarding unexpected response: " + this.statusLine.toString()); } this.statusLine = null; } } readResponseBody(state, conn); processResponseBody(state, conn); } /** * Read the response body from the given {@link HttpConnection}. * * <p> * 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> * Subclasses may want to override this method to to customize the * processing. * </p> * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. * * @see #readResponse * @see #processResponseBody */ protected void readResponseBody(HttpState state, HttpConnection conn) throws IOException, HttpException { LOG.trace( "enter HttpMethodBase.readResponseBody(HttpState, HttpConnection)"); // assume we are not done with the connection if we get a stream InputStream stream = readResponseBody(conn); if (stream == null) { // done using the connection! responseBodyConsumed(); } else { conn.setLastResponseInputStream(stream); setResponseStream(stream); } } /** * Returns the response body as an {@link InputStream input stream} * corresponding to the values of the <tt>Content-Length</tt> and * <tt>Transfer-Encoding</tt> headers. If no response body is available * returns <tt>null</tt>. * <p> * * @see #readResponse * @see #processResponseBody * * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. */ private InputStream readResponseBody(HttpConnection conn) throws HttpException, IOException { LOG.trace("enter HttpMethodBase.readResponseBody(HttpConnection)"); responseBody = null; InputStream is = conn.getResponseInputStream(); if (Wire.CONTENT_WIRE.enabled()) { is = new WireLogInputStream(is, Wire.CONTENT_WIRE); } boolean canHaveBody = canResponseHaveBody(statusLine.getStatusCode()); InputStream result = null; Header transferEncodingHeader = responseHeaders.getFirstHeader("Transfer-Encoding"); // We use Transfer-Encoding if present and ignore Content-Length. // RFC2616, 4.4 item number 3 if (transferEncodingHeader != null) { String transferEncoding = transferEncodingHeader.getValue(); if (!"chunked".equalsIgnoreCase(transferEncoding) && !"identity".equalsIgnoreCase(transferEncoding)) { if (LOG.isWarnEnabled()) { LOG.warn("Unsupported transfer encoding: " + transferEncoding); } } HeaderElement[] encodings = transferEncodingHeader.getElements(); // The chunked encoding must be the last one applied // RFC2616, 14.41 int len = encodings.length; if ((len > 0) && ("chunked".equalsIgnoreCase(encodings[len - 1].getName()))) { // if response body is empty if (conn.isResponseAvailable(conn.getParams().getSoTimeout())) { result = new ChunkedInputStream(is, this); } else { if (getParams().isParameterTrue(HttpMethodParams.STRICT_TRANSFER_ENCODING)) { throw new ProtocolException("Chunk-encoded body declared but not sent"); } else { LOG.warn("Chunk-encoded body missing"); } } } else { LOG.info("Response content is not chunk-encoded"); // The connection must be terminated by closing // the socket as per RFC 2616, 3.6 setConnectionCloseForced(true); result = is; } } else { long expectedLength = getResponseContentLength(); if (expectedLength == -1) { if (canHaveBody && this.effectiveVersion.greaterEquals(HttpVersion.HTTP_1_1)) { Header connectionHeader = responseHeaders.getFirstHeader("Connection"); String connectionDirective = null; if (connectionHeader != null) { connectionDirective = connectionHeader.getValue(); } if (!"close".equalsIgnoreCase(connectionDirective)) { LOG.info("Response content length is not known"); setConnectionCloseForced(true); } } result = is; } else { result = new ContentLengthInputStream(is, expectedLength); } } // See if the response is supposed to have a response body if (!canHaveBody) { result = null; } // 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, new ResponseConsumedWatcher() { public void responseConsumed() { responseBodyConsumed(); } } ); } return result; } /** * Reads the response headers from the given {@link HttpConnection connection}. * * <p> * Subclasses may want to override this method to to customize the * processing. * </p> * * <p> * "It must be possible to combine the multiple header fields into one * "field-name: field-value" pair, without changing the semantics of the * message, by appending each subsequent field-value to the first, each * separated by a comma." - HTTP/1.0 (4.3) * </p> * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. * * @see #readResponse * @see #processResponseHeaders */ protected void readResponseHeaders(HttpState state, HttpConnection conn) throws IOException, HttpException { LOG.trace("enter HttpMethodBase.readResponseHeaders(HttpState," + "HttpConnection)"); getResponseHeaderGroup().clear(); Header[] headers = HttpParser.parseHeaders( conn.getResponseInputStream(), getParams().getHttpElementCharset()); // Wire logging moved to HttpParser getResponseHeaderGroup().setHeaders(headers); } /** * Read the status line from the given {@link HttpConnection}, setting my * {@link #getStatusCode status code} and {@link #getStatusText status * text}. * * <p> * Subclasses may want to override this method to to customize the * processing. * </p> * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. * * @see StatusLine */ protected void readStatusLine(HttpState state, HttpConnection conn) throws IOException, HttpException { LOG.trace("enter HttpMethodBase.readStatusLine(HttpState, HttpConnection)"); final int maxGarbageLines = getParams(). getIntParameter(HttpMethodParams.STATUS_LINE_GARBAGE_LIMIT, Integer.MAX_VALUE); //read out the HTTP status string int count = 0; String s; do { s = conn.readLine(getParams().getHttpElementCharset()); if (s == null && count == 0) { // The server just dropped connection on us throw new NoHttpResponseException("The server " + conn.getHost() + " failed to respond"); } if (Wire.HEADER_WIRE.enabled()) { Wire.HEADER_WIRE.input(s + "\r\n"); } if (s != null && StatusLine.startsWithHTTP(s)) { // Got one break; } else if (s == null || count >= maxGarbageLines) { // Giving up throw new ProtocolException("The server " + conn.getHost() + " failed to respond with a valid HTTP response"); } count++; } while(true); //create the status line from the status string statusLine = new StatusLine(s); //check for a valid HTTP-Version String versionStr = statusLine.getHttpVersion(); if (getParams().isParameterFalse(HttpMethodParams.UNAMBIGUOUS_STATUS_LINE) && versionStr.equals("HTTP")) { getParams().setVersion(HttpVersion.HTTP_1_0); if (LOG.isWarnEnabled()) { LOG.warn("Ambiguous status line (HTTP protocol version missing):" + statusLine.toString()); } } else { this.effectiveVersion = HttpVersion.parse(versionStr); } } // ------------------------------------------------------ Protected Methods /** * <p> * Sends the request via the given {@link HttpConnection connection}. * </p> * * <p> * The request is written as the following sequence of actions: * </p> * * <ol> * <li> * {@link #writeRequestLine(HttpState, HttpConnection)} is invoked to * write the request line. * </li> * <li> * {@link #writeRequestHeaders(HttpState, HttpConnection)} is invoked * to write the associated headers. * </li> * <li> * <tt>\r\n</tt> is sent to close the head part of the request. * </li> * <li> * {@link #writeRequestBody(HttpState, HttpConnection)} is invoked to * write the body part of the request. * </li> * </ol> * * <p> * Subclasses may want to override one or more of the above methods to to * customize the processing. (Or they may choose to override this method * if dramatically different processing is required.) * </p> * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. */ protected void writeRequest(HttpState state, HttpConnection conn) throws IOException, HttpException { LOG.trace( "enter HttpMethodBase.writeRequest(HttpState, HttpConnection)"); writeRequestLine(state, conn); writeRequestHeaders(state, conn); conn.writeLine(); // close head if (Wire.HEADER_WIRE.enabled()) { Wire.HEADER_WIRE.output("\r\n"); } HttpVersion ver = getParams().getVersion(); Header expectheader = getRequestHeader("Expect"); String expectvalue = null; if (expectheader != null) { expectvalue = expectheader.getValue(); } if ((expectvalue != null) && (expectvalue.compareToIgnoreCase("100-continue") == 0)) { if (ver.greaterEquals(HttpVersion.HTTP_1_1)) { // make sure the status line and headers have been sent conn.flushRequestOutputStream(); int readTimeout = conn.getParams().getSoTimeout(); try { conn.setSocketTimeout(RESPONSE_WAIT_TIME_MS); readStatusLine(state, conn); processStatusLine(state, conn); readResponseHeaders(state, conn); processResponseHeaders(state, conn); if (this.statusLine.getStatusCode() == HttpStatus.SC_CONTINUE) { // Discard status line this.statusLine = null; LOG.debug("OK to continue received"); } else { return; } } catch (InterruptedIOException e) { if (!ExceptionUtil.isSocketTimeoutException(e)) { throw e; } // Most probably Expect header is not recongnized // Remove the header to signal the method // that it's okay to go ahead with sending data removeRequestHeader("Expect"); LOG.info("100 (continue) read timeout. Resume sending the request"); } finally { conn.setSocketTimeout(readTimeout); } } else { removeRequestHeader("Expect"); LOG.info("'Expect: 100-continue' handshake is only supported by " + "HTTP/1.1 or higher"); } } writeRequestBody(state, conn); // make sure the entire request body has been sent conn.flushRequestOutputStream(); } /** * Writes the request body to the given {@link HttpConnection connection}. * * <p> * This method should return <tt>true</tt> if the request body was actually * sent (or is empty), or <tt>false</tt> if it could not be sent for some * reason. * </p> * * <p> * This implementation writes nothing and returns <tt>true</tt>. * </p> * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @return <tt>true</tt> * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. */ protected boolean writeRequestBody(HttpState state, HttpConnection conn) throws IOException, HttpException { return true; } /** * Writes the request headers to the given {@link HttpConnection connection}. * * <p> * This implementation invokes {@link #addRequestHeaders(HttpState,HttpConnection)}, * and then writes each header to the request stream. * </p> * * <p> * Subclasses may want to override this method to to customize the * processing. * </p> * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. * * @see #addRequestHeaders * @see #getRequestHeaders */ protected void writeRequestHeaders(HttpState state, HttpConnection conn) throws IOException, HttpException { LOG.trace("enter HttpMethodBase.writeRequestHeaders(HttpState," + "HttpConnection)"); addRequestHeaders(state, conn); String charset = getParams().getHttpElementCharset(); Header[] headers = getRequestHeaders(); for (int i = 0; i < headers.length; i++) { String s = headers[i].toExternalForm(); if (Wire.HEADER_WIRE.enabled()) { Wire.HEADER_WIRE.output(s); } conn.print(s, charset); } } /** * Writes the request line to the given {@link HttpConnection connection}. * * <p> * Subclasses may want to override this method to to customize the * processing. * </p> * * @param state the {@link HttpState state} information associated with this method * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @throws IOException if an I/O (transport) error occurs. Some transport exceptions * can be recovered from. * @throws HttpException if a protocol exception occurs. Usually protocol exceptions * cannot be recovered from. * * @see #generateRequestLine */ protected void writeRequestLine(HttpState state, HttpConnection conn) throws IOException, HttpException { LOG.trace( "enter HttpMethodBase.writeRequestLine(HttpState, HttpConnection)"); String requestLine = getRequestLine(conn); if (Wire.HEADER_WIRE.enabled()) { Wire.HEADER_WIRE.output(requestLine); } conn.print(requestLine, getParams().getHttpElementCharset()); } /** * Returns the request line. * * @param conn the {@link HttpConnection connection} used to execute * this HTTP method * * @return The request line. */ private String getRequestLine(HttpConnection conn) { return HttpMethodBase.generateRequestLine(conn, getName(), getPath(), getQueryString(), this.effectiveVersion.toString()); } /** * Returns {@link HttpMethodParams HTTP protocol parameters} associated with this method. * * @return HTTP parameters. * * @since 3.0 */ public HttpMethodParams getParams() { return this.params; } /** * Assigns {@link HttpMethodParams HTTP protocol parameters} for this method. * * @since 3.0 * * @see HttpMethodParams */ public void setParams(final HttpMethodParams params) { if (params == null) { throw new IllegalArgumentException("Parameters may not be null"); } this.params = params; } /** * Returns the HTTP version used with this method (may be <tt>null</tt> * if undefined, that is, the method has not been executed) * * @return HTTP version. * * @since 3.0 */ public HttpVersion getEffectiveVersion() { return this.effectiveVersion; } /** * Per RFC 2616 section 4.3, some response can never contain a message * body. * * @param status - the HTTP status code * * @return <tt>true</tt> if the message may contain a body, <tt>false</tt> if it can not * contain a message body */ private static boolean canResponseHaveBody(int status) { LOG.trace("enter HttpMethodBase.canResponseHaveBody(int)"); boolean result = true; if ((status >= 100 && status <= 199) || (status == 204) || (status == 304)) { // NOT MODIFIED result = false; } return result; } /** * Returns proxy authentication realm, if it has been used during authentication process. * Otherwise returns <tt>null</tt>. * * @return proxy authentication realm * * @deprecated use #getProxyAuthState() */ public String getProxyAuthenticationRealm() { return this.proxyAuthState.getRealm(); } /** * Returns authentication realm, if it has been used during authentication process. * Otherwise returns <tt>null</tt>. * * @return authentication realm * * @deprecated use #getHostAuthState() */ public String getAuthenticationRealm() { return this.hostAuthState.getRealm(); } /** * Returns the character set from the <tt>Content-Type</tt> header. * * @param contentheader The content header. * @return String The character set. */ protected String getContentCharSet(Header contentheader) { LOG.trace("enter getContentCharSet( Header contentheader )"); String charset = null; if (contentheader != null) { HeaderElement values[] = contentheader.getElements(); // I expect only one header element to be there // No more. no less if (values.length == 1) { NameValuePair param = values[0].getParameterByName("charset"); if (param != null) { // If I get anything "funny" // UnsupportedEncondingException will result charset = param.getValue(); } } } if (charset == null) { charset = getParams().getContentCharset(); if (LOG.isDebugEnabled()) { LOG.debug("Default charset used: " + charset); } } return charset; } /** * Returns the character encoding of the request from the <tt>Content-Type</tt> header. * * @return String The character set. */ public String getRequestCharSet() { return getContentCharSet(getRequestHeader("Content-Type")); } /** * Returns the character encoding of the response from the <tt>Content-Type</tt> header. * * @return String The character set. */ public String getResponseCharSet() { return getContentCharSet(getResponseHeader("Content-Type")); } /** * @deprecated no longer used * * 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. */ public int getRecoverableExceptionCount() { return recoverableExceptionCount; } /** * 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() { // make sure this is the initial invocation of the notification, // ignore subsequent ones. responseStream = null; if (responseConnection != null) { responseConnection.setLastResponseInputStream(null); // At this point, no response data should be available. // If there is data available, regard the connection as being // unreliable and close it. if (shouldCloseConnection(responseConnection)) { responseConnection.close(); } else { try { if(responseConnection.isResponseAvailable()) { boolean logExtraInput = getParams().isParameterTrue(HttpMethodParams.WARN_EXTRA_INPUT); if(logExtraInput) { LOG.warn("Extra response data detected - closing connection"); } responseConnection.close(); } } catch (IOException e) { LOG.warn(e.getMessage()); responseConnection.close(); } } } this.connectionCloseForced = false; ensureConnectionRelease(); } /** * Insure that the connection is released back to the pool. */ private void ensureConnectionRelease() { if (responseConnection != null) { responseConnection.releaseConnection(); responseConnection = null; } } /** * Returns the {@link HostConfiguration host configuration}. * * @return the host configuration * * @deprecated no longer applicable */ public HostConfiguration getHostConfiguration() { HostConfiguration hostconfig = new HostConfiguration(); hostconfig.setHost(this.httphost); return hostconfig; } /** * Sets the {@link HostConfiguration host configuration}. * * @param hostconfig The hostConfiguration to set * * @deprecated no longer applicable */ public void setHostConfiguration(final HostConfiguration hostconfig) { if (hostconfig != null) { this.httphost = new HttpHost( hostconfig.getHost(), hostconfig.getPort(), hostconfig.getProtocol()); } else { this.httphost = null; } } /** * Returns the {@link MethodRetryHandler retry handler} for this HTTP method * * @return the methodRetryHandler * * @deprecated use {@link HttpMethodParams} */ public MethodRetryHandler getMethodRetryHandler() { return methodRetryHandler; } /** * Sets the {@link MethodRetryHandler retry handler} for this HTTP method * * @param handler the methodRetryHandler to use when this method executed * * @deprecated use {@link HttpMethodParams} */ public void setMethodRetryHandler(MethodRetryHandler handler) { methodRetryHandler = handler; } /** * This method is a dirty hack intended to work around * current (2.0) design flaw that prevents the user from * obtaining correct status code, headers and response body from the * preceding HTTP CONNECT method. * * TODO: Remove this crap as soon as possible */ void fakeResponse( StatusLine statusline, HeaderGroup responseheaders, InputStream responseStream ) { // set used so that the response can be read this.used = true; this.statusLine = statusline; this.responseHeaders = responseheaders; this.responseBody = null; this.responseStream = responseStream; } /** * Returns the target host {@link AuthState authentication state} * * @return host authentication state * * @since 3.0 */ public AuthState getHostAuthState() { return this.hostAuthState; } /** * Returns the proxy {@link AuthState authentication state} * * @return host authentication state * * @since 3.0 */ public AuthState getProxyAuthState() { return this.proxyAuthState; } /** * Tests whether the execution of this method has been aborted * * @return <tt>true</tt> if the execution of this method has been aborted, * <tt>false</tt> otherwise * * @since 3.0 */ public boolean isAborted() { return this.aborted; } /** * Returns <tt>true</tt> if the HTTP has been transmitted to the target * server in its entirety, <tt>false</tt> otherwise. This flag can be useful * for recovery logic. If the request has not been transmitted in its entirety, * it is safe to retry the failed method. * * @return <tt>true</tt> if the request has been sent, <tt>false</tt> otherwise */ public boolean isRequestSent() { return this.requestSent; } }