+ * HTTP "magic-cookie" represents a piece of state information
+ * that the HTTP agent and the target server can exchange to maintain
+ * a session.
+ *
+ *
+ * @author B.C. Holmes
+ * @author Park, Sung-Gu
+ * @author Doug Sale
+ * @author Rod Waldhoff
+ * @author dIon Gillard
+ * @author Sean C. Sullivan
+ * @author John Evans
+ * @author Marc A. Saegesser
+ * @author Oleg Kalnichevski
+ * @author Mike Bowler
+ *
+ * @version $Revision$ $Date$
+ */
+@SuppressWarnings({"serial","unchecked"}) // <- HERITRIX CHANGE
+public class Cookie extends NameValuePair implements Serializable, Comparator {
+
+ // ----------------------------------------------------------- Constructors
+
+ /**
+ * Default constructor. Creates a blank cookie
+ */
+
+ public Cookie() {
+ this(null, "noname", null, null, null, false);
+ }
+
+ /**
+ * Creates a cookie with the given name, value and domain attribute.
+ *
+ * @param name the cookie name
+ * @param value the cookie value
+ * @param domain the domain this cookie can be sent to
+ */
+ public Cookie(String domain, String name, String value) {
+ this(domain, name, value, null, null, false);
+ }
+
+ /**
+ * Creates a cookie with the given name, value, domain attribute,
+ * path attribute, expiration attribute, and secure attribute
+ *
+ * @param name the cookie name
+ * @param value the cookie value
+ * @param domain the domain this cookie can be sent to
+ * @param path the path prefix for which this cookie can be sent
+ * @param expires the {@link Date} at which this cookie expires,
+ * or null if the cookie expires at the end
+ * of the session
+ * @param secure if true this cookie can only be sent over secure
+ * connections
+ * @throws IllegalArgumentException If cookie name is null or blank,
+ * cookie name contains a blank, or cookie name starts with character $
+ *
+ */
+ public Cookie(String domain, String name, String value,
+ String path, Date expires, boolean secure) {
+
+ super(name, value);
+ LOG.trace("enter Cookie(String, String, String, String, Date, boolean)");
+ if (name == null) {
+ throw new IllegalArgumentException("Cookie name may not be null");
+ }
+ if (name.trim().equals("")) {
+ throw new IllegalArgumentException("Cookie name may not be blank");
+ }
+ this.setPath(path);
+ this.setDomain(domain);
+ this.setExpiryDate(expires);
+ this.setSecure(secure);
+ }
+
+ /**
+ * Creates a cookie with the given name, value, domain attribute,
+ * path attribute, maximum age attribute, and secure attribute
+ *
+ * @param name the cookie name
+ * @param value the cookie value
+ * @param domain the domain this cookie can be sent to
+ * @param path the path prefix for which this cookie can be sent
+ * @param maxAge the number of seconds for which this cookie is valid.
+ * maxAge is expected to be a non-negative number.
+ * -1 signifies that the cookie should never expire.
+ * @param secure if true this cookie can only be sent over secure
+ * connections
+ */
+ public Cookie(String domain, String name, String value, String path,
+ int maxAge, boolean secure) {
+
+ this(domain, name, value, path, null, secure);
+ if (maxAge < -1) {
+ throw new IllegalArgumentException("Invalid max age: " + Integer.toString(maxAge));
+ }
+ if (maxAge >= 0) {
+ setExpiryDate(new Date(System.currentTimeMillis() + maxAge * 1000L));
+ }
+ }
+
+ /**
+ * Returns the comment describing the purpose of this cookie, or
+ * null if no such comment has been defined.
+ *
+ * @return comment
+ *
+ * @see #setComment(String)
+ */
+ public String getComment() {
+ return cookieComment;
+ }
+
+ /**
+ * If a user agent (web browser) presents this cookie to a user, the
+ * cookie's purpose will be described using this comment.
+ *
+ * @param comment
+ *
+ * @see #getComment()
+ */
+ public void setComment(String comment) {
+ cookieComment = comment;
+ }
+
+ /**
+ * Returns the expiration {@link Date} of the cookie, or null
+ * if none exists.
+ *
Note: the object returned by this method is
+ * considered immutable. Changing it (e.g. using setTime()) could result
+ * in undefined behaviour. Do so at your peril.
Note: the object returned by this method is considered
+ * immutable. Changing it (e.g. using setTime()) could result in undefined
+ * behaviour. Do so at your peril.
+ *
+ * @param expiryDate the {@link Date} after which this cookie is no longer valid.
+ *
+ * @see #getExpiryDate
+ *
+ */
+ public void setExpiryDate (Date expiryDate) {
+ cookieExpiryDate = expiryDate;
+ }
+
+
+ /**
+ * Returns false if the cookie should be discarded at the end
+ * of the "session"; true otherwise.
+ *
+ * @return false if the cookie should be discarded at the end
+ * of the "session"; true otherwise
+ */
+ public boolean isPersistent() {
+ return (null != cookieExpiryDate);
+ }
+
+
+ /**
+ * Returns domain attribute of the cookie.
+ *
+ * @return the value of the domain attribute
+ *
+ * @see #setDomain(java.lang.String)
+ */
+ public String getDomain() {
+ return cookieDomain;
+ }
+
+ /**
+ * Sets the domain attribute.
+ *
+ * @param domain The value of the domain attribute
+ *
+ * @see #getDomain
+ */
+ public void setDomain(String domain) {
+ if (domain != null) {
+ int ndx = domain.indexOf(":");
+ if (ndx != -1) {
+ domain = domain.substring(0, ndx);
+ }
+ cookieDomain = domain.toLowerCase();
+ }
+ }
+
+
+ /**
+ * Returns the path attribute of the cookie
+ *
+ * @return The value of the path attribute.
+ *
+ * @see #setPath(java.lang.String)
+ */
+ public String getPath() {
+ return cookiePath;
+ }
+
+ /**
+ * Sets the path attribute.
+ *
+ * @param path The value of the path attribute
+ *
+ * @see #getPath
+ *
+ */
+ public void setPath(String path) {
+ cookiePath = path;
+ }
+
+ /**
+ * @return true if this cookie should only be sent over secure connections.
+ * @see #setSecure(boolean)
+ */
+ public boolean getSecure() {
+ return isSecure;
+ }
+
+ /**
+ * Sets the secure attribute of the cookie.
+ *
+ * When true the cookie should only be sent
+ * using a secure protocol (https). This should only be set when
+ * the cookie's originating server used a secure protocol to set the
+ * cookie's value.
+ *
+ * @param secure The value of the secure attribute
+ *
+ * @see #getSecure()
+ */
+ public void setSecure (boolean secure) {
+ isSecure = secure;
+ }
+
+ /**
+ * Returns the version of the cookie specification to which this
+ * cookie conforms.
+ *
+ * @return the version of the cookie.
+ *
+ * @see #setVersion(int)
+ *
+ */
+ public int getVersion() {
+ return cookieVersion;
+ }
+
+ /**
+ * Sets the version of the cookie specification to which this
+ * cookie conforms.
+ *
+ * @param version the version of the cookie.
+ *
+ * @see #getVersion
+ */
+ public void setVersion(int version) {
+ cookieVersion = version;
+ }
+
+ /**
+ * Returns true if this cookie has expired.
+ *
+ * @return true if the cookie has expired.
+ */
+ public boolean isExpired() {
+ return (cookieExpiryDate != null
+ && cookieExpiryDate.getTime() <= System.currentTimeMillis());
+ }
+
+ /**
+ * Returns true if this cookie has expired according to the time passed in.
+ *
+ * @param now The current time.
+ *
+ * @return true if the cookie expired.
+ */
+ public boolean isExpired(Date now) {
+ return (cookieExpiryDate != null
+ && cookieExpiryDate.getTime() <= now.getTime());
+ }
+
+
+ /**
+ * Indicates whether the cookie had a path specified in a
+ * path attribute of the Set-Cookie header. This value
+ * is important for generating the Cookie header because
+ * some cookie specifications require that the Cookie header
+ * should only include a path attribute if the cookie's path
+ * was specified in the Set-Cookie header.
+ *
+ * @param value true if the cookie's path was explicitly
+ * set, false otherwise.
+ *
+ * @see #isPathAttributeSpecified
+ */
+ public void setPathAttributeSpecified(boolean value) {
+ hasPathAttribute = value;
+ }
+
+ /**
+ * Returns true if cookie's path was set via a path attribute
+ * in the Set-Cookie header.
+ *
+ * @return value true if the cookie's path was explicitly
+ * set, false otherwise.
+ *
+ * @see #setPathAttributeSpecified
+ */
+ public boolean isPathAttributeSpecified() {
+ return hasPathAttribute;
+ }
+
+ /**
+ * Indicates whether the cookie had a domain specified in a
+ * domain attribute of the Set-Cookie header. This value
+ * is important for generating the Cookie header because
+ * some cookie specifications require that the Cookie header
+ * should only include a domain attribute if the cookie's domain
+ * was specified in the Set-Cookie header.
+ *
+ * @param value true if the cookie's domain was explicitly
+ * set, false otherwise.
+ *
+ * @see #isDomainAttributeSpecified
+ */
+ public void setDomainAttributeSpecified(boolean value) {
+ hasDomainAttribute = value;
+ }
+
+ /**
+ * Returns true if cookie's domain was set via a domain
+ * attribute in the Set-Cookie header.
+ *
+ * @return value true if the cookie's domain was explicitly
+ * set, false otherwise.
+ *
+ * @see #setDomainAttributeSpecified
+ */
+ public boolean isDomainAttributeSpecified() {
+ return hasDomainAttribute;
+ }
+
+ /**
+ * Returns a hash code in keeping with the
+ * {@link Object#hashCode} general hashCode contract.
+ * @return A hash code
+ */
+ public int hashCode() {
+ int hash = LangUtils.HASH_SEED;
+ hash = LangUtils.hashCode(hash, this.getName());
+ hash = LangUtils.hashCode(hash, this.cookieDomain);
+ hash = LangUtils.hashCode(hash, this.cookiePath);
+ return hash;
+ }
+
+
+ /**
+ * Two cookies are equal if the name, path and domain match.
+ * @param obj The object to compare against.
+ * @return true if the two objects are equal.
+ */
+ public boolean equals(Object obj) {
+ if (obj == null) return false;
+ if (this == obj) return true;
+ if (obj instanceof Cookie) {
+ Cookie that = (Cookie) obj;
+ return LangUtils.equals(this.getName(), that.getName())
+ && LangUtils.equals(this.cookieDomain, that.cookieDomain)
+ && LangUtils.equals(this.cookiePath, that.cookiePath);
+ } else {
+ return false;
+ }
+ }
+
+
+ /**
+ * Return a textual representation of the cookie.
+ *
+ * @return string.
+ */
+ public String toExternalForm() {
+ CookieSpec spec = null;
+ if (getVersion() > 0) {
+ spec = CookiePolicy.getDefaultSpec();
+ } else {
+ spec = CookiePolicy.getCookieSpec(CookiePolicy.NETSCAPE);
+ }
+ return spec.formatCookie(this);
+ }
+
+ /**
+ *
Compares two cookies to determine order for cookie header.
+ *
Most specific should be first.
+ *
This method is implemented so a cookie can be used as a comparator for
+ * a SortedSet of cookies. Specifically it's used above in the
+ * createCookieHeader method.
+ * @param o1 The first object to be compared
+ * @param o2 The second object to be compared
+ * @return See {@link java.util.Comparator#compare(Object,Object)}
+ */
+ public int compare(Object o1, Object o2) {
+ LOG.trace("enter Cookie.compare(Object, Object)");
+
+ if (!(o1 instanceof Cookie)) {
+ throw new ClassCastException(o1.getClass().getName());
+ }
+ if (!(o2 instanceof Cookie)) {
+ throw new ClassCastException(o2.getClass().getName());
+ }
+ Cookie c1 = (Cookie) o1;
+ Cookie c2 = (Cookie) o2;
+ if (c1.getPath() == null && c2.getPath() == null) {
+ return 0;
+ } else if (c1.getPath() == null) {
+ // null is assumed to be "/"
+ if (c2.getPath().equals(CookieSpec.PATH_DELIM)) {
+ return 0;
+ } else {
+ return -1;
+ }
+ } else if (c2.getPath() == null) {
+ // null is assumed to be "/"
+ if (c1.getPath().equals(CookieSpec.PATH_DELIM)) {
+ return 0;
+ } else {
+ return 1;
+ }
+ } else {
+ return STRING_COLLATOR.compare(c1.getPath(), c2.getPath());
+ }
+ }
+
+ /**
+ * Return a textual representation of the cookie.
+ *
+ * @return string.
+ *
+ * @see #toExternalForm
+ */
+ public String toString() {
+ return toExternalForm();
+ }
+
+// BEGIN IA/HERITRIX ADDITION
+ /**
+ * Create a 'sort key' for this Cookie that will cause it to sort
+ * alongside other Cookies of the same domain (with or without leading
+ * '.'). This helps cookie-match checks consider only narrow set of
+ * possible matches, rather than all cookies.
+ *
+ * Only two cookies that are equals() (same domain, path, name) will have
+ * the same sort key. The '\1' separator character is important in
+ * conjunction with Cookie.DOMAIN+OVERBOUNDS, allowing keys based on the
+ * domain plus an extension to define the relevant range in a SortedMap.
+ * @return String sort key for this cookie
+ */
+ public String getSortKey() {
+ String domain = getDomain();
+ return (domain.startsWith("."))
+ ? domain.substring(1) + "\1.\1" + getPath() + "\1" + getName()
+ : domain + "\1\1" + getPath() + "\1" + getName();
+ }
+// END IA/HERITRIX ADDITION
+
+ // ----------------------------------------------------- Instance Variables
+
+ /** Comment attribute. */
+ private String cookieComment;
+
+ /** Domain attribute. */
+ private String cookieDomain;
+
+ /** Expiration {@link Date}. */
+ private Date cookieExpiryDate;
+
+ /** Path attribute. */
+ private String cookiePath;
+
+ /** My secure flag. */
+ private boolean isSecure;
+
+ /**
+ * Specifies if the set-cookie header included a Path attribute for this
+ * cookie
+ */
+ private boolean hasPathAttribute = false;
+
+ /**
+ * Specifies if the set-cookie header included a Domain attribute for this
+ * cookie
+ */
+ private boolean hasDomainAttribute = false;
+
+ /** The version of the cookie specification I was created from. */
+ private int cookieVersion = 0;
+
+ // -------------------------------------------------------------- Constants
+
+ /**
+ * Collator for Cookie comparisons. Could be replaced with references to
+ * specific Locales.
+ */
+ private static final RuleBasedCollator STRING_COLLATOR =
+ (RuleBasedCollator) RuleBasedCollator.getInstance(
+ new Locale("en", "US", ""));
+
+ /** Log object for this class */
+ private static final Log LOG = LogFactory.getLog(Cookie.class);
+
+// BEGIN IA/HERITRIX ADDITION
+ /**
+ * Character which, if appended to end of a domain, will give a
+ * boundary key that sorts past all Cookie sortKeys for the same
+ * domain.
+ */
+ public static final char DOMAIN_OVERBOUNDS = '\2';
+// END IA/HERITRIX ADDITION
+}
+
diff --git a/commons/src/main/java/org/apache/commons/httpclient/HttpConnection.java b/commons/src/main/java/org/apache/commons/httpclient/HttpConnection.java
new file mode 100644
index 00000000..c9e86a61
--- /dev/null
+++ b/commons/src/main/java/org/apache/commons/httpclient/HttpConnection.java
@@ -0,0 +1,1406 @@
+/*
+ * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java,v 1.107 2005/01/14 21:30:59 olegk Exp $
+ * $Revision$
+ * $Date$
+ *
+ * ====================================================================
+ *
+ * Copyright 1999-2004 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.commons.httpclient;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.lang.reflect.Method;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketException;
+
+import org.apache.commons.httpclient.params.HttpConnectionParams;
+import org.apache.commons.httpclient.protocol.Protocol;
+import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
+import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
+import org.apache.commons.httpclient.util.EncodingUtil;
+import org.apache.commons.httpclient.util.ExceptionUtil;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.archive.util.Recorder; // <- // IA/HERITRIX import
+
+/**
+ * An abstraction of an HTTP {@link InputStream} and {@link OutputStream}
+ * pair, together with the relevant attributes.
+ *
+ * The following options are set on the socket before getting the input/output
+ * streams in the {@link #open()} method:
+ *
+ *
+ * @author Rod Waldhoff
+ * @author Sean C. Sullivan
+ * @author Ortwin Glueck
+ * @author Jeff Dever
+ * @author Mike Bowler
+ * @author Oleg Kalnichevski
+ * @author Michael Becke
+ * @author Eric E Johnson
+ * @author Laura Werner
+ *
+ * @version $Revision$ $Date$
+ */
+@SuppressWarnings("unchecked") // <- HERITRIX CHANGE
+public class HttpConnection {
+
+ // ----------------------------------------------------------- Constructors
+
+ /**
+ * Creates a new HTTP connection for the given host and port.
+ *
+ * @param host the host to connect to
+ * @param port the port to connect to
+ */
+ public HttpConnection(String host, int port) {
+ this(null, -1, host, null, port, Protocol.getProtocol("http"));
+ }
+
+ /**
+ * Creates a new HTTP connection for the given host and port
+ * using the given protocol.
+ *
+ * @param host the host to connect to
+ * @param port the port to connect to
+ * @param protocol the protocol to use
+ */
+ public HttpConnection(String host, int port, Protocol protocol) {
+ this(null, -1, host, null, port, protocol);
+ }
+
+ /**
+ * Creates a new HTTP connection for the given host with the virtual
+ * alias and port using given protocol.
+ *
+ * @param host the host to connect to
+ * @param virtualHost the virtual host requests will be sent to
+ * @param port the port to connect to
+ * @param protocol the protocol to use
+ */
+ public HttpConnection(String host, String virtualHost, int port, Protocol protocol) {
+ this(null, -1, host, virtualHost, port, protocol);
+ }
+
+ /**
+ * Creates a new HTTP connection for the given host and port via the
+ * given proxy host and port using the default protocol.
+ *
+ * @param proxyHost the host to proxy via
+ * @param proxyPort the port to proxy via
+ * @param host the host to connect to
+ * @param port the port to connect to
+ */
+ public HttpConnection(
+ String proxyHost,
+ int proxyPort,
+ String host,
+ int port) {
+ this(proxyHost, proxyPort, host, null, port, Protocol.getProtocol("http"));
+ }
+
+ /**
+ * Creates a new HTTP connection for the given host configuration.
+ *
+ * @param hostConfiguration the host/proxy/protocol to use
+ */
+ public HttpConnection(HostConfiguration hostConfiguration) {
+ this(hostConfiguration.getProxyHost(),
+ hostConfiguration.getProxyPort(),
+ hostConfiguration.getHost(),
+ hostConfiguration.getPort(),
+ hostConfiguration.getProtocol());
+ this.localAddress = hostConfiguration.getLocalAddress();
+ }
+
+ /**
+ * Creates a new HTTP connection for the given host with the virtual
+ * alias and port via the given proxy host and port using the given
+ * protocol.
+ *
+ * @param proxyHost the host to proxy via
+ * @param proxyPort the port to proxy via
+ * @param host the host to connect to. Parameter value must be non-null.
+ * @param virtualHost No longer applicable.
+ * @param port the port to connect to
+ * @param protocol The protocol to use. Parameter value must be non-null.
+ *
+ * @deprecated use #HttpConnection(String, int, String, int, Protocol)
+ */
+ public HttpConnection(
+ String proxyHost,
+ int proxyPort,
+ String host,
+ String virtualHost,
+ int port,
+ Protocol protocol) {
+ this(proxyHost, proxyPort, host, port, protocol);
+ }
+
+ /**
+ * Creates a new HTTP connection for the given host with the virtual
+ * alias and port via the given proxy host and port using the given
+ * protocol.
+ *
+ * @param proxyHost the host to proxy via
+ * @param proxyPort the port to proxy via
+ * @param host the host to connect to. Parameter value must be non-null.
+ * @param port the port to connect to
+ * @param protocol The protocol to use. Parameter value must be non-null.
+ */
+ public HttpConnection(
+ String proxyHost,
+ int proxyPort,
+ String host,
+ int port,
+ Protocol protocol) {
+
+ if (host == null) {
+ throw new IllegalArgumentException("host parameter is null");
+ }
+ if (protocol == null) {
+ throw new IllegalArgumentException("protocol is null");
+ }
+
+ proxyHostName = proxyHost;
+ proxyPortNumber = proxyPort;
+ hostName = host;
+ portNumber = protocol.resolvePort(port);
+ protocolInUse = protocol;
+ }
+
+ // ------------------------------------------ Attribute Setters and Getters
+
+ /**
+ * Returns the connection socket.
+ *
+ * @return the socket.
+ *
+ * @since 3.0
+ */
+ protected Socket getSocket() {
+ return this.socket;
+ }
+
+ /**
+ * Returns the host.
+ *
+ * @return the host.
+ */
+ public String getHost() {
+ return hostName;
+ }
+
+ /**
+ * Sets the host to connect to.
+ *
+ * @param host the host to connect to. Parameter value must be non-null.
+ * @throws IllegalStateException if the connection is already open
+ */
+ public void setHost(String host) throws IllegalStateException {
+ if (host == null) {
+ throw new IllegalArgumentException("host parameter is null");
+ }
+ assertNotOpen();
+ hostName = host;
+ }
+
+ /**
+ * Returns the target virtual host.
+ *
+ * @return the virtual host.
+ *
+ * @deprecated no longer applicable
+ */
+
+ public String getVirtualHost() {
+ return this.hostName;
+ }
+
+ /**
+ * Sets the virtual host to target.
+ *
+ * @param host the virtual host name that should be used instead of
+ * physical host name when sending HTTP requests. Virtual host
+ * name can be set to null if virtual host name is not
+ * to be used
+ *
+ * @throws IllegalStateException if the connection is already open
+ *
+ * @deprecated no longer applicable
+ */
+
+ public void setVirtualHost(String host) throws IllegalStateException {
+ assertNotOpen();
+ }
+
+ /**
+ * Returns the port of the host.
+ *
+ * If the port is -1 (or less than 0) the default port for
+ * the current protocol is returned.
+ *
+ * @return the port.
+ */
+ public int getPort() {
+ if (portNumber < 0) {
+ return isSecure() ? 443 : 80;
+ } else {
+ return portNumber;
+ }
+ }
+
+ /**
+ * Sets the port to connect to.
+ *
+ * @param port the port to connect to
+ *
+ * @throws IllegalStateException if the connection is already open
+ */
+ public void setPort(int port) throws IllegalStateException {
+ assertNotOpen();
+ portNumber = port;
+ }
+
+ /**
+ * Returns the proxy host.
+ *
+ * @return the proxy host.
+ */
+ public String getProxyHost() {
+ return proxyHostName;
+ }
+
+ /**
+ * Sets the host to proxy through.
+ *
+ * @param host the host to proxy through.
+ *
+ * @throws IllegalStateException if the connection is already open
+ */
+ public void setProxyHost(String host) throws IllegalStateException {
+ assertNotOpen();
+ proxyHostName = host;
+ }
+
+ /**
+ * Returns the port of the proxy host.
+ *
+ * @return the proxy port.
+ */
+ public int getProxyPort() {
+ return proxyPortNumber;
+ }
+
+ /**
+ * Sets the port of the host to proxy through.
+ *
+ * @param port the port of the host to proxy through.
+ *
+ * @throws IllegalStateException if the connection is already open
+ */
+ public void setProxyPort(int port) throws IllegalStateException {
+ assertNotOpen();
+ proxyPortNumber = port;
+ }
+
+ /**
+ * Returns true if the connection is established over
+ * a secure protocol.
+ *
+ * @return true if connected over a secure protocol.
+ */
+ public boolean isSecure() {
+ return protocolInUse.isSecure();
+ }
+
+ /**
+ * Returns the protocol used to establish the connection.
+ * @return The protocol
+ */
+ public Protocol getProtocol() {
+ return protocolInUse;
+ }
+
+ /**
+ * Sets the protocol used to establish the connection
+ *
+ * @param protocol The protocol to use.
+ *
+ * @throws IllegalStateException if the connection is already open
+ */
+ public void setProtocol(Protocol protocol) {
+ assertNotOpen();
+
+ if (protocol == null) {
+ throw new IllegalArgumentException("protocol is null");
+ }
+
+ protocolInUse = protocol;
+
+ }
+
+ /**
+ * Return the local address used when creating the connection.
+ * If null, the default address is used.
+ *
+ * @return InetAddress the local address to be used when creating Sockets
+ */
+ public InetAddress getLocalAddress() {
+ return this.localAddress;
+ }
+
+ /**
+ * Set the local address used when creating the connection.
+ * If unset or null, the default address is used.
+ *
+ * @param localAddress the local address to use
+ */
+ public void setLocalAddress(InetAddress localAddress) {
+ assertNotOpen();
+ this.localAddress = localAddress;
+ }
+
+ /**
+ * Tests if the connection is open.
+ *
+ * @return true if the connection is open
+ */
+ public boolean isOpen() {
+ return isOpen;
+ }
+
+ /**
+ * Closes the connection if stale.
+ *
+ * @return true if the connection was stale and therefore closed,
+ * false otherwise.
+ *
+ * @see #isStale()
+ *
+ * @since 3.0
+ */
+ public boolean closeIfStale() throws IOException {
+ if (isOpen && isStale()) {
+ LOG.debug("Connection is stale, closing...");
+ close();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Tests if stale checking is enabled.
+ *
+ * @return true if enabled
+ *
+ * @see #isStale()
+ *
+ * @deprecated Use {@link HttpConnectionParams#isStaleCheckingEnabled()},
+ * {@link HttpConnection#getParams()}.
+ */
+ public boolean isStaleCheckingEnabled() {
+ return this.params.isStaleCheckingEnabled();
+ }
+
+ /**
+ * Sets whether or not isStale() will be called when testing if this connection is open.
+ *
+ *
Setting this flag to false will increase performance when reusing
+ * connections, but it will also make them less reliable. Stale checking ensures that
+ * connections are viable before they are used. When set to false some
+ * method executions will result in IOExceptions and they will have to be retried.
+ *
+ * @param staleCheckEnabled true to enable isStale()
+ *
+ * @see #isStale()
+ * @see #isOpen()
+ *
+ * @deprecated Use {@link HttpConnectionParams#setStaleCheckingEnabled(boolean)},
+ * {@link HttpConnection#getParams()}.
+ */
+ public void setStaleCheckingEnabled(boolean staleCheckEnabled) {
+ this.params.setStaleCheckingEnabled(staleCheckEnabled);
+ }
+
+ /**
+ * Determines whether this connection is "stale", which is to say that either
+ * it is no longer open, or an attempt to read the connection would fail.
+ *
+ *
Unfortunately, due to the limitations of the JREs prior to 1.4, it is
+ * not possible to test a connection to see if both the read and write channels
+ * are open - except by reading and writing. This leads to a difficulty when
+ * some connections leave the "write" channel open, but close the read channel
+ * and ignore the request. This function attempts to ameliorate that
+ * problem by doing a test read, assuming that the caller will be doing a
+ * write followed by a read, rather than the other way around.
+ *
+ *
+ *
To avoid side-effects, the underlying connection is wrapped by a
+ * {@link BufferedInputStream}, so although data might be read, what is visible
+ * to clients of the connection will not change with this call.true if the connection is already closed, or a read would
+ * fail.
+ */
+ protected boolean isStale() throws IOException {
+ boolean isStale = true;
+ if (isOpen) {
+ // the connection is open, but now we have to see if we can read it
+ // assume the connection is not stale.
+ isStale = false;
+ try {
+ if (inputStream.available() <= 0) {
+ try {
+ socket.setSoTimeout(1);
+ inputStream.mark(1);
+ int byteRead = inputStream.read();
+ if (byteRead == -1) {
+ // again - if the socket is reporting all data read,
+ // probably stale
+ isStale = true;
+ } else {
+ inputStream.reset();
+ }
+ } finally {
+ socket.setSoTimeout(this.params.getSoTimeout());
+ }
+ }
+ } catch (InterruptedIOException e) {
+ if (!ExceptionUtil.isSocketTimeoutException(e)) {
+ throw e;
+ }
+ // aha - the connection is NOT stale - continue on!
+ } catch (IOException e) {
+ // oops - the connection is stale, the read or soTimeout failed.
+ LOG.debug(
+ "An error occurred while reading from the socket, is appears to be stale",
+ e
+ );
+ isStale = true;
+ }
+ }
+
+ return isStale;
+ }
+
+ /**
+ * Returns true if the connection is established via a proxy,
+ * false otherwise.
+ *
+ * @return true if a proxy is used to establish the connection,
+ * false otherwise.
+ */
+ public boolean isProxied() {
+ return (!(null == proxyHostName || 0 >= proxyPortNumber));
+ }
+
+ /**
+ * Set the state to keep track of the last response for the last request.
+ *
+ *
The connection managers use this to ensure that previous requests are
+ * properly closed before a new request is attempted. That way, a GET
+ * request need not be read in its entirety before a new request is issued.
+ * Instead, this stream can be closed as appropriate.
+ *
+ * @param inStream The stream associated with an HttpMethod.
+ */
+ public void setLastResponseInputStream(InputStream inStream) {
+ lastResponseInputStream = inStream;
+ }
+
+ /**
+ * Returns the stream used to read the last response's body.
+ *
+ *
Clients will generally not need to call this function unless
+ * using HttpConnection directly, instead of calling {@link HttpClient#executeMethod}.
+ * For those clients, call this function, and if it returns a non-null stream,
+ * close the stream before attempting to execute a method. Note that
+ * calling "close" on the stream returned by this function may close
+ * the connection if the previous response contained a "Connection: close" header.
+ *
+ * @return An {@link InputStream} corresponding to the body of the last
+ * response.
+ */
+ public InputStream getLastResponseInputStream() {
+ return lastResponseInputStream;
+ }
+
+ // --------------------------------------------------- Other Public Methods
+
+ /**
+ * Returns {@link HttpConnectionParams HTTP protocol parameters} associated with this method.
+ *
+ * @return HTTP parameters.
+ *
+ * @since 3.0
+ */
+ public HttpConnectionParams getParams() {
+ return this.params;
+ }
+
+ /**
+ * Assigns {@link HttpConnectionParams HTTP protocol parameters} for this method.
+ *
+ * @since 3.0
+ *
+ * @see HttpConnectionParams
+ */
+ public void setParams(final HttpConnectionParams params) {
+ if (params == null) {
+ throw new IllegalArgumentException("Parameters may not be null");
+ }
+ this.params = params;
+ }
+
+ /**
+ * Set the {@link Socket}'s timeout, via {@link Socket#setSoTimeout}. If the
+ * connection is already open, the SO_TIMEOUT is changed. If no connection
+ * is open, then subsequent connections will use the timeout value.
+ *
+ * Note: This is not a connection timeout but a timeout on network traffic!
+ *
+ * @param timeout the timeout value
+ * @throws SocketException - if there is an error in the underlying
+ * protocol, such as a TCP error.
+ *
+ * @deprecated Use {@link HttpConnectionParams#setSoTimeout(int)},
+ * {@link HttpConnection#getParams()}.
+ */
+ public void setSoTimeout(int timeout)
+ throws SocketException, IllegalStateException {
+ this.params.setSoTimeout(timeout);
+ if (this.socket != null) {
+ this.socket.setSoTimeout(timeout);
+ }
+ }
+
+ /**
+ * Sets SO_TIMEOUT value directly on the underlying {@link Socket socket}.
+ * This method does not change the default read timeout value set via
+ * {@link HttpConnectionParams}.
+ *
+ * @param timeout the timeout value
+ * @throws SocketException - if there is an error in the underlying
+ * protocol, such as a TCP error.
+ * @throws IllegalStateException if not connected
+ *
+ * @since 3.0
+ */
+ public void setSocketTimeout(int timeout)
+ throws SocketException, IllegalStateException {
+ assertOpen();
+ if (this.socket != null) {
+ this.socket.setSoTimeout(timeout);
+ }
+ }
+
+ /**
+ * Returns the {@link Socket}'s timeout, via {@link Socket#getSoTimeout}, if the
+ * connection is already open. If no connection is open, return the value subsequent
+ * connection will use.
+ *
+ * Note: This is not a connection timeout but a timeout on network traffic!
+ *
+ * @return the timeout value
+ *
+ * @deprecated Use {@link HttpConnectionParams#getSoTimeout()},
+ * {@link HttpConnection#getParams()}.
+ */
+ public int getSoTimeout() throws SocketException {
+ return this.params.getSoTimeout();
+ }
+
+ /**
+ * Sets the connection timeout. This is the maximum time that may be spent
+ * until a connection is established. The connection will fail after this
+ * amount of time.
+ * @param timeout The timeout in milliseconds. 0 means timeout is not used.
+ *
+ * @deprecated Use {@link HttpConnectionParams#setConnectionTimeout(int)},
+ * {@link HttpConnection#getParams()}.
+ */
+ public void setConnectionTimeout(int timeout) {
+ this.params.setConnectionTimeout(timeout);
+ }
+
+ /**
+ * Establishes a connection to the specified host and port
+ * (via a proxy if specified).
+ * The underlying socket is created from the {@link ProtocolSocketFactory}.
+ *
+ * @throws IOException if an attempt to establish the connection results in an
+ * I/O error.
+ */
+ public void open() throws IOException {
+ LOG.trace("enter HttpConnection.open()");
+
+ final String host = (proxyHostName == null) ? hostName : proxyHostName;
+ final int port = (proxyHostName == null) ? portNumber : proxyPortNumber;
+ assertNotOpen();
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Open connection to " + host + ":" + port);
+ }
+
+ try {
+ if (this.socket == null) {
+ usingSecureSocket = isSecure() && !isProxied();
+ // use the protocol's socket factory unless this is a secure
+ // proxied connection
+ ProtocolSocketFactory socketFactory = null;
+ if (isSecure() && isProxied()) {
+ Protocol defaultprotocol = Protocol.getProtocol("http");
+ socketFactory = defaultprotocol.getSocketFactory();
+ } else {
+ socketFactory = this.protocolInUse.getSocketFactory();
+ }
+ this.socket = socketFactory.createSocket(
+ host, port,
+ localAddress, 0,
+ this.params);
+ }
+
+ /*
+ "Nagling has been broadly implemented across networks,
+ including the Internet, and is generally performed by default
+ - although it is sometimes considered to be undesirable in
+ highly interactive environments, such as some client/server
+ situations. In such cases, nagling may be turned off through
+ use of the TCP_NODELAY sockets option." */
+
+ socket.setTcpNoDelay(this.params.getTcpNoDelay());
+ socket.setSoTimeout(this.params.getSoTimeout());
+
+ int linger = this.params.getLinger();
+ if (linger >= 0) {
+ socket.setSoLinger(linger > 0, linger);
+ }
+
+ int sndBufSize = this.params.getSendBufferSize();
+ if (sndBufSize >= 0) {
+ socket.setSendBufferSize(sndBufSize);
+ }
+ int rcvBufSize = this.params.getReceiveBufferSize();
+ if (rcvBufSize >= 0) {
+ socket.setReceiveBufferSize(rcvBufSize);
+ }
+ int outbuffersize = socket.getSendBufferSize();
+ if ((outbuffersize > 2048) || (outbuffersize <= 0)) {
+ outbuffersize = 2048;
+ }
+ int inbuffersize = socket.getReceiveBufferSize();
+ if ((inbuffersize > 2048) || (inbuffersize <= 0)) {
+ inbuffersize = 2048;
+ }
+
+ // START IA/HERITRIX change
+ Recorder httpRecorder = Recorder.getHttpRecorder();
+ if (httpRecorder == null || (isSecure() && isProxied())) {
+ // no recorder, OR defer recording for pre-tunnel leg
+ inputStream = new BufferedInputStream(
+ socket.getInputStream(), inbuffersize);
+ outputStream = new BufferedOutputStream(
+ socket.getOutputStream(), outbuffersize);
+ } else {
+ inputStream = httpRecorder.inputWrap((InputStream)
+ (new BufferedInputStream(socket.getInputStream(),
+ inbuffersize)));
+ outputStream = httpRecorder.outputWrap((OutputStream)
+ (new BufferedOutputStream(socket.getOutputStream(),
+ outbuffersize)));
+ }
+ // END IA/HERITRIX change
+
+ isOpen = true;
+ } catch (IOException e) {
+ // Connection wasn't opened properly
+ // so close everything out
+ closeSocketAndStreams();
+ throw e;
+ }
+ }
+
+ /**
+ * Instructs the proxy to establish a secure tunnel to the host. The socket will
+ * be switched to the secure socket. Subsequent communication is done via the secure
+ * socket. The method can only be called once on a proxied secure connection.
+ *
+ * @throws IllegalStateException if connection is not secure and proxied or
+ * if the socket is already secure.
+ * @throws IOException if an attempt to establish the secure tunnel results in an
+ * I/O error.
+ */
+ public void tunnelCreated() throws IllegalStateException, IOException {
+ LOG.trace("enter HttpConnection.tunnelCreated()");
+
+ if (!isSecure() || !isProxied()) {
+ throw new IllegalStateException(
+ "Connection must be secure "
+ + "and proxied to use this feature");
+ }
+
+ if (usingSecureSocket) {
+ throw new IllegalStateException("Already using a secure socket");
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Secure tunnel to " + this.hostName + ":" + this.portNumber);
+ }
+
+ SecureProtocolSocketFactory socketFactory =
+ (SecureProtocolSocketFactory) protocolInUse.getSocketFactory();
+
+ socket = socketFactory.createSocket(socket, hostName, portNumber, true);
+ int sndBufSize = this.params.getSendBufferSize();
+ if (sndBufSize >= 0) {
+ socket.setSendBufferSize(sndBufSize);
+ }
+ int rcvBufSize = this.params.getReceiveBufferSize();
+ if (rcvBufSize >= 0) {
+ socket.setReceiveBufferSize(rcvBufSize);
+ }
+ int outbuffersize = socket.getSendBufferSize();
+ if (outbuffersize > 2048) {
+ outbuffersize = 2048;
+ }
+ int inbuffersize = socket.getReceiveBufferSize();
+ if (inbuffersize > 2048) {
+ inbuffersize = 2048;
+ }
+
+ // START IA/HERITRIX change
+ Recorder httpRecorder = Recorder.getHttpRecorder();
+ if (httpRecorder == null) {
+ inputStream = new BufferedInputStream(socket.getInputStream(), inbuffersize);
+ outputStream = new BufferedOutputStream(socket.getOutputStream(), outbuffersize);
+ } else {
+ inputStream = httpRecorder.inputWrap((InputStream)
+ (new BufferedInputStream(socket.getInputStream(),
+ inbuffersize)));
+ outputStream = httpRecorder.outputWrap((OutputStream)
+ (new BufferedOutputStream(socket.getOutputStream(),
+ outbuffersize)));
+ }
+ // END IA/HERITRIX change
+
+ usingSecureSocket = true;
+ tunnelEstablished = true;
+ }
+
+ /**
+ * Indicates if the connection is completely transparent from end to end.
+ *
+ * @return true if conncetion is not proxied or tunneled through a transparent
+ * proxy; false otherwise.
+ */
+ public boolean isTransparent() {
+ return !isProxied() || tunnelEstablished;
+ }
+
+ /**
+ * Flushes the output request stream. This method should be called to
+ * ensure that data written to the request OutputStream is sent to the server.
+ *
+ * @throws IOException if an I/O problem occurs
+ */
+ public void flushRequestOutputStream() throws IOException {
+ LOG.trace("enter HttpConnection.flushRequestOutputStream()");
+ assertOpen();
+ outputStream.flush();
+ }
+
+ /**
+ * Returns an {@link OutputStream} suitable for writing the request.
+ *
+ * @throws IllegalStateException if the connection is not open
+ * @throws IOException if an I/O problem occurs
+ * @return a stream to write the request to
+ */
+ public OutputStream getRequestOutputStream()
+ throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.getRequestOutputStream()");
+ assertOpen();
+ OutputStream out = this.outputStream;
+ if (Wire.CONTENT_WIRE.enabled()) {
+ out = new WireLogOutputStream(out, Wire.CONTENT_WIRE);
+ }
+ return out;
+ }
+
+ /**
+ * Return a {@link InputStream} suitable for reading the response.
+ * @return InputStream The response input stream.
+ * @throws IOException If an IO problem occurs
+ * @throws IllegalStateException If the connection isn't open.
+ */
+ public InputStream getResponseInputStream()
+ throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.getResponseInputStream()");
+ assertOpen();
+ return inputStream;
+ }
+
+ /**
+ * Tests if input data avaialble. This method returns immediately
+ * and does not perform any read operations on the input socket
+ *
+ * @return boolean true if input data is available,
+ * false otherwise.
+ *
+ * @throws IOException If an IO problem occurs
+ * @throws IllegalStateException If the connection isn't open.
+ */
+ public boolean isResponseAvailable()
+ throws IOException {
+ LOG.trace("enter HttpConnection.isResponseAvailable()");
+ if (this.isOpen) {
+ return this.inputStream.available() > 0;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Tests if input data becomes available within the given period time in milliseconds.
+ *
+ * @param timeout The number milliseconds to wait for input data to become available
+ * @return boolean true if input data is availble,
+ * false otherwise.
+ *
+ * @throws IOException If an IO problem occurs
+ * @throws IllegalStateException If the connection isn't open.
+ */
+ public boolean isResponseAvailable(int timeout)
+ throws IOException {
+ LOG.trace("enter HttpConnection.isResponseAvailable(int)");
+ assertOpen();
+ boolean result = false;
+ if (this.inputStream.available() > 0) {
+ result = true;
+ } else {
+ try {
+ this.socket.setSoTimeout(timeout);
+ inputStream.mark(1);
+ int byteRead = inputStream.read();
+ if (byteRead != -1) {
+ inputStream.reset();
+ LOG.debug("Input data available");
+ result = true;
+ } else {
+ LOG.debug("Input data not available");
+ }
+ } catch (InterruptedIOException e) {
+ if (!ExceptionUtil.isSocketTimeoutException(e)) {
+ throw e;
+ }
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Input data not available after " + timeout + " ms");
+ }
+ } finally {
+ try {
+ socket.setSoTimeout(this.params.getSoTimeout());
+ } catch (IOException ioe) {
+ LOG.debug("An error ocurred while resetting soTimeout, we will assume that"
+ + " no response is available.",
+ ioe);
+ result = false;
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Writes the specified bytes to the output stream.
+ *
+ * @param data the data to be written
+ * @throws IllegalStateException if not connected
+ * @throws IOException if an I/O problem occurs
+ * @see #write(byte[],int,int)
+ */
+ public void write(byte[] data)
+ throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.write(byte[])");
+ this.write(data, 0, data.length);
+ }
+
+ /**
+ * Writes length bytes in data starting at
+ * offset to the output stream.
+ *
+ * The general contract for
+ * write(b, off, len) is that some of the bytes in the array b are written
+ * to the output stream in order; element b[off] is the first byte written
+ * and b[off+len-1] is the last byte written by this operation.
+ *
+ * @param data array containing the data to be written.
+ * @param offset the start offset in the data.
+ * @param length the number of bytes to write.
+ * @throws IllegalStateException if not connected
+ * @throws IOException if an I/O problem occurs
+ */
+ public void write(byte[] data, int offset, int length)
+ throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.write(byte[], int, int)");
+
+ if (offset < 0) {
+ throw new IllegalArgumentException("Array offset may not be negative");
+ }
+ if (length < 0) {
+ throw new IllegalArgumentException("Array length may not be negative");
+ }
+ if (offset + length > data.length) {
+ throw new IllegalArgumentException("Given offset and length exceed the array length");
+ }
+ assertOpen();
+ this.outputStream.write(data, offset, length);
+ }
+
+ /**
+ * Writes the specified bytes, followed by "\r\n".getBytes() to the
+ * output stream.
+ *
+ * @param data the bytes to be written
+ * @throws IllegalStateException if the connection is not open
+ * @throws IOException if an I/O problem occurs
+ */
+ public void writeLine(byte[] data)
+ throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.writeLine(byte[])");
+ write(data);
+ writeLine();
+ }
+
+ /**
+ * Writes "\r\n".getBytes() to the output stream.
+ *
+ * @throws IllegalStateException if the connection is not open
+ * @throws IOException if an I/O problem occurs
+ */
+ public void writeLine()
+ throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.writeLine()");
+ write(CRLF);
+ }
+
+ /**
+ * @deprecated Use {@link #print(String, String)}
+ *
+ * Writes the specified String (as bytes) to the output stream.
+ *
+ * @param data the string to be written
+ * @throws IllegalStateException if the connection is not open
+ * @throws IOException if an I/O problem occurs
+ */
+ public void print(String data)
+ throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.print(String)");
+ write(EncodingUtil.getBytes(data, "ISO-8859-1"));
+ }
+
+ /**
+ * Writes the specified String (as bytes) to the output stream.
+ *
+ * @param data the string to be written
+ * @param charset the charset to use for writing the data
+ * @throws IllegalStateException if the connection is not open
+ * @throws IOException if an I/O problem occurs
+ *
+ * @since 3.0
+ */
+ public void print(String data, String charset)
+ throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.print(String)");
+ write(EncodingUtil.getBytes(data, charset));
+ }
+
+ /**
+ * @deprecated Use {@link #printLine(String, String)}
+ *
+ * Writes the specified String (as bytes), followed by
+ * "\r\n".getBytes() to the output stream.
+ *
+ * @param data the data to be written
+ * @throws IllegalStateException if the connection is not open
+ * @throws IOException if an I/O problem occurs
+ */
+ public void printLine(String data)
+ throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.printLine(String)");
+ writeLine(EncodingUtil.getBytes(data, "ISO-8859-1"));
+ }
+
+ /**
+ * Writes the specified String (as bytes), followed by
+ * "\r\n".getBytes() to the output stream.
+ *
+ * @param data the data to be written
+ * @param charset the charset to use for writing the data
+ * @throws IllegalStateException if the connection is not open
+ * @throws IOException if an I/O problem occurs
+ *
+ * @since 3.0
+ */
+ public void printLine(String data, String charset)
+ throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.printLine(String)");
+ writeLine(EncodingUtil.getBytes(data, charset));
+ }
+
+ /**
+ * Writes "\r\n".getBytes() to the output stream.
+ *
+ * @throws IllegalStateException if the connection is not open
+ * @throws IOException if an I/O problem occurs
+ */
+ public void printLine()
+ throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.printLine()");
+ writeLine();
+ }
+
+ /**
+ * Reads up to "\n" from the (unchunked) input stream.
+ * If the stream ends before the line terminator is found,
+ * the last part of the string will still be returned.
+ *
+ * @throws IllegalStateException if the connection is not open
+ * @throws IOException if an I/O problem occurs
+ * @return a line from the response
+ *
+ * @deprecated use #readLine(String)
+ */
+ public String readLine() throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.readLine()");
+
+ assertOpen();
+ return HttpParser.readLine(inputStream);
+ }
+
+ /**
+ * Reads up to "\n" from the (unchunked) input stream.
+ * If the stream ends before the line terminator is found,
+ * the last part of the string will still be returned.
+ *
+ * @param charset the charset to use for reading the data
+ *
+ * @throws IllegalStateException if the connection is not open
+ * @throws IOException if an I/O problem occurs
+ * @return a line from the response
+ *
+ * @since 3.0
+ */
+ public String readLine(final String charset) throws IOException, IllegalStateException {
+ LOG.trace("enter HttpConnection.readLine()");
+
+ assertOpen();
+ return HttpParser.readLine(inputStream, charset);
+ }
+
+ /**
+ * Attempts to shutdown the {@link Socket}'s output, via Socket.shutdownOutput()
+ * when running on JVM 1.3 or higher.
+ *
+ * @deprecated unused
+ */
+ public void shutdownOutput() {
+ LOG.trace("enter HttpConnection.shutdownOutput()");
+
+ try {
+ // Socket.shutdownOutput is a JDK 1.3
+ // method. We'll use reflection in case
+ // we're running in an older VM
+ Class[] paramsClasses = new Class[0];
+ Method shutdownOutput =
+ socket.getClass().getMethod("shutdownOutput", paramsClasses);
+ Object[] params = new Object[0];
+ shutdownOutput.invoke(socket, params);
+ } catch (Exception ex) {
+ LOG.debug("Unexpected Exception caught", ex);
+ // Ignore, and hope everything goes right
+ }
+ // close output stream?
+ }
+
+ /**
+ * Closes the socket and streams.
+ */
+ public void close() {
+ LOG.trace("enter HttpConnection.close()");
+ closeSocketAndStreams();
+ }
+
+ /**
+ * Returns the httpConnectionManager.
+ * @return HttpConnectionManager
+ */
+ public HttpConnectionManager getHttpConnectionManager() {
+ return httpConnectionManager;
+ }
+
+ /**
+ * Sets the httpConnectionManager.
+ * @param httpConnectionManager The httpConnectionManager to set
+ */
+ public void setHttpConnectionManager(HttpConnectionManager httpConnectionManager) {
+ this.httpConnectionManager = httpConnectionManager;
+ }
+
+ /**
+ * Releases the connection. If the connection is locked or does not have a connection
+ * manager associated with it, this method has no effect. Note that it is completely safe
+ * to call this method multiple times.
+ */
+ public void releaseConnection() {
+ LOG.trace("enter HttpConnection.releaseConnection()");
+ if (locked) {
+ LOG.debug("Connection is locked. Call to releaseConnection() ignored.");
+ } else if (httpConnectionManager != null) {
+ LOG.debug("Releasing connection back to connection manager.");
+ httpConnectionManager.releaseConnection(this);
+ } else {
+ LOG.warn("HttpConnectionManager is null. Connection cannot be released.");
+ }
+ }
+
+ /**
+ * Tests if the connection is locked. Locked connections cannot be released.
+ * An attempt to release a locked connection will have no effect.
+ *
+ * @return true if the connection is locked, false otherwise.
+ *
+ * @since 3.0
+ */
+ protected boolean isLocked() {
+ return locked;
+ }
+
+ /**
+ * Locks or unlocks the connection. Locked connections cannot be released.
+ * An attempt to release a locked connection will have no effect.
+ *
+ * @param locked true to lock the connection, false to unlock
+ * the connection.
+ *
+ * @since 3.0
+ */
+ protected void setLocked(boolean locked) {
+ this.locked = locked;
+ }
+ // ------------------------------------------------------ Protected Methods
+
+ /**
+ * Closes everything out.
+ */
+ protected void closeSocketAndStreams() {
+ LOG.trace("enter HttpConnection.closeSockedAndStreams()");
+
+ isOpen = false;
+
+ // no longer care about previous responses...
+ lastResponseInputStream = null;
+
+ if (null != outputStream) {
+ OutputStream temp = outputStream;
+ outputStream = null;
+ try {
+ temp.close();
+ } catch (Exception ex) {
+ LOG.debug("Exception caught when closing output", ex);
+ // ignored
+ }
+ }
+
+ if (null != inputStream) {
+ InputStream temp = inputStream;
+ inputStream = null;
+ try {
+ temp.close();
+ } catch (Exception ex) {
+ LOG.debug("Exception caught when closing input", ex);
+ // ignored
+ }
+ }
+
+ if (null != socket) {
+ Socket temp = socket;
+ socket = null;
+ try {
+ temp.close();
+ } catch (Exception ex) {
+ LOG.debug("Exception caught when closing socket", ex);
+ // ignored
+ }
+ }
+
+ tunnelEstablished = false;
+ usingSecureSocket = false;
+ }
+
+ /**
+ * Throws an {@link IllegalStateException} if the connection is already open.
+ *
+ * @throws IllegalStateException if connected
+ */
+ protected void assertNotOpen() throws IllegalStateException {
+ if (isOpen) {
+ throw new IllegalStateException("Connection is open");
+ }
+ }
+
+ /**
+ * Throws an {@link IllegalStateException} if the connection is not open.
+ *
+ * @throws IllegalStateException if not connected
+ */
+ protected void assertOpen() throws IllegalStateException {
+ if (!isOpen) {
+ throw new IllegalStateException("Connection is not open");
+ }
+ }
+
+ /**
+ * Gets the socket's sendBufferSize.
+ *
+ * @return the size of the buffer for the socket OutputStream, -1 if the value
+ * has not been set and the socket has not been opened
+ *
+ * @throws SocketException if an error occurs while getting the socket value
+ *
+ * @see Socket#getSendBufferSize()
+ */
+ public int getSendBufferSize() throws SocketException {
+ if (socket == null) {
+ return -1;
+ } else {
+ return socket.getSendBufferSize();
+ }
+ }
+
+ /**
+ * Sets the socket's sendBufferSize.
+ *
+ * @param sendBufferSize the size to set for the socket OutputStream
+ *
+ * @throws SocketException if an error occurs while setting the socket value
+ *
+ * @see Socket#setSendBufferSize(int)
+ *
+ * @deprecated Use {@link HttpConnectionParams#setSendBufferSize(int)},
+ * {@link HttpConnection#getParams()}.
+ */
+ public void setSendBufferSize(int sendBufferSize) throws SocketException {
+ this.params.setSendBufferSize(sendBufferSize);
+ }
+
+ // ------------------------------------------------------- Static Variable
+
+ /** "\r\n", as bytes. */
+ private static final byte[] CRLF = new byte[] {(byte) 13, (byte) 10};
+
+ /** Log object for this class. */
+ private static final Log LOG = LogFactory.getLog(HttpConnection.class);
+
+ // ----------------------------------------------------- Instance Variables
+
+ /** My host. */
+ private String hostName = null;
+
+ /** My port. */
+ private int portNumber = -1;
+
+ /** My proxy host. */
+ private String proxyHostName = null;
+
+ /** My proxy port. */
+ private int proxyPortNumber = -1;
+
+ /** My client Socket. */
+ private Socket socket = null;
+
+ /** My InputStream. */
+ private InputStream inputStream = null;
+
+ /** My OutputStream. */
+ private OutputStream outputStream = null;
+
+ /** An {@link InputStream} for the response to an individual request. */
+ private InputStream lastResponseInputStream = null;
+
+ /** Whether or not the connection is connected. */
+ protected boolean isOpen = false;
+
+ /** the protocol being used */
+ private Protocol protocolInUse;
+
+ /** Collection of HTTP parameters associated with this HTTP connection*/
+ private HttpConnectionParams params = new HttpConnectionParams();
+
+ /** flag to indicate if this connection can be released, if locked the connection cannot be
+ * released */
+ private boolean locked = false;
+
+ /** Whether or not the socket is a secure one. */
+ private boolean usingSecureSocket = false;
+
+ /** Whether the connection is open via a secure tunnel or not */
+ private boolean tunnelEstablished = false;
+
+ /** the connection manager that created this connection or null */
+ private HttpConnectionManager httpConnectionManager;
+
+ /** The local interface on which the connection is created, or null for the default */
+ private InetAddress localAddress;
+}
diff --git a/commons/src/main/java/org/apache/commons/httpclient/HttpMethodBase.java b/commons/src/main/java/org/apache/commons/httpclient/HttpMethodBase.java
new file mode 100644
index 00000000..41644817
--- /dev/null
+++ b/commons/src/main/java/org/apache/commons/httpclient/HttpMethodBase.java
@@ -0,0 +1,2422 @@
+/*
+ * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/HttpMethodBase.java,v 1.222 2005/01/14 21:16:40 olegk Exp $
+ * $Revision$
+ * $Date$
+ *
+ * ====================================================================
+ *
+ * Copyright 1999-2004 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.commons.httpclient;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.util.Collection;
+
+import org.apache.commons.httpclient.auth.AuthState;
+import org.apache.commons.httpclient.cookie.CookiePolicy;
+import org.apache.commons.httpclient.cookie.CookieSpec;
+import org.apache.commons.httpclient.cookie.MalformedCookieException;
+import org.apache.commons.httpclient.params.HttpMethodParams;
+import org.apache.commons.httpclient.protocol.Protocol;
+import org.apache.commons.httpclient.util.EncodingUtil;
+import org.apache.commons.httpclient.util.ExceptionUtil;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * An abstract base implementation of HttpMethod.
+ *
+ * At minimum, subclasses will need to override:
+ *
+ *
{@link #getName} to return the approriate name for this method
+ *
+ *
+ *
+ *
+ *
+ * When a method requires additional request headers, subclasses will typically
+ * want to override:
+ *
+ *
{@link #addRequestHeaders addRequestHeaders(HttpState,HttpConnection)}
+ * to write those headers
+ *
+ *
+ *
+ *
+ *
+ * When a method expects specific response headers, subclasses may want to
+ * override:
+ *
+ *
{@link #processResponseHeaders processResponseHeaders(HttpState,HttpConnection)}
+ * to handle those headers
+ *
+ *
+ *
+ *
+ *
+ * @author Remy Maucherat
+ * @author Rodney Waldhoff
+ * @author Sean C. Sullivan
+ * @author dIon Gillard
+ * @author Jeff Dever
+ * @author Davanum Srinivas
+ * @author Ortwin Glueck
+ * @author Eric Johnson
+ * @author Michael Becke
+ * @author Oleg Kalnichevski
+ * @author Mike Bowler
+ * @author Gary Gregory
+ * @author Christian Kohlschuetter
+ *
+ * @version $Revision$ $Date$
+ */
+@SuppressWarnings({"deprecation","unchecked"}) // <- // IA/HERITRIX change
+public abstract class HttpMethodBase implements HttpMethod {
+
+ // -------------------------------------------------------------- Constants
+
+ /** Log object for this class. */
+ private static final Log LOG = LogFactory.getLog(HttpMethodBase.class);
+
+ // ----------------------------------------------------- Instance variables
+
+ /** Request headers, if any. */
+ private HeaderGroup requestHeaders = new HeaderGroup();
+
+ /** The Status-Line from the response. */
+ private StatusLine statusLine = null;
+
+ /** Response headers, if any. */
+ private HeaderGroup responseHeaders = new HeaderGroup();
+
+ /** Response trailer headers, if any. */
+ private HeaderGroup responseTrailerHeaders = new HeaderGroup();
+
+ /** Path of the HTTP method. */
+ private String path = null;
+
+ /** Query string of the HTTP method, if any. */
+ private String queryString = null;
+
+ /** The response body of the HTTP method, assuming it has not be
+ * intercepted by a sub-class. */
+ private InputStream responseStream = null;
+
+ /** The connection that the response stream was read from. */
+ private HttpConnection responseConnection = null;
+
+ /** Buffer for the response */
+ private byte[] responseBody = null;
+
+ /** True if the HTTP method should automatically follow HTTP redirects.*/
+ private boolean followRedirects = false;
+
+ /** True if the HTTP method should automatically handle
+ * HTTP authentication challenges. */
+ private boolean doAuthentication = true;
+
+ /** HTTP protocol parameters. */
+ private HttpMethodParams params = new HttpMethodParams();
+
+ /** Host authentication state */
+ private AuthState hostAuthState = new AuthState();
+
+ /** Proxy authentication state */
+ private AuthState proxyAuthState = new AuthState();
+
+ /** True if this method has already been executed. */
+ private boolean used = false;
+
+ /** Count of how many times did this HTTP method transparently handle
+ * a recoverable exception. */
+ private int recoverableExceptionCount = 0;
+
+ /** the host for this HTTP method, can be null */
+ private HttpHost httphost = null;
+
+ /**
+ * Handles method retries
+ *
+ * @deprecated no loner used
+ */
+ private MethodRetryHandler methodRetryHandler;
+
+ /** True if the connection must be closed when no longer needed */
+ private boolean connectionCloseForced = false;
+
+ /** Number of milliseconds to wait for 100-contunue response. */
+ private static final int RESPONSE_WAIT_TIME_MS = 3000;
+
+ /** HTTP protocol version used for execution of this method. */
+ private HttpVersion effectiveVersion = null;
+
+ /** Whether the execution of this method has been aborted */
+ private transient boolean aborted = false;
+
+ /** Whether the HTTP request has been transmitted to the target
+ * server it its entirety */
+ private boolean requestSent = false;
+
+ /** Actual cookie policy */
+ private CookieSpec cookiespec = null;
+
+ /** Default initial size of the response buffer if content length is unknown. */
+ private static final int DEFAULT_INITIAL_BUFFER_SIZE = 4*1024; // 4 kB
+
+ // ----------------------------------------------------------- Constructors
+
+ /**
+ * No-arg constructor.
+ */
+ public HttpMethodBase() {
+ }
+
+ /**
+ * Constructor specifying a URI.
+ * It is responsibility of the caller to ensure that URI elements
+ * (path & query parameters) are properly encoded (URL safe).
+ *
+ * @param uri either an absolute or relative URI. The URI is expected
+ * to be URL-encoded
+ *
+ * @throws IllegalArgumentException when URI is invalid
+ * @throws IllegalStateException when protocol of the absolute URI is not recognised
+ */
+ public HttpMethodBase(String uri)
+ throws IllegalArgumentException, IllegalStateException {
+
+ try {
+
+ // create a URI and allow for null/empty uri values
+ if (uri == null || uri.equals("")) {
+ uri = "/";
+ }
+// BEGIN IA/HERITRIX CHANGES
+// setURI(new URI(uri, true));
+ setURI(new org.archive.net.LaxURI(uri, true));
+// END IA/HERITRIX CHANGES
+ } catch (URIException e) {
+ throw new IllegalArgumentException("Invalid uri '"
+ + uri + "': " + e.getMessage()
+ );
+ }
+ }
+
+ // ------------------------------------------- Property Setters and Getters
+
+ /**
+ * Obtains the name of the HTTP method as used in the HTTP request line,
+ * for example "GET" or "POST".
+ *
+ * @return the name of this method
+ */
+ public abstract String getName();
+
+ /**
+ * Returns the URI of the HTTP method
+ *
+ * @return The URI
+ *
+ * @throws URIException If the URI cannot be created.
+ *
+ * @see org.apache.commons.httpclient.HttpMethod#getURI()
+ */
+ public URI getURI() throws URIException {
+ StringBuffer buffer = new StringBuffer();
+ if (this.httphost != null) {
+ buffer.append(this.httphost.getProtocol().getScheme());
+ buffer.append("://");
+ buffer.append(this.httphost.getHostName());
+ int port = this.httphost.getPort();
+ if (port != -1 && port != this.httphost.getProtocol().getDefaultPort()) {
+ buffer.append(":");
+ buffer.append(port);
+ }
+ }
+ buffer.append(this.path);
+ if (this.queryString != null) {
+ buffer.append('?');
+ buffer.append(this.queryString);
+ }
+// BEGIN IA/HERITRIX CHANGES
+// return new URI(buffer.toString(), true);
+ return new org.archive.net.LaxURI(buffer.toString(), true);
+// END IA/HERITRIX CHANGES
+ }
+
+ /**
+ * Sets the URI for this method.
+ *
+ * @param uri URI to be set
+ *
+ * @throws URIException if a URI cannot be set
+ *
+ * @since 3.0
+ */
+ public void setURI(URI uri) throws URIException {
+ // only set the host if specified by the URI
+ if (uri.isAbsoluteURI()) {
+ this.httphost = new HttpHost(uri);
+ }
+ // set the path, defaulting to root
+ setPath(
+ uri.getPath() == null
+ ? "/"
+ : uri.getEscapedPath()
+ );
+ setQueryString(uri.getEscapedQuery());
+ }
+
+ /**
+ * Sets whether or not the HTTP method should automatically follow HTTP redirects
+ * (status code 302, etc.)
+ *
+ * @param followRedirects true if the method will automatically follow redirects,
+ * false otherwise.
+ */
+ public void setFollowRedirects(boolean followRedirects) {
+ this.followRedirects = followRedirects;
+ }
+
+ /**
+ * Returns true if the HTTP method should automatically follow HTTP redirects
+ * (status code 302, etc.), false otherwise.
+ *
+ * @return true if the method will automatically follow HTTP redirects,
+ * false otherwise.
+ */
+ public boolean getFollowRedirects() {
+ return this.followRedirects;
+ }
+
+ /** Sets whether version 1.1 of the HTTP protocol should be used per default.
+ *
+ * @param http11 true to use HTTP/1.1, false to use 1.0
+ *
+ * @deprecated Use {@link HttpMethodParams#setVersion(HttpVersion)}
+ */
+ public void setHttp11(boolean http11) {
+ if (http11) {
+ this.params.setVersion(HttpVersion.HTTP_1_1);
+ } else {
+ this.params.setVersion(HttpVersion.HTTP_1_0);
+ }
+ }
+
+ /**
+ * Returns true if the HTTP method should automatically handle HTTP
+ * authentication challenges (status code 401, etc.), false otherwise
+ *
+ * @return true if authentication challenges will be processed
+ * automatically, false otherwise.
+ *
+ * @since 2.0
+ */
+ public boolean getDoAuthentication() {
+ return doAuthentication;
+ }
+
+ /**
+ * Sets whether or not the HTTP method should automatically handle HTTP
+ * authentication challenges (status code 401, etc.)
+ *
+ * @param doAuthentication true to process authentication challenges
+ * authomatically, false otherwise.
+ *
+ * @since 2.0
+ */
+ public void setDoAuthentication(boolean doAuthentication) {
+ this.doAuthentication = doAuthentication;
+ }
+
+ // ---------------------------------------------- Protected Utility Methods
+
+ /**
+ * Returns true if version 1.1 of the HTTP protocol should be
+ * used per default, false if version 1.0 should be used.
+ *
+ * @return true to use HTTP/1.1, false to use 1.0
+ *
+ * @deprecated Use {@link HttpMethodParams#getVersion()}
+ */
+ public boolean isHttp11() {
+ return this.params.getVersion().equals(HttpVersion.HTTP_1_1);
+ }
+
+ /**
+ * Sets the path of the HTTP method.
+ * It is responsibility of the caller to ensure that the path is
+ * properly encoded (URL safe).
+ *
+ * @param path the path of the HTTP method. The path is expected
+ * to be URL-encoded
+ */
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ /**
+ * Adds the specified request header, NOT overwriting any previous value.
+ * Note that header-name matching is case insensitive.
+ *
+ * @param header the header to add to the request
+ */
+ public void addRequestHeader(Header header) {
+ LOG.trace("HttpMethodBase.addRequestHeader(Header)");
+
+ if (header == null) {
+ LOG.debug("null header value ignored");
+ } else {
+ getRequestHeaderGroup().addHeader(header);
+ }
+ }
+
+ /**
+ * Use this method internally to add footers.
+ *
+ * @param footer The footer to add.
+ */
+ public void addResponseFooter(Header footer) {
+ getResponseTrailerHeaderGroup().addHeader(footer);
+ }
+
+ /**
+ * Gets the path of this HTTP method.
+ * Calling this method after the request has been executed will
+ * return the actual path, following any redirects automatically
+ * handled by this HTTP method.
+ *
+ * @return the path to request or "/" if the path is blank.
+ */
+ public String getPath() {
+ return (path == null || path.equals("")) ? "/" : path;
+ }
+
+ /**
+ * Sets the query string of this HTTP method. The caller must ensure that the string
+ * is properly URL encoded. The query string should not start with the question
+ * mark character.
+ *
+ * @param queryString the query string
+ *
+ * @see EncodingUtil#formUrlEncode(NameValuePair[], String)
+ */
+ public void setQueryString(String queryString) {
+ this.queryString = queryString;
+ }
+
+ /**
+ * Sets the query string of this HTTP method. The pairs are encoded as UTF-8 characters.
+ * To use a different charset the parameters can be encoded manually using EncodingUtil
+ * and set as a single String.
+ *
+ * @param params an array of {@link NameValuePair}s to add as query string
+ * parameters. The name/value pairs will be automcatically
+ * URL encoded
+ *
+ * @see EncodingUtil#formUrlEncode(NameValuePair[], String)
+ * @see #setQueryString(String)
+ */
+ public void setQueryString(NameValuePair[] params) {
+ LOG.trace("enter HttpMethodBase.setQueryString(NameValuePair[])");
+ queryString = EncodingUtil.formUrlEncode(params, "UTF-8");
+ }
+
+ /**
+ * Gets the query string of this HTTP method.
+ *
+ * @return The query string
+ */
+ public String getQueryString() {
+ return queryString;
+ }
+
+ /**
+ * Set the specified request header, overwriting any previous value. Note
+ * that header-name matching is case-insensitive.
+ *
+ * @param headerName the header's name
+ * @param headerValue the header's value
+ */
+ public void setRequestHeader(String headerName, String headerValue) {
+ Header header = new Header(headerName, headerValue);
+ setRequestHeader(header);
+ }
+
+ /**
+ * Sets the specified request header, overwriting any previous value.
+ * Note that header-name matching is case insensitive.
+ *
+ * @param header the header
+ */
+ public void setRequestHeader(Header header) {
+
+ Header[] headers = getRequestHeaderGroup().getHeaders(header.getName());
+
+ for (int i = 0; i < headers.length; i++) {
+ getRequestHeaderGroup().removeHeader(headers[i]);
+ }
+
+ getRequestHeaderGroup().addHeader(header);
+
+ }
+
+ /**
+ * Returns the specified request header. Note that header-name matching is
+ * case insensitive. null will be returned if either
+ * headerName is null or there is no matching header for
+ * headerName.
+ *
+ * @param headerName The name of the header to be returned.
+ *
+ * @return The specified request header.
+ *
+ * @since 3.0
+ */
+ public Header getRequestHeader(String headerName) {
+ if (headerName == null) {
+ return null;
+ } else {
+ return getRequestHeaderGroup().getCondensedHeader(headerName);
+ }
+ }
+
+ /**
+ * Returns an array of the requests headers that the HTTP method currently has
+ *
+ * @return an array of my request headers.
+ */
+ public Header[] getRequestHeaders() {
+ return getRequestHeaderGroup().getAllHeaders();
+ }
+
+ /**
+ * @see org.apache.commons.httpclient.HttpMethod#getRequestHeaders(java.lang.String)
+ */
+ public Header[] getRequestHeaders(String headerName) {
+ return getRequestHeaderGroup().getHeaders(headerName);
+ }
+
+ /**
+ * Gets the {@link HeaderGroup header group} storing the request headers.
+ *
+ * @return a HeaderGroup
+ *
+ * @since 2.0beta1
+ */
+ protected HeaderGroup getRequestHeaderGroup() {
+ return requestHeaders;
+ }
+
+ /**
+ * Gets the {@link HeaderGroup header group} storing the response trailer headers
+ * as per RFC 2616 section 3.6.1.
+ *
+ * @return a HeaderGroup
+ *
+ * @since 2.0beta1
+ */
+ protected HeaderGroup getResponseTrailerHeaderGroup() {
+ return responseTrailerHeaders;
+ }
+
+ /**
+ * Gets the {@link HeaderGroup header group} storing the response headers.
+ *
+ * @return a HeaderGroup
+ *
+ * @since 2.0beta1
+ */
+ protected HeaderGroup getResponseHeaderGroup() {
+ return responseHeaders;
+ }
+
+ /**
+ * @see org.apache.commons.httpclient.HttpMethod#getResponseHeaders(java.lang.String)
+ *
+ * @since 3.0
+ */
+ public Header[] getResponseHeaders(String headerName) {
+ return getResponseHeaderGroup().getHeaders(headerName);
+ }
+
+ /**
+ * Returns the response status code.
+ *
+ * @return the status code associated with the latest response.
+ */
+ public int getStatusCode() {
+ return statusLine.getStatusCode();
+ }
+
+ /**
+ * Provides access to the response status line.
+ *
+ * @return the status line object from the latest response.
+ * @since 2.0
+ */
+ public StatusLine getStatusLine() {
+ return statusLine;
+ }
+
+ /**
+ * Checks if response data is available.
+ * @return true if response data is available, false otherwise.
+ */
+ private boolean responseAvailable() {
+ return (responseBody != null) || (responseStream != null);
+ }
+
+ /**
+ * Returns an array of the response headers that the HTTP method currently has
+ * in the order in which they were read.
+ *
+ * @return an array of response headers.
+ */
+ public Header[] getResponseHeaders() {
+ return getResponseHeaderGroup().getAllHeaders();
+ }
+
+ /**
+ * Gets the response header associated with the given name. Header name
+ * matching is case insensitive. null will be returned if either
+ * headerName is null or there is no matching header for
+ * headerName.
+ *
+ * @param headerName the header name to match
+ *
+ * @return the matching header
+ */
+ public Header getResponseHeader(String headerName) {
+ if (headerName == null) {
+ return null;
+ } else {
+ return getResponseHeaderGroup().getCondensedHeader(headerName);
+ }
+ }
+
+
+ /**
+ * Return the length (in bytes) of the response body, as specified in a
+ * Content-Length header.
+ *
+ *
+ * Return -1 when the content-length is unknown.
+ *
+ *
+ * @return content length, if Content-Length header is available.
+ * 0 indicates that the request has no body.
+ * If Content-Length header is not present, the method
+ * returns -1.
+ */
+ public long getResponseContentLength() {
+ Header[] headers = getResponseHeaderGroup().getHeaders("Content-Length");
+ if (headers.length == 0) {
+ return -1;
+ }
+ if (headers.length > 1) {
+ LOG.warn("Multiple content-length headers detected");
+ }
+ for (int i = headers.length - 1; i >= 0; i--) {
+ Header header = headers[i];
+ try {
+ return Long.parseLong(header.getValue());
+ } catch (NumberFormatException e) {
+ if (LOG.isWarnEnabled()) {
+ LOG.warn("Invalid content-length value: " + e.getMessage());
+ }
+ }
+ // See if we can have better luck with another header, if present
+ }
+ return -1;
+ }
+
+
+ /**
+ * Returns the response body of the HTTP method, if any, as an array of bytes.
+ * If response body is not available or cannot be read, returns null
+ *
+ * Note: This will cause the entire response body to be buffered in memory. A
+ * malicious server may easily exhaust all the VM memory. It is strongly
+ * recommended, to use getResponseAsStream if the content length of the response
+ * is unknown or resonably large.
+ *
+ * @return The response body.
+ *
+ * @throws IOException If an I/O (transport) problem occurs while obtaining the
+ * response body.
+ */
+ public byte[] getResponseBody() throws IOException {
+ if (this.responseBody == null) {
+ InputStream instream = getResponseBodyAsStream();
+ if (instream != null) {
+ long contentLength = getResponseContentLength();
+ if (contentLength > Integer.MAX_VALUE) { //guard below cast from overflow
+ throw new IOException("Content too large to be buffered: "+ contentLength +" bytes");
+ }
+ int limit = getParams().getIntParameter(HttpMethodParams.BUFFER_WARN_TRIGGER_LIMIT, 1024*1024);
+ if ((contentLength == -1) || (contentLength > limit)) {
+ LOG.warn("Going to buffer response body of large or unknown size. "
+ +"Using getResponseBodyAsStream instead is recommended.");
+ }
+ LOG.debug("Buffering response body");
+ ByteArrayOutputStream outstream = new ByteArrayOutputStream(
+ contentLength > 0 ? (int) contentLength : DEFAULT_INITIAL_BUFFER_SIZE);
+ byte[] buffer = new byte[4096];
+ int len;
+ while ((len = instream.read(buffer)) > 0) {
+ outstream.write(buffer, 0, len);
+ }
+ outstream.close();
+ setResponseStream(null);
+ this.responseBody = outstream.toByteArray();
+ }
+ }
+ return this.responseBody;
+ }
+
+ /**
+ * Returns the response body of the HTTP method, if any, as an {@link InputStream}.
+ * If response body is not available, returns null
+ *
+ * @return The response body
+ *
+ * @throws IOException If an I/O (transport) problem occurs while obtaining the
+ * response body.
+ */
+ public InputStream getResponseBodyAsStream() throws IOException {
+ if (responseStream != null) {
+ return responseStream;
+ }
+ if (responseBody != null) {
+ InputStream byteResponseStream = new ByteArrayInputStream(responseBody);
+ LOG.debug("re-creating response stream from byte array");
+ return byteResponseStream;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the response body of the HTTP method, if any, as a {@link String}.
+ * If response body is not available or cannot be read, returns null
+ * The string conversion on the data is done using the character encoding specified
+ * in Content-Type header.
+ *
+ * Note: This will cause the entire response body to be buffered in memory. A
+ * malicious server may easily exhaust all the VM memory. It is strongly
+ * recommended, to use getResponseAsStream if the content length of the response
+ * is unknown or resonably large.
+ *
+ * @return The response body.
+ *
+ * @throws IOException If an I/O (transport) problem occurs while obtaining the
+ * response body.
+ */
+ public String getResponseBodyAsString() throws IOException {
+ byte[] rawdata = null;
+ if (responseAvailable()) {
+ rawdata = getResponseBody();
+ }
+ if (rawdata != null) {
+ return EncodingUtil.getString(rawdata, getResponseCharSet());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns an array of the response footers that the HTTP method currently has
+ * in the order in which they were read.
+ *
+ * @return an array of footers
+ */
+ public Header[] getResponseFooters() {
+ return getResponseTrailerHeaderGroup().getAllHeaders();
+ }
+
+ /**
+ * Gets the response footer associated with the given name.
+ * Footer name matching is case insensitive.
+ * null will be returned if either footerName is
+ * null or there is no matching footer for footerName
+ * or there are no footers available. If there are multiple footers
+ * with the same name, there values will be combined with the ',' separator
+ * as specified by RFC2616.
+ *
+ * @param footerName the footer name to match
+ * @return the matching footer
+ */
+ public Header getResponseFooter(String footerName) {
+ if (footerName == null) {
+ return null;
+ } else {
+ return getResponseTrailerHeaderGroup().getCondensedHeader(footerName);
+ }
+ }
+
+ /**
+ * Sets the response stream.
+ * @param responseStream The new response stream.
+ */
+ protected void setResponseStream(InputStream responseStream) {
+ this.responseStream = responseStream;
+ }
+
+ /**
+ * Returns a stream from which the body of the current response may be read.
+ * If the method has not yet been executed, if responseBodyConsumed
+ * has been called, or if the stream returned by a previous call has been closed,
+ * null will be returned.
+ *
+ * @return the current response stream
+ */
+ protected InputStream getResponseStream() {
+ return responseStream;
+ }
+
+ /**
+ * Returns the status text (or "reason phrase") associated with the latest
+ * response.
+ *
+ * @return The status text.
+ */
+ public String getStatusText() {
+ return statusLine.getReasonPhrase();
+ }
+
+ /**
+ * Defines how strictly HttpClient follows the HTTP protocol specification
+ * (RFC 2616 and other relevant RFCs). In the strict mode HttpClient precisely
+ * implements the requirements of the specification, whereas in non-strict mode
+ * it attempts to mimic the exact behaviour of commonly used HTTP agents,
+ * which many HTTP servers expect.
+ *
+ * @param strictMode true for strict mode, false otherwise
+ *
+ * @deprecated Use {@link org.apache.commons.httpclient.params.HttpParams#setParameter(String, Object)}
+ * to exercise a more granular control over HTTP protocol strictness.
+ */
+ public void setStrictMode(boolean strictMode) {
+ if (strictMode) {
+ this.params.makeStrict();
+ } else {
+ this.params.makeLenient();
+ }
+ }
+
+ /**
+ * @deprecated Use {@link org.apache.commons.httpclient.params.HttpParams#setParameter(String, Object)}
+ * to exercise a more granular control over HTTP protocol strictness.
+ *
+ * @return false
+ */
+ public boolean isStrictMode() {
+ return false;
+ }
+
+ /**
+ * Adds the specified request header, NOT overwriting any previous value.
+ * Note that header-name matching is case insensitive.
+ *
+ * @param headerName the header's name
+ * @param headerValue the header's value
+ */
+ public void addRequestHeader(String headerName, String headerValue) {
+ addRequestHeader(new Header(headerName, headerValue));
+ }
+
+ /**
+ * Tests if the connection should be force-closed when no longer needed.
+ *
+ * @return true if the connection must be closed
+ */
+ protected boolean isConnectionCloseForced() {
+ return this.connectionCloseForced;
+ }
+
+ /**
+ * Sets whether or not the connection should be force-closed when no longer
+ * needed. This value should only be set to true in abnormal
+ * circumstances, such as HTTP protocol violations.
+ *
+ * @param b true if the connection must be closed, false
+ * otherwise.
+ */
+ protected void setConnectionCloseForced(boolean b) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Force-close connection: " + b);
+ }
+ this.connectionCloseForced = b;
+ }
+
+ /**
+ * Tests if the connection should be closed after the method has been executed.
+ * The connection will be left open when using HTTP/1.1 or if Connection:
+ * keep-alive header was sent.
+ *
+ * @param conn the connection in question
+ *
+ * @return boolean true if we should close the connection.
+ */
+ protected boolean shouldCloseConnection(HttpConnection conn) {
+ // Connection must be closed due to an abnormal circumstance
+ if (isConnectionCloseForced()) {
+ LOG.debug("Should force-close connection.");
+ return true;
+ }
+
+ Header connectionHeader = null;
+ // In case being connected via a proxy server
+ if (!conn.isTransparent()) {
+ // Check for 'proxy-connection' directive
+ connectionHeader = responseHeaders.getFirstHeader("proxy-connection");
+ }
+ // In all cases Check for 'connection' directive
+ // some non-complaint proxy servers send it instread of
+ // expected 'proxy-connection' directive
+ if (connectionHeader == null) {
+ connectionHeader = responseHeaders.getFirstHeader("connection");
+ }
+ // In case the response does not contain any explict connection
+ // directives, check whether the request does
+ if (connectionHeader == null) {
+ connectionHeader = requestHeaders.getFirstHeader("connection");
+ }
+ if (connectionHeader != null) {
+ if (connectionHeader.getValue().equalsIgnoreCase("close")) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Should close connection in response to directive: "
+ + connectionHeader.getValue());
+ }
+ return true;
+ } else if (connectionHeader.getValue().equalsIgnoreCase("keep-alive")) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Should NOT close connection in response to directive: "
+ + connectionHeader.getValue());
+ }
+ return false;
+ } else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Unknown directive: " + connectionHeader.toExternalForm());
+ }
+ }
+ }
+ LOG.debug("Resorting to protocol version default close connection policy");
+ // missing or invalid connection header, do the default
+ if (this.effectiveVersion.greaterEquals(HttpVersion.HTTP_1_1)) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Should NOT close connection, using " + this.effectiveVersion.toString());
+ }
+ } else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Should close connection, using " + this.effectiveVersion.toString());
+ }
+ }
+ return this.effectiveVersion.lessEquals(HttpVersion.HTTP_1_0);
+ }
+
+ /**
+ * Tests if the this method is ready to be executed.
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} to be used
+ * @throws HttpException If the method is in invalid state.
+ */
+ private void checkExecuteConditions(HttpState state, HttpConnection conn)
+ throws HttpException {
+
+ if (state == null) {
+ throw new IllegalArgumentException("HttpState parameter may not be null");
+ }
+ if (conn == null) {
+ throw new IllegalArgumentException("HttpConnection parameter may not be null");
+ }
+ if (this.aborted) {
+ throw new IllegalStateException("Method has been aborted");
+ }
+ if (!validate()) {
+ throw new ProtocolException("HttpMethodBase object not valid");
+ }
+ }
+
+ /**
+ * Executes this method using the specified HttpConnection and
+ * HttpState.
+ *
+ * @param state {@link HttpState state} information to associate with this
+ * request. Must be non-null.
+ * @param conn the {@link HttpConnection connection} to used to execute
+ * this HTTP method. Must be non-null.
+ *
+ * @return the integer status code if one was obtained, or -1
+ *
+ * @throws IOException if an I/O (transport) error occurs
+ * @throws HttpException if a protocol exception occurs.
+ */
+ public int execute(HttpState state, HttpConnection conn)
+ throws HttpException, IOException {
+
+ LOG.trace("enter HttpMethodBase.execute(HttpState, HttpConnection)");
+
+ // this is our connection now, assign it to a local variable so
+ // that it can be released later
+ this.responseConnection = conn;
+
+ checkExecuteConditions(state, conn);
+ this.statusLine = null;
+ this.connectionCloseForced = false;
+
+ conn.setLastResponseInputStream(null);
+
+ // determine the effective protocol version
+ if (this.effectiveVersion == null) {
+ this.effectiveVersion = this.params.getVersion();
+ }
+
+ writeRequest(state, conn);
+ this.requestSent = true;
+ readResponse(state, conn);
+ // the method has successfully executed
+ used = true;
+
+ return statusLine.getStatusCode();
+ }
+
+ /**
+ * Aborts the execution of this method.
+ *
+ * @since 3.0
+ */
+ public void abort() {
+ if (this.aborted) {
+ return;
+ }
+ this.aborted = true;
+ HttpConnection conn = this.responseConnection;
+ if (conn != null) {
+ conn.close();
+ }
+ }
+
+ /**
+ * Returns true if the HTTP method has been already {@link #execute executed},
+ * but not {@link #recycle recycled}.
+ *
+ * @return true if the method has been executed, false otherwise
+ */
+ public boolean hasBeenUsed() {
+ return used;
+ }
+
+ /**
+ * Recycles the HTTP method so that it can be used again.
+ * Note that all of the instance variables will be reset
+ * once this method has been called. This method will also
+ * release the connection being used by this HTTP method.
+ *
+ * @see #releaseConnection()
+ *
+ * @deprecated no longer supported and will be removed in the future
+ * version of HttpClient
+ */
+ public void recycle() {
+ LOG.trace("enter HttpMethodBase.recycle()");
+
+ releaseConnection();
+
+ path = null;
+ followRedirects = false;
+ doAuthentication = true;
+ queryString = null;
+ getRequestHeaderGroup().clear();
+ getResponseHeaderGroup().clear();
+ getResponseTrailerHeaderGroup().clear();
+ statusLine = null;
+ effectiveVersion = null;
+ aborted = false;
+ used = false;
+ params = new HttpMethodParams();
+ responseBody = null;
+ recoverableExceptionCount = 0;
+ connectionCloseForced = false;
+ hostAuthState.invalidate();
+ proxyAuthState.invalidate();
+ cookiespec = null;
+ requestSent = false;
+ }
+
+ /**
+ * Releases the connection being used by this HTTP method. In particular the
+ * connection is used to read the response(if there is one) and will be held
+ * until the response has been read. If the connection can be reused by other
+ * HTTP methods it is NOT closed at this point.
+ *
+ * @since 2.0
+ */
+ public void releaseConnection() {
+ try {
+ if (this.responseStream != null) {
+ try {
+ // FYI - this may indirectly invoke responseBodyConsumed.
+ this.responseStream.close();
+ } catch (IOException ignore) {
+ }
+ }
+ } finally {
+ ensureConnectionRelease();
+ }
+ }
+
+ /**
+ * Remove the request header associated with the given name. Note that
+ * header-name matching is case insensitive.
+ *
+ * @param headerName the header name
+ */
+ public void removeRequestHeader(String headerName) {
+
+ Header[] headers = getRequestHeaderGroup().getHeaders(headerName);
+ for (int i = 0; i < headers.length; i++) {
+ getRequestHeaderGroup().removeHeader(headers[i]);
+ }
+
+ }
+
+ /**
+ * Removes the given request header.
+ *
+ * @param header the header
+ */
+ public void removeRequestHeader(final Header header) {
+ if (header == null) {
+ return;
+ }
+ getRequestHeaderGroup().removeHeader(header);
+ }
+
+ // ---------------------------------------------------------------- Queries
+
+ /**
+ * Returns true the method is ready to execute, false otherwise.
+ *
+ * @return This implementation always returns true.
+ */
+ public boolean validate() {
+ return true;
+ }
+
+
+ /**
+ * Returns the actual cookie policy
+ *
+ * @param state HTTP state. TODO: to be removed in the future
+ *
+ * @return cookie spec
+ */
+ private CookieSpec getCookieSpec(final HttpState state) {
+ if (this.cookiespec == null) {
+ int i = state.getCookiePolicy();
+ if (i == -1) {
+ this.cookiespec = CookiePolicy.getCookieSpec(this.params.getCookiePolicy());
+ } else {
+ this.cookiespec = CookiePolicy.getSpecByPolicy(i);
+ }
+ this.cookiespec.setValidDateFormats(
+ (Collection)this.params.getParameter(HttpMethodParams.DATE_PATTERNS));
+ }
+ return this.cookiespec;
+ }
+
+ /**
+ * Generates Cookie request headers for those {@link Cookie cookie}s
+ * that match the given host, port and path.
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ */
+ protected void addCookieRequestHeader(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+
+ LOG.trace("enter HttpMethodBase.addCookieRequestHeader(HttpState, "
+ + "HttpConnection)");
+
+ Header[] cookieheaders = getRequestHeaderGroup().getHeaders("Cookie");
+ for (int i = 0; i < cookieheaders.length; i++) {
+ Header cookieheader = cookieheaders[i];
+ if (cookieheader.isAutogenerated()) {
+ getRequestHeaderGroup().removeHeader(cookieheader);
+ }
+ }
+
+ CookieSpec matcher = getCookieSpec(state);
+ String host = this.params.getVirtualHost();
+ if (host == null) {
+ host = conn.getHost();
+ }
+ // BEGIN IA/HERITRIX CHANGES
+ Cookie[] cookies = matcher.match(host, conn.getPort(),
+ getPath(), conn.isSecure(), state.getCookiesMap());
+ // END IA/HERITRIX CHANGES
+ if ((cookies != null) && (cookies.length > 0)) {
+ if (getParams().isParameterTrue(HttpMethodParams.SINGLE_COOKIE_HEADER)) {
+ // In strict mode put all cookies on the same header
+ String s = matcher.formatCookies(cookies);
+ getRequestHeaderGroup().addHeader(new Header("Cookie", s, true));
+ } else {
+ // In non-strict mode put each cookie on a separate header
+ for (int i = 0; i < cookies.length; i++) {
+ String s = matcher.formatCookie(cookies[i]);
+ getRequestHeaderGroup().addHeader(new Header("Cookie", s, true));
+ }
+ }
+ }
+ }
+
+ /**
+ * Generates Host request header, as long as no Host request
+ * header already exists.
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ */
+ protected void addHostRequestHeader(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ LOG.trace("enter HttpMethodBase.addHostRequestHeader(HttpState, "
+ + "HttpConnection)");
+
+ // Per 19.6.1.1 of RFC 2616, it is legal for HTTP/1.0 based
+ // applications to send the Host request-header.
+ // TODO: Add the ability to disable the sending of this header for
+ // HTTP/1.0 requests.
+ String host = this.params.getVirtualHost();
+ if (host != null) {
+ LOG.debug("Using virtual host name: " + host);
+ } else {
+ host = conn.getHost();
+ }
+ int port = conn.getPort();
+
+ // Note: RFC 2616 uses the term "internet host name" for what goes on the
+ // host line. It would seem to imply that host should be blank if the
+ // host is a number instead of an name. Based on the behavior of web
+ // browsers, and the fact that RFC 2616 never defines the phrase "internet
+ // host name", and the bad behavior of HttpClient that follows if we
+ // send blank, I interpret this as a small misstatement in the RFC, where
+ // they meant to say "internet host". So IP numbers get sent as host
+ // entries too. -- Eric Johnson 12/13/2002
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Adding Host request header");
+ }
+
+ //appends the port only if not using the default port for the protocol
+ if (conn.getProtocol().getDefaultPort() != port) {
+ host += (":" + port);
+ }
+
+ setRequestHeader("Host", host);
+ }
+
+ /**
+ * Generates Proxy-Connection: Keep-Alive request header when
+ * communicating via a proxy server.
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ */
+ protected void addProxyConnectionHeader(HttpState state,
+ HttpConnection conn)
+ throws IOException, HttpException {
+ LOG.trace("enter HttpMethodBase.addProxyConnectionHeader("
+ + "HttpState, HttpConnection)");
+ if (!conn.isTransparent()) {
+ if (getRequestHeader("Proxy-Connection") == null) {
+ addRequestHeader("Proxy-Connection", "Keep-Alive");
+ }
+ }
+ }
+
+ /**
+ * Generates all the required request {@link Header header}s
+ * to be submitted via the given {@link HttpConnection connection}.
+ *
+ *
+ * This implementation adds User-Agent, Host,
+ * Cookie, Authorization, Proxy-Authorization
+ * and Proxy-Connection headers, when appropriate.
+ *
+ *
+ *
+ * Subclasses may want to override this method to to add additional
+ * headers, and may choose to invoke this implementation (via
+ * super) to add the "standard" headers.
+ *
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ *
+ * @see #writeRequestHeaders
+ */
+ protected void addRequestHeaders(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ LOG.trace("enter HttpMethodBase.addRequestHeaders(HttpState, "
+ + "HttpConnection)");
+
+ addUserAgentRequestHeader(state, conn);
+ addHostRequestHeader(state, conn);
+ addCookieRequestHeader(state, conn);
+ addProxyConnectionHeader(state, conn);
+ }
+
+ /**
+ * Generates default User-Agent request header, as long as no
+ * User-Agent request header already exists.
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ */
+ protected void addUserAgentRequestHeader(HttpState state,
+ HttpConnection conn)
+ throws IOException, HttpException {
+ LOG.trace("enter HttpMethodBase.addUserAgentRequestHeaders(HttpState, "
+ + "HttpConnection)");
+
+ if (getRequestHeader("User-Agent") == null) {
+ String agent = (String)getParams().getParameter(HttpMethodParams.USER_AGENT);
+ if (agent == null) {
+ agent = "Jakarta Commons-HttpClient";
+ }
+ setRequestHeader("User-Agent", agent);
+ }
+ }
+
+ /**
+ * Throws an {@link IllegalStateException} if the HTTP method has been already
+ * {@link #execute executed}, but not {@link #recycle recycled}.
+ *
+ * @throws IllegalStateException if the method has been used and not
+ * recycled
+ */
+ protected void checkNotUsed() throws IllegalStateException {
+ if (used) {
+ throw new IllegalStateException("Already used.");
+ }
+ }
+
+ /**
+ * Throws an {@link IllegalStateException} if the HTTP method has not been
+ * {@link #execute executed} since last {@link #recycle recycle}.
+ *
+ *
+ * @throws IllegalStateException if not used
+ */
+ protected void checkUsed() throws IllegalStateException {
+ if (!used) {
+ throw new IllegalStateException("Not Used.");
+ }
+ }
+
+ // ------------------------------------------------- Static Utility Methods
+
+ /**
+ * Generates HTTP request line according to the specified attributes.
+ *
+ * @param connection the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ * @param name the method name generate a request for
+ * @param requestPath the path string for the request
+ * @param query the query string for the request
+ * @param version the protocol version to use (e.g. HTTP/1.0)
+ *
+ * @return HTTP request line
+ */
+ protected static String generateRequestLine(HttpConnection connection,
+ String name, String requestPath, String query, String version) {
+ LOG.trace("enter HttpMethodBase.generateRequestLine(HttpConnection, "
+ + "String, String, String, String)");
+
+ StringBuffer buf = new StringBuffer();
+ // Append method name
+ buf.append(name);
+ buf.append(" ");
+ // Absolute or relative URL?
+ if (!connection.isTransparent()) {
+ Protocol protocol = connection.getProtocol();
+ buf.append(protocol.getScheme().toLowerCase());
+ buf.append("://");
+ buf.append(connection.getHost());
+ if ((connection.getPort() != -1)
+ && (connection.getPort() != protocol.getDefaultPort())
+ ) {
+ buf.append(":");
+ buf.append(connection.getPort());
+ }
+ }
+ // Append path, if any
+ if (requestPath == null) {
+ buf.append("/");
+ } else {
+ if (!connection.isTransparent() && !requestPath.startsWith("/")) {
+ buf.append("/");
+ }
+ buf.append(requestPath);
+ }
+ // Append query, if any
+ if (query != null) {
+ if (query.indexOf("?") != 0) {
+ buf.append("?");
+ }
+ buf.append(query);
+ }
+ // Append protocol
+ buf.append(" ");
+ buf.append(version);
+ buf.append("\r\n");
+
+ return buf.toString();
+ }
+
+ /**
+ * This method is invoked immediately after
+ * {@link #readResponseBody(HttpState,HttpConnection)} and can be overridden by
+ * sub-classes in order to provide custom body processing.
+ *
+ *
+ * This implementation does nothing.
+ *
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @see #readResponse
+ * @see #readResponseBody
+ */
+ protected void processResponseBody(HttpState state, HttpConnection conn) {
+ }
+
+ /**
+ * This method is invoked immediately after
+ * {@link #readResponseHeaders(HttpState,HttpConnection)} and can be overridden by
+ * sub-classes in order to provide custom response headers processing.
+
+ *
+ * This implementation will handle the Set-Cookie and
+ * Set-Cookie2 headers, if any, adding the relevant cookies to
+ * the given {@link HttpState}.
+ *
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @see #readResponse
+ * @see #readResponseHeaders
+ */
+ protected void processResponseHeaders(HttpState state,
+ HttpConnection conn) {
+ LOG.trace("enter HttpMethodBase.processResponseHeaders(HttpState, "
+ + "HttpConnection)");
+
+ Header[] headers = getResponseHeaderGroup().getHeaders("set-cookie2");
+ //Only process old style set-cookie headers if new style headres
+ //are not present
+ if (headers.length == 0) {
+ headers = getResponseHeaderGroup().getHeaders("set-cookie");
+ }
+
+ CookieSpec parser = getCookieSpec(state);
+ String host = this.params.getVirtualHost();
+ if (host == null) {
+ host = conn.getHost();
+ }
+ for (int i = 0; i < headers.length; i++) {
+ Header header = headers[i];
+ Cookie[] cookies = null;
+ try {
+ cookies = parser.parse(
+ host,
+ conn.getPort(),
+ getPath(),
+ conn.isSecure(),
+ header);
+ } catch (MalformedCookieException e) {
+ if (LOG.isWarnEnabled()) {
+ LOG.warn("Invalid cookie header: \""
+ + header.getValue()
+ + "\". " + e.getMessage());
+ }
+ }
+ if (cookies != null) {
+ for (int j = 0; j < cookies.length; j++) {
+ Cookie cookie = cookies[j];
+ try {
+ parser.validate(
+ host,
+ conn.getPort(),
+ getPath(),
+ conn.isSecure(),
+ cookie);
+ state.addCookie(cookie);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Cookie accepted: \""
+ + parser.formatCookie(cookie) + "\"");
+ }
+ } catch (MalformedCookieException e) {
+ if (LOG.isWarnEnabled()) {
+ LOG.warn("Cookie rejected: \"" + parser.formatCookie(cookie)
+ + "\". " + e.getMessage());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * This method is invoked immediately after
+ * {@link #readStatusLine(HttpState,HttpConnection)} and can be overridden by
+ * sub-classes in order to provide custom response status line processing.
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @see #readResponse
+ * @see #readStatusLine
+ */
+ protected void processStatusLine(HttpState state, HttpConnection conn) {
+ }
+
+ /**
+ * Reads the response from the given {@link HttpConnection connection}.
+ *
+ *
+ * The response is processed as the following sequence of actions:
+ *
+ *
+ *
+ * {@link #readStatusLine(HttpState,HttpConnection)} is
+ * invoked to read the request line.
+ *
+ *
+ * {@link #processStatusLine(HttpState,HttpConnection)}
+ * is invoked, allowing the method to process the status line if
+ * desired.
+ *
+ *
+ * {@link #readResponseHeaders(HttpState,HttpConnection)} is invoked to read
+ * the associated headers.
+ *
+ *
+ * {@link #processResponseHeaders(HttpState,HttpConnection)} is invoked, allowing
+ * the method to process the headers if desired.
+ *
+ *
+ * {@link #readResponseBody(HttpState,HttpConnection)} is
+ * invoked to read the associated body (if any).
+ *
+ *
+ * {@link #processResponseBody(HttpState,HttpConnection)} is invoked, allowing the
+ * method to process the response body if desired.
+ *
+ *
+ *
+ * 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.)
+ *
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ */
+ protected void readResponse(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ LOG.trace(
+ "enter HttpMethodBase.readResponse(HttpState, HttpConnection)");
+ // Status line & line may have already been received
+ // if 'expect - continue' handshake has been used
+ while (this.statusLine == null) {
+ readStatusLine(state, conn);
+ processStatusLine(state, conn);
+ readResponseHeaders(state, conn);
+ processResponseHeaders(state, conn);
+
+ int status = this.statusLine.getStatusCode();
+ if ((status >= 100) && (status < 200)) {
+ if (LOG.isInfoEnabled()) {
+ LOG.info("Discarding unexpected response: " + this.statusLine.toString());
+ }
+ this.statusLine = null;
+ }
+ }
+ readResponseBody(state, conn);
+ processResponseBody(state, conn);
+ }
+
+ /**
+ * Read the response body from the given {@link HttpConnection}.
+ *
+ *
+ * The current implementation wraps the socket level stream with
+ * an appropriate stream for the type of response (chunked, content-length,
+ * or auto-close). If there is no response body, the connection associated
+ * with the request will be returned to the connection manager.
+ *
+ *
+ *
+ * Subclasses may want to override this method to to customize the
+ * processing.
+ *
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ *
+ * @see #readResponse
+ * @see #processResponseBody
+ */
+ protected void readResponseBody(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ LOG.trace(
+ "enter HttpMethodBase.readResponseBody(HttpState, HttpConnection)");
+
+ // assume we are not done with the connection if we get a stream
+ InputStream stream = readResponseBody(conn);
+ if (stream == null) {
+ // done using the connection!
+ responseBodyConsumed();
+ } else {
+ conn.setLastResponseInputStream(stream);
+ setResponseStream(stream);
+ }
+ }
+
+ /**
+ * Returns the response body as an {@link InputStream input stream}
+ * corresponding to the values of the Content-Length and
+ * Transfer-Encoding headers. If no response body is available
+ * returns null.
+ *
+ *
+ * @see #readResponse
+ * @see #processResponseBody
+ *
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ */
+ private InputStream readResponseBody(HttpConnection conn)
+ throws HttpException, IOException {
+
+ LOG.trace("enter HttpMethodBase.readResponseBody(HttpConnection)");
+
+ responseBody = null;
+ InputStream is = conn.getResponseInputStream();
+ if (Wire.CONTENT_WIRE.enabled()) {
+ is = new WireLogInputStream(is, Wire.CONTENT_WIRE);
+ }
+ boolean canHaveBody = canResponseHaveBody(statusLine.getStatusCode());
+ InputStream result = null;
+ Header transferEncodingHeader = responseHeaders.getFirstHeader("Transfer-Encoding");
+ // We use Transfer-Encoding if present and ignore Content-Length.
+ // RFC2616, 4.4 item number 3
+ if (transferEncodingHeader != null) {
+
+ String transferEncoding = transferEncodingHeader.getValue();
+ if (!"chunked".equalsIgnoreCase(transferEncoding)
+ && !"identity".equalsIgnoreCase(transferEncoding)) {
+ if (LOG.isWarnEnabled()) {
+ LOG.warn("Unsupported transfer encoding: " + transferEncoding);
+ }
+ }
+ HeaderElement[] encodings = transferEncodingHeader.getElements();
+ // The chunked encoding must be the last one applied
+ // RFC2616, 14.41
+ int len = encodings.length;
+ if ((len > 0) && ("chunked".equalsIgnoreCase(encodings[len - 1].getName()))) {
+ // if response body is empty
+ if (conn.isResponseAvailable(conn.getParams().getSoTimeout())) {
+ result = new ChunkedInputStream(is, this);
+ } else {
+ if (getParams().isParameterTrue(HttpMethodParams.STRICT_TRANSFER_ENCODING)) {
+ throw new ProtocolException("Chunk-encoded body declared but not sent");
+ } else {
+ LOG.warn("Chunk-encoded body missing");
+ }
+ }
+ } else {
+ LOG.info("Response content is not chunk-encoded");
+ // The connection must be terminated by closing
+ // the socket as per RFC 2616, 3.6
+ setConnectionCloseForced(true);
+ result = is;
+ }
+ } else {
+ long expectedLength = getResponseContentLength();
+ if (expectedLength == -1) {
+ if (canHaveBody && this.effectiveVersion.greaterEquals(HttpVersion.HTTP_1_1)) {
+ Header connectionHeader = responseHeaders.getFirstHeader("Connection");
+ String connectionDirective = null;
+ if (connectionHeader != null) {
+ connectionDirective = connectionHeader.getValue();
+ }
+ if (!"close".equalsIgnoreCase(connectionDirective)) {
+ LOG.info("Response content length is not known");
+ setConnectionCloseForced(true);
+ }
+ }
+ result = is;
+ } else {
+ result = new ContentLengthInputStream(is, expectedLength);
+ }
+ }
+
+ // See if the response is supposed to have a response body
+ if (!canHaveBody) {
+ result = null;
+ }
+ // if there is a result - ALWAYS wrap it in an observer which will
+ // close the underlying stream as soon as it is consumed, and notify
+ // the watcher that the stream has been consumed.
+ if (result != null) {
+
+ result = new AutoCloseInputStream(
+ result,
+ new ResponseConsumedWatcher() {
+ public void responseConsumed() {
+ responseBodyConsumed();
+ }
+ }
+ );
+ }
+
+ return result;
+ }
+
+ /**
+ * Reads the response headers from the given {@link HttpConnection connection}.
+ *
+ *
+ * Subclasses may want to override this method to to customize the
+ * processing.
+ *
+ *
+ *
+ * "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 state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ *
+ * @see #readResponse
+ * @see #processResponseHeaders
+ */
+ protected void readResponseHeaders(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ LOG.trace("enter HttpMethodBase.readResponseHeaders(HttpState,"
+ + "HttpConnection)");
+
+ getResponseHeaderGroup().clear();
+
+ Header[] headers = HttpParser.parseHeaders(
+ conn.getResponseInputStream(), getParams().getHttpElementCharset());
+ if (Wire.HEADER_WIRE.enabled()) {
+ for (int i = 0; i < headers.length; i++) {
+ Wire.HEADER_WIRE.input(headers[i].toExternalForm());
+ }
+ }
+ getResponseHeaderGroup().setHeaders(headers);
+ }
+
+ /**
+ * Read the status line from the given {@link HttpConnection}, setting my
+ * {@link #getStatusCode status code} and {@link #getStatusText status
+ * text}.
+ *
+ *
+ * Subclasses may want to override this method to to customize the
+ * processing.
+ *
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ *
+ * @see StatusLine
+ */
+ protected void readStatusLine(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ LOG.trace("enter HttpMethodBase.readStatusLine(HttpState, HttpConnection)");
+
+ final int maxGarbageLines = getParams().
+ getIntParameter(HttpMethodParams.STATUS_LINE_GARBAGE_LIMIT, Integer.MAX_VALUE);
+
+ //read out the HTTP status string
+ int count = 0;
+ String s;
+ do {
+ s = conn.readLine(getParams().getHttpElementCharset());
+ if (s == null && count == 0) {
+ // The server just dropped connection on us
+ throw new NoHttpResponseException("The server " + conn.getHost() +
+ " failed to respond");
+ }
+ if (Wire.HEADER_WIRE.enabled()) {
+ Wire.HEADER_WIRE.input(s + "\r\n");
+ }
+ if (s != null && StatusLine.startsWithHTTP(s)) {
+ // Got one
+ break;
+ } else if (s == null || count >= maxGarbageLines) {
+ // Giving up
+ throw new ProtocolException("The server " + conn.getHost() +
+ " failed to respond with a valid HTTP response");
+ }
+ count++;
+ } while(true);
+
+ //create the status line from the status string
+ statusLine = new StatusLine(s);
+
+ //check for a valid HTTP-Version
+ String versionStr = statusLine.getHttpVersion();
+ if (getParams().isParameterFalse(HttpMethodParams.UNAMBIGUOUS_STATUS_LINE)
+ && versionStr.equals("HTTP")) {
+ getParams().setVersion(HttpVersion.HTTP_1_0);
+ if (LOG.isWarnEnabled()) {
+ LOG.warn("Ambiguous status line (HTTP protocol version missing):" +
+ statusLine.toString());
+ }
+ } else {
+ this.effectiveVersion = HttpVersion.parse(versionStr);
+ }
+
+ }
+
+ // ------------------------------------------------------ Protected Methods
+
+ /**
+ *
+ * Sends the request via the given {@link HttpConnection connection}.
+ *
+ *
+ *
+ * The request is written as the following sequence of actions:
+ *
+ *
+ *
+ *
+ * {@link #writeRequestLine(HttpState, HttpConnection)} is invoked to
+ * write the request line.
+ *
+ *
+ * {@link #writeRequestHeaders(HttpState, HttpConnection)} is invoked
+ * to write the associated headers.
+ *
+ *
+ * \r\n is sent to close the head part of the request.
+ *
+ *
+ * {@link #writeRequestBody(HttpState, HttpConnection)} is invoked to
+ * write the body part of the request.
+ *
+ *
+ *
+ *
+ * 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.)
+ *
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ */
+ protected void writeRequest(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ LOG.trace(
+ "enter HttpMethodBase.writeRequest(HttpState, HttpConnection)");
+ writeRequestLine(state, conn);
+ writeRequestHeaders(state, conn);
+ conn.writeLine(); // close head
+ if (Wire.HEADER_WIRE.enabled()) {
+ Wire.HEADER_WIRE.output("\r\n");
+ }
+
+ HttpVersion ver = getParams().getVersion();
+ Header expectheader = getRequestHeader("Expect");
+ String expectvalue = null;
+ if (expectheader != null) {
+ expectvalue = expectheader.getValue();
+ }
+ if ((expectvalue != null)
+ && (expectvalue.compareToIgnoreCase("100-continue") == 0)) {
+ if (ver.greaterEquals(HttpVersion.HTTP_1_1)) {
+
+ // make sure the status line and headers have been sent
+ conn.flushRequestOutputStream();
+
+ int readTimeout = conn.getParams().getSoTimeout();
+ try {
+ conn.setSocketTimeout(RESPONSE_WAIT_TIME_MS);
+ readStatusLine(state, conn);
+ processStatusLine(state, conn);
+ readResponseHeaders(state, conn);
+ processResponseHeaders(state, conn);
+
+ if (this.statusLine.getStatusCode() == HttpStatus.SC_CONTINUE) {
+ // Discard status line
+ this.statusLine = null;
+ LOG.debug("OK to continue received");
+ } else {
+ return;
+ }
+ } catch (InterruptedIOException e) {
+ if (!ExceptionUtil.isSocketTimeoutException(e)) {
+ throw e;
+ }
+ // Most probably Expect header is not recongnized
+ // Remove the header to signal the method
+ // that it's okay to go ahead with sending data
+ removeRequestHeader("Expect");
+ LOG.info("100 (continue) read timeout. Resume sending the request");
+ } finally {
+ conn.setSocketTimeout(readTimeout);
+ }
+
+ } else {
+ removeRequestHeader("Expect");
+ LOG.info("'Expect: 100-continue' handshake is only supported by "
+ + "HTTP/1.1 or higher");
+ }
+ }
+
+ writeRequestBody(state, conn);
+ // make sure the entire request body has been sent
+ conn.flushRequestOutputStream();
+ }
+
+ /**
+ * Writes the request body to the given {@link HttpConnection connection}.
+ *
+ *
+ * This method should return true if the request body was actually
+ * sent (or is empty), or false if it could not be sent for some
+ * reason.
+ *
+ *
+ *
+ * This implementation writes nothing and returns true.
+ *
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @return true
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ */
+ protected boolean writeRequestBody(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ return true;
+ }
+
+ /**
+ * Writes the request headers to the given {@link HttpConnection connection}.
+ *
+ *
+ * This implementation invokes {@link #addRequestHeaders(HttpState,HttpConnection)},
+ * and then writes each header to the request stream.
+ *
+ *
+ *
+ * Subclasses may want to override this method to to customize the
+ * processing.
+ *
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ *
+ * @see #addRequestHeaders
+ * @see #getRequestHeaders
+ */
+ protected void writeRequestHeaders(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ LOG.trace("enter HttpMethodBase.writeRequestHeaders(HttpState,"
+ + "HttpConnection)");
+ addRequestHeaders(state, conn);
+
+ String charset = getParams().getHttpElementCharset();
+
+ Header[] headers = getRequestHeaders();
+ for (int i = 0; i < headers.length; i++) {
+ String s = headers[i].toExternalForm();
+ if (Wire.HEADER_WIRE.enabled()) {
+ Wire.HEADER_WIRE.output(s);
+ }
+ conn.print(s, charset);
+ }
+ }
+
+ /**
+ * Writes the request line to the given {@link HttpConnection connection}.
+ *
+ *
+ * Subclasses may want to override this method to to customize the
+ * processing.
+ *
+ *
+ * @param state the {@link HttpState state} information associated with this method
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
+ * can be recovered from.
+ * @throws HttpException if a protocol exception occurs. Usually protocol exceptions
+ * cannot be recovered from.
+ *
+ * @see #generateRequestLine
+ */
+ protected void writeRequestLine(HttpState state, HttpConnection conn)
+ throws IOException, HttpException {
+ LOG.trace(
+ "enter HttpMethodBase.writeRequestLine(HttpState, HttpConnection)");
+ String requestLine = getRequestLine(conn);
+ if (Wire.HEADER_WIRE.enabled()) {
+ Wire.HEADER_WIRE.output(requestLine);
+ }
+ conn.print(requestLine, getParams().getHttpElementCharset());
+ }
+
+ /**
+ * Returns the request line.
+ *
+ * @param conn the {@link HttpConnection connection} used to execute
+ * this HTTP method
+ *
+ * @return The request line.
+ */
+ private String getRequestLine(HttpConnection conn) {
+ return HttpMethodBase.generateRequestLine(conn, getName(),
+ getPath(), getQueryString(), this.effectiveVersion.toString());
+ }
+
+ /**
+ * Returns {@link HttpMethodParams HTTP protocol parameters} associated with this method.
+ *
+ * @return HTTP parameters.
+ *
+ * @since 3.0
+ */
+ public HttpMethodParams getParams() {
+ return this.params;
+ }
+
+ /**
+ * Assigns {@link HttpMethodParams HTTP protocol parameters} for this method.
+ *
+ * @since 3.0
+ *
+ * @see HttpMethodParams
+ */
+ public void setParams(final HttpMethodParams params) {
+ if (params == null) {
+ throw new IllegalArgumentException("Parameters may not be null");
+ }
+ this.params = params;
+ }
+
+ /**
+ * Returns the HTTP version used with this method (may be null
+ * if undefined, that is, the method has not been executed)
+ *
+ * @return HTTP version.
+ *
+ * @since 3.0
+ */
+ public HttpVersion getEffectiveVersion() {
+ return this.effectiveVersion;
+ }
+
+ /**
+ * Per RFC 2616 section 4.3, some response can never contain a message
+ * body.
+ *
+ * @param status - the HTTP status code
+ *
+ * @return 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)");
+
+ boolean result = true;
+
+ if ((status >= 100 && status <= 199) || (status == 204)
+ || (status == 304)) { // NOT MODIFIED
+ result = false;
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns proxy authentication realm, if it has been used during authentication process.
+ * Otherwise returns null.
+ *
+ * @return proxy authentication realm
+ *
+ * @deprecated use #getProxyAuthState()
+ */
+ public String getProxyAuthenticationRealm() {
+ return this.proxyAuthState.getRealm();
+ }
+
+ /**
+ * Returns authentication realm, if it has been used during authentication process.
+ * Otherwise returns null.
+ *
+ * @return authentication realm
+ *
+ * @deprecated use #getHostAuthState()
+ */
+ public String getAuthenticationRealm() {
+ return this.hostAuthState.getRealm();
+ }
+
+ /**
+ * Returns the character set from the Content-Type header.
+ *
+ * @param contentheader The content header.
+ * @return String The character set.
+ */
+ protected String getContentCharSet(Header contentheader) {
+ LOG.trace("enter getContentCharSet( Header contentheader )");
+ String charset = null;
+ if (contentheader != null) {
+ HeaderElement values[] = contentheader.getElements();
+ // I expect only one header element to be there
+ // No more. no less
+ if (values.length == 1) {
+ NameValuePair param = values[0].getParameterByName("charset");
+ if (param != null) {
+ // If I get anything "funny"
+ // UnsupportedEncondingException will result
+ charset = param.getValue();
+ }
+ }
+ }
+ if (charset == null) {
+ charset = getParams().getContentCharset();
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Default charset used: " + charset);
+ }
+ }
+ return charset;
+ }
+
+
+ /**
+ * Returns the character encoding of the request from the Content-Type header.
+ *
+ * @return String The character set.
+ */
+ public String getRequestCharSet() {
+ return getContentCharSet(getRequestHeader("Content-Type"));
+ }
+
+
+ /**
+ * Returns the character encoding of the response from the Content-Type header.
+ *
+ * @return String The character set.
+ */
+ public String getResponseCharSet() {
+ return getContentCharSet(getResponseHeader("Content-Type"));
+ }
+
+ /**
+ * @deprecated no longer used
+ *
+ * Returns the number of "recoverable" exceptions thrown and handled, to
+ * allow for monitoring the quality of the connection.
+ *
+ * @return The number of recoverable exceptions handled by the method.
+ */
+ public int getRecoverableExceptionCount() {
+ return recoverableExceptionCount;
+ }
+
+ /**
+ * A response has been consumed.
+ *
+ *
The default behavior for this class is to check to see if the connection
+ * should be closed, and close if need be, and to ensure that the connection
+ * is returned to the connection manager - if and only if we are not still
+ * inside the execute call.
+ *
+ */
+ protected void responseBodyConsumed() {
+
+ // make sure this is the initial invocation of the notification,
+ // ignore subsequent ones.
+ responseStream = null;
+ if (responseConnection != null) {
+ responseConnection.setLastResponseInputStream(null);
+
+ // At this point, no response data should be available.
+ // If there is data available, regard the connection as being
+ // unreliable and close it.
+
+ if (shouldCloseConnection(responseConnection)) {
+ responseConnection.close();
+ } else {
+ try {
+ if(responseConnection.isResponseAvailable()) {
+ boolean logExtraInput =
+ getParams().isParameterTrue(HttpMethodParams.WARN_EXTRA_INPUT);
+
+ if(logExtraInput) {
+ LOG.warn("Extra response data detected - closing connection");
+ }
+ responseConnection.close();
+ }
+ }
+ catch (IOException e) {
+ LOG.warn(e.getMessage());
+ responseConnection.close();
+ }
+ }
+ }
+ this.connectionCloseForced = false;
+ ensureConnectionRelease();
+ }
+
+ /**
+ * Insure that the connection is released back to the pool.
+ */
+ private void ensureConnectionRelease() {
+ if (responseConnection != null) {
+ responseConnection.releaseConnection();
+ responseConnection = null;
+ }
+ }
+
+ /**
+ * Returns the {@link HostConfiguration host configuration}.
+ *
+ * @return the host configuration
+ *
+ * @deprecated no longer applicable
+ */
+ public HostConfiguration getHostConfiguration() {
+ HostConfiguration hostconfig = new HostConfiguration();
+ hostconfig.setHost(this.httphost);
+ return hostconfig;
+ }
+ /**
+ * Sets the {@link HostConfiguration host configuration}.
+ *
+ * @param hostconfig The hostConfiguration to set
+ *
+ * @deprecated no longer applicable
+ */
+ public void setHostConfiguration(final HostConfiguration hostconfig) {
+ if (hostconfig != null) {
+ this.httphost = new HttpHost(
+ hostconfig.getHost(),
+ hostconfig.getPort(),
+ hostconfig.getProtocol());
+ } else {
+ this.httphost = null;
+ }
+ }
+
+ /**
+ * Returns the {@link MethodRetryHandler retry handler} for this HTTP method
+ *
+ * @return the methodRetryHandler
+ *
+ * @deprecated use {@link HttpMethodParams}
+ */
+ public MethodRetryHandler getMethodRetryHandler() {
+ return methodRetryHandler;
+ }
+
+ /**
+ * Sets the {@link MethodRetryHandler retry handler} for this HTTP method
+ *
+ * @param handler the methodRetryHandler to use when this method executed
+ *
+ * @deprecated use {@link HttpMethodParams}
+ */
+ public void setMethodRetryHandler(MethodRetryHandler handler) {
+ methodRetryHandler = handler;
+ }
+
+ /**
+ * This method is a dirty hack intended to work around
+ * current (2.0) design flaw that prevents the user from
+ * obtaining correct status code, headers and response body from the
+ * preceding HTTP CONNECT method.
+ *
+ * TODO: Remove this crap as soon as possible
+ */
+ void fakeResponse(
+ StatusLine statusline,
+ HeaderGroup responseheaders,
+ InputStream responseStream
+ ) {
+ // set used so that the response can be read
+ this.used = true;
+ this.statusLine = statusline;
+ this.responseHeaders = responseheaders;
+ this.responseBody = null;
+ this.responseStream = responseStream;
+ }
+
+ /**
+ * Returns the target host {@link AuthState authentication state}
+ *
+ * @return host authentication state
+ *
+ * @since 3.0
+ */
+ public AuthState getHostAuthState() {
+ return this.hostAuthState;
+ }
+
+ /**
+ * Returns the proxy {@link AuthState authentication state}
+ *
+ * @return host authentication state
+ *
+ * @since 3.0
+ */
+ public AuthState getProxyAuthState() {
+ return this.proxyAuthState;
+ }
+
+ /**
+ * Tests whether the execution of this method has been aborted
+ *
+ * @return true if the execution of this method has been aborted,
+ * false otherwise
+ *
+ * @since 3.0
+ */
+ public boolean isAborted() {
+ return this.aborted;
+ }
+
+ /**
+ * Returns true if the HTTP has been transmitted to the target
+ * server in its entirety, false otherwise. This flag can be useful
+ * for recovery logic. If the request has not been transmitted in its entirety,
+ * it is safe to retry the failed method.
+ *
+ * @return true if the request has been sent, false otherwise
+ */
+ public boolean isRequestSent() {
+ return this.requestSent;
+ }
+
+}
diff --git a/commons/src/main/java/org/apache/commons/httpclient/HttpParser.java b/commons/src/main/java/org/apache/commons/httpclient/HttpParser.java
new file mode 100644
index 00000000..7ae07c97
--- /dev/null
+++ b/commons/src/main/java/org/apache/commons/httpclient/HttpParser.java
@@ -0,0 +1,236 @@
+/*
+ * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/HttpParser.java,v 1.13 2005/01/11 13:57:06 oglueck Exp $
+ * $Revision$
+ * $Date$
+ *
+ * ====================================================================
+ *
+ * Copyright 1999-2004 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.commons.httpclient;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+
+import org.apache.commons.httpclient.util.EncodingUtil;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * A utility class for parsing http header values according to
+ * RFC-2616 Section 4 and 19.3.
+ *
+ * @author Michael Becke
+ * @author Oleg Kalnichevski
+ *
+ * @since 2.0beta1
+ */
+@SuppressWarnings("unchecked") // <- IA/HERITRIX CHANGE
+public class HttpParser {
+
+ /** Log object for this class. */
+ private static final Log LOG = LogFactory.getLog(HttpParser.class);
+
+ /**
+ * Constructor for HttpParser.
+ */
+ private HttpParser() { }
+
+ /**
+ * Return byte array from an (unchunked) input stream.
+ * Stop reading when "\n" terminator encountered
+ * If the stream ends before the line terminator is found,
+ * the last part of the string will still be returned.
+ * If no input data available, null is returned.
+ *
+ * @param inputStream the stream to read from
+ *
+ * @throws IOException if an I/O problem occurs
+ * @return a byte array from the stream
+ */
+ public static byte[] readRawLine(InputStream inputStream) throws IOException {
+ LOG.trace("enter HttpParser.readRawLine()");
+
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ int ch;
+ while ((ch = inputStream.read()) >= 0) {
+ buf.write(ch);
+ if (ch == '\n') { // be tolerant (RFC-2616 Section 19.3)
+ break;
+ }
+ }
+ if (buf.size() == 0) {
+ return null;
+ }
+ return buf.toByteArray();
+ }
+
+ /**
+ * Read up to "\n" from an (unchunked) input stream.
+ * If the stream ends before the line terminator is found,
+ * the last part of the string will still be returned.
+ * If no input data available, null is returned.
+ *
+ * @param inputStream the stream to read from
+ * @param charset charset of HTTP protocol elements
+ *
+ * @throws IOException if an I/O problem occurs
+ * @return a line from the stream
+ *
+ * @since 3.0
+ */
+ public static String readLine(InputStream inputStream, String charset) throws IOException {
+ LOG.trace("enter HttpParser.readLine(InputStream, String)");
+ byte[] rawdata = readRawLine(inputStream);
+ if (rawdata == null) {
+ return null;
+ }
+ // strip CR and LF from the end
+ int len = rawdata.length;
+ int offset = 0;
+ if (len > 0) {
+ if (rawdata[len - 1] == '\n') {
+ offset++;
+ if (len > 1) {
+ if (rawdata[len - 2] == '\r') {
+ offset++;
+ }
+ }
+ }
+ }
+ return EncodingUtil.getString(rawdata, 0, len - offset, charset);
+ }
+
+ /**
+ * Read up to "\n" from an (unchunked) input stream.
+ * If the stream ends before the line terminator is found,
+ * the last part of the string will still be returned.
+ * If no input data available, null is returned
+ *
+ * @param inputStream the stream to read from
+ *
+ * @throws IOException if an I/O problem occurs
+ * @return a line from the stream
+ *
+ * @deprecated use #readLine(InputStream, String)
+ */
+
+ public static String readLine(InputStream inputStream) throws IOException {
+ LOG.trace("enter HttpParser.readLine(InputStream)");
+ return readLine(inputStream, "US-ASCII");
+ }
+
+ /**
+ * Parses headers from the given stream. Headers with the same name are not
+ * combined.
+ *
+ * @param is the stream to read headers from
+ * @param charset the charset to use for reading the data
+ *
+ * @return an array of headers in the order in which they were parsed
+ *
+ * @throws IOException if an IO error occurs while reading from the stream
+ * @throws HttpException if there is an error parsing a header value
+ *
+ * @since 3.0
+ */
+ public static Header[] parseHeaders(InputStream is, String charset) throws IOException, HttpException {
+ LOG.trace("enter HeaderParser.parseHeaders(InputStream, String)");
+
+ ArrayList headers = new ArrayList();
+ String name = null;
+ StringBuffer value = null;
+ for (; ;) {
+ String line = HttpParser.readLine(is, charset);
+ if ((line == null) || (line.trim().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
+ if ((line.charAt(0) == ' ') || (line.charAt(0) == '\t')) {
+ // we have continuation folded header
+ // so append value
+ if (value != null) {
+ value.append(' ');
+ value.append(line.trim());
+ }
+ } else {
+ // make sure we save the previous name,value pair if present
+ if (name != null) {
+ headers.add(new Header(name, value.toString()));
+ }
+
+ // Otherwise we should have normal HTTP header line
+ // Parse the header name and value
+ int colon = line.indexOf(":");
+
+ // START IA/HERITRIX change
+ // Don't throw an exception if can't parse. We want to keep
+ // going even though header is bad. Rather, create
+ // pseudo-header.
+ if (colon < 0) {
+ // throw new ProtocolException("Unable to parse header: " +
+ // line);
+ name = "HttpClient-Bad-Header-Line-Failed-Parse";
+ value = new StringBuffer(line);
+
+ } else {
+ name = line.substring(0, colon).trim();
+ value = new StringBuffer(line.substring(colon + 1).trim());
+ }
+ // END IA/HERITRIX change
+ }
+
+ }
+
+ // make sure we save the last name,value pair if present
+ if (name != null) {
+ headers.add(new Header(name, value.toString()));
+ }
+
+ return (Header[]) headers.toArray(new Header[headers.size()]);
+ }
+
+ /**
+ * Parses headers from the given stream. Headers with the same name are not
+ * combined.
+ *
+ * @param is the stream to read headers from
+ *
+ * @return an array of headers in the order in which they were parsed
+ *
+ * @throws IOException if an IO error occurs while reading from the stream
+ * @throws HttpException if there is an error parsing a header value
+ *
+ * @deprecated use #parseHeaders(InputStream, String)
+ */
+ public static Header[] parseHeaders(InputStream is) throws IOException, HttpException {
+ LOG.trace("enter HeaderParser.parseHeaders(InputStream, String)");
+ return parseHeaders(is, "US-ASCII");
+ }
+}
diff --git a/commons/src/main/java/org/apache/commons/httpclient/HttpState.java b/commons/src/main/java/org/apache/commons/httpclient/HttpState.java
new file mode 100644
index 00000000..a6aa715a
--- /dev/null
+++ b/commons/src/main/java/org/apache/commons/httpclient/HttpState.java
@@ -0,0 +1,717 @@
+/*
+ * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/HttpState.java,v 1.38 2004/12/20 11:50:54 olegk Exp $
+ * $Revision$
+ * $Date$
+ *
+ * ====================================================================
+ *
+ * Copyright 1999-2004 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.commons.httpclient;
+
+import java.util.ArrayList;
+import java.util.Collection; // <- IA/HERITRIX CHANGE
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.List;
+import java.util.Iterator;
+import java.util.SortedMap; // <- IA/HERITRIX CHANGE
+import java.util.TreeMap; // <- IA/HERITRIX CHANGE
+
+import org.apache.commons.httpclient.cookie.CookieSpec;
+import org.apache.commons.httpclient.cookie.CookiePolicy;
+import org.apache.commons.httpclient.auth.AuthScope;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.sleepycat.collections.StoredIterator; // <- IA/HERITRIX CHANGE
+
+/**
+ *
+ * A container for HTTP attributes that may persist from request
+ * to request, such as {@link Cookie cookies} and authentication
+ * {@link Credentials credentials}.
+ *
+ *
+ * @author Remy Maucherat
+ * @author Rodney Waldhoff
+ * @author Jeff Dever
+ * @author Sean C. Sullivan
+ * @author Michael Becke
+ * @author Oleg Kalnichevski
+ * @author Mike Bowler
+ * @author Adrian Sutton
+ *
+ * @version $Revision$ $Date$
+ *
+ */
+@SuppressWarnings({"unchecked","unused"}) // <- IA/HERITRIX CHANGE
+public class HttpState {
+
+ // ----------------------------------------------------- Instance Variables
+
+ /**
+ * Map of {@link Credentials credentials} by realm that this
+ * HTTP state contains.
+ */
+ private HashMap credMap = new HashMap();
+
+ /**
+ * Map of {@link Credentials proxy credentials} by realm that this
+ * HTTP state contains
+ */
+ private HashMap proxyCred = new HashMap();
+
+// BEGIN IA/HERITRIX CHANGES
+// /**
+// * Array of {@link Cookie cookies} that this HTTP state contains.
+// */
+// private ArrayList cookiesArrayList = new ArrayList();
+ /**
+ * SortedMap of {@link Cookie cookies} that this HTTP state contains.
+ */
+ private SortedMap cookiesMap = new TreeMap();
+// END IA/HERITRIX CHANGES
+
+ private boolean preemptive = false;
+
+ private int cookiePolicy = -1;
+ // -------------------------------------------------------- Class Variables
+
+ /**
+ * The boolean system property name to turn on preemptive authentication.
+ * @deprecated This field and feature will be removed following HttpClient 3.0.
+ */
+ public static final String PREEMPTIVE_PROPERTY = "httpclient.authentication.preemptive";
+
+ /**
+ * The default value for {@link #PREEMPTIVE_PROPERTY}.
+ * @deprecated This field and feature will be removed following HttpClient 3.0.
+ */
+ public static final String PREEMPTIVE_DEFAULT = "false";
+
+ /** Log object for this class. */
+ private static final Log LOG = LogFactory.getLog(HttpState.class);
+
+ /**
+ * Default constructor.
+ */
+ public HttpState() {
+ super();
+ }
+
+ // ------------------------------------------------------------- Properties
+
+ /**
+ * Adds an {@link Cookie HTTP cookie}, replacing any existing equivalent cookies.
+ * If the given cookie has already expired it will not be added, but existing
+ * values will still be removed.
+ *
+ * @param cookie the {@link Cookie cookie} to be added
+ *
+ * @see #addCookies(Cookie[])
+ *
+ */
+ public synchronized void addCookie(Cookie cookie) {
+ LOG.trace("enter HttpState.addCookie(Cookie)");
+
+// BEGIN IA/HERITRIX CHANGES
+// PRIOR IMPL & COMPARISON HARNESS LEFT COMMENTED OUT FOR TEMPORARY REFERENCE
+// Cookie removed1 = null;
+// Cookie removed2 = null;
+ if (cookie != null) {
+ // first remove any old cookie that is equivalent
+// for (Iterator it = cookiesArrayList.iterator(); it.hasNext();) {
+// Cookie tmp = (Cookie) it.next();
+// if (cookie.equals(tmp)) {
+// it.remove();
+// removed1 = tmp;
+// break;
+// }
+// }
+ if (!cookie.isExpired()) {
+// cookiesArrayList.add(cookie);
+ cookiesMap.put(cookie.getSortKey(),cookie);
+ } else {
+ cookiesMap.remove(cookie.getSortKey());
+ }
+ }
+// if(removed1!=null && !removed1.equals(removed2)) {
+// System.out.println("addCookie discrepancy");
+// }
+// END IA/HERITRIX CHANGES
+ }
+
+ /**
+ * Adds an array of {@link Cookie HTTP cookies}. Cookies are added individually and
+ * in the given array order. If any of the given cookies has already expired it will
+ * not be added, but existing values will still be removed.
+ *
+ * @param cookies the {@link Cookie cookies} to be added
+ *
+ * @see #addCookie(Cookie)
+ *
+ *
+ */
+ public synchronized void addCookies(Cookie[] cookies) {
+ LOG.trace("enter HttpState.addCookies(Cookie[])");
+
+ if (cookies != null) {
+ for (int i = 0; i < cookies.length; i++) {
+ this.addCookie(cookies[i]);
+ }
+ }
+ }
+
+ /**
+ * Returns an array of {@link Cookie cookies} that this HTTP
+ * state currently contains.
+ *
+ * @return an array of {@link Cookie cookies}.
+ *
+ * @see #getCookies(String, int, String, boolean)
+ *
+ * @deprecated use getCookiesMap() // <- IA/HERITRIX CHANGE
+ */
+ public synchronized Cookie[] getCookies() {
+ LOG.trace("enter HttpState.getCookies()");
+// BEGIN IA/HERITRIX CHANGES
+// PRIOR IMPL & COMPARISON HARNESS LEFT COMMENTED OUT FOR TEMPORARY REFERENCE
+// Cookie[] arrayListAnswer = (Cookie[]) (cookiesArrayList.toArray(new Cookie[cookiesArrayList.size()]));
+ ArrayList arrayableCookies = new ArrayList();
+ Iterator iter = cookiesMap.values().iterator();
+ while(iter.hasNext()) {
+ arrayableCookies.add(iter.next());
+ }
+ StoredIterator.close(iter);
+ Cookie[] mapAnswer =
+ (Cookie[]) arrayableCookies.toArray(new Cookie[arrayableCookies.size()]);
+
+// if(cookiesArrayList.size()!=arrayableCookies.size()) {
+// System.out.println("discrepancy");
+// }
+ return mapAnswer;
+// END IA/HERITRIX CHANGES
+ }
+
+// START IA/HERITRIX ADDITIONS
+ /**
+ * Returns a sorted map of {@link Cookie cookies} that this HTTP
+ * state currently contains.
+ *
+ * Any operations on this map should be synchronized with respect
+ * to this HttpState instance.
+ *
+ * @return sorter map of {@link Cookie cookies}
+ */
+ public synchronized SortedMap getCookiesMap() {
+ return cookiesMap;
+ }
+
+ /**
+ * Replace the standard sorted map with an external implemenations
+ * (such as one backed by persistent store, like BDB's StoredSortedMap.)
+ *
+ * @param map alternate sorted map to use to store cookies
+ */
+ public synchronized void setCookiesMap(SortedMap map) {
+ this.cookiesMap = map;
+ }
+// END IA/HERITRIX ADDITIONS
+
+ /**
+ * Returns an array of {@link Cookie cookies} in this HTTP
+ * state that match the given request parameters.
+ *
+ * @param domain the request domain
+ * @param port the request port
+ * @param path the request path
+ * @param secure true when using HTTPS
+ *
+ * @return an array of {@link Cookie cookies}.
+ *
+ * @see #getCookies()
+ *
+ * @deprecated use CookieSpec#match(String, int, String, boolean, Cookie)
+ */
+ public synchronized Cookie[] getCookies(
+ String domain,
+ int port,
+ String path,
+ boolean secure
+ ) {
+ LOG.trace("enter HttpState.getCookies(String, int, String, boolean)");
+
+ CookieSpec matcher = CookiePolicy.getDefaultSpec();
+// BEGIN IA/HERITRIX CHANGES
+// PRIOR IMPL & COMPARISON HARNESS LEFT COMMENTED OUT FOR TEMPORARY REFERENCE
+// ArrayList list = new ArrayList(cookiesArrayList.size());
+// for (int i = 0, m = cookiesArrayList.size(); i < m; i++) {
+// Cookie cookie = (Cookie) (cookiesArrayList.get(i));
+// if (matcher.match(domain, port, path, secure, cookie)) {
+// list.add(cookie);
+// }
+// }
+// Cookie[] arrayListAnswer = (Cookie[]) (list.toArray(new Cookie[list.size()]));
+ Cookie[] mapAnswer = matcher.match(domain,port,path,secure,cookiesMap);
+
+// if(! (new HashSet(list).equals(new HashSet(Arrays.asList(mapAnswer))))) {
+// System.out.println("discrepancy");
+// }
+ return mapAnswer;
+// END IA/HERITRIX CHANGES
+ }
+
+ /**
+ * Removes all of {@link Cookie cookies} in this HTTP state
+ * that have expired according to the current system time.
+ *
+ * @see #purgeExpiredCookies(java.util.Date)
+ *
+ */
+ public synchronized boolean purgeExpiredCookies() {
+ LOG.trace("enter HttpState.purgeExpiredCookies()");
+ return purgeExpiredCookies(new Date());
+ }
+
+ /**
+ * Removes all of {@link Cookie cookies} in this HTTP state
+ * that have expired by the specified {@link java.util.Date date}.
+ *
+ * @param date The {@link java.util.Date date} to compare against.
+ *
+ * @return true if any cookies were purged.
+ *
+ * @see Cookie#isExpired(java.util.Date)
+ *
+ * @see #purgeExpiredCookies()
+ */
+ public synchronized boolean purgeExpiredCookies(Date date) {
+ LOG.trace("enter HttpState.purgeExpiredCookies(Date)");
+// BEGIN IA/HERITRIX CHANGES
+// PRIOR IMPL & COMPARISON HARNESS LEFT COMMENTED OUT FOR TEMPORARY REFERENCE
+// boolean arrayRemoved = false;
+// Iterator ita = cookiesArrayList.iterator();
+// while (ita.hasNext()) {
+// if (((Cookie) (ita.next())).isExpired(date)) {
+// ita.remove();
+// arrayRemoved = true;
+// }
+// }
+ boolean removed = false;
+ Iterator it = cookiesMap.values().iterator();
+ while (it.hasNext()) {
+ if (((Cookie) (it.next())).isExpired(date)) {
+ it.remove();
+ removed = true;
+ }
+ }
+ StoredIterator.close(it);
+// assert removed == arrayRemoved : "discrepancy"
+// END IA/HERITRIX CHANGES
+ return removed;
+ }
+
+
+ /**
+ * Returns the current {@link CookiePolicy cookie policy} for this
+ * HTTP state.
+ *
+ * @return The {@link CookiePolicy cookie policy}.
+ *
+ * @deprecated Use
+ * {@link org.apache.commons.httpclient.params.HttpMethodParams#getCookiePolicy()},
+ * {@link HttpMethod#getParams()}.
+ */
+
+ public int getCookiePolicy() {
+ return this.cookiePolicy;
+ }
+
+
+ /**
+ * Defines whether preemptive authentication should be
+ * attempted.
+ *
+ * @param value true if preemptive authentication should be
+ * attempted, false otherwise.
+ *
+ * @deprecated Use
+ * {@link org.apache.commons.httpclient.params.HttpClientParams#setAuthenticationPreemptive(boolean)},
+ * {@link HttpClient#getParams()}.
+ */
+
+ public void setAuthenticationPreemptive(boolean value) {
+ this.preemptive = value;
+ }
+
+
+ /**
+ * Returns true if preemptive authentication should be
+ * attempted, false otherwise.
+ *
+ * @return boolean flag.
+ *
+ * @deprecated Use
+ * {@link org.apache.commons.httpclient.params.HttpClientParams#isAuthenticationPreemptive()},
+ * {@link HttpClient#getParams()}.
+ */
+
+ public boolean isAuthenticationPreemptive() {
+ return this.preemptive;
+ }
+
+
+ /**
+ * Sets the current {@link CookiePolicy cookie policy} for this HTTP
+ * state to one of the following supported policies:
+ * {@link CookiePolicy#COMPATIBILITY},
+ * {@link CookiePolicy#NETSCAPE_DRAFT} or
+ * {@link CookiePolicy#RFC2109}.
+ *
+ * @param policy new {@link CookiePolicy cookie policy}
+ *
+ * @deprecated
+ * Use {@link org.apache.commons.httpclient.params.HttpMethodParams#setCookiePolicy(String)},
+ * {@link HttpMethod#getParams()}.
+ */
+
+ public void setCookiePolicy(int policy) {
+ this.cookiePolicy = policy;
+ }
+
+ /**
+ * Sets the {@link Credentials credentials} for the given authentication
+ * realm on the given host. The null realm signifies default
+ * credentials for the given host, which should be used when no
+ * {@link Credentials credentials} have been explictly supplied for the
+ * challenging realm. The null host signifies default
+ * credentials, which should be used when no {@link Credentials credentials}
+ * have been explictly supplied for the challenging host. Any previous
+ * credentials for the given realm on the given host will be overwritten.
+ *
+ * @param realm the authentication realm
+ * @param host the host the realm belongs to
+ * @param credentials the authentication {@link Credentials credentials}
+ * for the given realm.
+ *
+ * @see #getCredentials(String, String)
+ * @see #setProxyCredentials(String, String, Credentials)
+ *
+ * @deprecated use #setCredentials(AuthScope, Credentials)
+ */
+
+ public synchronized void setCredentials(String realm, String host, Credentials credentials) {
+ LOG.trace("enter HttpState.setCredentials(String, String, Credentials)");
+ credMap.put(new AuthScope(host, AuthScope.ANY_PORT, realm, AuthScope.ANY_SCHEME), credentials);
+ }
+
+ /**
+ * Sets the {@link Credentials credentials} for the given authentication
+ * scope. Any previous credentials for the given scope will be overwritten.
+ *
+ * @param authscope the {@link AuthScope authentication scope}
+ * @param credentials the authentication {@link Credentials credentials}
+ * for the given scope.
+ *
+ * @see #getCredentials(AuthScope)
+ * @see #setProxyCredentials(AuthScope, Credentials)
+ *
+ * @since 3.0
+ */
+ public synchronized void setCredentials(final AuthScope authscope, final Credentials credentials) {
+ if (authscope == null) {
+ throw new IllegalArgumentException("Authentication scope may not be null");
+ }
+ LOG.trace("enter HttpState.setCredentials(AuthScope, Credentials)");
+ credMap.put(authscope, credentials);
+ }
+
+ /**
+ * Find matching {@link Credentials credentials} for the given authentication scope.
+ *
+ * @param map the credentials hash map
+ * @param token the {@link AuthScope authentication scope}
+ * @return the credentials
+ *
+ */
+ private static Credentials matchCredentials(final HashMap map, final AuthScope authscope) {
+ // see if we get a direct hit
+ Credentials creds = (Credentials)map.get(authscope);
+ if (creds == null) {
+ // Nope.
+ // Do a full scan
+ int bestMatchFactor = -1;
+ AuthScope bestMatch = null;
+ Iterator items = map.keySet().iterator();
+ while (items.hasNext()) {
+ AuthScope current = (AuthScope)items.next();
+ int factor = authscope.match(current);
+ if (factor > bestMatchFactor) {
+ bestMatchFactor = factor;
+ bestMatch = current;
+ }
+ }
+ if (bestMatch != null) {
+ creds = (Credentials)map.get(bestMatch);
+ }
+ }
+ return creds;
+ }
+
+ /**
+ * Get the {@link Credentials credentials} for the given authentication scope on the
+ * given host.
+ *
+ * If the realm exists on host, return the coresponding credentials.
+ * If the host exists with a nullrealm, return the corresponding
+ * credentials.
+ * If the realm exists with a nullhost, return the
+ * corresponding credentials. If the realm does not exist, return
+ * the default Credentials. If there are no default credentials, return
+ * null.
+ *
+ * @param realm the authentication realm
+ * @param host the host the realm is on
+ * @return the credentials
+ *
+ * @see #setCredentials(String, String, Credentials)
+ *
+ * @deprecated use #getCredentials(AuthScope)
+ */
+
+ public synchronized Credentials getCredentials(String realm, String host) {
+ LOG.trace("enter HttpState.getCredentials(String, String");
+ return matchCredentials(this.credMap,
+ new AuthScope(host, AuthScope.ANY_PORT, realm, AuthScope.ANY_SCHEME));
+ }
+
+ /**
+ * Get the {@link Credentials credentials} for the given authentication scope.
+ *
+ * @param authscope the {@link AuthScope authentication scope}
+ * @return the credentials
+ *
+ * @see #setCredentials(AuthScope, Credentials)
+ *
+ * @since 3.0
+ */
+ public synchronized Credentials getCredentials(final AuthScope authscope) {
+ if (authscope == null) {
+ throw new IllegalArgumentException("Authentication scope may not be null");
+ }
+ LOG.trace("enter HttpState.getCredentials(AuthScope)");
+ return matchCredentials(this.credMap, authscope);
+ }
+
+ /**
+ * Sets the {@link Credentials credentials} for the given proxy authentication
+ * realm on the given proxy host. The null proxy realm signifies
+ * default credentials for the given proxy host, which should be used when no
+ * {@link Credentials credentials} have been explictly supplied for the
+ * challenging proxy realm. The null proxy host signifies default
+ * credentials, which should be used when no {@link Credentials credentials}
+ * have been explictly supplied for the challenging proxy host. Any previous
+ * credentials for the given proxy realm on the given proxy host will be
+ * overwritten.
+ *
+ * @param realm the authentication realm
+ * @param proxyHost the proxy host
+ * @param credentials the authentication credentials for the given realm
+ *
+ * @see #getProxyCredentials(AuthScope)
+ * @see #setCredentials(AuthScope, Credentials)
+ *
+ * @deprecated use #setProxyCredentials(AuthScope, Credentials)
+ */
+ public synchronized void setProxyCredentials(
+ String realm,
+ String proxyHost,
+ Credentials credentials
+ ) {
+ LOG.trace("enter HttpState.setProxyCredentials(String, String, Credentials");
+ proxyCred.put(new AuthScope(proxyHost, AuthScope.ANY_PORT, realm, AuthScope.ANY_SCHEME), credentials);
+ }
+
+ /**
+ * Sets the {@link Credentials proxy credentials} for the given authentication
+ * realm. Any previous credentials for the given realm will be overwritten.
+ *
+ * @param authscope the {@link AuthScope authentication scope}
+ * @param credentials the authentication {@link Credentials credentials}
+ * for the given realm.
+ *
+ * @see #getProxyCredentials(AuthScope)
+ * @see #setCredentials(AuthScope, Credentials)
+ *
+ * @since 3.0
+ */
+ public synchronized void setProxyCredentials(final AuthScope authscope,
+ final Credentials credentials)
+ {
+ if (authscope == null) {
+ throw new IllegalArgumentException("Authentication scope may not be null");
+ }
+ LOG.trace("enter HttpState.setProxyCredentials(AuthScope, Credentials)");
+ proxyCred.put(authscope, credentials);
+ }
+
+ /**
+ * Get the {@link Credentials credentials} for the proxy host with the given
+ * authentication scope.
+ *
+ * If the realm exists on host, return the coresponding credentials.
+ * If the host exists with a nullrealm, return the corresponding
+ * credentials.
+ * If the realm exists with a nullhost, return the
+ * corresponding credentials. If the realm does not exist, return
+ * the default Credentials. If there are no default credentials, return
+ * null.
+ *
+ * @param realm the authentication realm
+ * @param proxyHost the proxy host the realm is on
+ * @return the credentials
+ * @see #setProxyCredentials(String, String, Credentials)
+ *
+ * @deprecated use #getProxyCredentials(AuthScope)
+ */
+ public synchronized Credentials getProxyCredentials(String realm, String proxyHost) {
+ LOG.trace("enter HttpState.getCredentials(String, String");
+ return matchCredentials(this.proxyCred,
+ new AuthScope(proxyHost, AuthScope.ANY_PORT, realm, AuthScope.ANY_SCHEME));
+ }
+
+ /**
+ * Get the {@link Credentials proxy credentials} for the given authentication scope.
+ *
+ * @param authscope the {@link AuthScope authentication scope}
+ * @return the credentials
+ *
+ * @see #setProxyCredentials(AuthScope, Credentials)
+ *
+ * @since 3.0
+ */
+ public synchronized Credentials getProxyCredentials(final AuthScope authscope) {
+ if (authscope == null) {
+ throw new IllegalArgumentException("Authentication scope may not be null");
+ }
+ LOG.trace("enter HttpState.getProxyCredentials(AuthScope)");
+ return matchCredentials(this.proxyCred, authscope);
+ }
+
+ /**
+ * Returns a string representation of this HTTP state.
+ *
+ * @return The string representation of the HTTP state.
+ *
+ * @see java.lang.Object#toString()
+ */
+ public synchronized String toString() {
+ StringBuffer sbResult = new StringBuffer();
+
+ sbResult.append("[");
+ sbResult.append(getCredentialsStringRepresentation(proxyCred));
+ sbResult.append(" | ");
+ sbResult.append(getCredentialsStringRepresentation(credMap));
+ sbResult.append(" | ");
+ sbResult.append(getCookiesStringRepresentation(cookiesMap.values())); // <- IA/HERITRIX CHANGE
+ sbResult.append("]");
+
+ String strResult = sbResult.toString();
+
+ return strResult;
+ }
+
+ /**
+ * Returns a string representation of the credentials.
+ * @param credMap The credentials.
+ * @return The string representation.
+ */
+ private static String getCredentialsStringRepresentation(final Map credMap) {
+ StringBuffer sbResult = new StringBuffer();
+ Iterator iter = credMap.keySet().iterator();
+ while (iter.hasNext()) {
+ Object key = iter.next();
+ Credentials cred = (Credentials) credMap.get(key);
+ if (sbResult.length() > 0) {
+ sbResult.append(", ");
+ }
+ sbResult.append(key);
+ sbResult.append("#");
+ sbResult.append(cred.toString());
+ }
+ return sbResult.toString();
+ }
+
+ /**
+ * Returns a string representation of the cookies.
+ * @param cookies The cookies
+ * @return The string representation.
+ */
+ private static String getCookiesStringRepresentation(final Collection cookies) { // <- IA/HERITRIX CHANGE
+ StringBuffer sbResult = new StringBuffer();
+ Iterator iter = cookies.iterator();
+ while (iter.hasNext()) {
+ Cookie ck = (Cookie) iter.next();
+ if (sbResult.length() > 0) {
+ sbResult.append("#");
+ }
+ sbResult.append(ck.toExternalForm());
+ }
+ return sbResult.toString();
+ }
+
+ /**
+ * Clears all credentials.
+ */
+ public void clearCredentials() {
+ this.credMap.clear();
+ }
+
+ /**
+ * Clears all proxy credentials.
+ */
+ public void clearProxyCredentials() {
+ this.proxyCred.clear();
+ }
+
+ /**
+ * Clears all cookies.
+ */
+ public void clearCookies() {
+// BEGIN IA/HERITRIX CHANGES
+// this.cookiesArrayList.clear();
+ this.cookiesMap.clear();
+// END IA/HERITRIX CHANGES
+ }
+
+ /**
+ * Clears the state information (all cookies, credentials and proxy credentials).
+ */
+ public void clear() {
+ clearCookies();
+ clearCredentials();
+ clearProxyCredentials();
+ }
+}
diff --git a/commons/src/main/java/org/apache/commons/httpclient/cookie/CookieSpec.java b/commons/src/main/java/org/apache/commons/httpclient/cookie/CookieSpec.java
new file mode 100644
index 00000000..5b14bff4
--- /dev/null
+++ b/commons/src/main/java/org/apache/commons/httpclient/cookie/CookieSpec.java
@@ -0,0 +1,264 @@
+/*
+ * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/cookie/CookieSpec.java,v 1.11 2004/09/14 20:11:31 olegk Exp $
+ * $Revision$
+ * $Date$
+ *
+ * ====================================================================
+ *
+ * Copyright 2002-2004 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.commons.httpclient.cookie;
+
+import java.util.Collection;
+import java.util.SortedMap; // <- IA/HERITRIX CHANGE
+
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.NameValuePair;
+import org.apache.commons.httpclient.Cookie;
+
+/**
+ * Defines the cookie management specification.
+ *
Cookie management specification must define
+ *
+ *
rules of parsing "Set-Cookie" header
+ *
rules of validation of parsed cookies
+ *
formatting of "Cookie" header
+ *
+ * for a given host, port and path of origin
+ *
+ * @author Oleg Kalnichevski
+ * @author Jeff Dever
+ *
+ * @since 2.0
+ */
+@SuppressWarnings("unchecked") // <- IA/HERITRIX CHANGE
+public interface CookieSpec {
+
+ /** Path delimiter */
+ static final String PATH_DELIM = "/";
+
+ /** Path delimiting charachter */
+ static final char PATH_DELIM_CHAR = PATH_DELIM.charAt(0);
+
+ /**
+ * Parse the "Set-Cookie" header value into Cookie array.
+ *
+ *
This method will not perform the validation of the resultant
+ * {@link Cookie}s
+ *
+ * @see #validate(String, int, String, boolean, Cookie)
+ *
+ * @param host the host which sent the Set-Cookie header
+ * @param port the port which sent the Set-Cookie header
+ * @param path the path which sent the Set-Cookie header
+ * @param secure true when the Set-Cookie header
+ * was received over secure conection
+ * @param header the Set-Cookie received from the server
+ * @return an array of Cookies parsed from the Set-Cookie value
+ * @throws MalformedCookieException if an exception occurs during parsing
+ * @throws IllegalArgumentException if an input parameter is illegal
+ */
+ Cookie[] parse(String host, int port, String path, boolean secure,
+ final String header)
+ throws MalformedCookieException, IllegalArgumentException;
+
+ /**
+ * Parse the "Set-Cookie" Header into an array of Cookies.
+ *
+ *
This method will not perform the validation of the resultant
+ * {@link Cookie}s
+ *
+ * @see #validate(String, int, String, boolean, Cookie)
+ *
+ * @param host the host which sent the Set-Cookie header
+ * @param port the port which sent the Set-Cookie header
+ * @param path the path which sent the Set-Cookie header
+ * @param secure true when the Set-Cookie header
+ * was received over secure conection
+ * @param header the Set-Cookie received from the server
+ * @return an array of Cookies parsed from the header
+ * @throws MalformedCookieException if an exception occurs during parsing
+ * @throws IllegalArgumentException if an input parameter is illegal
+ */
+ Cookie[] parse(String host, int port, String path, boolean secure,
+ final Header header)
+ throws MalformedCookieException, IllegalArgumentException;
+
+ /**
+ * Parse the cookie attribute and update the corresponsing Cookie
+ * properties.
+ *
+ * @param attribute cookie attribute from the Set-Cookie
+ * @param cookie the to be updated
+ * @throws MalformedCookieException if an exception occurs during parsing
+ * @throws IllegalArgumentException if an input parameter is illegal
+ */
+ void parseAttribute(NameValuePair attribute, Cookie cookie)
+ throws MalformedCookieException, IllegalArgumentException;
+
+ /**
+ * Validate the cookie according to validation rules defined by the
+ * cookie specification.
+ *
+ * @param host the host from which the {@link Cookie} was received
+ * @param port the port from which the {@link Cookie} was received
+ * @param path the path from which the {@link Cookie} was received
+ * @param secure true when the {@link Cookie} was received
+ * using a secure connection
+ * @param cookie the Cookie to validate
+ * @throws MalformedCookieException if the cookie is invalid
+ * @throws IllegalArgumentException if an input parameter is illegal
+ */
+ void validate(String host, int port, String path, boolean secure,
+ final Cookie cookie)
+ throws MalformedCookieException, IllegalArgumentException;
+
+
+ /**
+ * Sets the {@link Collection} of date patterns used for parsing. The String patterns must be
+ * compatible with {@link java.text.SimpleDateFormat}.
+ *
+ * @param datepatterns collection of date patterns
+ */
+ void setValidDateFormats(Collection datepatterns);
+
+ /**
+ * Returns the {@link Collection} of date patterns used for parsing. The String patterns are compatible
+ * with the {@link java.text.SimpleDateFormat}.
+ *
+ * @return collection of date patterns
+ */
+ Collection getValidDateFormats();
+
+ /**
+ * Determines if a Cookie matches a location.
+ *
+ * @param host the host to which the request is being submitted
+ * @param port the port to which the request is being submitted
+ * @param path the path to which the request is being submitted
+ * @param secure true if the request is using a secure connection
+ * @param cookie the Cookie to be matched
+ *
+ * @return true if the cookie should be submitted with a request
+ * with given attributes, false otherwise.
+ */
+ boolean match(String host, int port, String path, boolean secure,
+ final Cookie cookie);
+
+ /**
+ * Determines which of an array of Cookies matches a location.
+ *
+ * @param host the host to which the request is being submitted
+ * @param port the port to which the request is being submitted
+ * (currenlty ignored)
+ * @param path the path to which the request is being submitted
+ * @param secure true if the request is using a secure protocol
+ * @param cookies an array of Cookies to be matched
+ *
+ * @return true if the cookie should be submitted with a request
+ * with given attributes, false otherwise.
+ *
+// BEGIN IA/HERITRIX CHANGES
+ * @deprecated use match(String, int, String, boolean, SortedMap)
+// END IA/HERITRIX CHANGES
+ */
+ Cookie[] match(String host, int port, String path, boolean secure,
+ final Cookie cookies[]);
+
+// BEGIN IA/HERITRIX CHANGES
+ /**
+ * Determines which of an array of Cookies matches a location.
+ *
+ * If the SortedMap comes from an HttpState and is not itself
+ * thread-safe, it may be necessary to synchronize on the HttpState
+ * instance to protect against concurrent modification.
+ *
+ * @param host the host to which the request is being submitted
+ * @param port the port to which the request is being submitted
+ * (currenlty ignored)
+ * @param path the path to which the request is being submitted
+ * @param secure true if the request is using a secure protocol
+ * @param cookies SortedMap of Cookies to be matched
+ *
+ * @return true if the cookie should be submitted with a request
+ * with given attributes, false otherwise.
+ */
+ Cookie[] match(String domain, int port, String path, boolean secure, SortedMap cookiesMap);
+// END IA/HERITRIX CHANGES
+
+ /**
+ * Performs domain-match as defined by the cookie specification.
+ * @param host The target host.
+ * @param domain The cookie domain attribute.
+ * @return true if the specified host matches the given domain.
+ *
+ * @since 3.0
+ */
+ boolean domainMatch(String host, String domain);
+
+ /**
+ * Performs path-match as defined by the cookie specification.
+ * @param path The target path.
+ * @param topmostPath The cookie path attribute.
+ * @return true if the paths match
+ *
+ * @since 3.0
+ */
+ boolean pathMatch(String path, String topmostPath);
+
+ /**
+ * Create a "Cookie" header value for an array of cookies.
+ *
+ * @param cookie the cookie to be formatted as string
+ * @return a string suitable for sending in a "Cookie" header.
+ */
+ String formatCookie(Cookie cookie);
+
+ /**
+ * Create a "Cookie" header value for an array of cookies.
+ *
+ * @param cookies the Cookies to be formatted
+ * @return a string suitable for sending in a Cookie header.
+ * @throws IllegalArgumentException if an input parameter is illegal
+ */
+ String formatCookies(Cookie[] cookies) throws IllegalArgumentException;
+
+ /**
+ * Create a "Cookie" Header for an array of Cookies.
+ *
+ * @param cookies the Cookies format into a Cookie header
+ * @return a Header for the given Cookies.
+ * @throws IllegalArgumentException if an input parameter is illegal
+ */
+ Header formatCookieHeader(Cookie[] cookies) throws IllegalArgumentException;
+
+ /**
+ * Create a "Cookie" Header for single Cookie.
+ *
+ * @param cookie the Cookie format as a Cookie header
+ * @return a Cookie header.
+ * @throws IllegalArgumentException if an input parameter is illegal
+ */
+ Header formatCookieHeader(Cookie cookie) throws IllegalArgumentException;
+
+}
diff --git a/commons/src/main/java/org/apache/commons/httpclient/cookie/CookieSpecBase.java b/commons/src/main/java/org/apache/commons/httpclient/cookie/CookieSpecBase.java
new file mode 100644
index 00000000..d15121d6
--- /dev/null
+++ b/commons/src/main/java/org/apache/commons/httpclient/cookie/CookieSpecBase.java
@@ -0,0 +1,716 @@
+/*
+ * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/cookie/CookieSpecBase.java,v 1.28 2004/11/06 19:15:42 mbecke Exp $
+ * $Revision$
+ * $Date$
+ *
+ * ====================================================================
+ *
+ * Copyright 2002-2004 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.commons.httpclient.cookie;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator; // <- IA/HERITRIX CHANGE
+import java.util.LinkedList;
+import java.util.List;
+import java.util.SortedMap; // <- IA/HERITRIX CHANGE
+
+import org.apache.commons.httpclient.Cookie;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HeaderElement;
+import org.apache.commons.httpclient.NameValuePair;
+import org.apache.commons.httpclient.util.DateParseException;
+import org.apache.commons.httpclient.util.DateUtil;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.sleepycat.collections.StoredIterator; // <- IA/HERITRIX CHANGE
+
+/**
+ *
+ * Cookie management functions shared by all specification.
+ *
+ * @author B.C. Holmes
+ * @author Park, Sung-Gu
+ * @author Doug Sale
+ * @author Rod Waldhoff
+ * @author dIon Gillard
+ * @author Sean C. Sullivan
+ * @author John Evans
+ * @author Marc A. Saegesser
+ * @author Oleg Kalnichevski
+ * @author Mike Bowler
+ *
+ * @since 2.0
+ */
+@SuppressWarnings("unchecked") // <- IA/HERITRIX CHANGE
+public class CookieSpecBase implements CookieSpec {
+
+ /** Log object */
+ protected static final Log LOG = LogFactory.getLog(CookieSpec.class);
+
+ /** Valid date patterns */
+ private Collection datepatterns = null;
+
+ /** Default constructor */
+ public CookieSpecBase() {
+ super();
+ }
+
+
+ /**
+ * Parses the Set-Cookie value into an array of Cookies.
+ *
+ *
The syntax for the Set-Cookie response header is:
+ *
+ *
+ * set-cookie = "Set-Cookie:" cookies
+ * cookies = 1#cookie
+ * cookie = NAME "=" VALUE * (";" cookie-av)
+ * NAME = attr
+ * VALUE = value
+ * cookie-av = "Comment" "=" value
+ * | "Domain" "=" value
+ * | "Max-Age" "=" value
+ * | "Path" "=" value
+ * | "Secure"
+ * | "Version" "=" 1*DIGIT
+ *
+ *
+ * @param host the host from which the Set-Cookie value was
+ * received
+ * @param port the port from which the Set-Cookie value was
+ * received
+ * @param path the path from which the Set-Cookie value was
+ * received
+ * @param secure true when the Set-Cookie value was
+ * received over secure conection
+ * @param header the Set-Cookie received from the server
+ * @return an array of Cookies parsed from the Set-Cookie value
+ * @throws MalformedCookieException if an exception occurs during parsing
+ */
+ public Cookie[] parse(String host, int port, String path,
+ boolean secure, final String header)
+ throws MalformedCookieException {
+
+ LOG.trace("enter CookieSpecBase.parse("
+ + "String, port, path, boolean, Header)");
+
+ if (host == null) {
+ throw new IllegalArgumentException(
+ "Host of origin may not be null");
+ }
+ if (host.trim().equals("")) {
+ throw new IllegalArgumentException(
+ "Host of origin may not be blank");
+ }
+ if (port < 0) {
+ throw new IllegalArgumentException("Invalid port: " + port);
+ }
+ if (path == null) {
+ throw new IllegalArgumentException(
+ "Path of origin may not be null.");
+ }
+ if (header == null) {
+ throw new IllegalArgumentException("Header may not be null.");
+ }
+
+ if (path.trim().equals("")) {
+ path = PATH_DELIM;
+ }
+ host = host.toLowerCase();
+
+ String defaultPath = path;
+ int lastSlashIndex = defaultPath.lastIndexOf(PATH_DELIM);
+ if (lastSlashIndex >= 0) {
+ if (lastSlashIndex == 0) {
+ //Do not remove the very first slash
+ lastSlashIndex = 1;
+ }
+ defaultPath = defaultPath.substring(0, lastSlashIndex);
+ }
+
+ HeaderElement[] headerElements = null;
+
+ boolean isNetscapeCookie = false;
+ int i1 = header.toLowerCase().indexOf("expires=");
+ if (i1 != -1) {
+ i1 += "expires=".length();
+ int i2 = header.indexOf(";", i1);
+ if (i2 == -1) {
+ i2 = header.length();
+ }
+ try {
+ DateUtil.parseDate(header.substring(i1, i2), this.datepatterns);
+ isNetscapeCookie = true;
+ } catch (DateParseException e) {
+ // Does not look like a valid expiry date
+ }
+ }
+ if (isNetscapeCookie) {
+ headerElements = new HeaderElement[] {
+ new HeaderElement(header.toCharArray())
+ };
+ } else {
+ headerElements = HeaderElement.parseElements(header.toCharArray());
+ }
+
+ Cookie[] cookies = new Cookie[headerElements.length];
+
+ for (int i = 0; i < headerElements.length; i++) {
+
+ HeaderElement headerelement = headerElements[i];
+ Cookie cookie = null;
+ try {
+ cookie = new Cookie(host,
+ headerelement.getName(),
+ headerelement.getValue(),
+ defaultPath,
+ null,
+ false);
+ } catch (IllegalArgumentException e) {
+ throw new MalformedCookieException(e.getMessage());
+ }
+ // cycle through the parameters
+ NameValuePair[] parameters = headerelement.getParameters();
+ // could be null. In case only a header element and no parameters.
+ if (parameters != null) {
+
+ for (int j = 0; j < parameters.length; j++) {
+ parseAttribute(parameters[j], cookie);
+ }
+ }
+ cookies[i] = cookie;
+ }
+ return cookies;
+ }
+
+
+ /**
+ * Parse the "Set-Cookie" {@link Header} into an array of {@link
+ * Cookie}s.
+ *
+ *
The syntax for the Set-Cookie response header is:
+ *
+ *
+ * set-cookie = "Set-Cookie:" cookies
+ * cookies = 1#cookie
+ * cookie = NAME "=" VALUE * (";" cookie-av)
+ * NAME = attr
+ * VALUE = value
+ * cookie-av = "Comment" "=" value
+ * | "Domain" "=" value
+ * | "Max-Age" "=" value
+ * | "Path" "=" value
+ * | "Secure"
+ * | "Version" "=" 1*DIGIT
+ *
+ *
+ * @param host the host from which the Set-Cookie header was
+ * received
+ * @param port the port from which the Set-Cookie header was
+ * received
+ * @param path the path from which the Set-Cookie header was
+ * received
+ * @param secure true when the Set-Cookie header was
+ * received over secure conection
+ * @param header the Set-Cookie received from the server
+ * @return an array of Cookies parsed from the "Set-Cookie"
+ * header
+ * @throws MalformedCookieException if an exception occurs during parsing
+ */
+ public Cookie[] parse(
+ String host, int port, String path, boolean secure, final Header header)
+ throws MalformedCookieException {
+
+ LOG.trace("enter CookieSpecBase.parse("
+ + "String, port, path, boolean, String)");
+ if (header == null) {
+ throw new IllegalArgumentException("Header may not be null.");
+ }
+ return parse(host, port, path, secure, header.getValue());
+ }
+
+
+ /**
+ * Parse the cookie attribute and update the corresponsing {@link Cookie}
+ * properties.
+ *
+ * @param attribute {@link HeaderElement} cookie attribute from the
+ * Set- Cookie
+ * @param cookie {@link Cookie} to be updated
+ * @throws MalformedCookieException if an exception occurs during parsing
+ */
+
+ public void parseAttribute(
+ final NameValuePair attribute, final Cookie cookie)
+ throws MalformedCookieException {
+
+ if (attribute == null) {
+ throw new IllegalArgumentException("Attribute may not be null.");
+ }
+ if (cookie == null) {
+ throw new IllegalArgumentException("Cookie may not be null.");
+ }
+ final String paramName = attribute.getName().toLowerCase();
+ String paramValue = attribute.getValue();
+
+ if (paramName.equals("path")) {
+
+ if ((paramValue == null) || (paramValue.trim().equals(""))) {
+ paramValue = "/";
+ }
+ cookie.setPath(paramValue);
+ cookie.setPathAttributeSpecified(true);
+
+ } else if (paramName.equals("domain")) {
+
+ if (paramValue == null) {
+ throw new MalformedCookieException(
+ "Missing value for domain attribute");
+ }
+ if (paramValue.trim().equals("")) {
+ throw new MalformedCookieException(
+ "Blank value for domain attribute");
+ }
+ cookie.setDomain(paramValue);
+ cookie.setDomainAttributeSpecified(true);
+
+ } else if (paramName.equals("max-age")) {
+
+ if (paramValue == null) {
+ throw new MalformedCookieException(
+ "Missing value for max-age attribute");
+ }
+ int age;
+ try {
+ age = Integer.parseInt(paramValue);
+ } catch (NumberFormatException e) {
+ throw new MalformedCookieException ("Invalid max-age "
+ + "attribute: " + e.getMessage());
+ }
+ cookie.setExpiryDate(
+ new Date(System.currentTimeMillis() + age * 1000L));
+
+ } else if (paramName.equals("secure")) {
+
+ cookie.setSecure(true);
+
+ } else if (paramName.equals("comment")) {
+
+ cookie.setComment(paramValue);
+
+ } else if (paramName.equals("expires")) {
+
+ if (paramValue == null) {
+ throw new MalformedCookieException(
+ "Missing value for expires attribute");
+ }
+
+ try {
+ cookie.setExpiryDate(DateUtil.parseDate(paramValue, this.datepatterns));
+ } catch (DateParseException dpe) {
+ LOG.debug("Error parsing cookie date", dpe);
+ throw new MalformedCookieException(
+ "Unable to parse expiration date parameter: "
+ + paramValue);
+ }
+ } else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Unrecognized cookie attribute: "
+ + attribute.toString());
+ }
+ }
+ }
+
+
+ public Collection getValidDateFormats() {
+ return this.datepatterns;
+ }
+
+ public void setValidDateFormats(final Collection datepatterns) {
+ this.datepatterns = datepatterns;
+ }
+
+ /**
+ * Performs most common {@link Cookie} validation
+ *
+ * @param host the host from which the {@link Cookie} was received
+ * @param port the port from which the {@link Cookie} was received
+ * @param path the path from which the {@link Cookie} was received
+ * @param secure true when the {@link Cookie} was received using a
+ * secure connection
+ * @param cookie The cookie to validate.
+ * @throws MalformedCookieException if an exception occurs during
+ * validation
+ */
+
+ public void validate(String host, int port, String path,
+ boolean secure, final Cookie cookie)
+ throws MalformedCookieException {
+
+ LOG.trace("enter CookieSpecBase.validate("
+ + "String, port, path, boolean, Cookie)");
+ if (host == null) {
+ throw new IllegalArgumentException(
+ "Host of origin may not be null");
+ }
+ if (host.trim().equals("")) {
+ throw new IllegalArgumentException(
+ "Host of origin may not be blank");
+ }
+ if (port < 0) {
+ throw new IllegalArgumentException("Invalid port: " + port);
+ }
+ if (path == null) {
+ throw new IllegalArgumentException(
+ "Path of origin may not be null.");
+ }
+ if (path.trim().equals("")) {
+ path = PATH_DELIM;
+ }
+ host = host.toLowerCase();
+ // check version
+ if (cookie.getVersion() < 0) {
+ throw new MalformedCookieException ("Illegal version number "
+ + cookie.getValue());
+ }
+
+ // security check... we musn't allow the server to give us an
+ // invalid domain scope
+
+ // Validate the cookies domain attribute. NOTE: Domains without
+ // any dots are allowed to support hosts on private LANs that don't
+ // have DNS names. Since they have no dots, to domain-match the
+ // request-host and domain must be identical for the cookie to sent
+ // back to the origin-server.
+ if (host.indexOf(".") >= 0) {
+ // Not required to have at least two dots. RFC 2965.
+ // A Set-Cookie2 with Domain=ajax.com will be accepted.
+
+ // domain must match host
+ if (!host.endsWith(cookie.getDomain())) {
+ String s = cookie.getDomain();
+ if (s.startsWith(".")) {
+ s = s.substring(1, s.length());
+ }
+ if (!host.equals(s)) {
+ throw new MalformedCookieException(
+ "Illegal domain attribute \"" + cookie.getDomain()
+ + "\". Domain of origin: \"" + host + "\"");
+ }
+ }
+ } else {
+ if (!host.equals(cookie.getDomain())) {
+ throw new MalformedCookieException(
+ "Illegal domain attribute \"" + cookie.getDomain()
+ + "\". Domain of origin: \"" + host + "\"");
+ }
+ }
+
+ // another security check... we musn't allow the server to give us a
+ // cookie that doesn't match this path
+
+ if (!path.startsWith(cookie.getPath())) {
+ throw new MalformedCookieException(
+ "Illegal path attribute \"" + cookie.getPath()
+ + "\". Path of origin: \"" + path + "\"");
+ }
+ }
+
+
+ /**
+ * Return true if the cookie should be submitted with a request
+ * with given attributes, false otherwise.
+ * @param host the host to which the request is being submitted
+ * @param port the port to which the request is being submitted (ignored)
+ * @param path the path to which the request is being submitted
+ * @param secure true if the request is using a secure connection
+ * @param cookie {@link Cookie} to be matched
+ * @return true if the cookie matches the criterium
+ */
+
+ public boolean match(String host, int port, String path,
+ boolean secure, final Cookie cookie) {
+
+ LOG.trace("enter CookieSpecBase.match("
+ + "String, int, String, boolean, Cookie");
+
+ if (host == null) {
+ throw new IllegalArgumentException(
+ "Host of origin may not be null");
+ }
+ if (host.trim().equals("")) {
+ throw new IllegalArgumentException(
+ "Host of origin may not be blank");
+ }
+ if (port < 0) {
+ throw new IllegalArgumentException("Invalid port: " + port);
+ }
+ if (path == null) {
+ throw new IllegalArgumentException(
+ "Path of origin may not be null.");
+ }
+ if (cookie == null) {
+ throw new IllegalArgumentException("Cookie may not be null");
+ }
+ if (path.trim().equals("")) {
+ path = PATH_DELIM;
+ }
+ host = host.toLowerCase();
+ if (cookie.getDomain() == null) {
+ LOG.warn("Invalid cookie state: domain not specified");
+ return false;
+ }
+ if (cookie.getPath() == null) {
+ LOG.warn("Invalid cookie state: path not specified");
+ return false;
+ }
+
+ return
+ // only add the cookie if it hasn't yet expired
+ (cookie.getExpiryDate() == null
+ || cookie.getExpiryDate().after(new Date()))
+ // and the domain pattern matches
+ && (domainMatch(host, cookie.getDomain()))
+ // and the path is null or matching
+ && (pathMatch(path, cookie.getPath()))
+ // and if the secure flag is set, only if the request is
+ // actually secure
+ && (cookie.getSecure() ? secure : true);
+ }
+
+ /**
+ * Performs domain-match as implemented in common browsers.
+ * @param host The target host.
+ * @param domain The cookie domain attribute.
+ * @return true if the specified host matches the given domain.
+ */
+ public boolean domainMatch(final String host, String domain) {
+ if (host.equals(domain)) {
+ return true;
+ }
+ if (!domain.startsWith(".")) {
+ domain = "." + domain;
+ }
+ return host.endsWith(domain) || host.equals(domain.substring(1));
+ }
+
+ /**
+ * Performs path-match as implemented in common browsers.
+ * @param path The target path.
+ * @param topmostPath The cookie path attribute.
+ * @return true if the paths match
+ */
+ public boolean pathMatch(final String path, final String topmostPath) {
+ boolean match = path.startsWith (topmostPath);
+ // if there is a match and these values are not exactly the same we have
+ // to make sure we're not matcing "/foobar" and "/foo"
+ if (match && path.length() != topmostPath.length()) {
+ if (!topmostPath.endsWith(PATH_DELIM)) {
+ match = (path.charAt(topmostPath.length()) == PATH_DELIM_CHAR);
+ }
+ }
+ return match;
+ }
+
+ /**
+ * Return an array of {@link Cookie}s that should be submitted with a
+ * request with given attributes, false otherwise.
+ * @param host the host to which the request is being submitted
+ * @param port the port to which the request is being submitted (currently
+ * ignored)
+ * @param path the path to which the request is being submitted
+ * @param secure true if the request is using a secure protocol
+ * @param cookies an array of Cookies to be matched
+ * @return an array of Cookies matching the criterium
+ *
+// BEGIN IA/HERITRIX CHANGES
+ * @deprecated use match(String, int, String, boolean, SortedMap)
+// END IA/HERITRIX CHANGES
+ */
+
+ public Cookie[] match(String host, int port, String path,
+ boolean secure, final Cookie cookies[]) {
+
+ LOG.trace("enter CookieSpecBase.match("
+ + "String, int, String, boolean, Cookie[])");
+
+ if (cookies == null) {
+ return null;
+ }
+ List matching = new LinkedList();
+ for (int i = 0; i < cookies.length; i++) {
+ if (match(host, port, path, secure, cookies[i])) {
+ addInPathOrder(matching, cookies[i]);
+ }
+ }
+ return (Cookie[]) matching.toArray(new Cookie[matching.size()]);
+ }
+
+// BEGIN IA/HERITRIX CHANGES
+ /**
+ * Return an array of {@link Cookie}s that should be submitted with a
+ * request with given attributes, false otherwise.
+ *
+ * If the SortedMap comes from an HttpState and is not itself
+ * thread-safe, it may be necessary to synchronize on the HttpState
+ * instance to protect against concurrent modification.
+ *
+ * @param host the host to which the request is being submitted
+ * @param port the port to which the request is being submitted (currently
+ * ignored)
+ * @param path the path to which the request is being submitted
+ * @param secure true if the request is using a secure protocol
+ * @param cookies SortedMap of Cookies to be matched
+ * @return an array of Cookies matching the criterium
+ */
+
+ public Cookie[] match(String host, int port, String path,
+ boolean secure, final SortedMap cookies) {
+
+ LOG.trace("enter CookieSpecBase.match("
+ + "String, int, String, boolean, SortedMap)");
+
+ // TODO: skip meaningless 'narrowing' when host is a numeric IP
+ // (harmless in the meantime)
+
+ if (cookies == null) {
+ return null;
+ }
+ List matching = new LinkedList();
+ String narrowHost = host;
+ do {
+ Iterator iter = cookies.subMap(narrowHost,
+ narrowHost + Cookie.DOMAIN_OVERBOUNDS).values().iterator();
+ while (iter.hasNext()) {
+ Cookie cookie = (Cookie) (iter.next());
+ if (match(host, port, path, secure, cookie)) {
+ addInPathOrder(matching, cookie);
+ }
+ }
+ StoredIterator.close(iter);
+ int trimTo = narrowHost.indexOf('.', 1);
+ narrowHost = (trimTo < 0) ? null : narrowHost.substring(trimTo+1);
+ } while (narrowHost != null);
+
+ return (Cookie[]) matching.toArray(new Cookie[matching.size()]);
+ }
+// END IA/HERITRIX CHANGES
+
+ /**
+ * Adds the given cookie into the given list in descending path order. That
+ * is, more specific path to least specific paths. This may not be the
+ * fastest algorythm, but it'll work OK for the small number of cookies
+ * we're generally dealing with.
+ *
+ * @param list - the list to add the cookie to
+ * @param addCookie - the Cookie to add to list
+ */
+ private static void addInPathOrder(List list, Cookie addCookie) {
+ int i = 0;
+
+ for (i = 0; i < list.size(); i++) {
+ Cookie c = (Cookie) list.get(i);
+ if (addCookie.compare(addCookie, c) > 0) {
+ break;
+ }
+ }
+ list.add(i, addCookie);
+ }
+
+ /**
+ * Return a string suitable for sending in a "Cookie" header
+ * @param cookie a {@link Cookie} to be formatted as string
+ * @return a string suitable for sending in a "Cookie" header.
+ */
+ public String formatCookie(Cookie cookie) {
+ LOG.trace("enter CookieSpecBase.formatCookie(Cookie)");
+ if (cookie == null) {
+ throw new IllegalArgumentException("Cookie may not be null");
+ }
+ StringBuffer buf = new StringBuffer();
+ buf.append(cookie.getName());
+ buf.append("=");
+ String s = cookie.getValue();
+ if (s != null) {
+ buf.append(s);
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Create a "Cookie" header value containing all {@link Cookie}s in
+ * cookies suitable for sending in a "Cookie" header
+ * @param cookies an array of {@link Cookie}s to be formatted
+ * @return a string suitable for sending in a Cookie header.
+ * @throws IllegalArgumentException if an input parameter is illegal
+ */
+
+ public String formatCookies(Cookie[] cookies)
+ throws IllegalArgumentException {
+ LOG.trace("enter CookieSpecBase.formatCookies(Cookie[])");
+ if (cookies == null) {
+ throw new IllegalArgumentException("Cookie array may not be null");
+ }
+ if (cookies.length == 0) {
+ throw new IllegalArgumentException("Cookie array may not be empty");
+ }
+
+ StringBuffer buffer = new StringBuffer();
+ for (int i = 0; i < cookies.length; i++) {
+ if (i > 0) {
+ buffer.append("; ");
+ }
+ buffer.append(formatCookie(cookies[i]));
+ }
+ return buffer.toString();
+ }
+
+
+ /**
+ * Create a "Cookie" {@link Header} containing all {@link Cookie}s
+ * in cookies.
+ * @param cookies an array of {@link Cookie}s to be formatted as a "
+ * Cookie" header
+ * @return a "Cookie" {@link Header}.
+ */
+ public Header formatCookieHeader(Cookie[] cookies) {
+ LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie[])");
+ return new Header("Cookie", formatCookies(cookies));
+ }
+
+
+ /**
+ * Create a "Cookie" {@link Header} containing the {@link Cookie}.
+ * @param cookie Cookies to be formatted as a Cookie
+ * header
+ * @return a Cookie header.
+ */
+ public Header formatCookieHeader(Cookie cookie) {
+ LOG.trace("enter CookieSpecBase.formatCookieHeader(Cookie)");
+ return new Header("Cookie", formatCookie(cookie));
+ }
+
+}
diff --git a/commons/src/main/java/org/apache/commons/httpclient/cookie/IgnoreCookiesSpec.java b/commons/src/main/java/org/apache/commons/httpclient/cookie/IgnoreCookiesSpec.java
new file mode 100644
index 00000000..82e1a105
--- /dev/null
+++ b/commons/src/main/java/org/apache/commons/httpclient/cookie/IgnoreCookiesSpec.java
@@ -0,0 +1,160 @@
+/*
+ * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/cookie/IgnoreCookiesSpec.java,v 1.6 2004/09/14 20:11:31 olegk Exp $
+ * $Revision$
+ * $Date$
+ *
+ * ====================================================================
+ *
+ * Copyright 2002-2004 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.commons.httpclient.cookie;
+
+import java.util.Collection;
+import java.util.SortedMap; // <- IA/HERITRIX CHANGE
+
+import org.apache.commons.httpclient.Cookie;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.NameValuePair;
+
+/**
+ * A cookie spec that does nothing. Cookies are neither parsed, formatted nor matched.
+ * It can be used to effectively disable cookies altogether.
+ *
+ * @since 3.0
+ */
+@SuppressWarnings("unchecked") // <- IA/HERITRIX CHANGE
+public class IgnoreCookiesSpec implements CookieSpec {
+
+ /**
+ *
+ */
+ public IgnoreCookiesSpec() {
+ super();
+ }
+
+ /**
+ * Returns an empty {@link Cookie cookie} array. All parameters are ignored.
+ */
+ public Cookie[] parse(String host, int port, String path, boolean secure, String header)
+ throws MalformedCookieException {
+ return new Cookie[0];
+ }
+
+ /**
+ * @return null
+ */
+ public Collection getValidDateFormats() {
+ return null;
+ }
+
+ /**
+ * Does nothing.
+ */
+ public void setValidDateFormats(Collection datepatterns) {
+ }
+
+ /**
+ * @return null
+ */
+ public String formatCookie(Cookie cookie) {
+ return null;
+ }
+
+ /**
+ * @return null
+ */
+ public Header formatCookieHeader(Cookie cookie) throws IllegalArgumentException {
+ return null;
+ }
+
+ /**
+ * @return null
+ */
+ public Header formatCookieHeader(Cookie[] cookies) throws IllegalArgumentException {
+ return null;
+ }
+
+ /**
+ * @return null
+ */
+ public String formatCookies(Cookie[] cookies) throws IllegalArgumentException {
+ return null;
+ }
+
+ /**
+ * @return false
+ */
+ public boolean match(String host, int port, String path, boolean secure, Cookie cookie) {
+ return false;
+ }
+
+ /**
+ * Returns an empty {@link Cookie cookie} array. All parameters are ignored.
+ */
+ public Cookie[] match(String host, int port, String path, boolean secure, Cookie[] cookies) {
+ return new Cookie[0];
+ }
+
+ /**
+ * Returns an empty {@link Cookie cookie} array. All parameters are ignored.
+ */
+ public Cookie[] parse(String host, int port, String path, boolean secure, Header header)
+ throws MalformedCookieException, IllegalArgumentException {
+ return new Cookie[0];
+ }
+
+ /**
+ * Does nothing.
+ */
+ public void parseAttribute(NameValuePair attribute, Cookie cookie)
+ throws MalformedCookieException, IllegalArgumentException {
+ }
+
+ /**
+ * Does nothing.
+ */
+ public void validate(String host, int port, String path, boolean secure, Cookie cookie)
+ throws MalformedCookieException, IllegalArgumentException {
+ }
+
+ /**
+ * @return false
+ */
+ public boolean domainMatch(final String host, final String domain) {
+ return false;
+ }
+
+ /**
+ * @return false
+ */
+ public boolean pathMatch(final String path, final String topmostPath) {
+ return false;
+ }
+
+// BEGIN IA/HERITRIX ADDITION
+ public Cookie[] match(String domain, int port, String path, boolean secure,
+ SortedMap cookiesMap) {
+ return new Cookie[0];
+ }
+// END IA/HERITRIX CHANGE
+}
diff --git a/commons/src/main/java/org/apache/commons/pool/impl/FairGenericObjectPool.java b/commons/src/main/java/org/apache/commons/pool/impl/FairGenericObjectPool.java
new file mode 100644
index 00000000..569295dd
--- /dev/null
+++ b/commons/src/main/java/org/apache/commons/pool/impl/FairGenericObjectPool.java
@@ -0,0 +1,310 @@
+/* FairGenericObjectPool
+*
+* $Id$
+*
+* Created on Apr 7, 2006
+*
+* Copyright (C) 2006 Internet Archive.
+*/
+
+package org.apache.commons.pool.impl;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.apache.commons.pool.PoolableObjectFactory;
+import org.apache.commons.pool.impl.GenericKeyedObjectPool.ObjectTimestampPair;
+
+/**
+ * Version of GenericObjectPool which is 'fair' with respect to the client
+ * threads using {@link #borrowObject borrowObject}. Those which enter
+ * first will receive objects from the pool first.
+ *
+ *
+ * @see GenericObjectPool
+ * @author Gordon Mohr
+ * @version $Revision$ $Date$
+ */
+@SuppressWarnings("unchecked")
+public class FairGenericObjectPool extends GenericObjectPool {
+
+ //--- constructors -----------------------------------------------
+ // (all copied from superclass; only last adds one additional line of
+ // initialization and call to superclass)
+
+ /**
+ * Create a new FairGenericObjectPool.
+ */
+ public FairGenericObjectPool() {
+ this(null,DEFAULT_MAX_ACTIVE,DEFAULT_WHEN_EXHAUSTED_ACTION,DEFAULT_MAX_WAIT,DEFAULT_MAX_IDLE,DEFAULT_MIN_IDLE,DEFAULT_TEST_ON_BORROW,DEFAULT_TEST_ON_RETURN,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new FairGenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ */
+ public FairGenericObjectPool(PoolableObjectFactory factory) {
+ this(factory,DEFAULT_MAX_ACTIVE,DEFAULT_WHEN_EXHAUSTED_ACTION,DEFAULT_MAX_WAIT,DEFAULT_MAX_IDLE,DEFAULT_MIN_IDLE,DEFAULT_TEST_ON_BORROW,DEFAULT_TEST_ON_RETURN,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new FairGenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param config a non-null {@link GenericObjectPool.Config} describing my configuration
+ */
+ public FairGenericObjectPool(PoolableObjectFactory factory, GenericObjectPool.Config config) {
+ this(factory,config.maxActive,config.whenExhaustedAction,config.maxWait,config.maxIdle,config.minIdle,config.testOnBorrow,config.testOnReturn,config.timeBetweenEvictionRunsMillis,config.numTestsPerEvictionRun,config.minEvictableIdleTimeMillis,config.testWhileIdle);
+ }
+
+ /**
+ * Create a new FairGenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ */
+ public FairGenericObjectPool(PoolableObjectFactory factory, int maxActive) {
+ this(factory,maxActive,DEFAULT_WHEN_EXHAUSTED_ACTION,DEFAULT_MAX_WAIT,DEFAULT_MAX_IDLE,DEFAULT_MIN_IDLE,DEFAULT_TEST_ON_BORROW,DEFAULT_TEST_ON_RETURN,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new FairGenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #getWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #getMaxWait})
+ */
+ public FairGenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait) {
+ this(factory,maxActive,whenExhaustedAction,maxWait,DEFAULT_MAX_IDLE,DEFAULT_MIN_IDLE,DEFAULT_TEST_ON_BORROW,DEFAULT_TEST_ON_RETURN,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new FairGenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #getWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #getMaxWait})
+ * @param testOnBorrow whether or not to validate objects before they are returned by the {@link #borrowObject} method (see {@link #getTestOnBorrow})
+ * @param testOnReturn whether or not to validate objects after they are returned to the {@link #returnObject} method (see {@link #getTestOnReturn})
+ */
+ public FairGenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, boolean testOnBorrow, boolean testOnReturn) {
+ this(factory,maxActive,whenExhaustedAction,maxWait,DEFAULT_MAX_IDLE,DEFAULT_MIN_IDLE,testOnBorrow,testOnReturn,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new FairGenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #getWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #getMaxWait})
+ * @param maxIdle the maximum number of idle objects in my pool (see {@link #getMaxIdle})
+ */
+ public FairGenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle) {
+ this(factory,maxActive,whenExhaustedAction,maxWait,maxIdle,DEFAULT_MIN_IDLE,DEFAULT_TEST_ON_BORROW,DEFAULT_TEST_ON_RETURN,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new FairGenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #getWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #getMaxWait})
+ * @param maxIdle the maximum number of idle objects in my pool (see {@link #getMaxIdle})
+ * @param testOnBorrow whether or not to validate objects before they are returned by the {@link #borrowObject} method (see {@link #getTestOnBorrow})
+ * @param testOnReturn whether or not to validate objects after they are returned to the {@link #returnObject} method (see {@link #getTestOnReturn})
+ */
+ public FairGenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle, boolean testOnBorrow, boolean testOnReturn) {
+ this(factory,maxActive,whenExhaustedAction,maxWait,maxIdle,DEFAULT_MIN_IDLE,testOnBorrow,testOnReturn,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new FairGenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #setWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #setMaxWait})
+ * @param maxIdle the maximum number of idle objects in my pool (see {@link #setMaxIdle})
+ * @param testOnBorrow whether or not to validate objects before they are returned by the {@link #borrowObject} method (see {@link #setTestOnBorrow})
+ * @param testOnReturn whether or not to validate objects after they are returned to the {@link #returnObject} method (see {@link #setTestOnReturn})
+ * @param timeBetweenEvictionRunsMillis the amount of time (in milliseconds) to sleep between examining idle objects for eviction (see {@link #setTimeBetweenEvictionRunsMillis})
+ * @param numTestsPerEvictionRun the number of idle objects to examine per run within the idle object eviction thread (if any) (see {@link #setNumTestsPerEvictionRun})
+ * @param minEvictableIdleTimeMillis the minimum number of milliseconds an object can sit idle in the pool before it is eligable for evcition (see {@link #setMinEvictableIdleTimeMillis})
+ * @param testWhileIdle whether or not to validate objects in the idle object eviction thread, if any (see {@link #setTestWhileIdle})
+ */
+ public FairGenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle, boolean testOnBorrow, boolean testOnReturn, long timeBetweenEvictionRunsMillis, int numTestsPerEvictionRun, long minEvictableIdleTimeMillis, boolean testWhileIdle) {
+ this(factory, maxActive, whenExhaustedAction, maxWait, maxIdle, DEFAULT_MIN_IDLE, testOnBorrow, testOnReturn, timeBetweenEvictionRunsMillis, numTestsPerEvictionRun, minEvictableIdleTimeMillis, testWhileIdle);
+ }
+
+ /**
+ * Create a new FairGenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #setWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #setMaxWait})
+ * @param maxIdle the maximum number of idle objects in my pool (see {@link #setMaxIdle})
+ * @param minIdle the minimum number of idle objects in my pool (see {@link #setMinIdle})
+ * @param testOnBorrow whether or not to validate objects before they are returned by the {@link #borrowObject} method (see {@link #setTestOnBorrow})
+ * @param testOnReturn whether or not to validate objects after they are returned to the {@link #returnObject} method (see {@link #setTestOnReturn})
+ * @param timeBetweenEvictionRunsMillis the amount of time (in milliseconds) to sleep between examining idle objects for eviction (see {@link #setTimeBetweenEvictionRunsMillis})
+ * @param numTestsPerEvictionRun the number of idle objects to examine per run within the idle object eviction thread (if any) (see {@link #setNumTestsPerEvictionRun})
+ * @param minEvictableIdleTimeMillis the minimum number of milliseconds an object can sit idle in the pool before it is eligable for evcition (see {@link #setMinEvictableIdleTimeMillis})
+ * @param testWhileIdle whether or not to validate objects in the idle object eviction thread, if any (see {@link #setTestWhileIdle})
+ */
+ public FairGenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle, int minIdle, boolean testOnBorrow, boolean testOnReturn, long timeBetweenEvictionRunsMillis, int numTestsPerEvictionRun, long minEvictableIdleTimeMillis, boolean testWhileIdle) {
+ this(factory, maxActive, whenExhaustedAction, maxWait, maxIdle, minIdle, testOnBorrow, testOnReturn, timeBetweenEvictionRunsMillis, numTestsPerEvictionRun, minEvictableIdleTimeMillis, testWhileIdle, DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS);
+ }
+
+ /**
+ * Create a new FairGenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #setWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #setMaxWait})
+ * @param maxIdle the maximum number of idle objects in my pool (see {@link #setMaxIdle})
+ * @param minIdle the minimum number of idle objects in my pool (see {@link #setMinIdle})
+ * @param testOnBorrow whether or not to validate objects before they are returned by the {@link #borrowObject} method (see {@link #setTestOnBorrow})
+ * @param testOnReturn whether or not to validate objects after they are returned to the {@link #returnObject} method (see {@link #setTestOnReturn})
+ * @param timeBetweenEvictionRunsMillis the amount of time (in milliseconds) to sleep between examining idle objects for eviction (see {@link #setTimeBetweenEvictionRunsMillis})
+ * @param numTestsPerEvictionRun the number of idle objects to examine per run within the idle object eviction thread (if any) (see {@link #setNumTestsPerEvictionRun})
+ * @param minEvictableIdleTimeMillis the minimum number of milliseconds an object can sit idle in the pool before it is eligable for evcition (see {@link #setMinEvictableIdleTimeMillis})
+ * @param testWhileIdle whether or not to validate objects in the idle object eviction thread, if any (see {@link #setTestWhileIdle})
+ * @param softMinEvictableIdleTimeMillis the minimum number of milliseconds an object can sit idle in the pool before it is eligable for evcition with the extra condition that at least "minIdle" amount of object remain in the pool. (see {@link #setSoftMinEvictableIdleTimeMillis})
+ */
+ public FairGenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle, int minIdle, boolean testOnBorrow, boolean testOnReturn, long timeBetweenEvictionRunsMillis, int numTestsPerEvictionRun, long minEvictableIdleTimeMillis, boolean testWhileIdle, long softMinEvictableIdleTimeMillis) {
+ super(factory, maxActive, whenExhaustedAction, maxWait, maxIdle,
+ minIdle, testOnBorrow, testOnReturn,
+ timeBetweenEvictionRunsMillis, numTestsPerEvictionRun,
+ minEvictableIdleTimeMillis, testWhileIdle,
+ softMinEvictableIdleTimeMillis);
+ _borrowerQueue = Collections.synchronizedList(new LinkedList());
+ }
+
+ //-- ObjectPool methods ------------------------------------------
+
+ /**
+ *
+ * @see org.apache.commons.pool.ObjectPool#borrowObject()
+ */
+ public Object borrowObject() throws Exception {
+ assertOpen();
+ long starttime = System.currentTimeMillis();
+
+
+
+ try {
+ synchronized(this) {
+ // use borrowerQueue
+ _borrowerQueue.add(Thread.currentThread());
+
+ for(;;) {
+ ObjectTimestampPair pair = null;
+
+ // Only allow current thread to receive pool object if
+ // thread is top of queue
+ boolean eligible = _borrowerQueue.get(0)==Thread.currentThread();
+ if(eligible) {
+ // if there are any sleeping, just grab one of those
+ try {
+ pair = (ObjectTimestampPair)(_pool.removeFirst());
+ } catch(NoSuchElementException e) {
+ ; /* ignored */
+ }
+ }
+
+ // otherwise
+ if(null == pair) {
+ // check if we can create one
+ // (note we know that the num sleeping is 0, else we wouldn't be here)
+ if(eligible && (_maxActive < 0 || _numActive < _maxActive)) {
+ // allow new object to be created
+ } else {
+ // the pool is exhausted
+ // or current thread is ineligible due to fairness
+ switch(_whenExhaustedAction) {
+ case WHEN_EXHAUSTED_GROW:
+ // allow new object to be created
+ break;
+ case WHEN_EXHAUSTED_FAIL:
+ throw new NoSuchElementException("Pool exhausted");
+ case WHEN_EXHAUSTED_BLOCK:
+ try {
+ if(_maxWait <= 0) {
+ wait();
+ } else {
+ // this code may be executed again after a notify then continue cycle
+ // so, need to calculate the amount of time to wait
+ final long elapsed = (System.currentTimeMillis() - starttime);
+ final long waitTime = _maxWait - elapsed;
+ if (waitTime > 0)
+ {
+ wait(waitTime);
+ }
+ }
+ } catch(InterruptedException e) {
+ // ignored
+ }
+ if(_maxWait > 0 && ((System.currentTimeMillis() - starttime) >= _maxWait)) {
+ throw new NoSuchElementException("Timeout waiting for idle object");
+ } else {
+ continue; // keep looping
+ }
+ default:
+ throw new IllegalArgumentException("WhenExhaustedAction property " + _whenExhaustedAction + " not recognized.");
+ }
+ }
+ }
+ _numActive++;
+
+ // create new object when needed
+ boolean newlyCreated = false;
+ if(null == pair) {
+ try {
+ Object obj = _factory.makeObject();
+ pair = new ObjectTimestampPair(obj);
+ newlyCreated = true;
+ return pair.value;
+ } finally {
+ if (!newlyCreated) {
+ // object cannot be created
+ _numActive--;
+ notifyAll();
+ }
+ }
+ }
+
+ // activate & validate the object
+ try {
+ _factory.activateObject(pair.value);
+ if(_testOnBorrow && !_factory.validateObject(pair.value)) {
+ throw new Exception("ValidateObject failed");
+ }
+ return pair.value;
+ }
+ catch (Throwable e) {
+ // object cannot be activated or is invalid
+ _numActive--;
+ notifyAll();
+ try {
+ _factory.destroyObject(pair.value);
+ }
+ catch (Throwable e2) {
+ // cannot destroy broken object
+ }
+ if(newlyCreated) {
+ throw new NoSuchElementException("Could not create a validated object, cause: " + e.getMessage());
+ }
+ else {
+ continue; // keep looping
+ }
+ }
+ }
+ }
+ } finally {
+ // remove thread from queue on any method exit
+ _borrowerQueue.remove(Thread.currentThread());
+ }
+ }
+
+ /** Waiting borrowers (threads in #borrowObject ) */
+ protected List _borrowerQueue;
+}
diff --git a/commons/src/main/java/org/apache/commons/pool/impl/FairGenericObjectPoolTest.java b/commons/src/main/java/org/apache/commons/pool/impl/FairGenericObjectPoolTest.java
new file mode 100644
index 00000000..c9ce73ac
--- /dev/null
+++ b/commons/src/main/java/org/apache/commons/pool/impl/FairGenericObjectPoolTest.java
@@ -0,0 +1,122 @@
+/* FairGenericObjectPoolTest
+*
+* $Id$
+*
+* Created on Apr 7, 2006
+*
+* Copyright (C) 2006 Internet Archive.
+*
+*/
+package org.apache.commons.pool.impl;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+import org.apache.commons.pool.BasePoolableObjectFactory;
+
+/**
+ * Test for FairGenericObjectPool.
+ *
+ * @author gojomo
+ */
+@SuppressWarnings("unchecked")
+public class FairGenericObjectPoolTest extends TestCase {
+// public void testUnfair() throws InterruptedException {
+//// System.out.println("unfair");
+// GenericObjectPool pool = new GenericObjectPool();
+//
+// Object[] borrowOrder = tryPool(pool);
+//
+// Object[] sortedOrder = (Object[]) borrowOrder.clone();
+// Arrays.sort(sortedOrder);
+// assertFalse("unexpectedly fair", Arrays.equals(borrowOrder,sortedOrder));
+// }
+
+ public void testFair() throws InterruptedException {
+// System.out.println("fair");
+ GenericObjectPool pool = new FairGenericObjectPool();
+
+ Object[] borrowOrder = tryPool(pool);
+
+ Object[] sortedOrder = (Object[]) borrowOrder.clone();
+ Arrays.sort(sortedOrder);
+ assertTrue("unexpectedly unfair", Arrays.equals(borrowOrder,sortedOrder));
+ }
+
+ /**
+ * Test the given pool for fairness.
+ *
+ * @param pool GenericObjectPool to test
+ * @throws InterruptedException
+ */
+ private Object[] tryPool(GenericObjectPool pool) throws InterruptedException {
+ BlockerObjectFactory factory = new BlockerObjectFactory();
+ pool.setFactory(factory);
+ pool.setMaxActive(1);
+ List borrowOrder = Collections.synchronizedList(new LinkedList());
+ for(int i = 0; i < 10; i++) {
+ Contender c = new Contender(borrowOrder);
+ c.pool = pool;
+ c.ordinal = i;
+ (new Thread(c)).start();
+ Thread.sleep(500);
+ }
+ factory.single.release();
+ Thread.sleep(5000);
+ return borrowOrder.toArray();
+ }
+
+ class Contender implements Runnable {
+ public GenericObjectPool pool;
+ public int ordinal;
+ public List reportList;
+
+ public Contender(List borrowOrder) {
+ reportList = borrowOrder;
+ }
+
+ public void run() {
+ try {
+ Blocker block = (Blocker) pool.borrowObject();
+ System.out.println("borrowed #"+ordinal);
+ reportList.add(new Integer(ordinal));
+ block.acquire();
+ System.out.println("returning #"+ordinal);
+ pool.returnObject(block);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ }
+
+ class BlockerObjectFactory extends BasePoolableObjectFactory {
+ public Blocker single = new Blocker();
+ public Object makeObject() throws Exception {
+ System.out.println("makeObject");
+ return single;
+ }
+ }
+
+ class Blocker {
+ boolean block = true;
+ public synchronized void acquire() {
+ // only block first time through
+ if(block) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ block = false;
+ }
+ public synchronized void release() {
+ notifyAll();
+ }
+ }
+}
diff --git a/commons/src/main/java/org/apache/commons/pool/impl/GenericObjectPool.java b/commons/src/main/java/org/apache/commons/pool/impl/GenericObjectPool.java
new file mode 100644
index 00000000..45dcbbec
--- /dev/null
+++ b/commons/src/main/java/org/apache/commons/pool/impl/GenericObjectPool.java
@@ -0,0 +1,1312 @@
+/*
+ * Copyright 1999-2004 The Apache Software Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.pool.impl;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.LinkedList;
+import java.util.ListIterator;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.apache.commons.pool.BaseObjectPool;
+import org.apache.commons.pool.ObjectPool;
+import org.apache.commons.pool.PoolableObjectFactory;
+import org.apache.commons.pool.impl.GenericKeyedObjectPool.ObjectTimestampPair;
+
+/**
+ * A configurable {@link ObjectPool} implementation.
+ *
+ * When coupled with the appropriate {@link PoolableObjectFactory},
+ * GenericObjectPool provides robust pooling functionality for
+ * arbitrary objects.
+ *
+ * A GenericObjectPool provides a number of configurable parameters:
+ *
+ *
+ * {@link #setMaxActive maxActive} controls the maximum number of objects that can
+ * be borrowed from the pool at one time. When non-positive, there
+ * is no limit to the number of objects that may be active at one time.
+ * When {@link #setMaxActive maxActive} is exceeded, the pool is said to be exhausted.
+ *
+ *
+ * {@link #setMaxIdle maxIdle} controls the maximum number of objects that can
+ * sit idle in the pool at any time. When negative, there
+ * is no limit to the number of objects that may be idle at one time.
+ *
+ *
+ * {@link #setWhenExhaustedAction whenExhaustedAction} specifies the
+ * behaviour of the {@link #borrowObject} method when the pool is exhausted:
+ *
+ *
+ * When {@link #setWhenExhaustedAction whenExhaustedAction} is
+ * {@link #WHEN_EXHAUSTED_FAIL}, {@link #borrowObject} will throw
+ * a {@link NoSuchElementException}
+ *
+ *
+ * When {@link #setWhenExhaustedAction whenExhaustedAction} is
+ * {@link #WHEN_EXHAUSTED_GROW}, {@link #borrowObject} will create a new
+ * object and return it(essentially making {@link #setMaxActive maxActive}
+ * meaningless.)
+ *
+ *
+ * When {@link #setWhenExhaustedAction whenExhaustedAction}
+ * is {@link #WHEN_EXHAUSTED_BLOCK}, {@link #borrowObject} will block
+ * (invoke {@link Object#wait} until a new or idle object is available.
+ * If a positive {@link #setMaxWait maxWait}
+ * value is supplied, the {@link #borrowObject} will block for at
+ * most that many milliseconds, after which a {@link NoSuchElementException}
+ * will be thrown. If {@link #setMaxWait maxWait} is non-positive,
+ * the {@link #borrowObject} method will block indefinitely.
+ *
+ *
+ *
+ *
+ * When {@link #setTestOnBorrow testOnBorrow} is set, the pool will
+ * attempt to validate each object before it is returned from the
+ * {@link #borrowObject} method. (Using the provided factory's
+ * {@link PoolableObjectFactory#validateObject} method.) Objects that fail
+ * to validate will be dropped from the pool, and a different object will
+ * be borrowed.
+ *
+ *
+ * When {@link #setTestOnReturn testOnReturn} is set, the pool will
+ * attempt to validate each object before it is returned to the pool in the
+ * {@link #returnObject} method. (Using the provided factory's
+ * {@link PoolableObjectFactory#validateObject}
+ * method.) Objects that fail to validate will be dropped from the pool.
+ *
+ *
+ *
+ * Optionally, one may configure the pool to examine and possibly evict objects as they
+ * sit idle in the pool. This is performed by an "idle object eviction" thread, which
+ * runs asychronously. The idle object eviction thread may be configured using the
+ * following attributes:
+ *
+ *
+ * {@link #setTimeBetweenEvictionRunsMillis timeBetweenEvictionRunsMillis}
+ * indicates how long the eviction thread should sleep before "runs" of examining
+ * idle objects. When non-positive, no eviction thread will be launched.
+ *
+ *
+ * {@link #setMinEvictableIdleTimeMillis minEvictableIdleTimeMillis}
+ * specifies the minimum amount of time that an object may sit idle in the pool
+ * before it is eligable for eviction due to idle time. When non-positive, no object
+ * will be dropped from the pool due to idle time alone.
+ *
+ *
+ * {@link #setTestWhileIdle testWhileIdle} indicates whether or not idle
+ * objects should be validated using the factory's
+ * {@link PoolableObjectFactory#validateObject} method. Objects
+ * that fail to validate will be dropped from the pool.
+ *
+ *
+ *
+ * GenericObjectPool is not usable without a {@link PoolableObjectFactory}. A
+ * non-null factory must be provided either as a constructor argument
+ * or via a call to {@link #setFactory} before the pool is used.
+ *
+ * @see GenericKeyedObjectPool
+ * @author Rodney Waldhoff
+ * @author Dirk Verbeeck
+ * @version $Revision$ $Date$
+ */
+@SuppressWarnings("unchecked")
+public class GenericObjectPool extends BaseObjectPool implements ObjectPool {
+
+ //--- public constants -------------------------------------------
+
+ /**
+ * A "when exhausted action" type indicating that when the pool is
+ * exhausted (i.e., the maximum number of active objects has
+ * been reached), the {@link #borrowObject}
+ * method should fail, throwing a {@link NoSuchElementException}.
+ * @see #WHEN_EXHAUSTED_BLOCK
+ * @see #WHEN_EXHAUSTED_GROW
+ * @see #setWhenExhaustedAction
+ */
+ public static final byte WHEN_EXHAUSTED_FAIL = 0;
+
+ /**
+ * A "when exhausted action" type indicating that when the pool
+ * is exhausted (i.e., the maximum number
+ * of active objects has been reached), the {@link #borrowObject}
+ * method should block until a new object is available, or the
+ * {@link #getMaxWait maximum wait time} has been reached.
+ * @see #WHEN_EXHAUSTED_FAIL
+ * @see #WHEN_EXHAUSTED_GROW
+ * @see #setMaxWait
+ * @see #getMaxWait
+ * @see #setWhenExhaustedAction
+ */
+ public static final byte WHEN_EXHAUSTED_BLOCK = 1;
+
+ /**
+ * A "when exhausted action" type indicating that when the pool is
+ * exhausted (i.e., the maximum number
+ * of active objects has been reached), the {@link #borrowObject}
+ * method should simply create a new object anyway.
+ * @see #WHEN_EXHAUSTED_FAIL
+ * @see #WHEN_EXHAUSTED_GROW
+ * @see #setWhenExhaustedAction
+ */
+ public static final byte WHEN_EXHAUSTED_GROW = 2;
+
+ /**
+ * The default cap on the number of "sleeping" instances in the pool.
+ * @see #getMaxIdle
+ * @see #setMaxIdle
+ */
+ public static final int DEFAULT_MAX_IDLE = 8;
+
+ /**
+ * The default minimum number of "sleeping" instances in the pool
+ * before before the evictor thread (if active) spawns new objects.
+ * @see #getMinIdle
+ * @see #setMinIdle
+ */
+ public static final int DEFAULT_MIN_IDLE = 0;
+
+ /**
+ * The default cap on the total number of active instances from the pool.
+ * @see #getMaxActive
+ */
+ public static final int DEFAULT_MAX_ACTIVE = 8;
+
+ /**
+ * The default "when exhausted action" for the pool.
+ * @see #WHEN_EXHAUSTED_BLOCK
+ * @see #WHEN_EXHAUSTED_FAIL
+ * @see #WHEN_EXHAUSTED_GROW
+ * @see #setWhenExhaustedAction
+ */
+ public static final byte DEFAULT_WHEN_EXHAUSTED_ACTION = WHEN_EXHAUSTED_BLOCK;
+
+ /**
+ * The default maximum amount of time (in millis) the
+ * {@link #borrowObject} method should block before throwing
+ * an exception when the pool is exhausted and the
+ * {@link #getWhenExhaustedAction "when exhausted" action} is
+ * {@link #WHEN_EXHAUSTED_BLOCK}.
+ * @see #getMaxWait
+ * @see #setMaxWait
+ */
+ public static final long DEFAULT_MAX_WAIT = -1L;
+
+ /**
+ * The default "test on borrow" value.
+ * @see #getTestOnBorrow
+ * @see #setTestOnBorrow
+ */
+ public static final boolean DEFAULT_TEST_ON_BORROW = false;
+
+ /**
+ * The default "test on return" value.
+ * @see #getTestOnReturn
+ * @see #setTestOnReturn
+ */
+ public static final boolean DEFAULT_TEST_ON_RETURN = false;
+
+ /**
+ * The default "test while idle" value.
+ * @see #getTestWhileIdle
+ * @see #setTestWhileIdle
+ * @see #getTimeBetweenEvictionRunsMillis
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ public static final boolean DEFAULT_TEST_WHILE_IDLE = false;
+
+ /**
+ * The default "time between eviction runs" value.
+ * @see #getTimeBetweenEvictionRunsMillis
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ public static final long DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS = -1L;
+
+ /**
+ * The default number of objects to examine per run in the
+ * idle object evictor.
+ * @see #getNumTestsPerEvictionRun
+ * @see #setNumTestsPerEvictionRun
+ * @see #getTimeBetweenEvictionRunsMillis
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ public static final int DEFAULT_NUM_TESTS_PER_EVICTION_RUN = 3;
+
+ /**
+ * The default value for {@link #getMinEvictableIdleTimeMillis}.
+ * @see #getMinEvictableIdleTimeMillis
+ * @see #setMinEvictableIdleTimeMillis
+ */
+ public static final long DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS = 1000L * 60L * 30L;
+
+ /**
+ * The default value for {@link #getSoftMinEvictableIdleTimeMillis}.
+ * @see #getSoftMinEvictableIdleTimeMillis
+ * @see #setSoftMinEvictableIdleTimeMillis
+ */
+ public static final long DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS = -1;
+
+ //--- package constants -------------------------------------------
+
+ /**
+ * Idle object evition Timer. Shared between all {@link GenericObjectPool}s and {@link GenericKeyedObjectPool} s.
+ */
+ static final Timer EVICTION_TIMER = new Timer(true);
+
+ //--- constructors -----------------------------------------------
+
+ /**
+ * Create a new GenericObjectPool.
+ */
+ public GenericObjectPool() {
+ this(null,DEFAULT_MAX_ACTIVE,DEFAULT_WHEN_EXHAUSTED_ACTION,DEFAULT_MAX_WAIT,DEFAULT_MAX_IDLE,DEFAULT_MIN_IDLE,DEFAULT_TEST_ON_BORROW,DEFAULT_TEST_ON_RETURN,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new GenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ */
+ public GenericObjectPool(PoolableObjectFactory factory) {
+ this(factory,DEFAULT_MAX_ACTIVE,DEFAULT_WHEN_EXHAUSTED_ACTION,DEFAULT_MAX_WAIT,DEFAULT_MAX_IDLE,DEFAULT_MIN_IDLE,DEFAULT_TEST_ON_BORROW,DEFAULT_TEST_ON_RETURN,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new GenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param config a non-null {@link GenericObjectPool.Config} describing my configuration
+ */
+ public GenericObjectPool(PoolableObjectFactory factory, GenericObjectPool.Config config) {
+ this(factory,config.maxActive,config.whenExhaustedAction,config.maxWait,config.maxIdle,config.minIdle,config.testOnBorrow,config.testOnReturn,config.timeBetweenEvictionRunsMillis,config.numTestsPerEvictionRun,config.minEvictableIdleTimeMillis,config.testWhileIdle);
+ }
+
+ /**
+ * Create a new GenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ */
+ public GenericObjectPool(PoolableObjectFactory factory, int maxActive) {
+ this(factory,maxActive,DEFAULT_WHEN_EXHAUSTED_ACTION,DEFAULT_MAX_WAIT,DEFAULT_MAX_IDLE,DEFAULT_MIN_IDLE,DEFAULT_TEST_ON_BORROW,DEFAULT_TEST_ON_RETURN,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new GenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #getWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #getMaxWait})
+ */
+ public GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait) {
+ this(factory,maxActive,whenExhaustedAction,maxWait,DEFAULT_MAX_IDLE,DEFAULT_MIN_IDLE,DEFAULT_TEST_ON_BORROW,DEFAULT_TEST_ON_RETURN,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new GenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #getWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #getMaxWait})
+ * @param testOnBorrow whether or not to validate objects before they are returned by the {@link #borrowObject} method (see {@link #getTestOnBorrow})
+ * @param testOnReturn whether or not to validate objects after they are returned to the {@link #returnObject} method (see {@link #getTestOnReturn})
+ */
+ public GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, boolean testOnBorrow, boolean testOnReturn) {
+ this(factory,maxActive,whenExhaustedAction,maxWait,DEFAULT_MAX_IDLE,DEFAULT_MIN_IDLE,testOnBorrow,testOnReturn,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new GenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #getWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #getMaxWait})
+ * @param maxIdle the maximum number of idle objects in my pool (see {@link #getMaxIdle})
+ */
+ public GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle) {
+ this(factory,maxActive,whenExhaustedAction,maxWait,maxIdle,DEFAULT_MIN_IDLE,DEFAULT_TEST_ON_BORROW,DEFAULT_TEST_ON_RETURN,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new GenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #getWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #getMaxWait})
+ * @param maxIdle the maximum number of idle objects in my pool (see {@link #getMaxIdle})
+ * @param testOnBorrow whether or not to validate objects before they are returned by the {@link #borrowObject} method (see {@link #getTestOnBorrow})
+ * @param testOnReturn whether or not to validate objects after they are returned to the {@link #returnObject} method (see {@link #getTestOnReturn})
+ */
+ public GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle, boolean testOnBorrow, boolean testOnReturn) {
+ this(factory,maxActive,whenExhaustedAction,maxWait,maxIdle,DEFAULT_MIN_IDLE,testOnBorrow,testOnReturn,DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS,DEFAULT_NUM_TESTS_PER_EVICTION_RUN,DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS,DEFAULT_TEST_WHILE_IDLE);
+ }
+
+ /**
+ * Create a new GenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #setWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #setMaxWait})
+ * @param maxIdle the maximum number of idle objects in my pool (see {@link #setMaxIdle})
+ * @param testOnBorrow whether or not to validate objects before they are returned by the {@link #borrowObject} method (see {@link #setTestOnBorrow})
+ * @param testOnReturn whether or not to validate objects after they are returned to the {@link #returnObject} method (see {@link #setTestOnReturn})
+ * @param timeBetweenEvictionRunsMillis the amount of time (in milliseconds) to sleep between examining idle objects for eviction (see {@link #setTimeBetweenEvictionRunsMillis})
+ * @param numTestsPerEvictionRun the number of idle objects to examine per run within the idle object eviction thread (if any) (see {@link #setNumTestsPerEvictionRun})
+ * @param minEvictableIdleTimeMillis the minimum number of milliseconds an object can sit idle in the pool before it is eligable for evcition (see {@link #setMinEvictableIdleTimeMillis})
+ * @param testWhileIdle whether or not to validate objects in the idle object eviction thread, if any (see {@link #setTestWhileIdle})
+ */
+ public GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle, boolean testOnBorrow, boolean testOnReturn, long timeBetweenEvictionRunsMillis, int numTestsPerEvictionRun, long minEvictableIdleTimeMillis, boolean testWhileIdle) {
+ this(factory, maxActive, whenExhaustedAction, maxWait, maxIdle, DEFAULT_MIN_IDLE, testOnBorrow, testOnReturn, timeBetweenEvictionRunsMillis, numTestsPerEvictionRun, minEvictableIdleTimeMillis, testWhileIdle);
+ }
+
+ /**
+ * Create a new GenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #setWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #setMaxWait})
+ * @param maxIdle the maximum number of idle objects in my pool (see {@link #setMaxIdle})
+ * @param minIdle the minimum number of idle objects in my pool (see {@link #setMinIdle})
+ * @param testOnBorrow whether or not to validate objects before they are returned by the {@link #borrowObject} method (see {@link #setTestOnBorrow})
+ * @param testOnReturn whether or not to validate objects after they are returned to the {@link #returnObject} method (see {@link #setTestOnReturn})
+ * @param timeBetweenEvictionRunsMillis the amount of time (in milliseconds) to sleep between examining idle objects for eviction (see {@link #setTimeBetweenEvictionRunsMillis})
+ * @param numTestsPerEvictionRun the number of idle objects to examine per run within the idle object eviction thread (if any) (see {@link #setNumTestsPerEvictionRun})
+ * @param minEvictableIdleTimeMillis the minimum number of milliseconds an object can sit idle in the pool before it is eligable for evcition (see {@link #setMinEvictableIdleTimeMillis})
+ * @param testWhileIdle whether or not to validate objects in the idle object eviction thread, if any (see {@link #setTestWhileIdle})
+ */
+ public GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle, int minIdle, boolean testOnBorrow, boolean testOnReturn, long timeBetweenEvictionRunsMillis, int numTestsPerEvictionRun, long minEvictableIdleTimeMillis, boolean testWhileIdle) {
+ this(factory, maxActive, whenExhaustedAction, maxWait, maxIdle, minIdle, testOnBorrow, testOnReturn, timeBetweenEvictionRunsMillis, numTestsPerEvictionRun, minEvictableIdleTimeMillis, testWhileIdle, DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS);
+ }
+
+ /**
+ * Create a new GenericObjectPool using the specified values.
+ * @param factory the (possibly null)PoolableObjectFactory to use to create, validate and destroy objects
+ * @param maxActive the maximum number of objects that can be borrowed from me at one time (see {@link #setMaxActive})
+ * @param whenExhaustedAction the action to take when the pool is exhausted (see {@link #setWhenExhaustedAction})
+ * @param maxWait the maximum amount of time to wait for an idle object when the pool is exhausted an and whenExhaustedAction is {@link #WHEN_EXHAUSTED_BLOCK} (otherwise ignored) (see {@link #setMaxWait})
+ * @param maxIdle the maximum number of idle objects in my pool (see {@link #setMaxIdle})
+ * @param minIdle the minimum number of idle objects in my pool (see {@link #setMinIdle})
+ * @param testOnBorrow whether or not to validate objects before they are returned by the {@link #borrowObject} method (see {@link #setTestOnBorrow})
+ * @param testOnReturn whether or not to validate objects after they are returned to the {@link #returnObject} method (see {@link #setTestOnReturn})
+ * @param timeBetweenEvictionRunsMillis the amount of time (in milliseconds) to sleep between examining idle objects for eviction (see {@link #setTimeBetweenEvictionRunsMillis})
+ * @param numTestsPerEvictionRun the number of idle objects to examine per run within the idle object eviction thread (if any) (see {@link #setNumTestsPerEvictionRun})
+ * @param minEvictableIdleTimeMillis the minimum number of milliseconds an object can sit idle in the pool before it is eligable for evcition (see {@link #setMinEvictableIdleTimeMillis})
+ * @param testWhileIdle whether or not to validate objects in the idle object eviction thread, if any (see {@link #setTestWhileIdle})
+ * @param softMinEvictableIdleTimeMillis the minimum number of milliseconds an object can sit idle in the pool before it is eligable for evcition with the extra condition that at least "minIdle" amount of object remain in the pool. (see {@link #setSoftMinEvictableIdleTimeMillis})
+ */
+ public GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle, int minIdle, boolean testOnBorrow, boolean testOnReturn, long timeBetweenEvictionRunsMillis, int numTestsPerEvictionRun, long minEvictableIdleTimeMillis, boolean testWhileIdle, long softMinEvictableIdleTimeMillis) {
+ _factory = factory;
+ _maxActive = maxActive;
+ switch(whenExhaustedAction) {
+ case WHEN_EXHAUSTED_BLOCK:
+ case WHEN_EXHAUSTED_FAIL:
+ case WHEN_EXHAUSTED_GROW:
+ _whenExhaustedAction = whenExhaustedAction;
+ break;
+ default:
+ throw new IllegalArgumentException("whenExhaustedAction " + whenExhaustedAction + " not recognized.");
+ }
+ _maxWait = maxWait;
+ _maxIdle = maxIdle;
+ _minIdle = minIdle;
+ _testOnBorrow = testOnBorrow;
+ _testOnReturn = testOnReturn;
+ _timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
+ _numTestsPerEvictionRun = numTestsPerEvictionRun;
+ _minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
+ _softMinEvictableIdleTimeMillis = softMinEvictableIdleTimeMillis;
+ _testWhileIdle = testWhileIdle;
+
+ _pool = new LinkedList();
+ startEvictor(_timeBetweenEvictionRunsMillis);
+ }
+
+ //--- public methods ---------------------------------------------
+
+ //--- configuration methods --------------------------------------
+
+ /**
+ * Returns the cap on the total number of active instances from my pool.
+ * @return the cap on the total number of active instances from my pool.
+ * @see #setMaxActive
+ */
+ public synchronized int getMaxActive() {
+ return _maxActive;
+ }
+
+ /**
+ * Sets the cap on the total number of active instances from my pool.
+ * @param maxActive The cap on the total number of active instances from my pool.
+ * Use a negative value for an infinite number of instances.
+ * @see #getMaxActive
+ */
+ public synchronized void setMaxActive(int maxActive) {
+ _maxActive = maxActive;
+ notifyAll();
+ }
+
+ /**
+ * Returns the action to take when the {@link #borrowObject} method
+ * is invoked when the pool is exhausted (the maximum number
+ * of "active" objects has been reached).
+ *
+ * @return one of {@link #WHEN_EXHAUSTED_BLOCK}, {@link #WHEN_EXHAUSTED_FAIL} or {@link #WHEN_EXHAUSTED_GROW}
+ * @see #setWhenExhaustedAction
+ */
+ public synchronized byte getWhenExhaustedAction() {
+ return _whenExhaustedAction;
+ }
+
+ /**
+ * Sets the action to take when the {@link #borrowObject} method
+ * is invoked when the pool is exhausted (the maximum number
+ * of "active" objects has been reached).
+ *
+ * @param whenExhaustedAction the action code, which must be one of
+ * {@link #WHEN_EXHAUSTED_BLOCK}, {@link #WHEN_EXHAUSTED_FAIL},
+ * or {@link #WHEN_EXHAUSTED_GROW}
+ * @see #getWhenExhaustedAction
+ */
+ public synchronized void setWhenExhaustedAction(byte whenExhaustedAction) {
+ switch(whenExhaustedAction) {
+ case WHEN_EXHAUSTED_BLOCK:
+ case WHEN_EXHAUSTED_FAIL:
+ case WHEN_EXHAUSTED_GROW:
+ _whenExhaustedAction = whenExhaustedAction;
+ notifyAll();
+ break;
+ default:
+ throw new IllegalArgumentException("whenExhaustedAction " + whenExhaustedAction + " not recognized.");
+ }
+ }
+
+
+ /**
+ * Returns the maximum amount of time (in milliseconds) the
+ * {@link #borrowObject} method should block before throwing
+ * an exception when the pool is exhausted and the
+ * {@link #setWhenExhaustedAction "when exhausted" action} is
+ * {@link #WHEN_EXHAUSTED_BLOCK}.
+ *
+ * When less than 0, the {@link #borrowObject} method
+ * may block indefinitely.
+ *
+ * @see #setMaxWait
+ * @see #setWhenExhaustedAction
+ * @see #WHEN_EXHAUSTED_BLOCK
+ */
+ public synchronized long getMaxWait() {
+ return _maxWait;
+ }
+
+ /**
+ * Sets the maximum amount of time (in milliseconds) the
+ * {@link #borrowObject} method should block before throwing
+ * an exception when the pool is exhausted and the
+ * {@link #setWhenExhaustedAction "when exhausted" action} is
+ * {@link #WHEN_EXHAUSTED_BLOCK}.
+ *
+ * When less than 0, the {@link #borrowObject} method
+ * may block indefinitely.
+ *
+ * @see #getMaxWait
+ * @see #setWhenExhaustedAction
+ * @see #WHEN_EXHAUSTED_BLOCK
+ */
+ public synchronized void setMaxWait(long maxWait) {
+ _maxWait = maxWait;
+ notifyAll();
+ }
+
+ /**
+ * Returns the cap on the number of "idle" instances in the pool.
+ * @return the cap on the number of "idle" instances in the pool.
+ * @see #setMaxIdle
+ */
+ public synchronized int getMaxIdle() {
+ return _maxIdle;
+ }
+
+ /**
+ * Sets the cap on the number of "idle" instances in the pool.
+ * @param maxIdle The cap on the number of "idle" instances in the pool.
+ * Use a negative value to indicate an unlimited number
+ * of idle instances.
+ * @see #getMaxIdle
+ */
+ public synchronized void setMaxIdle(int maxIdle) {
+ _maxIdle = maxIdle;
+ notifyAll();
+ }
+
+ /**
+ * Sets the minimum number of objects allowed in the pool
+ * before the evictor thread (if active) spawns new objects.
+ * (Note no objects are created when: numActive + numIdle >= maxActive)
+ *
+ * @param minIdle The minimum number of objects.
+ * @see #getMinIdle
+ */
+ public synchronized void setMinIdle(int minIdle) {
+ _minIdle = minIdle;
+ notifyAll();
+ }
+
+ /**
+ * Returns the minimum number of objects allowed in the pool
+ * before the evictor thread (if active) spawns new objects.
+ * (Note no objects are created when: numActive + numIdle >= maxActive)
+ *
+ * @return The minimum number of objects.
+ * @see #setMinIdle
+ */
+ public synchronized int getMinIdle() {
+ return _minIdle;
+ }
+
+ /**
+ * When true, objects will be
+ * {@link PoolableObjectFactory#validateObject validated}
+ * before being returned by the {@link #borrowObject}
+ * method. If the object fails to validate,
+ * it will be dropped from the pool, and we will attempt
+ * to borrow another.
+ *
+ * @see #setTestOnBorrow
+ */
+ public synchronized boolean getTestOnBorrow() {
+ return _testOnBorrow;
+ }
+
+ /**
+ * When true, objects will be
+ * {@link PoolableObjectFactory#validateObject validated}
+ * before being returned by the {@link #borrowObject}
+ * method. If the object fails to validate,
+ * it will be dropped from the pool, and we will attempt
+ * to borrow another.
+ *
+ * @see #getTestOnBorrow
+ */
+ public synchronized void setTestOnBorrow(boolean testOnBorrow) {
+ _testOnBorrow = testOnBorrow;
+ }
+
+ /**
+ * When true, objects will be
+ * {@link PoolableObjectFactory#validateObject validated}
+ * before being returned to the pool within the
+ * {@link #returnObject}.
+ *
+ * @see #setTestOnReturn
+ */
+ public synchronized boolean getTestOnReturn() {
+ return _testOnReturn;
+ }
+
+ /**
+ * When true, objects will be
+ * {@link PoolableObjectFactory#validateObject validated}
+ * before being returned to the pool within the
+ * {@link #returnObject}.
+ *
+ * @see #getTestOnReturn
+ */
+ public synchronized void setTestOnReturn(boolean testOnReturn) {
+ _testOnReturn = testOnReturn;
+ }
+
+ /**
+ * Returns the number of milliseconds to sleep between runs of the
+ * idle object evictor thread.
+ * When non-positive, no idle object evictor thread will be
+ * run.
+ *
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ public synchronized long getTimeBetweenEvictionRunsMillis() {
+ return _timeBetweenEvictionRunsMillis;
+ }
+
+ /**
+ * Sets the number of milliseconds to sleep between runs of the
+ * idle object evictor thread.
+ * When non-positive, no idle object evictor thread will be
+ * run.
+ *
+ * @see #getTimeBetweenEvictionRunsMillis
+ */
+ public synchronized void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) {
+ _timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
+ startEvictor(_timeBetweenEvictionRunsMillis);
+ }
+
+ /**
+ * Returns the max number of objects to examine during each run of the
+ * idle object evictor thread (if any).
+ *
+ * @see #setNumTestsPerEvictionRun
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ public synchronized int getNumTestsPerEvictionRun() {
+ return _numTestsPerEvictionRun;
+ }
+
+ /**
+ * Sets the max number of objects to examine during each run of the
+ * idle object evictor thread (if any).
+ *
+ * When a negative value is supplied, ceil({@link #getNumIdle})/abs({@link #getNumTestsPerEvictionRun})
+ * tests will be run. I.e., when the value is -n, roughly one nth of the
+ * idle objects will be tested per run.
+ *
+ * @see #getNumTestsPerEvictionRun
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ public synchronized void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) {
+ _numTestsPerEvictionRun = numTestsPerEvictionRun;
+ }
+
+ /**
+ * Returns the minimum amount of time an object may sit idle in the pool
+ * before it is eligable for eviction by the idle object evictor
+ * (if any).
+ *
+ * @see #setMinEvictableIdleTimeMillis
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ public synchronized long getMinEvictableIdleTimeMillis() {
+ return _minEvictableIdleTimeMillis;
+ }
+
+ /**
+ * Sets the minimum amount of time an object may sit idle in the pool
+ * before it is eligable for eviction by the idle object evictor
+ * (if any).
+ * When non-positive, no objects will be evicted from the pool
+ * due to idle time alone.
+ *
+ * @see #getMinEvictableIdleTimeMillis
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ public synchronized void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) {
+ _minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
+ }
+
+ /**
+ * Returns the minimum amount of time an object may sit idle in the pool
+ * before it is eligable for eviction by the idle object evictor
+ * (if any), with the extra condition that at least
+ * "minIdle" amount of object remain in the pool.
+ *
+ * @see #setSoftMinEvictableIdleTimeMillis
+ */
+ public synchronized long getSoftMinEvictableIdleTimeMillis() {
+ return _softMinEvictableIdleTimeMillis;
+ }
+
+ /**
+ * Sets the minimum amount of time an object may sit idle in the pool
+ * before it is eligable for eviction by the idle object evictor
+ * (if any), with the extra condition that at least
+ * "minIdle" amount of object remain in the pool.
+ * When non-positive, no objects will be evicted from the pool
+ * due to idle time alone.
+ *
+ * @see #getSoftMinEvictableIdleTimeMillis
+ */
+ public synchronized void setSoftMinEvictableIdleTimeMillis(long softMinEvictableIdleTimeMillis) {
+ _softMinEvictableIdleTimeMillis = softMinEvictableIdleTimeMillis;
+ }
+
+ /**
+ * When true, objects will be
+ * {@link PoolableObjectFactory#validateObject validated}
+ * by the idle object evictor (if any). If an object
+ * fails to validate, it will be dropped from the pool.
+ *
+ * @see #setTestWhileIdle
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ public synchronized boolean getTestWhileIdle() {
+ return _testWhileIdle;
+ }
+
+ /**
+ * When true, objects will be
+ * {@link PoolableObjectFactory#validateObject validated}
+ * by the idle object evictor (if any). If an object
+ * fails to validate, it will be dropped from the pool.
+ *
+ * @see #getTestWhileIdle
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ public synchronized void setTestWhileIdle(boolean testWhileIdle) {
+ _testWhileIdle = testWhileIdle;
+ }
+
+ /**
+ * Sets my configuration.
+ * @see GenericObjectPool.Config
+ */
+ public synchronized void setConfig(GenericObjectPool.Config conf) {
+ setMaxIdle(conf.maxIdle);
+ setMinIdle(conf.minIdle);
+ setMaxActive(conf.maxActive);
+ setMaxWait(conf.maxWait);
+ setWhenExhaustedAction(conf.whenExhaustedAction);
+ setTestOnBorrow(conf.testOnBorrow);
+ setTestOnReturn(conf.testOnReturn);
+ setTestWhileIdle(conf.testWhileIdle);
+ setNumTestsPerEvictionRun(conf.numTestsPerEvictionRun);
+ setMinEvictableIdleTimeMillis(conf.minEvictableIdleTimeMillis);
+ setTimeBetweenEvictionRunsMillis(conf.timeBetweenEvictionRunsMillis);
+ notifyAll();
+ }
+
+ //-- ObjectPool methods ------------------------------------------
+
+ public synchronized Object borrowObject() throws Exception {
+ assertOpen();
+ long starttime = System.currentTimeMillis();
+ for(;;) {
+ ObjectTimestampPair pair = null;
+
+ // if there are any sleeping, just grab one of those
+ try {
+ pair = (ObjectTimestampPair)(_pool.removeFirst());
+ } catch(NoSuchElementException e) {
+ ; /* ignored */
+ }
+
+ // otherwise
+ if(null == pair) {
+ // check if we can create one
+ // (note we know that the num sleeping is 0, else we wouldn't be here)
+ if(_maxActive < 0 || _numActive < _maxActive) {
+ // allow new object to be created
+ } else {
+ // the pool is exhausted
+ switch(_whenExhaustedAction) {
+ case WHEN_EXHAUSTED_GROW:
+ // allow new object to be created
+ break;
+ case WHEN_EXHAUSTED_FAIL:
+ throw new NoSuchElementException("Pool exhausted");
+ case WHEN_EXHAUSTED_BLOCK:
+ try {
+ if(_maxWait <= 0) {
+ wait();
+ } else {
+ // this code may be executed again after a notify then continue cycle
+ // so, need to calculate the amount of time to wait
+ final long elapsed = (System.currentTimeMillis() - starttime);
+ final long waitTime = _maxWait - elapsed;
+ if (waitTime > 0)
+ {
+ wait(waitTime);
+ }
+ }
+ } catch(InterruptedException e) {
+ // ignored
+ }
+ if(_maxWait > 0 && ((System.currentTimeMillis() - starttime) >= _maxWait)) {
+ throw new NoSuchElementException("Timeout waiting for idle object");
+ } else {
+ continue; // keep looping
+ }
+ default:
+ throw new IllegalArgumentException("WhenExhaustedAction property " + _whenExhaustedAction + " not recognized.");
+ }
+ }
+ }
+ _numActive++;
+
+ // create new object when needed
+ boolean newlyCreated = false;
+ if(null == pair) {
+ try {
+ Object obj = _factory.makeObject();
+ pair = new ObjectTimestampPair(obj);
+ newlyCreated = true;
+ } finally {
+ if (!newlyCreated) {
+ // object cannot be created
+ _numActive--;
+ notifyAll();
+ }
+ }
+ }
+
+ // activate & validate the object
+ try {
+ _factory.activateObject(pair.value);
+ if(_testOnBorrow && !_factory.validateObject(pair.value)) {
+ throw new Exception("ValidateObject failed");
+ }
+ return pair.value;
+ }
+ catch (Throwable e) {
+ // object cannot be activated or is invalid
+ _numActive--;
+ notifyAll();
+ try {
+ _factory.destroyObject(pair.value);
+ }
+ catch (Throwable e2) {
+ // cannot destroy broken object
+ }
+ if(newlyCreated) {
+ throw new NoSuchElementException("Could not create a validated object, cause: " + e.getMessage());
+ }
+ else {
+ continue; // keep looping
+ }
+ }
+ }
+ }
+
+ public synchronized void invalidateObject(Object obj) throws Exception {
+ assertOpen();
+ try {
+ _factory.destroyObject(obj);
+ }
+ finally {
+ _numActive--;
+ notifyAll(); // _numActive has changed
+ }
+ }
+
+ public synchronized void clear() {
+ assertOpen();
+ for(Iterator it = _pool.iterator(); it.hasNext(); ) {
+ try {
+ _factory.destroyObject(((ObjectTimestampPair)(it.next())).value);
+ } catch(Exception e) {
+ // ignore error, keep destroying the rest
+ }
+ it.remove();
+ }
+ _pool.clear();
+ notifyAll(); // num sleeping has changed
+ }
+
+ public synchronized int getNumActive() {
+ assertOpen();
+ return _numActive;
+ }
+
+ public synchronized int getNumIdle() {
+ assertOpen();
+ return _pool.size();
+ }
+
+ public synchronized void returnObject(Object obj) throws Exception {
+ assertOpen();
+ addObjectToPool(obj, true);
+ }
+
+ private void addObjectToPool(Object obj, boolean decrementNumActive) throws Exception {
+ boolean success = true;
+ if(_testOnReturn && !(_factory.validateObject(obj))) {
+ success = false;
+ } else {
+ try {
+ _factory.passivateObject(obj);
+ } catch(Exception e) {
+ success = false;
+ }
+ }
+
+ boolean shouldDestroy = !success;
+
+ if (decrementNumActive) {
+ _numActive--;
+ }
+ if((_maxIdle >= 0) && (_pool.size() >= _maxIdle)) {
+ shouldDestroy = true;
+ } else if(success) {
+ _pool.addLast(new ObjectTimestampPair(obj));
+ }
+ notifyAll(); // _numActive has changed
+
+ if(shouldDestroy) {
+ try {
+ _factory.destroyObject(obj);
+ } catch(Exception e) {
+ // ignored
+ }
+ }
+ }
+
+ public synchronized void close() throws Exception {
+ clear();
+ _pool = null;
+ _factory = null;
+ startEvictor(-1L);
+ super.close();
+ }
+
+ public synchronized void setFactory(PoolableObjectFactory factory) throws IllegalStateException {
+ assertOpen();
+ if(0 < getNumActive()) {
+ throw new IllegalStateException("Objects are already active");
+ } else {
+ clear();
+ _factory = factory;
+ }
+ }
+
+ public synchronized void evict() throws Exception {
+ assertOpen();
+ if(!_pool.isEmpty()) {
+ ListIterator iter;
+ if (evictLastIndex < 0) {
+ iter = _pool.listIterator(_pool.size());
+ } else {
+ iter = _pool.listIterator(evictLastIndex);
+ }
+ for(int i=0,m=getNumTests();i 0)
+ && (idleTimeMilis > _minEvictableIdleTimeMillis)) {
+ removeObject = true;
+ } else if ((_softMinEvictableIdleTimeMillis > 0)
+ && (idleTimeMilis > _softMinEvictableIdleTimeMillis)
+ && (getNumIdle() > getMinIdle())) {
+ removeObject = true;
+ }
+ if(_testWhileIdle && !removeObject) {
+ boolean active = false;
+ try {
+ _factory.activateObject(pair.value);
+ active = true;
+ } catch(Exception e) {
+ removeObject=true;
+ }
+ if(active) {
+ if(!_factory.validateObject(pair.value)) {
+ removeObject=true;
+ } else {
+ try {
+ _factory.passivateObject(pair.value);
+ } catch(Exception e) {
+ removeObject=true;
+ }
+ }
+ }
+ }
+ if(removeObject) {
+ try {
+ iter.remove();
+ _factory.destroyObject(pair.value);
+ } catch(Exception e) {
+ // ignored
+ }
+ }
+ }
+ evictLastIndex = iter.previousIndex(); // resume from here
+ } // if !empty
+ }
+
+ /**
+ * Check to see if we are below our minimum number of objects
+ * if so enough to bring us back to our minimum.
+ */
+ private void ensureMinIdle() throws Exception {
+ // this method isn't synchronized so the
+ // calculateDeficit is done at the beginning
+ // as a loop limit and a second time inside the loop
+ // to stop when another thread already returned the
+ // needed objects
+ int objectDeficit = calculateDeficit();
+ for ( int j = 0 ; j < objectDeficit && calculateDeficit() > 0 ; j++ ) {
+ addObject();
+ }
+ }
+
+ private synchronized int calculateDeficit() {
+ int objectDeficit = getMinIdle() - getNumIdle();
+ if (_maxActive > 0) {
+ int growLimit = Math.max(0, getMaxActive() - getNumActive() - getNumIdle());
+ objectDeficit = Math.min(objectDeficit, growLimit);
+ }
+ return objectDeficit;
+ }
+
+ /**
+ * Create an object, and place it into the pool.
+ * addObject() is useful for "pre-loading" a pool with idle objects.
+ */
+ public synchronized void addObject() throws Exception {
+ assertOpen();
+ Object obj = _factory.makeObject();
+ addObjectToPool(obj, false);
+ }
+
+ //--- non-public methods ----------------------------------------
+
+ /**
+ * Start the eviction thread or service, or when
+ * delay is non-positive, stop it
+ * if it is already running.
+ */
+ protected synchronized void startEvictor(long delay) {
+ if(null != _evictor) {
+ _evictor.cancel();
+ _evictor = null;
+ }
+ if(delay > 0) {
+ _evictor = new Evictor();
+ EVICTION_TIMER.schedule(_evictor, delay, delay);
+ }
+ }
+
+ synchronized String debugInfo() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("Active: ").append(getNumActive()).append("\n");
+ buf.append("Idle: ").append(getNumIdle()).append("\n");
+ buf.append("Idle Objects:\n");
+ Iterator it = _pool.iterator();
+ long time = System.currentTimeMillis();
+ while(it.hasNext()) {
+ ObjectTimestampPair pair = (ObjectTimestampPair)(it.next());
+ buf.append("\t").append(pair.value).append("\t").append(time - pair.tstamp).append("\n");
+ }
+ return buf.toString();
+ }
+
+ private int getNumTests() {
+ if(_numTestsPerEvictionRun >= 0) {
+ return Math.min(_numTestsPerEvictionRun, _pool.size());
+ } else {
+ return(int)(Math.ceil((double)_pool.size()/Math.abs((double)_numTestsPerEvictionRun)));
+ }
+ }
+
+ //--- inner classes ----------------------------------------------
+
+ /**
+ * The idle object evictor {@link TimerTask}.
+ * @see GenericObjectPool#setTimeBetweenEvictionRunsMillis
+ */
+ private class Evictor extends TimerTask {
+ public void run() {
+ try {
+ evict();
+ } catch(Exception e) {
+ // ignored
+ }
+ try {
+ ensureMinIdle();
+ } catch(Exception e) {
+ // ignored
+ }
+ }
+ }
+
+ /**
+ * A simple "struct" encapsulating the
+ * configuration information for a {@link GenericObjectPool}.
+ * @see GenericObjectPool#GenericObjectPool(org.apache.commons.pool.PoolableObjectFactory,org.apache.commons.pool.impl.GenericObjectPool.Config)
+ * @see GenericObjectPool#setConfig
+ */
+ public static class Config {
+ public int maxIdle = GenericObjectPool.DEFAULT_MAX_IDLE;
+ public int minIdle = GenericObjectPool.DEFAULT_MIN_IDLE;
+ public int maxActive = GenericObjectPool.DEFAULT_MAX_ACTIVE;
+ public long maxWait = GenericObjectPool.DEFAULT_MAX_WAIT;
+ public byte whenExhaustedAction = GenericObjectPool.DEFAULT_WHEN_EXHAUSTED_ACTION;
+ public boolean testOnBorrow = GenericObjectPool.DEFAULT_TEST_ON_BORROW;
+ public boolean testOnReturn = GenericObjectPool.DEFAULT_TEST_ON_RETURN;
+ public boolean testWhileIdle = GenericObjectPool.DEFAULT_TEST_WHILE_IDLE;
+ public long timeBetweenEvictionRunsMillis = GenericObjectPool.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
+ public int numTestsPerEvictionRun = GenericObjectPool.DEFAULT_NUM_TESTS_PER_EVICTION_RUN;
+ public long minEvictableIdleTimeMillis = GenericObjectPool.DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
+ public long softMinEvictableIdleTimeMillis = GenericObjectPool.DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
+ }
+
+ //--- private attributes ---------------------------------------
+
+ /**
+ * The cap on the number of idle instances in the pool.
+ * @see #setMaxIdle
+ * @see #getMaxIdle
+ */
+ private int _maxIdle = DEFAULT_MAX_IDLE;
+
+ /**
+ * The cap on the minimum number of idle instances in the pool.
+ * @see #setMinIdle
+ * @see #getMinIdle
+ */
+ private int _minIdle = DEFAULT_MIN_IDLE;
+
+ /**
+ * The cap on the total number of active instances from the pool.
+ * @see #setMaxActive
+ * @see #getMaxActive
+ */
+ protected int _maxActive = DEFAULT_MAX_ACTIVE;
+
+ /**
+ * The maximum amount of time (in millis) the
+ * {@link #borrowObject} method should block before throwing
+ * an exception when the pool is exhausted and the
+ * {@link #getWhenExhaustedAction "when exhausted" action} is
+ * {@link #WHEN_EXHAUSTED_BLOCK}.
+ *
+ * When less than 0, the {@link #borrowObject} method
+ * may block indefinitely.
+ *
+ * @see #setMaxWait
+ * @see #getMaxWait
+ * @see #WHEN_EXHAUSTED_BLOCK
+ * @see #setWhenExhaustedAction
+ * @see #getWhenExhaustedAction
+ */
+ protected long _maxWait = DEFAULT_MAX_WAIT;
+
+ /**
+ * The action to take when the {@link #borrowObject} method
+ * is invoked when the pool is exhausted (the maximum number
+ * of "active" objects has been reached).
+ *
+ * @see #WHEN_EXHAUSTED_BLOCK
+ * @see #WHEN_EXHAUSTED_FAIL
+ * @see #WHEN_EXHAUSTED_GROW
+ * @see #DEFAULT_WHEN_EXHAUSTED_ACTION
+ * @see #setWhenExhaustedAction
+ * @see #getWhenExhaustedAction
+ */
+ protected byte _whenExhaustedAction = DEFAULT_WHEN_EXHAUSTED_ACTION;
+
+ /**
+ * When true, objects will be
+ * {@link PoolableObjectFactory#validateObject validated}
+ * before being returned by the {@link #borrowObject}
+ * method. If the object fails to validate,
+ * it will be dropped from the pool, and we will attempt
+ * to borrow another.
+ *
+ * @see #setTestOnBorrow
+ * @see #getTestOnBorrow
+ */
+ protected boolean _testOnBorrow = DEFAULT_TEST_ON_BORROW;
+
+ /**
+ * When true, objects will be
+ * {@link PoolableObjectFactory#validateObject validated}
+ * before being returned to the pool within the
+ * {@link #returnObject}.
+ *
+ * @see #getTestOnReturn
+ * @see #setTestOnReturn
+ */
+ private boolean _testOnReturn = DEFAULT_TEST_ON_RETURN;
+
+ /**
+ * When true, objects will be
+ * {@link PoolableObjectFactory#validateObject validated}
+ * by the idle object evictor (if any). If an object
+ * fails to validate, it will be dropped from the pool.
+ *
+ * @see #setTestWhileIdle
+ * @see #getTestWhileIdle
+ * @see #getTimeBetweenEvictionRunsMillis
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ private boolean _testWhileIdle = DEFAULT_TEST_WHILE_IDLE;
+
+ /**
+ * The number of milliseconds to sleep between runs of the
+ * idle object evictor thread.
+ * When non-positive, no idle object evictor thread will be
+ * run.
+ *
+ * @see #setTimeBetweenEvictionRunsMillis
+ * @see #getTimeBetweenEvictionRunsMillis
+ */
+ private long _timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
+
+ /**
+ * The max number of objects to examine during each run of the
+ * idle object evictor thread (if any).
+ *
+ * When a negative value is supplied, ceil({@link #getNumIdle})/abs({@link #getNumTestsPerEvictionRun})
+ * tests will be run. I.e., when the value is -n, roughly one nth of the
+ * idle objects will be tested per run.
+ *
+ * @see #setNumTestsPerEvictionRun
+ * @see #getNumTestsPerEvictionRun
+ * @see #getTimeBetweenEvictionRunsMillis
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ private int _numTestsPerEvictionRun = DEFAULT_NUM_TESTS_PER_EVICTION_RUN;
+
+ /**
+ * The minimum amount of time an object may sit idle in the pool
+ * before it is eligable for eviction by the idle object evictor
+ * (if any).
+ * When non-positive, no objects will be evicted from the pool
+ * due to idle time alone.
+ *
+ * @see #setMinEvictableIdleTimeMillis
+ * @see #getMinEvictableIdleTimeMillis
+ * @see #getTimeBetweenEvictionRunsMillis
+ * @see #setTimeBetweenEvictionRunsMillis
+ */
+ private long _minEvictableIdleTimeMillis = DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
+
+ /**
+ * The minimum amount of time an object may sit idle in the pool
+ * before it is eligable for eviction by the idle object evictor
+ * (if any), with the extra condition that at least
+ * "minIdle" amount of object remain in the pool.
+ * When non-positive, no objects will be evicted from the pool
+ * due to idle time alone.
+ *
+ * @see #setSoftMinEvictableIdleTimeMillis
+ * @see #getSoftMinEvictableIdleTimeMillis
+ */
+ private long _softMinEvictableIdleTimeMillis = DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
+
+ /** My pool. */
+ protected LinkedList _pool = null;
+
+ /** My {@link PoolableObjectFactory}. */
+ protected PoolableObjectFactory _factory = null;
+
+ /**
+ * The number of objects {@link #borrowObject} borrowed
+ * from the pool, but not yet returned.
+ */
+ protected int _numActive = 0;
+
+ /**
+ * My idle object eviction {@link TimerTask}, if any.
+ */
+ private Evictor _evictor = null;
+
+ /**
+ * Position in the _pool where the _evictor last stopped.
+ */
+ private int evictLastIndex = -1;
+}
diff --git a/commons/src/main/java/org/archive/bdb/BdbModule.java b/commons/src/main/java/org/archive/bdb/BdbModule.java
new file mode 100644
index 00000000..2655b6a0
--- /dev/null
+++ b/commons/src/main/java/org/archive/bdb/BdbModule.java
@@ -0,0 +1,637 @@
+/*
+ * This file is part of the Heritrix web crawler (crawler.archive.org).
+ *
+ * Licensed to the Internet Archive (IA) by one or more individual
+ * contributors.
+ *
+ * The IA licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.archive.bdb;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.commons.io.FileUtils;
+import org.archive.checkpointing.CheckpointRecovery;
+import org.archive.checkpointing.Checkpointable;
+import org.archive.checkpointing.RecoverAction;
+import org.archive.spring.ConfigPath;
+import org.archive.util.CachedBdbMap;
+import org.archive.util.bdbje.EnhancedEnvironment;
+import org.springframework.context.Lifecycle;
+
+import com.sleepycat.bind.serial.StoredClassCatalog;
+import com.sleepycat.je.CheckpointConfig;
+import com.sleepycat.je.Database;
+import com.sleepycat.je.DatabaseConfig;
+import com.sleepycat.je.DatabaseException;
+import com.sleepycat.je.DatabaseNotFoundException;
+import com.sleepycat.je.DbInternal;
+import com.sleepycat.je.EnvironmentConfig;
+import com.sleepycat.je.SecondaryConfig;
+import com.sleepycat.je.SecondaryDatabase;
+import com.sleepycat.je.SecondaryKeyCreator;
+import com.sleepycat.je.dbi.EnvironmentImpl;
+import com.sleepycat.je.utilint.DbLsn;
+
+/**
+ * Utility module for managing a shared BerkeleyDB-JE environment
+ *
+ * @contributor pjack
+ * @contributor gojomo
+ */
+public class BdbModule implements Lifecycle, Checkpointable,
+Serializable, Closeable {
+ private static final long serialVersionUID = 1L;
+ final private static Logger LOGGER =
+ Logger.getLogger(BdbModule.class.getName());
+
+
+ private static class DatabasePlusConfig implements Serializable {
+ private static final long serialVersionUID = 1L;
+ public transient Database database;
+ public String name;
+ public String primaryName;
+ public BdbConfig config;
+ }
+
+
+ /**
+ * Configuration object for databases. Needed because
+ * {@link DatabaseConfig} is not serializable. Also it prevents invalid
+ * configurations. (All databases opened through this module must be
+ * deferred-write, because otherwise they can't sync(), and you can't
+ * run a checkpoint without doing sync() first.)
+ *
+ * @author pjack
+ *
+ */
+ public static class BdbConfig implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ boolean allowCreate;
+ boolean sortedDuplicates;
+ boolean transactional;
+
+
+ public BdbConfig() {
+ }
+
+
+ public boolean isAllowCreate() {
+ return allowCreate;
+ }
+
+
+ public void setAllowCreate(boolean allowCreate) {
+ this.allowCreate = allowCreate;
+ }
+
+
+ public boolean getSortedDuplicates() {
+ return sortedDuplicates;
+ }
+
+
+ public void setSortedDuplicates(boolean sortedDuplicates) {
+ this.sortedDuplicates = sortedDuplicates;
+ }
+
+ public DatabaseConfig toDatabaseConfig() {
+ DatabaseConfig result = new DatabaseConfig();
+ result.setDeferredWrite(true);
+ result.setTransactional(transactional);
+ result.setAllowCreate(allowCreate);
+ result.setSortedDuplicates(sortedDuplicates);
+ return result;
+ }
+
+
+ public boolean isTransactional() {
+ return transactional;
+ }
+
+
+ public void setTransactional(boolean transactional) {
+ this.transactional = transactional;
+ }
+ }
+
+
+ public static class SecondaryBdbConfig extends BdbConfig {
+ private static final long serialVersionUID = 1L;
+
+ private SecondaryKeyCreator keyCreator;
+
+ public SecondaryBdbConfig() {
+ }
+
+ public SecondaryKeyCreator getKeyCreator() {
+ return keyCreator;
+ }
+
+ public void setKeyCreator(SecondaryKeyCreator keyCreator) {
+ this.keyCreator = keyCreator;
+ }
+
+ public SecondaryConfig toSecondaryConfig() {
+ SecondaryConfig result = new SecondaryConfig();
+ result.setDeferredWrite(true);
+ result.setTransactional(transactional);
+ result.setAllowCreate(allowCreate);
+ result.setSortedDuplicates(sortedDuplicates);
+ result.setKeyCreator(keyCreator);
+ return result;
+ }
+
+ }
+
+ protected ConfigPath dir = new ConfigPath("state subdirectory","state");
+ public ConfigPath getDir() {
+ return dir;
+ }
+ public void setDir(ConfigPath dir) {
+ this.dir = dir;
+ }
+
+ int cachePercent = 60;
+ public int getCachePercent() {
+ return cachePercent;
+ }
+ public void setCachePercent(int cachePercent) {
+ this.cachePercent = cachePercent;
+ }
+
+ boolean useSharedCache = true;
+ public boolean getUseSharedCache() {
+ return useSharedCache;
+ }
+ public void setUseSharedCache(boolean useSharedCache) {
+ this.useSharedCache = useSharedCache;
+ }
+
+ boolean checkpointCopyLogs = true;
+ public boolean getCheckpointCopyLogs() {
+ return checkpointCopyLogs;
+ }
+ public void setCheckpointCopyLogs(boolean checkpointCopyLogs) {
+ this.checkpointCopyLogs = checkpointCopyLogs;
+ }
+
+ private transient EnhancedEnvironment bdbEnvironment;
+
+ private transient StoredClassCatalog classCatalog;
+
+ @SuppressWarnings("unchecked")
+ private Map bigMaps =
+ new ConcurrentHashMap();
+
+ private Map databases =
+ new ConcurrentHashMap();
+
+ private transient Thread shutdownHook;
+
+ public BdbModule() {
+ }
+
+
+ public void start() {
+ if (isRunning()) {
+ return;
+ }
+ try {
+ setUp(getDir().getFile(), getCachePercent(), true, getUseSharedCache());
+ } catch (DatabaseException e) {
+ throw new IllegalStateException(e);
+ }
+ shutdownHook = new BdbShutdownHook(this);
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
+ }
+
+ public boolean isRunning() {
+ return shutdownHook!=null;
+ }
+
+ public void stop() {
+ if (!isRunning()) {
+ return;
+ }
+ close();
+ }
+
+ private void setUp(File f, int cachePercent, boolean create, boolean sharedCache)
+ throws DatabaseException {
+ EnvironmentConfig config = new EnvironmentConfig();
+ config.setAllowCreate(create);
+ config.setLockTimeout(5000000);
+ config.setCachePercent(cachePercent);
+ config.setSharedCache(sharedCache);
+
+ f.mkdirs();
+ this.bdbEnvironment = new EnhancedEnvironment(f, config);
+
+ this.classCatalog = this.bdbEnvironment.getClassCatalog();
+ }
+
+
+ public void closeDatabase(Database db) {
+ try {
+ closeDatabase(db.getDatabaseName());
+ } catch (DatabaseException e) {
+ LOGGER.log(Level.SEVERE, "Error getting db name", e);
+ }
+ }
+
+ public void closeDatabase(String name) {
+ DatabasePlusConfig dpc = databases.remove(name);
+ if (dpc == null) {
+ throw new IllegalStateException("No such database: " + name);
+ }
+ Database db = dpc.database;
+ try {
+ db.sync();
+ db.close();
+ } catch (DatabaseException e) {
+ LOGGER.log(Level.SEVERE, "Error closing db " + name, e);
+ }
+ }
+
+
+ public Database openDatabase(String name, BdbConfig config,
+ boolean recycle)
+ throws DatabaseException {
+ if (databases.containsKey(name)) {
+ throw new IllegalStateException("Database already exists: " +name);
+ }
+ if (!recycle) {
+ try {
+ bdbEnvironment.truncateDatabase(null, name, false);
+ } catch (DatabaseNotFoundException e) {
+ // Ignored
+ }
+ }
+ DatabasePlusConfig dpc = new DatabasePlusConfig();
+ dpc.database = bdbEnvironment.openDatabase(null, name, config.toDatabaseConfig());
+ dpc.name = name;
+ dpc.config = config;
+ databases.put(name, dpc);
+ return dpc.database;
+ }
+
+
+ public SecondaryDatabase openSecondaryDatabase(String name, Database db,
+ SecondaryBdbConfig config) throws DatabaseException {
+ if (databases.containsKey(name)) {
+ throw new IllegalStateException("Database already exists: " +name);
+ }
+ SecondaryDatabase result = bdbEnvironment.openSecondaryDatabase(null,
+ name, db, config.toSecondaryConfig());
+ DatabasePlusConfig dpc = new DatabasePlusConfig();
+ dpc.database = result;
+ dpc.name = name;
+ dpc.primaryName = db.getDatabaseName();
+ dpc.config = config;
+ databases.put(name, dpc);
+ return result;
+ }
+
+ public StoredClassCatalog getClassCatalog() {
+ return classCatalog;
+ }
+
+
+ public Map getBigMap(String dbName, boolean recycle,
+ Class super K> key, Class super V> value)
+ throws DatabaseException {
+ @SuppressWarnings("unchecked")
+ CachedBdbMap r = bigMaps.get(dbName);
+ if (r != null) {
+ return r;
+ }
+
+ if (!recycle) {
+ try {
+ bdbEnvironment.truncateDatabase(null, dbName, false);
+ } catch (DatabaseNotFoundException e) {
+ // ignored
+ }
+ }
+
+ r = new CachedBdbMap(dbName);
+
+ r.initialize(bdbEnvironment, key, value, classCatalog);
+ bigMaps.put(dbName, r);
+ return r;
+ }
+
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ }
+
+
+
+ @SuppressWarnings("unchecked")
+ private void readObject(ObjectInputStream in)
+ throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ if (in instanceof CheckpointRecovery) {
+// CheckpointRecovery cr = (CheckpointRecovery)in;
+// path = cr.translatePath(path);
+// cr.setState(this, DIR, path);
+ }
+ try {
+ setUp(getDir().getFile(), getCachePercent(), false, getUseSharedCache());
+ for (CachedBdbMap map: bigMaps.values()) {
+ map.initialize(
+ this.bdbEnvironment,
+ map.getKeyClass(),
+ map.getValueClass(),
+ this.classCatalog);
+ }
+ for (DatabasePlusConfig dpc: databases.values()) {
+ if (!(dpc.config instanceof SecondaryBdbConfig)) {
+ dpc.database = bdbEnvironment.openDatabase(null,
+ dpc.name, dpc.config.toDatabaseConfig());
+ }
+ }
+ for (DatabasePlusConfig dpc: databases.values()) {
+ if (dpc.config instanceof SecondaryBdbConfig) {
+ SecondaryBdbConfig conf = (SecondaryBdbConfig)dpc.config;
+ Database primary = databases.get(dpc.primaryName).database;
+ dpc.database = bdbEnvironment.openSecondaryDatabase(null,
+ dpc.name, primary, conf.toSecondaryConfig());
+ }
+ }
+ } catch (DatabaseException e) {
+ IOException io = new IOException();
+ io.initCause(e);
+ throw io;
+ }
+ this.shutdownHook = new BdbShutdownHook(this);
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
+ }
+
+
+ @SuppressWarnings("unchecked")
+ public void checkpoint(File dir, List actions)
+ throws IOException {
+ if (checkpointCopyLogs) {
+ actions.add(new BdbRecover(getDir().getFile().getAbsolutePath()));
+ }
+ // First sync bigMaps
+ for (Map.Entry me: bigMaps.entrySet()) {
+ me.getValue().sync();
+ }
+
+ EnvironmentConfig envConfig;
+ try {
+ // sync all databases
+ for (DatabasePlusConfig dbc: databases.values()) {
+ dbc.database.sync();
+ }
+ envConfig = bdbEnvironment.getConfig();
+ } catch (DatabaseException e) {
+ IOException io = new IOException();
+ io.initCause(e);
+ throw io;
+ }
+
+ final List bkgrdThreads = Arrays.asList(new String []
+ {"je.env.runCheckpointer", "je.env.runCleaner",
+ "je.env.runINCompressor"});
+ try {
+ // Disable background threads
+ setBdbjeBkgrdThreads(envConfig, bkgrdThreads, "false");
+ // Do a force checkpoint. Thats what a sync does (i.e. doSync).
+ CheckpointConfig chkptConfig = new CheckpointConfig();
+ chkptConfig.setForce(true);
+
+ // Mark Hayes of sleepycat says:
+ // "The default for this property is false, which gives the current
+ // behavior (allow deltas). If this property is true, deltas are
+ // prohibited -- full versions of internal nodes are always logged
+ // during the checkpoint. When a full version of an internal node
+ // is logged during a checkpoint, recovery does not need to process
+ // it at all. It is only fetched if needed by the application,
+ // during normal DB operations after recovery. When a delta of an
+ // internal node is logged during a checkpoint, recovery must
+ // process it by fetching the full version of the node from earlier
+ // in the log, and then applying the delta to it. This can be
+ // pretty slow, since it is potentially a large amount of
+ // random I/O."
+ chkptConfig.setMinimizeRecoveryTime(true);
+ bdbEnvironment.checkpoint(chkptConfig);
+ LOGGER.fine("Finished bdb checkpoint.");
+
+ // From the sleepycat folks: A trick for flipping db logs.
+ EnvironmentImpl envImpl =
+ DbInternal.envGetEnvironmentImpl(bdbEnvironment);
+ long firstFileInNextSet =
+ DbLsn.getFileNumber(envImpl.forceLogFileFlip());
+ // So the last file in the checkpoint is firstFileInNextSet - 1.
+ // Write manifest of all log files into the bdb directory.
+ final String lastBdbCheckpointLog =
+ getBdbLogFileName(firstFileInNextSet - 1);
+ processBdbLogs(dir, lastBdbCheckpointLog);
+ LOGGER.fine("Finished processing bdb log files.");
+ } catch (DatabaseException e) {
+ IOException io = new IOException();
+ io.initCause(e);
+ throw io;
+ } finally {
+ // Restore background threads.
+ setBdbjeBkgrdThreads(envConfig, bkgrdThreads, "true");
+ }
+ }
+
+
+ private void processBdbLogs(final File checkpointDir,
+ final String lastBdbCheckpointLog) throws IOException {
+ File bdbDir = getBdbSubDirectory(checkpointDir);
+ if (!bdbDir.exists()) {
+ bdbDir.mkdir();
+ }
+ PrintWriter pw = new PrintWriter(new FileOutputStream(new File(
+ checkpointDir, "bdbje-logs-manifest.txt")));
+ try {
+ // Don't copy any beyond the last bdb log file (bdbje can keep
+ // writing logs after checkpoint).
+ boolean pastLastLogFile = false;
+ Set srcFilenames = null;
+ do {
+ FilenameFilter filter = new FilenameFilter() {
+ public boolean accept(File dir, String name) {
+ return name != null
+ && name.toLowerCase().endsWith(".jdb");
+ }
+ };
+
+ srcFilenames =
+ new HashSet(Arrays.asList(getDir().getFile().list(filter)));
+ List tgtFilenames = Arrays.asList(bdbDir.list(filter));
+ if (tgtFilenames != null && tgtFilenames.size() > 0) {
+ srcFilenames.removeAll(tgtFilenames);
+ }
+ if (srcFilenames.size() > 0) {
+ // Sort files.
+ srcFilenames = new TreeSet(srcFilenames);
+ int count = 0;
+ for (final Iterator i = srcFilenames.iterator();
+ i.hasNext() && !pastLastLogFile;) {
+ String name = (String) i.next();
+ if (this.checkpointCopyLogs) {
+ FileUtils.copyDirectory(new File(getDir().getFile(), name),
+ new File(bdbDir, name));
+ }
+ pw.println(name);
+ if (name.equals(lastBdbCheckpointLog)) {
+ // We're done.
+ pastLastLogFile = true;
+ }
+ count++;
+ }
+ if (LOGGER.isLoggable(Level.FINE)) {
+ LOGGER.fine("Copied " + count);
+ }
+ }
+ } while (!pastLastLogFile && srcFilenames != null &&
+ srcFilenames.size() > 0);
+ } finally {
+ pw.close();
+ }
+ }
+
+
+
+ private void setBdbjeBkgrdThreads(final EnvironmentConfig config,
+ final List threads, final String setting) {
+ for (final Iterator i = threads.iterator(); i.hasNext();) {
+ config.setConfigParam((String)i.next(), setting);
+ }
+ }
+
+
+ private String getBdbLogFileName(final long index) {
+ String lastBdbLogFileHex = Long.toHexString(index);
+ StringBuffer buffer = new StringBuffer();
+ for (int i = 0; i < (8 - lastBdbLogFileHex.length()); i++) {
+ buffer.append('0');
+ }
+ buffer.append(lastBdbLogFileHex);
+ buffer.append(".jdb");
+ return buffer.toString();
+ }
+
+
+ public void close() {
+ close2();
+ Runtime.getRuntime().removeShutdownHook(shutdownHook);
+ shutdownHook = null;
+ }
+
+ @SuppressWarnings("unchecked")
+ void close2() {
+ if (classCatalog == null) {
+ return;
+ }
+ for (Map.Entry me: bigMaps.entrySet()) try {
+ me.getValue().close();
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "Error closing bigMap " + me.getKey(), e);
+ }
+
+ List dbNames = new ArrayList(databases.keySet());
+ for (String dbName: dbNames) try {
+ closeDatabase(dbName);
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "Error closing db " + dbName, e);
+ }
+
+ try {
+ this.bdbEnvironment.sync();
+ this.bdbEnvironment.close();
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, "Error closing environment.", e);
+ }
+ }
+
+
+ private static File getBdbSubDirectory(File checkpointDir) {
+ return new File(checkpointDir, "bdbje-logs");
+ }
+
+
+ public Database getDatabase(String name) {
+ DatabasePlusConfig dpc = databases.get(name);
+ if (dpc == null) {
+ return null;
+ }
+ return dpc.database;
+ }
+
+
+ private static class BdbRecover implements RecoverAction {
+
+ private static final long serialVersionUID = 1L;
+
+ private String path;
+
+ public BdbRecover(String path) {
+ this.path = path;
+ }
+
+ public void recoverFrom(File checkpointDir,
+ CheckpointRecovery recovery) throws Exception {
+ File bdbDir = getBdbSubDirectory(checkpointDir);
+ path = recovery.translatePath(path);
+ FileUtils.copyDirectory(bdbDir, new File(path));
+ }
+
+ }
+
+
+ private static class BdbShutdownHook extends Thread {
+
+ final private BdbModule bdb;
+
+
+ public BdbShutdownHook(BdbModule bdb) {
+ this.bdb = bdb;
+ }
+
+ public void run() {
+ this.bdb.close2();
+ }
+
+ }
+}
diff --git a/commons/src/main/java/org/archive/checkpointing/CheckpointInputStream.java b/commons/src/main/java/org/archive/checkpointing/CheckpointInputStream.java
new file mode 100644
index 00000000..371a9113
--- /dev/null
+++ b/commons/src/main/java/org/archive/checkpointing/CheckpointInputStream.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2007 Internet Archive.
+ *
+ * This file is part of the Heritrix web crawler (crawler.archive.org).
+ *
+ * Heritrix is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * any later version.
+ *
+ * Heritrix is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser Public License
+ * along with Heritrix; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * CheckpointInputStream.java
+ *
+ * Created on Mar 6, 2007
+ *
+ * $Id:$
+ */
+
+package org.archive.checkpointing;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.net.URI;
+
+
+/**
+ * Object input stream that provides information useful during checkpoint
+ * recovery.
+ *
+ * @author pjack
+ */
+public class CheckpointInputStream extends ObjectInputStream
+implements CheckpointRecovery {
+
+
+ final private CheckpointRecovery recovery;
+
+
+ public CheckpointInputStream(InputStream input,
+ CheckpointRecovery recovery) throws IOException {
+ super(input);
+ this.recovery = recovery;
+ }
+
+
+ public String getRecoveredJobName() {
+ return recovery.getRecoveredJobName();
+ }
+
+// public void setState(Object module, Key key, T value) {
+// recovery.setState(module, key, value);
+// }
+
+
+ public String translatePath(String path) {
+ return recovery.translatePath(path);
+ }
+
+
+ public URI translateURI(URI uri) {
+ return recovery.translateURI(uri);
+ }
+
+
+// public void apply(SingleSheet global) {
+// throw new UnsupportedOperationException();
+// }
+}
diff --git a/commons/src/main/java/org/archive/checkpointing/CheckpointRecovery.java b/commons/src/main/java/org/archive/checkpointing/CheckpointRecovery.java
new file mode 100644
index 00000000..8a78c31f
--- /dev/null
+++ b/commons/src/main/java/org/archive/checkpointing/CheckpointRecovery.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2007 Internet Archive.
+ *
+ * This file is part of the Heritrix web crawler (crawler.archive.org).
+ *
+ * Heritrix is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * any later version.
+ *
+ * Heritrix is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser Public License
+ * along with Heritrix; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * CheckpointStream.java
+ *
+ * Created on Mar 8, 2007
+ *
+ * $Id:$
+ */
+
+package org.archive.checkpointing;
+
+import java.net.URI;
+
+/**
+ * The information about a checkpoint recovery.
+ *
+ * @author pjack
+ */
+public interface CheckpointRecovery {
+
+ String getRecoveredJobName();
+
+ String translatePath(String path);
+
+ URI translateURI(URI uri);
+
+// void setState(Object module, Key key, T value);
+//
+// void apply(SingleSheet global);
+}
diff --git a/commons/src/main/java/org/archive/checkpointing/Checkpointable.java b/commons/src/main/java/org/archive/checkpointing/Checkpointable.java
new file mode 100644
index 00000000..a615609d
--- /dev/null
+++ b/commons/src/main/java/org/archive/checkpointing/Checkpointable.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2007 Internet Archive.
+ *
+ * This file is part of the Heritrix web crawler (crawler.archive.org).
+ *
+ * Heritrix is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * any later version.
+ *
+ * Heritrix is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser Public License
+ * along with Heritrix; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Checkpointable.java
+ *
+ * Created on Mar 1, 2007
+ *
+ * $Id:$
+ */
+
+package org.archive.checkpointing;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+
+/**
+ * @author pjack
+ *
+ */
+public interface Checkpointable {
+
+
+ void checkpoint(File dir, List actions) throws IOException;
+
+
+}
diff --git a/commons/src/main/java/org/archive/checkpointing/Checkpointer.java b/commons/src/main/java/org/archive/checkpointing/Checkpointer.java
new file mode 100644
index 00000000..4e2b999c
--- /dev/null
+++ b/commons/src/main/java/org/archive/checkpointing/Checkpointer.java
@@ -0,0 +1,120 @@
+/*
+ * This file is part of the Heritrix web crawler (crawler.archive.org).
+ *
+ * Licensed to the Internet Archive (IA) by one or more individual
+ * contributors.
+ *
+ * The IA licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.archive.checkpointing;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.archive.util.IoUtils;
+
+/**
+ * Executes checkpoints and recovers.
+ *
+ * @author pjack
+ */
+public class Checkpointer {
+
+ final public static String ACTIONS_FILE = "actions.serialized";
+
+ final public static String OBJECT_GRAPH_FILE = "object_graph.serialized";
+
+ private Checkpointer() {
+ }
+
+
+ public static void checkpoint(/*SheetManager*/Object mgr, File dir)
+ throws IOException {
+ List actions = new ArrayList();
+// for (Checkpointable c: mgr.getCheckpointables()) {
+// c.checkpoint(dir, actions);
+// }
+
+ writeObject(new File(dir, ACTIONS_FILE), actions);
+ writeObject(new File(dir, OBJECT_GRAPH_FILE), mgr);
+ }
+
+
+ private static void writeObject(File f, Object o)
+ throws IOException {
+ ObjectOutputStream oout = null;
+ try {
+ oout = new ObjectOutputStream(new FileOutputStream(f));
+ oout.writeObject(o);
+ } finally {
+ IoUtils.close(oout);
+ }
+
+ }
+
+
+ @SuppressWarnings("unused")
+ private static List readActions(File dir)
+ throws IOException {
+ File actionsFile = new File(dir, ACTIONS_FILE);
+ ObjectInputStream oinp = null;
+ try {
+ oinp = new ObjectInputStream(
+ new FileInputStream(actionsFile));
+ @SuppressWarnings("unchecked")
+ List actions = (List)oinp.readObject();
+ return actions;
+ } catch (ClassNotFoundException e) {
+ IOException io = new IOException();
+ io.initCause(e);
+ throw io;
+ } finally {
+ IoUtils.close(oinp);
+ }
+ }
+
+// public static SheetManager recover(File dir, CheckpointRecovery recovery)
+// throws IOException {
+// List actions = readActions(dir);
+// for (RecoverAction action: actions) try {
+// action.recoverFrom(dir, recovery);
+// } catch (Exception e) {
+// IOException io = new IOException();
+// io.initCause(e);
+// throw io;
+// }
+//
+// CheckpointInputStream cinp = null;
+// try {
+// File f = new File(dir, OBJECT_GRAPH_FILE);
+// cinp = new CheckpointInputStream(new FileInputStream(f), recovery);
+// SheetManager mgr = (SheetManager)cinp.readObject();
+// recovery.apply(mgr.getGlobalSheet());
+// return mgr;
+// } catch (ClassNotFoundException e) {
+// IOException io = new IOException();
+// io.initCause(e);
+// throw io;
+// }finally {
+// IoUtils.close(cinp);
+// }
+// }
+
+}
diff --git a/commons/src/main/java/org/archive/checkpointing/DefaultCheckpointRecovery.java b/commons/src/main/java/org/archive/checkpointing/DefaultCheckpointRecovery.java
new file mode 100644
index 00000000..a92e2b25
--- /dev/null
+++ b/commons/src/main/java/org/archive/checkpointing/DefaultCheckpointRecovery.java
@@ -0,0 +1,109 @@
+/*
+ * This file is part of the Heritrix web crawler (crawler.archive.org).
+ *
+ * Licensed to the Internet Archive (IA) by one or more individual
+ * contributors.
+ *
+ * The IA licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.archive.checkpointing;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Default implementation.
+ *
+ * @author pjack
+ *
+ */
+public class DefaultCheckpointRecovery implements CheckpointRecovery {
+
+
+// final private Map