dion 2002/09/01 07:28:15
Modified: httpclient/src/java/org/apache/commons/httpclient
HttpMethodBase.java
Log:
Reformatting and documentation
Revision Changes Path
1.51 +1407 -1137jakarta-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.50
retrieving revision 1.51
diff -u -r1.50 -r1.51
--- HttpMethodBase.java 1 Sep 2002 02:31:55 -0000 1.50
+++ HttpMethodBase.java 1 Sep 2002 14:28:15 -0000 1.51
@@ -59,18 +59,13 @@
* [Additional notices, if required by prior licensing conditions]
*
*/
-
package org.apache.commons.httpclient;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
@@ -79,34 +74,78 @@
import java.util.Set;
import java.util.StringTokenizer;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
/**
- * <p>An abstract base implementation of {@link HttpMethod}.</p>
- * <p>At minimum, subclasses will need to override</p>
- * <ul><dl>
- * <dt>{@link #getName}</dt>
- * <dd>to return the approriate name for this method</dd>
- * </dl></ul>
- * <p>When a method's request may contain a body,
- * subclasses will typically want to override:</p>
- * <ul><dl>
- * <dt>{@link #getRequestContentLength}</dt>
- * <dd>to indicate the length (in bytes) of that body</dd>
- * <dt>{@link #writeRequestBody writeRequestBody(HttpState,HttpConnection)}</dt>
- * <dd>to write the body</dd>
- * </dl></ul>
- * <p>When a method requires additional request headers,
- * subclasses will typically want to override:</p>
- * <ul><dl>
- * <dt>{@link #addRequestHeaders addRequestHeaders(HttpState,HttpConnection)}</dt>
- * <dd>to write those headers</dd>
- * </dl></ul>
- * <p>When a method expects specific response headers,
- * subclasses may want to override:</p>
- * <ul><dl>
- * <dt>{@link #processResponseHeaders processResponseHeaders(HttpState,HttpConnection)}</dt>
- * <dd>to handle those headers</dd>
- * </dl></ul>
- *
+ * <p>
+ * An abstract base implementation of {@link HttpMethod}.
+ * </p>
+ *
+ * <p>
+ * At minimum, subclasses will need to override
+ * </p>
+ *
+ * <dl>
+ * <dt>
+ * {@link #getName}
+ * </dt>
+ * <dd>
+ * to return the approriate name for this method
+ * </dd>
+ * </dl>
+ *
+ * <p>
+ * When a method's request may contain a body, subclasses will typically want
+ * to override:
+ * </p>
+ *
+ * <dl>
+ * <dt>
+ * {@link #getRequestContentLength}
+ * </dt>
+ * <dd>
+ * to indicate the length (in bytes) of that body
+ * </dd>
+ * <dt>
+ * {@link #writeRequestBody writeRequestBody(HttpState,HttpConnection)}
+ * </dt>
+ * <dd>
+ * to write the body
+ * </dd>
+ * </dl>
+ *
+ * <p>
+ * When a method requires additional request headers, subclasses will typically
+ * want to override:
+ * </p>
+ *
+ * <dl>
+ * <dt>
+ * {@link #addRequestHeaders addRequestHeaders(HttpState,HttpConnection)}
+ * </dt>
+ * <dd>
+ * to write those headers
+ * </dd>
+ * </dl>
+ *
+ * <p>
+ * When a method expects specific response headers, subclasses may want to
+ * override:
+ * </p>
+ *
+ * <dl>
+ * <dt>
+ * {@link #processResponseHeaders
+ * processResponseHeaders(HttpState,HttpConnection)}
+ * </dt>
+ * <dd>
+ * to handle those headers
+ * </dd>
+ * </dl>
+ *
+ *
+ * @version $Revision$ $Date$
* @author <a href="mailto:remm@apache.org">Remy Maucherat</a>
* @author Rodney Waldhoff
* @author Sean C. Sullivan
@@ -114,9 +153,76 @@
* @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
* @author <a href="mailto:dims@apache.org">Davanum Srinivas</a>
* @author Ortwin Glück
- * @version $Revision$ $Date$
*/
public abstract class HttpMethodBase implements HttpMethod {
+ //~ Static variables/initializers ··········································
+
+ /** Maximum number of redirects and authentications that will be followed */
+ private static int maxForwards = 100;
+ // -------------------------------------------------------------- Constants
+
+ /** Log object for this class. */
+ private static final Log log = LogFactory.getLog(HttpMethod.class);
+
+ /** Log for any wire messages. */
+ private static final Log wireLog = LogFactory.getLog("httpclient.wire");
+
+ /** The User-Agent header sent on every request. */
+ protected static final Header USER_AGENT;
+
+ static {
+ String agent = System.getProperties()
+ .getProperty("httpclient.useragent",
+ "Jakarta Commons-HttpClient/2.0M1");
+ USER_AGENT = new Header("User-Agent", agent);
+ }
+
+ //~ Instance variables ·····················································
+
+ /** My request headers, if any. */
+ private HashMap requestHeaders = new HashMap();
+
+ /** My response headers, if any. */
+ private HashMap responseHeaders = new HashMap();
+ // ----------------------------------------------------- Instance Variables
+
+ /** My request path. */
+ private String path = null;
+
+ /** My query string, if any. */
+ private String queryString = null;
+
+ /** My response status text, if any. */
+ private String statusText = null;
+
+ /** The response body, assuming it has not be intercepted by a sub-class. */
+ private byte[] responseBody = null;
+
+ /** Whether or not the request body has been sent. */
+ private boolean bodySent = false;
+
+ /** Whether or not I should automatically follow redirects. */
+ private boolean followRedirects = false;
+
+ /** Whether or not I should use the HTTP/1.1 protocol. */
+ private boolean http11 = true;
+
+ /** True if we're in strict mode. */
+ private boolean strictMode = true;
+
+ /** Whether or not I have been executed. */
+ private boolean used = false;
+
+ /**
+ * The maximum number of attempts to attempt recovery from an
+ * HttpRecoverableException.
+ */
+ private int maxRetries = 3;
+
+ /** My response status code, if any. */
+ private int statusCode = -1;
+
+ //~ Constructors ···························································
// ----------------------------------------------------------- Constructors
@@ -128,200 +234,86 @@
/**
* Path-specifying constructor.
- *
+ *
* @param path my path
*/
public HttpMethodBase(String path) {
setPath(path);
}
+ //~ Methods ································································
+
// ------------------------------------------- Property Setters and Getters
/**
* Obtain the name of this method, suitable for use in the "request line",
* for example <tt>GET</tt> or <tt>POST</tt>.
+ *
* @return the name of this method
*/
public abstract String getName();
/**
- * Set the path part of my request.
- * @param path the path to request
- */
- public void setPath(String path) {
- this.path = path;
- }
-
- /**
- * Get the path part of my request.
- * @return the path to request or "/" if the path is blank.
- */
- public String getPath() {
- return (path == null || path.equals("")) ? "/" : path;
- }
-
- /**
- * Get the HTTP version.
- * @return HTTP/1.1 if http11, HTTP/1.0 otherwise
- * @since 2.0
- */
- private String getHttpVersion() {
- return (http11 ? "HTTP/1.1" : "HTTP/1.0");
- }
-
- /**
- * Turns strict mode on or off. In strict mode (the default)
- * we following the letter of RFC 2616, the Http 1.1 specification.
- * If strict mode is turned off we attempt to violate the specification
- * in the same way that most Http user agent's do (and many HTTP servers
- * expect.
- *
- * NOTE: StrictMode is currently experimental and its functionlaity may
- * change in the future.
- *
- */
- public void setStrictMode(boolean strictMode) {
- this.strictMode = strictMode;
- }
-
- /**
- * Returns the value of strictMode.
- *
- * NOTE: StrictMode is currently experimental and its functionlaity may
- * change in the future.
- *
- * @return true if strict mode is enabled.
- */
- public boolean isStrictMode() {
- return strictMode;
- }
-
- /**
- * 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);
- }
-
- /**
- * Set the specified request header, overwriting any
- * previous value.
- * Note that header-name matching is case insensitive.
- * @param header the header
+ * Set whether or not I should automatically follow HTTP redirects (status
+ * code 302, etc.)
+ *
+ * @param followRedirects true to follow redirects, false otherwise
*/
- public void setRequestHeader(Header header) {
- requestHeaders.put(header.getName().toLowerCase(), header);
+ public void setFollowRedirects(boolean followRedirects) {
+ this.followRedirects = followRedirects;
}
/**
- * Add 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
+ * Whether or not I should automatically follow HTTP redirects (status code
+ * 302, etc.)
+ *
+ * @return <tt>true</tt> if I will automatically follow HTTP redirects
*/
- public void addRequestHeader(String headerName, String headerValue) {
- // "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)
- Header header = getRequestHeader(headerName);
- if (null == header) {
- // header doesn't exist already, simply create with name and value
- header = new Header(headerName, headerValue);
- } else {
- // header exists, add this value to the comma separated list
- header.setValue(getNewHeaderValue(header, headerValue));
- }
- setRequestHeader(header);
+ public boolean getFollowRedirects() {
+ return this.followRedirects;
}
/**
- * "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."
- * //TODO: This method is trying to make up for deficiencies in Header.
- */
- private String getNewHeaderValue(Header existingHeader, String value) {
- String existingValue = existingHeader.getValue();
- if (existingValue == null) {
- existingValue = "";
- }
- String newValue = value;
- if (value == null) {
- newValue = "";
- }
- return existingValue + ", " + newValue;
- }
-
- /**
- * 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.
- *
- * @param header the header to add to the request
+ * Set whether or not I should use the HTTP/1.1 protocol. internal
+ *
+ * @param http11 true to use HTTP/1.1, false to use 1.0
*/
- public void addRequestHeader(Header header) {
- log.trace("HttpMethodBase.addRequestHeader(Header)");
-
- if (header == null) {
- log.debug("null header value ignored");
- } else {
- addRequestHeader(header.getName(), header.getValue());
- }
+ public void setHttp11(boolean http11) {
+ this.http11 = http11;
}
- /**
- * Get the request 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 getRequestHeader(String headerName) {
- return (headerName == null) ? null :
- (Header)(requestHeaders.get(headerName.toLowerCase()));
- }
+ // ---------------------------------------------- Protected Utility Methods
/**
- * Remove the request header associated with the given name.
- * Note that header-name matching is case insensitive.
- * @param headerName the header name
+ * Return <tt>true</tt> if I should use the HTTP/1.1 protocol. internal
+ *
+ * @return <tt>true</tt> if I should use the HTTP/1.1 protocol
*/
- public void removeRequestHeader(String headerName) {
- requestHeaders.remove(headerName.toLowerCase());
+ public boolean isHttp11() {
+ return http11;
}
/**
- * Whether or not I should automatically follow
- * HTTP redirects (status code 302, etc.)
- * @return <tt>true</tt> if I will automatically follow HTTP redirects
+ * Set the path part of my request.
+ *
+ * @param path the path to request
*/
- public boolean getFollowRedirects() {
- return this.followRedirects;
+ public void setPath(String path) {
+ this.path = path;
}
/**
- * Set whether or not I should automatically follow
- * HTTP redirects (status code 302, etc.)
+ * Get the path part of my request.
+ *
+ * @return the path to request or "/" if the path is blank.
*/
- public void setFollowRedirects(boolean followRedirects) {
- this.followRedirects = followRedirects;
+ public String getPath() {
+ return (path == null || path.equals("")) ? "/" : path;
}
/**
* Set my query string.
+ *
* @param queryString the query string
*/
public void setQueryString(String queryString) {
@@ -330,22 +322,22 @@
/**
* Set my query string.
- * @param params an array of {@link NameValuePair}s
- * to add as query string parameterss
+ *
+ * @param params an array of {@link NameValuePair}s to add as query string
+ * parameterss
*/
public void setQueryString(NameValuePair[] params) {
log.trace("enter HttpMethodBase.setQueryString(NameValuePair[])");
StringBuffer buf = new StringBuffer();
boolean needAmp = false;
- for(int i = 0;i < params.length; i++) {
+ for (int i = 0; i < params.length; i++) {
if (params[i].getName() != null) {
if (needAmp) {
buf.append("&");
} else {
needAmp = true;
}
- buf.append(URIUtil.encode(params[i].getName()))
- .append("=");
+ buf.append(URIUtil.encode(params[i].getName())).append("=");
if (params[i].getValue() != null) {
buf.append(URIUtil.encode(params[i].getValue()));
}
@@ -356,360 +348,231 @@
/**
* Get my query string.
+ *
+ * @return The query string portion of the request
*/
public String getQueryString() {
return queryString;
}
/**
- * Return an array of my request headers.
+ * 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 Header[] getRequestHeaders() {
- return (Header[])(requestHeaders.values().toArray(
- new Header[requestHeaders.size()]));
+ public void setRequestHeader(String headerName, String headerValue) {
+ Header header = new Header(headerName, headerValue);
+ setRequestHeader(header);
}
- // ---------------------------------------------------------------- Queries
+ /**
+ * Set 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) {
+ requestHeaders.put(header.getName().toLowerCase(), header);
+ }
/**
- * Confirm that I am ready to execute.
- * <p>
- * This implementation always returns <tt>true</tt>.
- * @return <tt>true</tt>
+ * Get the request 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 boolean validate() {
- return true;
+ public Header getRequestHeader(String headerName) {
+ return (headerName == null)
+ ? null : (Header) (requestHeaders.get(headerName.toLowerCase()));
}
/**
- * Return the status code associated with the latest response.
+ * Provides access to the request headers.
+ *
+ * @return an array of my request headers.
*/
- public int getStatusCode() {
- return statusCode;
+ public Header[] getRequestHeaders() {
+ return (Header[]) (requestHeaders.values().toArray(
+ new Header[requestHeaders.size()]));
}
/**
- * Return the status text (or "reason phrase") associated with the latest
- * response.
+ * Return my response body, if any, as a byte array. Otherwise return
+ * <tt>null</tt>.
+ *
+ * @return the response body
*/
- public String getStatusText() {
- return statusText;
+ public byte[] getResponseBody() {
+ return responseBody;
}
/**
- * Sets the specified response header.
- * @param header the header to set.
- * @since 2.0
- *
+ * Return my response body, if any, as an {@link InputStream}. Otherwise
+ * return <tt>null</tt>.
+ *
+ * @return the response body as an {@link InputStream}
+ *
+ * @throws IOException when there are errors obtaining the response
*/
- private void setResponseHeader(Header header){
- if (header == null) {
- return;
- }
- responseHeaders.put(header.getName().toLowerCase(), header);
+ public InputStream getResponseBodyAsStream() throws IOException {
+ return (null == responseBody)
+ ? null : new ByteArrayInputStream(responseBody);
}
/**
- * Return an array my response headers.
+ * Gets the response body as a string.
+ *
+ * @return my response body, if any, as a {@link String}. Otherwise return
+ * <tt>null</tt>.
*/
- public Header[] getResponseHeaders() {
- return (Header[])(responseHeaders.values().toArray(
- new Header[responseHeaders.size()]));
+ public String getResponseBodyAsString() {
+ return (null == responseBody) ? null : new String(responseBody);
}
/**
- * Get 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>.
+ * Get 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) {
- return (headerName == null) ? null :
- (Header)(responseHeaders.get(headerName.toLowerCase()));
+ return (headerName == null)
+ ? null
+ : (Header) (responseHeaders.get(headerName.toLowerCase()));
}
/**
- * Return my response body, if any,
- * as a byte array.
- * Otherwise return <tt>null</tt>.
+ * Provide access to the response headers
+ *
+ * @return an array of my response headers.
*/
- public byte[] getResponseBody() {
- return responseBody;
+ public Header[] getResponseHeaders() {
+ return (Header[]) (responseHeaders.values().toArray(
+ new Header[responseHeaders.size()]));
}
/**
- * Return my response body, if any,
- * as a {@link String}.
- * Otherwise return <tt>null</tt>.
+ * Provide access to the status code.
+ *
+ * @return the status code associated with the latest response.
*/
- public String getResponseBodyAsString() {
- return null == responseBody ? null : new String(responseBody);
+ public int getStatusCode() {
+ return statusCode;
}
/**
- * Return my response body, if any,
- * as an {@link InputStream}.
- * Otherwise return <tt>null</tt>.
+ * Provide access to the status text
+ *
+ * @return the status text (or "reason phrase") associated with the latest
+ * response.
*/
- public InputStream getResponseBodyAsStream() throws IOException {
- return null == responseBody ? null :
- new ByteArrayInputStream(responseBody);
+ public String getStatusText() {
+ return statusText;
}
/**
- * Return <tt>true</tt> if I have been {@link #execute executed}
- * but not recycled.
+ * Turns strict mode on or off. In strict mode (the default) we following
+ * the letter of RFC 2616, the Http 1.1 specification. If strict mode is
+ * turned off we attempt to violate the specification in the same way that
+ * most Http user agent's do (and many HTTP servers expect. NOTE:
+ * StrictMode is currently experimental and its functionlaity may change
+ * in the future.
+ *
+ * @param strictMode true for strict mode, false otherwise
*/
- public boolean hasBeenUsed() {
- return used;
- }
-
- /**
- * Write a request and read the response.
- *
- * Both the write to the server will be retried {@link #maxRetries}
- * times if the operation fails with a HttpRecoverableException.
- * The write will only be attempted if the read has succeeded.
- * <p>
- * The <i>used</i> is set to true if the write succeeds.
- *
- * @param state the current state
- * @param connection the connection for communication
- * @throws HttpException when errors occur as part of the HTTP protocol
- * conversation
- * @throws IOException when an I/O error occurs communicating with the
- * server
- * @see writeRequest(HttpState,HttpConnection)
- * @see readResponse(HttpState,HttpConnection)
- */
- private void processRequest(HttpState state, HttpConnection connection)
- throws HttpException, IOException {
- log.trace("enter HttpMethodBase.processRequest(HttpState, HttpConnection)");
-
- //try to do the write
- int retryCount = 0;
- do {
- retryCount++;
- if (log.isTraceEnabled()){
- log.trace("Attempt number " + retryCount + " to write request");
- }
- try {
- if (!connection.isOpen()) {
- log.debug("Opening the connection.");
- connection.open();
- }
- writeRequest(state, connection);
- used = true; //write worked, mark this method as used
- break; //move onto the write
-
- } catch (HttpRecoverableException httpre) {
- log.debug("Closing the connection.");
- connection.close();
- log.info("Recoverable exception caught when writing request");
- if (retryCount == maxRetries) {
- log.warn("Attempt to write request has reached max retries: " +
- maxRetries);
- throw httpre;
- }
- }
- } while (retryCount <= maxRetries);
-
- //try to do the read
- try {
- readResponse(state,connection);
- } catch (HttpRecoverableException httpre) {
- log.warn("Recoverable exception caught when reading response");
- log.debug("Closing the connection.");
- connection.close();
- throw httpre;
- }
-
- //everything should be OK at this point
+ public void setStrictMode(boolean strictMode) {
+ this.strictMode = strictMode;
}
-
/**
- * On a {@link HttpStatus#SC_CONTINUE continue}, if there are more request
- * bytes to be sent, write them to the connection
- *
- * @param state the current state
- * @param connection the connection for communication
- * @throws HttpException when errors occur as part of the HTTP protocol
- * conversation
- * @throws IOException when an I/O error occurs communicating with the
- * server
+ * Returns the value of strictMode. NOTE: StrictMode is currently
+ * experimental and its functionlaity may change in the future.
+ *
+ * @return true if strict mode is enabled.
*/
- private void writeRemainingRequestBody(HttpState state, HttpConnection connection)
- throws HttpException, IOException {
- log.trace("enter writeRemainingRequestBody(HttpState, HttpConnection)");
-
- if (HttpStatus.SC_CONTINUE == statusCode) {
- if (!bodySent) {
- bodySent = writeRequestBody(state, connection);
- } else {
- log.warn("Received status CONTINUE but he body has already been sent");
- // According to RFC 2616 this respose should be ignored
- }
- readResponse(state,connection);
- }
+ public boolean isStrictMode() {
+ return strictMode;
}
/**
- * 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, 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
*/
- private void closeConnection(HttpConnection connection) {
- log.trace("enter closeConnection(HttpConnection)");
-
- if (!http11) {
- if (getName().equals(ConnectMethod.NAME) &&
- (statusCode == HttpStatus.SC_OK)) {
- log.debug("Leaving connection open for tunneling");
- } else {
- log.debug("Closing connection since using HTTP/1.0, " +
- "ConnectMethod and status is OK");
- connection.close();
- }
+ public void addRequestHeader(String headerName, String headerValue) {
+ // "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)
+ Header header = getRequestHeader(headerName);
+ if (null == header) {
+ // header doesn't exist already, simply create with name and value
+ header = new Header(headerName, headerValue);
} else {
- Header connectionHeader = getResponseHeader("connection");
- if (null != connectionHeader
- && "close".equalsIgnoreCase(connectionHeader.getValue())) {
- log.debug("Closing connection since \"Connection: close\" header found.");
- connection.close();
- }
+ // header exists, add this value to the comma separated list
+ header.setValue(getNewHeaderValue(header, headerValue));
}
+ setRequestHeader(header);
}
-
/**
- * 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
+ * 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.
+ *
+ * @param header the header to add to the request
*/
- private boolean processAuthenticationResponse(HttpState state,
- HttpConnection connection) {
- log.trace("enter HttpMethodBase.processAuthenticationResponse(HttpState, HttpConnection)");
-
- Set realms = new HashSet();
- Set proxyRealms = new HashSet();
-
- // handle authentication required
- Header wwwauth = null;
- Set realmsUsed = null;
- switch (statusCode) {
- case HttpStatus.SC_UNAUTHORIZED:
- wwwauth = getResponseHeader(Authenticator.WWW_AUTH);
- realmsUsed = realms;
- break;
-
- case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
- wwwauth = getResponseHeader(Authenticator.PROXY_AUTH);
- realmsUsed = proxyRealms;
- break;
- }
- boolean authenticated = false;
- // if there was a header requesting authentication
- if (null != wwwauth) {
- String pathAndCreds = getPath() + ":" + wwwauth.getValue();
- if (realmsUsed.contains(pathAndCreds)) {
- if (log.isInfoEnabled()) {
- log.info("Already tried to authenticate to \""
- + wwwauth.getValue() + "\" but still receiving "
- + statusCode + ".");
- }
- return true;
- } else {
- realmsUsed.add(pathAndCreds);
- }
-
- try {
- //remove preemptive header and reauthenticate
- switch (statusCode) {
- case HttpStatus.SC_UNAUTHORIZED:
- removeRequestHeader(Authenticator.WWW_AUTH_RESP);
- authenticated = Authenticator.authenticate(this, state);
- break;
- case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
- removeRequestHeader(Authenticator.PROXY_AUTH_RESP);
- authenticated = Authenticator.authenticateProxy(this,state);
- break;
- }
- } catch (HttpException httpe) {
- log.warn(httpe.getMessage());
- return true; // finished request
- } catch (UnsupportedOperationException uoe) {
- log.warn(uoe.getMessage());
- //FIXME: should this return true?
- }
+ public void addRequestHeader(Header header) {
+ log.trace("HttpMethodBase.addRequestHeader(Header)");
- if (!authenticated) {
- // won't be able to authenticate to this challenge
- // without additional information
- log.debug("HttpMethodBase.execute(): Server demands "
- + "authentication credentials, but none are "
- + "available, so aborting.");
- } else {
- log.debug("HttpMethodBase.execute(): Server demanded "
- + "authentication credentials, will try again.");
- // let's try it again, using the credentials
- }
+ if (header == null) {
+ log.debug("null header value ignored");
+ } else {
+ addRequestHeader(header.getName(), header.getValue());
}
-
- return !authenticated; // finished processing if we aren't authenticated
- }
-
-
- /**
- * Generates a key used for idenifying visited URLs.
- */
- private String generateVisitedKey(HttpConnection conn){
- return conn.getHost() + ":" + conn.getPort() + "|" +
- generateRequestLine(conn, getName(), getPath(), getQueryString(), getHttpVersion() );
}
-
/**
- * Execute this method.
- *
- * Note that we cannot currently support redirects that change
- * the connection parameters (host, port, protocol) because we
- * don't yet have a good way to get the new connection. For
- * the time being, we just return the redirect response code,
- * and allow the user agent to resubmit if desired.
- *
- * @param state {@link HttpState} information to associate with this
- * request. Must be non-null.
- * @param connection the {@link HttpConnection} to write to/read from.
- * Must be non-null.
- *
- * Note that we cannot currently support
- * redirects that change the HttpConnection
- * parameters (host, port, protocol)
- * because we don't yet have a good way to
- * get the new connection.
- *
- * For the time being, we just return
- * the 302 response, and allow the user
+ * Execute this method. Note that we cannot currently support redirects
+ * that change the connection parameters (host, port, protocol) because
+ * we don't yet have a good way to get the new connection. For the time
+ * being, we just return the redirect response code, and allow the user
* agent to resubmit if desired.
- *
- * @throws IOException if an I/O error occurs
- * @throws HttpException if an protocol exception occurs
- *
+ *
+ * @param state {@link HttpState} information to associate with this
+ * request. Must be non-null.
+ * @param conn the{@link HttpConnection} to write to/read from. Must be
+ * non-null. Note that we cannot currently support redirects that
+ * change the HttpConnection parameters (host, port, protocol)
+ * because we don't yet have a good way to get the new connection.
+ * For the time being, we just return the 302 response, and allow
+ * the user agent to resubmit if desired.
+ *
* @return the integer status code if one was obtained, or <tt>-1</tt>
+ *
+ * @throws HttpException if an protocol exception occurs
+ * @throws IOException if an I/O error occurs
+ * @throws NullPointerException if the state is null
*/
- public int execute(HttpState state, HttpConnection conn)
- throws HttpException, IOException {
+ public int execute(HttpState state, HttpConnection conn)
+ throws HttpException, IOException, NullPointerException {
log.trace("enter HttpMethodBase.execute(HttpState, HttpConnection)");
- //TODO: This method is too large
+ //TODO: This method is too large
//check some error conditions
if (null == state) {
throw new NullPointerException("HttpState parameter");
@@ -733,49 +596,50 @@
Set visited = new HashSet();
int forwardCount = 0; //protect from an infinite loop
- while(forwardCount++ < maxForwards) {
-
- log.debug("Execute loop try " + forwardCount);
+ while (forwardCount++ < maxForwards) {
+ if (log.isDebugEnabled()) {
+ log.debug("Execute loop try " + forwardCount);
+ }
//write the request and read the response, will retry
processRequest(state, conn);
-
+
//if SC_CONTINUE write the request body
writeRemainingRequestBody(state, conn);
//close connection if required
closeConnection(conn);
- switch(statusCode){
+ switch (statusCode) {
case HttpStatus.SC_UNAUTHORIZED:
case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
// process authentication response
- boolean authenticated = processAuthenticationResponse(state, conn);
- if (authenticated){
+ if (processAuthenticationResponse(state, conn)) {
return statusCode;
}
break;
-
case HttpStatus.SC_MOVED_TEMPORARILY:
case HttpStatus.SC_MOVED_PERMANENTLY:
case HttpStatus.SC_TEMPORARY_REDIRECT:
//TODO: This block should be factored into a new
//method called processRedirectResponse
- if (!getFollowRedirects()){
- log.info("Redirect requested but followRedirects is disabled");
+ if (!getFollowRedirects()) {
+ log.info("Redirect requested but followRedirects is "
+ + "disabled");
return statusCode;
}
-
//get the location header to find out where to redirect to
Header locationHeader = getResponseHeader("location");
if (locationHeader == null) {
// got a redirect response, but no location header
- log.error("Received redirect response " + statusCode + " but no location header");
+ log.error("Received redirect response " + statusCode
+ + " but no location header");
return statusCode;
}
String location = locationHeader.getValue();
- if (log.isDebugEnabled()){
- log.debug("Redirect requested to location '" + location + "'");
+ if (log.isDebugEnabled()) {
+ log.debug("Redirect requested to location '" + location
+ + "'");
}
URL url; //the new url
@@ -783,41 +647,46 @@
if (isStrictMode()) {
//rfc2616 demands the location value be a complete URI
//Location = "Location" ":" absoluteURI
- try{
+ try {
url = new URL(location);
} catch (Exception ex) {
- log.warn("Redirected location '" + locationHeader.getValue() +
- "' is not acceptable in strict mode");
+ log.warn("Redirected location '"
+ + locationHeader.getValue()
+ + "' is not acceptable in strict mode");
return statusCode;
}
} else { //not strict mode
//try to construct the new url based on the current url
try {
- URL currentUrl = new URL(conn.getProtocol(), conn.getHost(),
- conn.getPort(), getPath());
+ URL currentUrl = new URL(conn.getProtocol(),
+ conn.getHost(),
+ conn.getPort(), getPath());
url = new URL(currentUrl, location);
} catch (Exception ex) {
- log.error("Redirected location '" + locationHeader.getValue() + "' is malformed");
+ log.error("Redirected location '"
+ + locationHeader.getValue()
+ + "' is malformed");
return statusCode;
}
}
-
//check for redirect to a different protocol, host or port
String error = null;
- if (! conn.getProtocol().equalsIgnoreCase(url.getProtocol())) {
- error = "Redirect from protocol " + conn.getProtocol() +
- " to " + url.getProtocol() + " is not supported";
+ if (!conn.getProtocol().equalsIgnoreCase(
+ url.getProtocol())) {
+ error = "Redirect from protocol " + conn.getProtocol()
+ + " to " + url.getProtocol()
+ + " is not supported";
}
- if (! conn.getHost().equalsIgnoreCase(url.getHost())) {
- error = "Redirect from host " + conn.getHost() +
- " to " + url.getHost() + " is not supported";
+ if (!conn.getHost().equalsIgnoreCase(url.getHost())) {
+ error = "Redirect from host " + conn.getHost() + " to "
+ + url.getHost() + " is not supported";
}
if (conn.getPort() != url.getPort()) {
- error = "Redirect from port " + conn.getPort() +
- " to " + url.getPort() + " is not supported";
+ error = "Redirect from port " + conn.getPort() + " to "
+ + url.getPort() + " is not supported";
}
- if (error != null){
+ if (error != null) {
log.warn(error);
//throw new HttpException(error);
return statusCode;
@@ -830,230 +699,264 @@
setQueryString(qs);
if (log.isDebugEnabled()) {
- log.debug("Changing path from \"" + getPath() + "\" to \"" +
- absolutePath + "\" in response to " + statusCode + " response.");
- log.debug("Changing query string from \"" + getQueryString() + "\" to \"" +
- qs + "\" in response to " + statusCode + " response.");
+ log.debug("Changing path from \"" + getPath()
+ + "\" to \"" + absolutePath
+ + "\" in response to " + statusCode
+ + " response.");
+ log.debug("Changing query string from \""
+ + getQueryString() + "\" to \"" + qs
+ + "\" in response to " + statusCode
+ + " response.");
}
break;
-
default:
// neither an unauthorized nor a redirect response
return statusCode;
-
- }//end of switch
+ } //end of switch
//check to see if we have visited this url before
- if (visited.contains(generateVisitedKey(conn)) ) {
+ if (visited.contains(generateVisitedKey(conn))) {
log.error("Link " + generateVisitedKey(conn) + "' revisited");
return statusCode;
}
visited.add(generateVisitedKey(conn));
-
- }//end of loop
+ } //end of loop
log.error("Narrowly avoided an infinite loop in execute");
return statusCode;
}
+ /**
+ * Whether the object has been used and not recycled.
+ *
+ * @return <tt>true</tt> if I have been {@link #execute executed} but not
+ * recycled.
+ */
+ public boolean hasBeenUsed() {
+ return used;
+ }
+ /**
+ * Recycle this method so that it can be used again. All of my instances
+ * variables will be reset once this method has been called.
+ */
+ public void recycle() {
+ log.trace("enter HttpMethodBase.recycle()");
-
- // ------------------------------------------------------ Protected Methods
+ path = null;
+ followRedirects = false;
+ queryString = null;
+ requestHeaders.clear();
+ responseHeaders.clear();
+ statusCode = -1;
+ statusText = null;
+ used = false;
+ http11 = true;
+ bodySent = false;
+ responseBody = null;
+ }
/**
- * <p>Writes my request to the given {@link HttpConnection}.</p>
- * <p>The request is written according to the following logic:</p>
- * <ol>
- * <li>
- * {@link #writeRequestLine writeRequestLine(HttpState, HttpConnection)}
- * is invoked to write the request line.
- * </li>
- * <li>
- * {@link #writeRequestHeaders 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 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 client state
- * @param conn the {@link HttpConnection} to write the request to
+ * Remove the request header associated with the given name. Note that
+ * header-name matching is case insensitive.
+ *
+ * @param headerName the header name
*/
- 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
- bodySent = writeRequestBody(state,conn);
+ public void removeRequestHeader(String headerName) {
+ requestHeaders.remove(headerName.toLowerCase());
}
+ // ---------------------------------------------------------------- Queries
/**
- * Writes the "request line" to the given {@link HttpConnection}.
+ * Confirm that I am ready to execute.
+ *
* <p>
- * Subclasses may want to override this method to
- * to customize the processing.
- *
- * @see #generateRequestLine
- *
- * @param state the client state
- * @param conn the {@link HttpConnection} to write to
+ * This implementation always returns <tt>true</tt>.
+ * </p>
+ *
+ * @return <tt>true</tt>
*/
- protected void writeRequestLine(HttpState state, HttpConnection conn)
- throws IOException, HttpException {
- log.trace("enter HttpMethodBase.writeRequestLine(HttpState, HttpConnection)");
- String requestLine = HttpMethodBase.generateRequestLine(conn, getName(),
- getPath(),getQueryString(), getHttpVersion());
- conn.print(requestLine);
+ public boolean validate() {
+ return true;
}
/**
- * Writes the request headers to the given {@link HttpConnection}.
- * <p>
- * This implementation invokes
- * {@link #addRequestHeaders addRequestHeaders(HttpState,HttpConnection)},
- * and then writes each header to the request stream.
- * <p>
- * Subclasses may want to override this method to
- * to customize the processing.
- *
- * @see #addRequestHeaders
- * @see #getRequestHeaders
- *
- * @param state the client state
- * @param conn the {@link HttpConnection} to write to
+ * Return the length (in bytes) of my request body, suitable for use in a
+ * <tt>Content-Length</tt> header.
+ *
+ * <p>
+ * Return <tt>-1</tt> when the content-length is unknown.
+ * </p>
+ *
+ * <p>
+ * This implementation returns <tt>0</tt>, indicating that the request has
+ * no body.
+ * </p>
+ *
+ * @return <tt>0</tt>, indicating that the request has no body.
*/
- protected void writeRequestHeaders(HttpState state, HttpConnection conn)
+ protected int getRequestContentLength() {
+ return 0;
+ }
+
+ /**
+ * Adds an <tt>Authorization</tt> request if needed, as long as no
+ * <tt>Authorization</tt> request header already exists.
+ *
+ * @param state current state of http requests
+ * @param conn the connection to use for I/O
+ *
+ * @throws IOException when errors occur reading or writing to/from the
+ * connection
+ * @throws HttpException when a recoverable error occurs
+ */
+ protected void addAuthorizationRequestHeader(HttpState state,
+ HttpConnection conn)
throws IOException, HttpException {
- log.trace("enter HttpMethodBase.writeRequestHeaders(HttpState,HttpConnection)");
- addRequestHeaders(state,conn);
- Iterator it = requestHeaders.values().iterator();
- while(it.hasNext()) {
- conn.print(((Header)it.next()).toExternalForm());
+ log.trace("enter HttpMethodBase.addAuthorizationRequestHeader("
+ + "HttpState, HttpConnection)");
+
+ // add authorization header, if needed
+ if (getRequestHeader(Authenticator.WWW_AUTH_RESP) == null) {
+ Header wwwAuthenticateHeader = getResponseHeader(
+ Authenticator.WWW_AUTH);
+ if (null != wwwAuthenticateHeader) {
+ try {
+ Authenticator.authenticate(this, state);
+ } catch (HttpException e) {
+ // ignored
+ }
+ }
}
}
/**
- * Populates the request headers map to
- * with additional {@link Header headers} to be
- * submitted to the given {@link HttpConnection}.
- * <p>
- * This implementation adds <tt>User-Agent</tt>,
- * <tt>Host</tt>, <tt>Cookie</tt>, <tt>Content-Length</tt>,
- * <tt>Transfer-Encoding</tt>, and <tt>Authorization</tt>
- * headers, when appropriate.
- * <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.
- *
- * @see #writeRequestHeaders
- *
- * @param state the client state
- * @param conn the {@link HttpConnection} the headers will eventually be written to
+ * Adds a <tt>Content-Length</tt> or <tt>Transer-Encoding: Chunked</tt>
+ * request header, as long as no <tt>Content-Length</tt> request header
+ * already exists.
+ *
+ * @param state current state of http requests
+ * @param conn the connection to use for I/O
+ *
+ * @throws IOException when errors occur reading or writing to/from the
+ * connection
+ * @throws HttpException when a recoverable error occurs
*/
- protected void addRequestHeaders(HttpState state, HttpConnection conn)
+ protected void addContentLengthRequestHeader(HttpState state,
+ HttpConnection conn)
throws IOException, HttpException {
- log.trace("enter HttpMethodBase.addRequestHeaders(HttpState, HttpConnection)");
+ log.trace("enter HttpMethodBase.addProxyAuthorizationRequestHeader("
+ + "HttpState, HttpConnection)");
- addUserAgentRequestHeader(state,conn);
- addHostRequestHeader(state,conn);
- addCookieRequestHeader(state,conn);
- addAuthorizationRequestHeader(state,conn);
- addProxyAuthorizationRequestHeader(state, conn);
- addContentLengthRequestHeader(state,conn);
+ // add content length or chunking
+ int len = getRequestContentLength();
+ if (getRequestHeader("content-length") == null) {
+ if (0 < len) {
+ setRequestHeader("Content-Length", String.valueOf(len));
+ } else if (http11 && (len < 0)) {
+ setRequestHeader("Transfer-Encoding", "chunked");
+ }
+ }
}
/**
- * Adds a default <tt>User-Agent</tt> request header,
- * as long as no <tt>User-Agent</tt> request header
- * already exists.
+ * Adds a <tt>Cookie</tt> request containing the matching {@link Cookie}s.
+ *
+ * @param state current state of http requests
+ * @param conn the connection to use for I/O
+ *
+ * @throws IOException when errors occur reading or writing to/from the
+ * connection
+ * @throws HttpException when a recoverable error occurs
*/
- protected void addUserAgentRequestHeader(HttpState state,
- HttpConnection conn) throws IOException, HttpException {
- log.trace("enter HttpMethodBase.addUserAgentRequestHeaders(HttpState, HttpConnection)");
+ protected void addCookieRequestHeader(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ log.trace("enter HttpMethodBase.addCookieRequestHeader(HttpState, "
+ + "HttpConnection)");
- if (getRequestHeader("user-agent") == null) {
- setRequestHeader(HttpMethodBase.USER_AGENT);
+ Header cookieHeader = Cookie.createCookieHeader(conn.getHost(),
+ conn.getPort(),
+ getPath(),
+ conn.isSecure(),
+ new Date(),
+ state.getCookies());
+ if (null != cookieHeader) {
+ setRequestHeader(cookieHeader);
}
}
/**
- * Adds a <tt>Host</tt> request header,
- * as long as no <tt>Host</tt> request header
- * already exists.
+ * Adds a <tt>Host</tt> request header, as long as no <tt>Host</tt> request
+ * header already exists.
+ *
+ * @param state current state of http requests
+ * @param conn the connection to use for I/O
+ *
+ * @throws IOException when errors occur reading or writing to/from the
+ * connection
+ * @throws HttpException when a recoverable error occurs
*/
- protected void addHostRequestHeader(HttpState state, HttpConnection conn)
+ protected void addHostRequestHeader(HttpState state, HttpConnection conn)
throws IOException, HttpException {
- log.trace("enter HttpMethodBase.addHostRequestHeader(HttpState, HttpConnection)");
+ 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.
-
+ // TODO: Add the ability to disable the sending of this header for
+ // HTTP/1.0 requests.
String host = conn.getHost();
int port = conn.getPort();
- if (getRequestHeader("host") != null){
- log.debug("Request to add Host header ignored: header already added");
+ if (getRequestHeader("host") != null) {
+ log.debug(
+ "Request to add Host header ignored: header already added");
return;
}
-
+
if (isIpAddress(host)) {
log.debug("Adding empty Host request header: host is an ipaddress");
setRequestHeader("Host", "");
return;
}
+ if (log.isDebugEnabled()) {
+ log.debug("Adding Host request header");
+ }
- log.debug("Adding Host request header");
//appends the port only if not using the default port for the protocol
- if(conn.isSecure()) {
- setRequestHeader("Host", (port==443) ? host : host+':'+port);
+ if (conn.isSecure()) {
+ setRequestHeader("Host", (port == 443) ? host : host + ':' + port);
} else {
- setRequestHeader("Host", (port==80) ? host : host+':'+port);
- }
- }
-
- /**
- * Adds a <tt>Cookie</tt> request containing the matching {@link Cookie}s.
- */
- protected void addCookieRequestHeader(HttpState state, HttpConnection conn)
- throws IOException, HttpException {
- log.trace("enter HttpMethodBase.addCookieRequestHeader(HttpState, HttpConnection)");
-
- Header cookieHeader = Cookie.createCookieHeader(conn.getHost(),
- conn.getPort(), getPath(), conn.isSecure(), new Date(),
- state.getCookies());
- if (null != cookieHeader) {
- setRequestHeader(cookieHeader);
+ setRequestHeader("Host", (port == 80) ? host : host + ':' + port);
}
}
/**
- * Adds an <tt>Authorization</tt> request if needed,
- * as long as no <tt>Authorization</tt> request header
- * already exists.
+ * Adds a <tt>Proxy-Authorization</tt> request if needed, as long as no
+ * <tt>Proxy-Authorization</tt> request header already exists.
+ *
+ * @param state current state of http requests
+ * @param conn the connection to use for I/O
+ *
+ * @throws IOException when errors occur reading or writing to/from the
+ * connection
+ * @throws HttpException when a recoverable error occurs
*/
- protected void addAuthorizationRequestHeader(HttpState state,
- HttpConnection conn) throws IOException, HttpException {
- log.trace("enter HttpMethodBase.addAuthorizationRequestHeader(HttpState, HttpConnection)");
+ protected void addProxyAuthorizationRequestHeader(HttpState state,
+ HttpConnection conn)
+ throws IOException, HttpException {
+ log.trace("enter HttpMethodBase.addProxyAuthorizationRequestHeader("
+ + "HttpState, HttpConnection)");
- // add authorization header, if needed
- if (getRequestHeader(Authenticator.WWW_AUTH_RESP) == null) {
- Header wwwAuthenticateHeader = getResponseHeader(Authenticator.WWW_AUTH);
+ // add proxy authorization header, if needed
+ if (getRequestHeader(Authenticator.PROXY_AUTH_RESP) == null) {
+ Header wwwAuthenticateHeader = getResponseHeader(
+ Authenticator.PROXY_AUTH);
if (null != wwwAuthenticateHeader) {
try {
- Authenticator.authenticate(this,state);
- } catch(HttpException e) {
+ Authenticator.authenticateProxy(this, state);
+ } catch (HttpException e) {
// ignored
}
}
@@ -1061,411 +964,372 @@
}
/**
- * Adds a <tt>Proxy-Authorization</tt> request if needed,
- * as long as no <tt>Proxy-Authorization</tt> request header
- * already exists.
+ * Populates the request headers map to with additional {@link Header
+ * headers} to be submitted to the given {@link HttpConnection}.
+ *
+ * <p>
+ * This implementation adds <tt>User-Agent</tt>, <tt>Host</tt>,
+ * <tt>Cookie</tt>, <tt>Content-Length</tt>, <tt>Transfer-Encoding</tt>,
+ * and <tt>Authorization</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 client state
+ * @param conn the {@link HttpConnection} the headers will eventually be
+ * written to
+ * @throws IOException when an error occurs writing the request
+ * @throws HttpException when a HTTP protocol error occurs
+ *
+ * @see #writeRequestHeaders
*/
- protected void addProxyAuthorizationRequestHeader(HttpState state,
- HttpConnection conn) throws IOException, HttpException {
- log.trace("enter HttpMethodBase.addProxyAuthorizationRequestHeader(HttpState, HttpConnection)");
+ protected void addRequestHeaders(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ log.trace("enter HttpMethodBase.addRequestHeaders(HttpState, "
+ + "HttpConnection)");
- // add proxy authorization header, if needed
- if (getRequestHeader(Authenticator.PROXY_AUTH_RESP) == null) {
- Header wwwAuthenticateHeader = getResponseHeader(Authenticator.PROXY_AUTH);
- if (null != wwwAuthenticateHeader) {
- try {
- Authenticator.authenticateProxy(this,state);
- } catch(HttpException e) {
- // ignored
- }
- }
+ addUserAgentRequestHeader(state, conn);
+ addHostRequestHeader(state, conn);
+ addCookieRequestHeader(state, conn);
+ addAuthorizationRequestHeader(state, conn);
+ addProxyAuthorizationRequestHeader(state, conn);
+ addContentLengthRequestHeader(state, conn);
+ }
+
+ /**
+ * Adds a default <tt>User-Agent</tt> request header, as long as no
+ * <tt>User-Agent</tt> request header already exists.
+ *
+ * @param state the client state
+ * @param conn the {@link HttpConnection} the headers will eventually be
+ * written to
+ * @throws IOException when an error occurs writing the request
+ * @throws HttpException when a HTTP protocol error occurs
+ */
+ protected void addUserAgentRequestHeader(HttpState state,
+ HttpConnection conn)
+ throws IOException, HttpException {
+ log.trace("enter HttpMethodBase.addUserAgentRequestHeaders(HttpState, "
+ + "HttpConnection)");
+
+ if (getRequestHeader("user-agent") == null) {
+ setRequestHeader(HttpMethodBase.USER_AGENT);
}
}
/**
- * Adds a <tt>Content-Length</tt> or
- * <tt>Transer-Encoding: Chunked</tt> request header,
- * as long as no <tt>Content-Length</tt> request header
- * already exists.
+ * Throws an {@link IllegalStateException} if used but not recycled.
+ *
+ * @throws IllegalStateException if the method has been used and not
+ * recycled
*/
- protected void addContentLengthRequestHeader(HttpState state,
- HttpConnection conn) throws IOException, HttpException {
- log.trace("enter HttpMethodBase.addProxyAuthorizationRequestHeader(HttpState, HttpConnection)");
+ protected void checkNotUsed() throws IllegalStateException {
+ if (used) {
+ throw new IllegalStateException("Already used.");
+ }
+ }
- // add content length or chunking
- int len = getRequestContentLength();
- if (getRequestHeader("content-length") == null) {
- if (0 < len) {
- setRequestHeader("Content-Length",String.valueOf(len));
- } else if (http11 && (len < 0)) {
- setRequestHeader("Transfer-Encoding","chunked");
+ /**
+ * Throws an {@link IllegalStateException} if not used since last recycle.
+ *
+ * @throws IllegalStateException if not used
+ */
+ protected void checkUsed() throws IllegalStateException {
+ if (!used) {
+ throw new IllegalStateException("Not Used.");
+ }
+ }
+
+ // ------------------------------------------------- Static Utility Methods
+
+ /**
+ * Generate an HTTP/S request line according to the specified attributes.
+ *
+ * @param connection the connection the request will be sent to
+ * @param name the method name generate a request for
+ * @param reqPath the path for the request
+ * @param qString the query string for the request
+ * @param protocol the protocol to use (e.g. HTTP/1.0)
+ *
+ * @return a line to send to the server that will fulfil the request
+ */
+ protected static String generateRequestLine(HttpConnection connection,
+ String name, String reqPath,
+ String qString, String protocol) {
+ log.trace("enter HttpMethodBase.generateRequestLine(HttpConnection, "
+ + "String, String, String, String)");
+
+ StringBuffer buf = new StringBuffer();
+ buf.append((null == reqPath)
+ ? "/" : URIUtil.encode(reqPath, URIUtil.pathSafe()));
+ if (null != qString) {
+ if (qString.indexOf("?") < 0) {
+ buf.append("?");
+ }
+ buf.append(qString);
+ }
+
+ if (!connection.isProxied() || connection.isTransparent()) {
+ return (name + " " + buf.toString() + " " + protocol + "\r\n");
+ } else {
+ if (connection.isSecure()) {
+ return (name + " https://" + connection.getHost()
+ + ((443 == connection.getPort()
+ || -1 == connection.getPort())
+ ? "" : (":" + connection.getPort())) + buf.toString()
+ + " " + protocol + "\r\n");
+ } else {
+ return (name + " http://" + connection.getHost()
+ + ((80 == connection.getPort()
+ || -1 == connection.getPort())
+ ? "" : (":" + connection.getPort())) + buf.toString()
+ + " " + protocol + "\r\n");
}
}
}
/**
- * Return the length (in bytes) of
- * my request body, suitable for use in
- * a <tt>Content-Length</tt> header.
+ * When this method is invoked, {@link #readResponseBody
+ * readResponseBody(HttpState,HttpConnection)} will have been invoked.
+ *
* <p>
- * Return <tt>-1</tt> when the content-length
- * is unknown.
+ * This implementation does nothing.
+ * </p>
+ *
* <p>
- * This implementation returns <tt>0</tt>,
- * indicating that the request has no
- * body.
- * @return <tt>0</tt>, indicating that the request has no body.
+ * Subclasses may want to override this method.
+ * </p>
+ *
+ * @param state the client state
+ * @param conn the {@link HttpConnection} to read the response from
+ *
+ * @see #readResponse
+ * @see #readResponseBody
*/
- protected int getRequestContentLength() {
- return 0;
+ protected void processResponseBody(HttpState state, HttpConnection conn) {
}
/**
- * Write the request body to the given {@link HttpConnection}
+ * When this method is invoked, the response headers map will have been
+ * populated with the response headers (in other words, {@link
+ * #readResponseHeaders readResponseHeaders(HttpState,HttpConnection)}
+ * will have been invoked).
+ *
* <p>
- * If an expectation is required, this method should
- * ensure that it has been sent by checking the
- * {@link #getStatusCode status code}.
- * <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 (for example, expectation required but
- * not present).
+ * 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>
+ *
+ * <p>
+ * Subclasses may want to override this method to specially process
+ * additional headers, and/or invoke this method (via <tt>super</tt>) to
+ * process the <tt>Set-Cookie</tt> and <tt>Set-Cookie2</tt> headers.
+ * </p>
+ *
+ * @param state the client state
+ * @param conn the {@link HttpConnection} to read the response from
+ *
+ * @see #readResponse
+ * @see #readResponseHeaders
+ */
+ protected void processResponseHeaders(HttpState state,
+ HttpConnection conn) {
+ log.trace("enter HttpMethodBase.processResponseHeaders(HttpState, "
+ + "HttpConnection)");
+
+ // add cookies, if any
+ // should we set cookies?
+ Header setCookieHeader = getResponseHeader("set-cookie2");
+ if (null == setCookieHeader) { //ignore old-style if new is supported
+ setCookieHeader = getResponseHeader("set-cookie");
+ }
+
+ if (setCookieHeader != null) {
+ try {
+ Cookie[] cookies = Cookie.parse(conn.getHost(), conn.getPort(),
+ getPath(), conn.isSecure(),
+ setCookieHeader);
+ state.addCookies(cookies);
+ } catch (Exception e) {
+ // FIXME: Shouldn't be catching exception!
+ log.error("Exception processing response headers", e);
+ }
+ }
+ }
+
+ /**
+ * When this method is invoked, the {@link #getStatusCode status code} and
+ * {@link #getStatusText status text} values will have been set (in other
+ * words, {@link #readStatusLine readStatusLine(HttpState,HttpConnection}
+ * will have been invoked).
+ *
* <p>
- * This implementation writes nothing and returns <tt>true</tt>.
- * @return <tt>true</tt>
+ * Subclasses may want to override this method to respond to these value.
+ * This implementation does nothing.
+ * </p>
+ *
+ * @param state the client state
+ * @param conn the {@link HttpConnection} to read the response from
+ *
+ * @see #readResponse
+ * @see #readStatusLine
*/
- protected boolean writeRequestBody(HttpState state, HttpConnection conn)
- throws IOException, HttpException {
- return true;
+ protected void processStatusLine(HttpState state, HttpConnection conn) {
}
/**
* Reads the response from the given {@link HttpConnection}.
+ *
* <p>
* The response is written according to the following logic:
+ *
* <ol>
* <li>
- * {@link #readStatusLine readStatusLine(HttpState,HttpConnection)}
- * is invoked to read the request line.
+ * {@link #readStatusLine readStatusLine(HttpState,HttpConnection)} is
+ * invoked to read the request line.
* </li>
* <li>
- * {@link #processStatusLine processStatusLine(HttpState,HttpConnection)}
- * is invoked, allowing the method to respond to the status line if desired.
+ * {@link #processStatusLine processStatusLine(HttpState,HttpConnection)}
+ * is invoked, allowing the method to respond to the status line if
+ * desired.
* </li>
* <li>
- * {@link #readResponseHeaders readResponseHeaders(HttpState,HttpConnection}
- * is invoked to read the associated headers.
+ * {@link #readResponseHeaders
+ * readResponseHeaders(HttpState,HttpConnection} is invoked to read the
+ * associated headers.
* </li>
* <li>
- * {@link #processResponseHeaders processResponseHeaders(HttpState,HttpConnection}
- * is invoked, allowing the method to respond to the headers if desired.
+ * {@link #processResponseHeaders
+ * processResponseHeaders(HttpState,HttpConnection} is invoked, allowing
+ * the method to respond to the headers if desired.
* </li>
* <li>
- * {@link #readResponseBody readResponseBody(HttpState,HttpConnection)}
- * is invoked to read the associated body (if any).
+ * {@link #readResponseBody readResponseBody(HttpState,HttpConnection)} is
+ * invoked to read the associated body (if any).
* </li>
* <li>
- * {@link #processResponseBody processResponseBody(HttpState,HttpConnection}
- * is invoked, allowing the method to respond to the body if desired.
+ * {@link #processResponseBody
+ * processResponseBody(HttpState,HttpConnection} is invoked, allowing the
+ * method to respond to the 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
+ *
+ * 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 client state
* @param conn the {@link HttpConnection} to read the response from
+ * @throws IOException when i/o errors occur reading the response
+ * @throws HttpException when a protocol error occurs or state is invalid
*/
protected void readResponse(HttpState state, HttpConnection conn)
throws IOException, HttpException {
- log.trace("enter HttpMethodBase.readResponse(HttpState, HttpConnection)");
- readStatusLine(state,conn);
- processStatusLine(state,conn);
- readResponseHeaders(state,conn);
- processResponseHeaders(state,conn);
- readResponseBody(state,conn);
- processResponseBody(state,conn);
+ log.trace(
+ "enter HttpMethodBase.readResponse(HttpState, HttpConnection)");
+ readStatusLine(state, conn);
+ processStatusLine(state, conn);
+ readResponseHeaders(state, conn);
+ processResponseHeaders(state, conn);
+ readResponseBody(state, conn);
+ processResponseBody(state, conn);
}
/**
- * Read the status line from the given {@link HttpConnection},
- * setting my {@link #getStatusCode status code} and
- * {@link #getStatusText status text}.
+ * Read the response body from the given {@link HttpConnection}.
+ *
* <p>
- * Subclasses may want to override this method to
- * to customize the processing.
- *
- * @see #readResponse
- * @see #processStatusLine
- *
+ * 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).
+ * </p>
+ *
+ * <p>
+ * Subclasses may want to override this method to to customize the
+ * processing.
+ * </p>
+ *
* @param state the client state
* @param conn the {@link HttpConnection} to read the response from
+ * @throws IOException when i/o errors occur reading the response
+ * @throws HttpException when a protocol error occurs or state is invalid
+ *
+ * @see #readResponse
+ * @see #processResponseBody
*/
- protected void readStatusLine(HttpState state, HttpConnection conn)
+ protected void readResponseBody(HttpState state, HttpConnection conn)
throws IOException, HttpException {
- log.trace("enter HttpMethodBase.readStatusLine(HttpState, HttpConnection)");
-
- statusCode = -1;
- statusText = null;
-
- String statusLine = conn.readLine();
+ log.trace(
+ "enter HttpMethodBase.readResponseBody(HttpState, HttpConnection)");
- while(statusLine != null && !statusLine.startsWith("HTTP/")) {
- statusLine = conn.readLine();
- }
- if (statusLine == null) {
- // A null statusLine means the connection was lost before we got a response. Try again.
- throw new HttpRecoverableException("Error in parsing the status line from the response: unable to find line starting with \"HTTP/\"");
- }
-
- if ((!statusLine.startsWith("HTTP/1.1") &&
- !statusLine.startsWith("HTTP/1.0"))) {
- throw new HttpException("Unrecognized server protocol :" + statusLine);
- }
-
- http11 = statusLine.startsWith("HTTP/1.1");
-
- int at = statusLine.indexOf(" ");
- if (at < 0) {
- throw new HttpException("Unable to parse the status line: " + statusLine);
- }
-
- int to = statusLine.indexOf(" ", at + 1);
- if (to < 0) {
- to = statusLine.length();
- }
-
- try {
- statusCode = Integer.parseInt(statusLine.substring(at + 1, to));
- } catch (NumberFormatException e) {
- throw new HttpException("Unable to parse status code from status line: " + statusLine);
- }
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ readResponseBody(state, conn, out);
- try {
- if (to < statusLine.length()) {
- statusText = statusLine.substring(to + 1);
- }
- } catch (StringIndexOutOfBoundsException e) {
- throw new HttpException("Status text not specified: " + statusLine);
- }
+ out.close();
+ responseBody = out.toByteArray();
}
/**
- * When this method is invoked, the {@link #getStatusCode status code}
- * and {@link #getStatusText status text} values will have been set (in other
- * words, {@link #readStatusLine readStatusLine(HttpState,HttpConnection} will
- * have been invoked).
+ * Read the response body from the given {@link HttpConnection}.
+ *
* <p>
- * Subclasses may want to override this method to respond to these value.
- * This implementation does nothing.
- *
- * @see #readResponse
- * @see #readStatusLine
- *
+ * 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).
+ * </p>
+ *
+ * <p>
+ * Subclasses may want to override this method to to customize the
+ * processing.
+ * </p>
+ *
* @param state the client state
* @param conn the {@link HttpConnection} to read the response from
- */
- protected void processStatusLine(HttpState state, HttpConnection conn) {
- }
-
- /**
- * Read response headers from the given {@link HttpConnection},
- * populating the response headers map.
- * <p>
- * Subclasses may want to override this method to
- * to customize the processing.
- * <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)
+ * @param out OutputStream to write the response body to
+ * @throws IOException when i/o errors occur reading the response
*
* @see #readResponse
- * @see #processResponseHeaders
- *
- * @param state the client state
- * @param conn the {@link HttpConnection} to read the response from
+ * @see #processResponseBody
*/
- protected void readResponseHeaders(HttpState state, HttpConnection conn)
- throws IOException, HttpException {
- log.trace("enter HttpMethodBase.readResponseHeaders(HttpState,HttpConnection)");
-
- responseHeaders.clear();
-
- String name = null;
- String value = null;
- for(;;) {
- String line = conn.readLine();
- if ((line == null) || (line.length() < 1)) {
- break;
- }
-
- // Parse the header name and value
- // Check for folded headers first
- // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2
- // discussion on folded headers
-
- boolean isFolded = false;
- if ( line.charAt(0) == ' ' || line.charAt(0) == '\t' )
- {
- // we have continuation folded header
- // so append value
- isFolded = true;
- value = line.substring(1).trim();
- }
- else
- {
- // Otherwise we should have normal HTTP header line
- // Parse the header name and value
- int colon = line.indexOf(":");
- if (colon < 0) {
- throw new HttpException("Unable to parse header: " + line);
- }
- name = line.substring(0, colon).trim();
- value = line.substring(colon + 1).trim();
- }
- Header header = getResponseHeader(name);
- if (null == header) {
- header = new Header(name, value);
- } else {
- String oldvalue = header.getValue();
- if (null != oldvalue) {
- if ( isFolded ) {
- // LWS becomes space plus extended value
- header = new Header(name,oldvalue + " " + value);
- }
- else {
- // Append additional header value
- header = new Header(name,oldvalue + ", " + value);
- }
- } else {
- header = new Header(name,value);
- }
- }
- setResponseHeader(header);
- }
- }
-
- /**
- * When this method is invoked, the response headers
- * map will have been populated with the response headers
- * (in other words,
- * {@link #readResponseHeaders readResponseHeaders(HttpState,HttpConnection)}
- * will have been invoked).
- * <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>
- * Subclasses may want to override this method to
- * specially process additional headers, and/or
- * invoke this method (via <tt>super</tt>) to process
- * the <tt>Set-Cookie</tt> and <tt>Set-Cookie2</tt> headers.
- *
- * @see #readResponse
- * @see #readResponseHeaders
- *
- * @param state the client state
- * @param conn the {@link HttpConnection} to read the response from
- */
- protected void processResponseHeaders(HttpState state, HttpConnection conn)
- {
- log.trace("enter HttpMethodBase.processResponseHeaders(HttpState, HttpConnection)");
- // add cookies, if any
- // should we set cookies?
- Header setCookieHeader = getResponseHeader("set-cookie2");
- if (null == setCookieHeader) { //ignore old-style if new is supported
- setCookieHeader = getResponseHeader("set-cookie");
- }
-
- if (setCookieHeader != null) {
- try {
- Cookie[] cookies = Cookie.parse(conn.getHost(), conn.getPort(), getPath(), conn.isSecure(), setCookieHeader);
- state.addCookies(cookies);
- } catch (Exception e) {
- // FIXME: Shouldn't be catching exception!
- log.error("Exception processing response headers", e);
- }
- }
- }
-
-
- /**
- * 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).
- * <p>
- * Subclasses may want to override this method to
- * to customize the processing.
- *
- * @see #readResponse
- * @see #processResponseBody
- *
- * @param state the client state
- * @param conn the {@link HttpConnection} to read the response from
- */
- protected void readResponseBody(HttpState state, HttpConnection conn)
- throws IOException, HttpException {
- log.trace("enter HttpMethodBase.readResponseBody(HttpState, HttpConnection)");
-
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- readResponseBody(state, conn, out);
-
- out.close();
- responseBody = out.toByteArray();
- }
-
- /**
- * 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).
- * <p>
- * Subclasses may want to override this method to
- * to customize the processing.
- *
- * @see #readResponse
- * @see #processResponseBody
- *
- * @param state the client state
- * @param conn the {@link HttpConnection} to read the response from
- * @param out OutputStream to write the response body to
- */
- protected void readResponseBody(HttpState state, HttpConnection conn,
- OutputStream out) throws IOException {
- log.trace("enter HttpMethodBase.readResponseBody(HttpState, HttpConnection)");
+ protected void readResponseBody(HttpState state, HttpConnection conn,
+ OutputStream out)
+ throws IOException {
+ log.trace(
+ "enter HttpMethodBase.readResponseBody(HttpState, HttpConnection)");
responseBody = null;
int expectedLength = 0;
int foundLength = 0;
{
Header lengthHeader = getResponseHeader("Content-Length");
- Header transferEncodingHeader = getResponseHeader("Transfer-Encoding");
+ Header transferEncodingHeader = getResponseHeader(
+ "Transfer-Encoding");
if (null != lengthHeader) {
try {
expectedLength = Integer.parseInt(lengthHeader.getValue());
- } catch(NumberFormatException e) {
+ } catch (NumberFormatException e) {
// ignored
}
} else if (null != transferEncodingHeader) {
- if ("chunked".equalsIgnoreCase(transferEncodingHeader.getValue())) {
+ if ("chunked".equalsIgnoreCase(
+ transferEncodingHeader.getValue())) {
expectedLength = -1;
}
- } else if(canResponseHaveBody(statusCode) && !getName().equals(ConnectMethod.NAME)){
+ } else if (canResponseHaveBody(statusCode)
+ && !getName().equals(ConnectMethod.NAME)) {
/*
- * According to the specification, a response with neither Content-Length
- * nor Transfer-Encoding indicates that the response has no body. In
- * the real world, this usually means that the server just didn't know
- * the content-length when it sent the response. FIXME: Should we do
- * this only in non-strict mode?
+ * According to the specification, a response with neither
+ * Content-Length nor Transfer-Encoding indicates that the
+ * response has no body. In the real world, this usually means
+ * that the server just didn't know the content-length when it
+ * sent the response.
+ * FIXME: Should we do this only in non-strict mode?
* If we do this we will hang forever waiting for a body!
*/
expectedLength = -1;
@@ -1474,7 +1338,7 @@
InputStream is = conn.getResponseInputStream(this);
byte[] buffer = new byte[4096];
int nb = 0;
- while(expectedLength == -1 || foundLength < expectedLength) {
+ while ((expectedLength == -1) || (foundLength < expectedLength)) {
nb = is.read(buffer);
if (nb == -1) {
break;
@@ -1491,7 +1355,10 @@
if (foundLength == expectedLength) {
break;
} else if (foundLength > expectedLength) {
- log.warn("HttpMethodBase.readResponseBody(): expected length (" + expectedLength + ") exceeded. Found " + foundLength + " bytes.");
+ log.warn(
+ "HttpMethodBase.readResponseBody(): expected length ("
+ + expectedLength + ") exceeded. Found " + foundLength
+ + " bytes.");
break;
}
}
@@ -1499,153 +1366,320 @@
}
/**
- * When this method is invoked,
- * {@link #readResponseBody readResponseBody(HttpState,HttpConnection)}
- * will have been invoked.
- * <p>
- * This implementation does nothing.
- * <p>
- * Subclasses may want to override this method.
- *
- * @see #readResponse
- * @see #readResponseBody
- *
+ * Read response headers from the given {@link HttpConnection}, populating
+ * the response headers map.
+ *
+ * <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 client state
* @param conn the {@link HttpConnection} to read the response from
+ * @throws IOException when i/o errors occur reading the response
+ * @throws HttpException when a protocol error occurs or state is invalid
+ *
+ * @see #readResponse
+ * @see #processResponseHeaders
*/
- protected void processResponseBody(HttpState state, HttpConnection conn) {
+ protected void readResponseHeaders(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ log.trace("enter HttpMethodBase.readResponseHeaders(HttpState,"
+ + "HttpConnection)");
+
+ responseHeaders.clear();
+
+ String name = null;
+ String value = null;
+ for (; ;) {
+ String line = conn.readLine();
+ if ((line == null) || (line.length() < 1)) {
+ break;
+ }
+
+ // Parse the header name and value
+ // Check for folded headers first
+ // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2
+ // discussion on folded headers
+ boolean isFolded = false;
+ if ((line.charAt(0) == ' ') || (line.charAt(0) == '\t')) {
+ // we have continuation folded header
+ // so append value
+ isFolded = true;
+ value = line.substring(1).trim();
+ } else {
+ // Otherwise we should have normal HTTP header line
+ // Parse the header name and value
+ int colon = line.indexOf(":");
+ if (colon < 0) {
+ throw new HttpException("Unable to parse header: " + line);
+ }
+ name = line.substring(0, colon).trim();
+ value = line.substring(colon + 1).trim();
+ }
+ Header header = getResponseHeader(name);
+ if (null == header) {
+ header = new Header(name, value);
+ } else {
+ String oldvalue = header.getValue();
+ if (null != oldvalue) {
+ if (isFolded) {
+ // LWS becomes space plus extended value
+ header = new Header(name, oldvalue + " " + value);
+ } else {
+ // Append additional header value
+ header = new Header(name, oldvalue + ", " + value);
+ }
+ } else {
+ header = new Header(name, value);
+ }
+ }
+ setResponseHeader(header);
+ }
}
/**
- * Recycle this method so that it can be used again.
- * All of my instances variables will be reset
- * once this method has been called.
+ * 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 client state
+ * @param conn the {@link HttpConnection} to read the response from
+ *
+ * @throws IOException when errors occur reading the status line
+ * @throws HttpException If there is no status line, the protocol is not
+ * recognised, if we are unable to parse the status code from the line,
+ * or there was no status text
+ * @throws HttpRecoverableException when the status line is null and the
+ * request should be retried
+ *
+ * @see #readResponse
+ * @see #processStatusLine
*/
- public void recycle() {
- log.trace("enter HttpMethodBase.recycle()");
+ protected void readStatusLine(HttpState state, HttpConnection conn)
+ throws IOException, HttpRecoverableException, HttpException {
+ log.trace(
+ "enter HttpMethodBase.readStatusLine(HttpState, HttpConnection)");
- path = null;
- followRedirects = false;
- queryString = null;
- requestHeaders.clear();
- responseHeaders.clear();
statusCode = -1;
statusText = null;
- used = false;
- http11 = true;
- bodySent = false;
- responseBody = null;
- }
- // ---------------------------------------------- Protected Utility Methods
+ String statusLine = conn.readLine();
- /**
- * Return <tt>true</tt> if I should use the HTTP/1.1 protocol.
- * @internal
- */
- public boolean isHttp11() {
- return http11;
+ while ((statusLine != null) && !statusLine.startsWith("HTTP/")) {
+ statusLine = conn.readLine();
+ }
+ if (statusLine == null) {
+ // A null statusLine means the connection was lost before we got a
+ // response. Try again.
+ throw new HttpRecoverableException("Error in parsing the status "
+ + " line from the response: unable to find line starting with"
+ + " \"HTTP/\"");
+ }
+
+ if ((!statusLine.startsWith("HTTP/1.1")
+ && !statusLine.startsWith("HTTP/1.0"))) {
+ throw new HttpException("Unrecognized server protocol :"
+ + statusLine);
+ }
+
+ http11 = statusLine.startsWith("HTTP/1.1");
+
+ int at = statusLine.indexOf(" ");
+ if (at < 0) {
+ throw new HttpException("Unable to parse the status line: "
+ + statusLine);
+ }
+
+ int to = statusLine.indexOf(" ", at + 1);
+ if (to < 0) {
+ to = statusLine.length();
+ }
+
+ try {
+ statusCode = Integer.parseInt(statusLine.substring(at + 1, to));
+ } catch (NumberFormatException e) {
+ throw new HttpException(
+ "Unable to parse status code from status line: " + statusLine);
+ }
+
+ try {
+ if (to < statusLine.length()) {
+ statusText = statusLine.substring(to + 1);
+ }
+ } catch (StringIndexOutOfBoundsException e) {
+ throw new HttpException("Status text not specified: " + statusLine);
+ }
}
+ // ------------------------------------------------------ Protected Methods
+
/**
- * Set whether or not I should use the HTTP/1.1 protocol.
- * @internal
+ * <p>
+ * Writes my request to the given {@link HttpConnection}.
+ * </p>
+ *
+ * <p>
+ * The request is written according to the following logic:
+ * </p>
+ *
+ * <ol>
+ * <li>
+ * {@link #writeRequestLine writeRequestLine(HttpState, HttpConnection)} is
+ * invoked to write the request line.
+ * </li>
+ * <li>
+ * {@link #writeRequestHeaders 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 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 client state
+ * @param conn the {@link HttpConnection} to write the request to
+ * @throws IOException when i/o errors occur reading the response
+ * @throws HttpException when a protocol error occurs or state is invalid
*/
- public void setHttp11(boolean http11) {
- this.http11 = http11;
+ 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
+ bodySent = writeRequestBody(state, conn);
}
/**
- * Throws an {@link IllegalStateException} if
- * used by not recycled.
+ * Write the request body to the given {@link HttpConnection}
+ *
+ * <p>
+ * If an expectation is required, this method should ensure that it has
+ * been sent by checking the {@link #getStatusCode status code}.
+ * </p>
+ *
+ * <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 (for example, expectation required but not present).
+ * </p>
+ *
+ * <p>
+ * This implementation writes nothing and returns <tt>true</tt>.
+ * </p>
+ *
+ * @param state the client state
+ * @param conn the connection to write to
+ *
+ * @return <tt>true</tt>
+ * @throws IOException when i/o errors occur reading the response
+ * @throws HttpException when a protocol error occurs or state is invalid
*/
- protected void checkNotUsed() {
- if (used) {
- throw new IllegalStateException("Already used.");
- }
+ protected boolean writeRequestBody(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ return true;
}
/**
- * Throws an {@link IllegalStateException} if
- * not used since last recycle.
+ * Writes the request headers to the given {@link HttpConnection}.
+ *
+ * <p>
+ * This implementation invokes {@link #addRequestHeaders
+ * 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 client state
+ * @param conn the {@link HttpConnection} to write to
+ * @throws IOException when i/o errors occur reading the response
+ * @throws HttpException when a protocol error occurs or state is invalid
+ *
+ * @see #addRequestHeaders
+ * @see #getRequestHeaders
*/
- protected void checkUsed() {
- if (!used) {
- throw new IllegalStateException("Not Used.");
+ protected void writeRequestHeaders(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ log.trace("enter HttpMethodBase.writeRequestHeaders(HttpState,"
+ + "HttpConnection)");
+ addRequestHeaders(state, conn);
+ Iterator it = requestHeaders.values().iterator();
+ while (it.hasNext()) {
+ conn.print(((Header) it.next()).toExternalForm());
}
}
- // ------------------------------------------------- Static Utility Methods
-
/**
- * Generate an HTTP/S request line according to
- * the specified attributes.
+ * Writes the "request line" to the given {@link HttpConnection}.
+ *
+ * <p>
+ * Subclasses may want to override this method to to customize the
+ * processing.
+ * </p>
+ *
+ * @param state the client state
+ * @param conn the {@link HttpConnection} to write to
+ * @throws IOException when i/o errors occur reading the response
+ * @throws HttpException when a protocol error occurs or state is invalid
+ *
+ * @see #generateRequestLine
*/
- protected static String generateRequestLine(HttpConnection connection,
- String name, String reqPath, String qString, String protocol) {
- log.trace("enter HttpMethodBase.generateRequestLine(HttpConnection, String, String, String, String)");
-
- StringBuffer buf = new StringBuffer();
- buf.append(null == reqPath ? "/" : URIUtil.encode(reqPath,URIUtil.pathSafe()));
- if (null != qString) {
- if (qString.indexOf("?") < 0) {
- buf.append("?");
- }
- buf.append(qString);
- }
-
- if (!connection.isProxied() || connection.isTransparent()) {
- return (name + " " + buf.toString() + " " + protocol + "\r\n");
- } else {
- if (connection.isSecure()) {
- return (name +
- " https://" +
- connection.getHost() +
- ((443 == connection.getPort() || -1 == connection.getPort()) ? "" : (":" + connection.getPort()) ) +
- buf.toString() +
- " " +
- protocol +
- "\r\n");
- } else {
- return (name +
- " http://" +
- connection.getHost() +
- ((80 == connection.getPort() || -1 == connection.getPort()) ? "" : (":" + connection.getPort()) ) +
- buf.toString() +
- " " +
- protocol +
- "\r\n");
- }
- }
+ protected void writeRequestLine(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ log.trace(
+ "enter HttpMethodBase.writeRequestLine(HttpState, HttpConnection)");
+ String requestLine =
+ HttpMethodBase.generateRequestLine(conn, getName(),
+ getPath(), getQueryString(),
+ getHttpVersion());
+ conn.print(requestLine);
}
/**
- * Per RFC 2616 section 4.3, some response can never contain
- * a message body.
- *
- * @param status - the HTTP status code
- * @return true if the message may contain a body, false if it can not contain a message body
+ * Get the HTTP version.
+ *
+ * @return HTTP/1.1 if http11, HTTP/1.0 otherwise
+ *
+ * @since 2.0
*/
- private static boolean canResponseHaveBody(int status) {
- log.trace("enter HttpMethodBase.canResponseHaveBody(int)");
-
- boolean result = true;
-
- if((status >= 100 && status <= 199) || // 1XX
- status == 204 || // NO CONTENT
- status == 304){ // NOT MODIFIED
- result = false;
- }
-
- return result;
+ private String getHttpVersion() {
+ return (http11 ? "HTTP/1.1" : "HTTP/1.0");
}
/**
- * Determines if the provided value is a valid IPv4
- * internet address.
+ * Determines if the provided value is a valid IPv4 internet address.
+ *
* @param value - value to check
+ *
* @return boolean - true if value is valid, otherwise false
- *
*/
private static boolean isIpAddress(String value) {
log.trace("enter HttpMethodBase.isIpAddress(String)");
@@ -1662,7 +1696,7 @@
while (tokenizer.hasMoreTokens()) {
try {
int i = Integer.parseInt(tokenizer.nextToken());
- if (i < 0 || i > 255) {
+ if ((i < 0) || (i > 255)) {
// parsed section of address is not in the proper range
return false;
}
@@ -1677,52 +1711,288 @@
return true;
}
- // ----------------------------------------------------- Instance Variables
- /** My request path. */
- private String path = null;
- /** Whether or not I should automatically follow redirects. */
- private boolean followRedirects = false;
- /** My query string, if any. */
- private String queryString = null;
- /** My request headers, if any. */
- private HashMap requestHeaders = new HashMap();
- /** My response headers, if any. */
- private HashMap responseHeaders = new HashMap();
- /** My response status code, if any. */
- private int statusCode = -1;
- /** My response status text, if any. */
- private String statusText = null;
- /** Whether or not I have been executed. */
- private boolean used = false;
- /** Whether or not I should use the HTTP/1.1 protocol. */
- private boolean http11 = true;
- /** Whether or not the request body has been sent. */
- private boolean bodySent = false;
- /** The response body, assuming it has not be intercepted by a sub-class. */
- private byte[] responseBody = null;
- /** The maximum number of attempts to attempt recovery from an HttpRecoverableException. */
- private int maxRetries = 3;
- /** Maximum number of redirects and authentications that will be followed. */
- private static int maxForwards = 100;
- /** True if we're in strict mode. */
- private boolean strictMode = true;
+ /**
+ * "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."
+ * //TODO: This method is trying to make up for deficiencies in Header.
+ *
+ * @param existingHeader the current header
+ * @param value DOCUMENT ME!
+ *
+ * @return DOCUMENT ME!
+ */
+ private String getNewHeaderValue(Header existingHeader, String value) {
+ String existingValue = existingHeader.getValue();
+ if (existingValue == null) {
+ existingValue = "";
+ }
+ String newValue = value;
+ if (value == null) {
+ newValue = "";
+ }
+ return existingValue + ", " + newValue;
+ }
- // -------------------------------------------------------------- Constants
+ /**
+ * Sets the specified response header.
+ *
+ * @param header the header to set.
+ *
+ * @since 2.0
+ */
+ private void setResponseHeader(Header header) {
+ if (header == null) {
+ return;
+ }
+ responseHeaders.put(header.getName().toLowerCase(), header);
+ }
- /** Log object for this class. */
- private static final Log log = LogFactory.getLog(HttpMethod.class);
+ /**
+ * Per RFC 2616 section 4.3, some response can never contain a message
+ * body.
+ *
+ * @param status - the HTTP status code
+ *
+ * @return true if the message may contain a body, false if it can not
+ * contain a message body
+ */
+ private static boolean canResponseHaveBody(int status) {
+ log.trace("enter HttpMethodBase.canResponseHaveBody(int)");
- /** Log for any wire messages. */
- private static final Log wireLog = LogFactory.getLog("httpclient.wire");
+ boolean result = true;
- /** The User-Agent header sent on every request. */
- protected static final Header USER_AGENT;
+ if ((status >= 100 && status <= 199) || (status == 204)
+ || (status == 304)) { // NOT MODIFIED
+ result = false;
+ }
- static {
- String agent = System.getProperties().getProperty(
- "httpclient.useragent", "Jakarta Commons-HttpClient/2.0M1");
- USER_AGENT = new Header("User-Agent", agent);
+ return result;
}
-}
+ /**
+ * 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
+ */
+ private void closeConnection(HttpConnection connection) {
+ log.trace("enter closeConnection(HttpConnection)");
+ if (!http11) {
+ if (getName().equals(ConnectMethod.NAME)
+ && (statusCode == HttpStatus.SC_OK)) {
+ log.debug("Leaving connection open for tunneling");
+ } else {
+ log.debug("Closing connection since using HTTP/1.0, "
+ + "ConnectMethod and status is OK");
+ connection.close();
+ }
+ } else {
+ Header connectionHeader = getResponseHeader("connection");
+ if ((null != connectionHeader)
+ && "close".equalsIgnoreCase(connectionHeader.getValue())) {
+ log.debug("Closing connection since \"Connection: close\" "
+ + "header found.");
+ connection.close();
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ log.trace("enter HttpMethodBase.processAuthenticationResponse("
+ + "HttpState, HttpConnection)");
+
+ Set realms = new HashSet();
+ Set proxyRealms = new HashSet();
+
+ // handle authentication required
+ Header wwwauth = null;
+ Set realmsUsed = null;
+ switch (statusCode) {
+ case HttpStatus.SC_UNAUTHORIZED:
+ wwwauth = getResponseHeader(Authenticator.WWW_AUTH);
+ realmsUsed = realms;
+ break;
+ case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
+ wwwauth = getResponseHeader(Authenticator.PROXY_AUTH);
+ realmsUsed = proxyRealms;
+ break;
+ }
+ boolean authenticated = false;
+ // if there was a header requesting authentication
+ if (null != wwwauth) {
+ String pathAndCreds = getPath() + ":" + wwwauth.getValue();
+ if (realmsUsed.contains(pathAndCreds)) {
+ if (log.isInfoEnabled()) {
+ log.info("Already tried to authenticate to \""
+ + wwwauth.getValue() + "\" but still receiving "
+ + statusCode + ".");
+ }
+ return true;
+ } else {
+ realmsUsed.add(pathAndCreds);
+ }
+
+ try {
+ //remove preemptive header and reauthenticate
+ switch (statusCode) {
+ case HttpStatus.SC_UNAUTHORIZED:
+ removeRequestHeader(Authenticator.WWW_AUTH_RESP);
+ authenticated = Authenticator.authenticate(this, state);
+ break;
+ case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
+ removeRequestHeader(Authenticator.PROXY_AUTH_RESP);
+ authenticated = Authenticator.authenticateProxy(this,
+ state);
+ break;
+ }
+ } catch (HttpException httpe) {
+ log.warn(httpe.getMessage());
+ return true; // finished request
+ } catch (UnsupportedOperationException uoe) {
+ log.warn(uoe.getMessage());
+ //FIXME: should this return true?
+ }
+
+ if (!authenticated) {
+ // won't be able to authenticate to this challenge
+ // without additional information
+ log.debug("HttpMethodBase.execute(): Server demands "
+ + "authentication credentials, but none are "
+ + "available, so aborting.");
+ } else {
+ log.debug("HttpMethodBase.execute(): Server demanded "
+ + "authentication credentials, will try again.");
+ // let's try it again, using the credentials
+ }
+ }
+
+ return !authenticated; // finished processing if we aren't authenticated
+ }
+
+ /**
+ * Write a request and read the response. Both the write to the server will
+ * be retried {@link #maxRetries} times if the operation fails with a
+ * HttpRecoverableException. The write will only be attempted if the read
+ * has succeeded.
+ *
+ * <p>
+ * The <i>used</i> is set to true if the write succeeds.
+ * </p>
+ *
+ * @param state the current state
+ * @param connection the connection for communication
+ *
+ * @throws HttpException when errors occur as part of the HTTP protocol
+ * conversation
+ * @throws IOException when an I/O error occurs communicating with the
+ * server
+ *
+ * @see writeRequest(HttpState,HttpConnection)
+ * @see readResponse(HttpState,HttpConnection)
+ */
+ private void processRequest(HttpState state, HttpConnection connection)
+ throws HttpException, IOException {
+ log.trace(
+ "enter HttpMethodBase.processRequest(HttpState, HttpConnection)");
+
+ //try to do the write
+ int retryCount = 0;
+ do {
+ retryCount++;
+ if (log.isTraceEnabled()) {
+ log.trace("Attempt number " + retryCount + " to write request");
+ }
+ try {
+ if (!connection.isOpen()) {
+ log.debug("Opening the connection.");
+ connection.open();
+ }
+ writeRequest(state, connection);
+ used = true; //write worked, mark this method as used
+ break; //move onto the write
+ } catch (HttpRecoverableException httpre) {
+ if (log.isDebugEnabled()) {
+ log.debug("Closing the connection.");
+ }
+
+ connection.close();
+ log.info("Recoverable exception caught when writing request");
+ if (retryCount == maxRetries) {
+ log.warn(
+ "Attempt to write request has reached max retries: "
+ + maxRetries);
+ throw httpre;
+ }
+ }
+ } while (retryCount <= maxRetries);
+
+ //try to do the read
+ try {
+ readResponse(state, connection);
+ } catch (HttpRecoverableException httpre) {
+ log.warn("Recoverable exception caught when reading response");
+ if (log.isDebugEnabled()) {
+ log.debug("Closing the connection.");
+ }
+
+ connection.close();
+ throw httpre;
+ }
+ //everything should be OK at this point
+ }
+
+ /**
+ * On a {@link HttpStatus#SC_CONTINUE continue}, if there are more request
+ * bytes to be sent, write them to the connection
+ *
+ * @param state the current state
+ * @param connection the connection for communication
+ *
+ * @throws HttpException when errors occur as part of the HTTP protocol
+ * conversation
+ * @throws IOException when an I/O error occurs communicating with the
+ * server
+ */
+ private void writeRemainingRequestBody(HttpState state,
+ HttpConnection connection)
+ throws HttpException, IOException {
+ log.trace("enter writeRemainingRequestBody(HttpState, HttpConnection)");
+
+ if (HttpStatus.SC_CONTINUE == statusCode) {
+ if (!bodySent) {
+ bodySent = writeRequestBody(state, connection);
+ } else {
+ log.warn("Received status CONTINUE but he body has already been"
+ + "sent");
+ // According to RFC 2616 this respose should be ignored
+ }
+ readResponse(state, connection);
+ }
+ }
+}
\ No newline at end of file
--
To unsubscribe, e-mail: <mailto:commons-dev-unsubscribe@jakarta.apache.org>
For additional commands, e-mail: <mailto:commons-dev-help@jakarta.apache.org>
|