diff --git a/pom.xml b/pom.xml
index 045026284d..b65610df90 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,17 +9,17 @@
com.ning
async-http-client
Asynchronous Http Client
- 1.7.6-SNAPSHOT
+ 1.7.22-SNAPSHOT
jar
Async Http Client library purpose is to allow Java applications to easily execute HTTP requests and
asynchronously process the HTTP responses.
- http://github.com/sonatype/async-http-client
+ http://github.com/AsyncHttpClient/async-http-client
- scm:git:git@github.com:sonatype/async-http-client.git
- https://github.com/sonatype/async-http-client
- scm:git:git@github.com:sonatype/async-http-client.git
+ scm:git:git@github.com:AsyncHttpClient/async-http-client.git
+ https://github.com/AsyncHttpClient/async-http-client
+ scm:git:git@github.com:AsyncHttpClient/async-http-client.git
jira
@@ -58,6 +58,11 @@
neotyk
Hubert Iwaniuk
+
+ slandelle
+ Stephane Landelle
+ slandelle@excilys.com
+
@@ -76,38 +81,27 @@
io.netty
netty
- 3.4.4.Final
-
-
- javax.servlet
- servlet-api
-
-
- commons-logging
- commons-logging
-
-
- org.slf4j
- slf4j-api
-
-
- log4j
- log4j
-
-
+ 3.6.6.Final
org.slf4j
slf4j-api
- 1.6.2
+ 1.7.5
+
+
+
+ com.google.guava
+ guava
+ 11.0.2
+ true
ch.qos.logback
logback-classic
- 0.9.26
+ 1.0.13
test
@@ -252,6 +246,7 @@
maven-surefire-plugin
${surefire.version}
+
${surefire.redirectTestOutputToFile}
@@ -431,40 +426,6 @@
-
- org.codehaus.mojo
- clirr-maven-plugin
- 2.3
-
-
- **/NettyAsyncHttpProvider$*
- **/AsyncHandler$STATE
- **/ProxyServer$Protocol
- **/Realm$AuthScheme
- **/SimpleAsyncHttpClient$ErrorDocumentBehaviour
- **/SpnegoEngine
- **/Request
- **/Request$EntityWriter
- **/RequestBuilderBase
- **/Response
- **/Response$*
- **/FilterContext
- **/FilterContext$*
- **/NettyResponseFuture
- **/**ResponseBodyPart
- **/**WebSocket
-
-
-
-
- check-api-compat
- verify
-
- check-no-fork
-
-
-
-
@@ -528,9 +489,15 @@
org.glassfish.grizzly
grizzly-websockets
- 2.2.10
+ 2.3.6
true
+
+ org.glassfish.grizzly
+ grizzly-http-server
+ 2.3.5
+ test
+
@@ -609,7 +576,7 @@
github
- gitsite:git@github.com/sonatype/async-http-client.git
+ gitsite:git@github.com/AsyncHttpClient/async-http-client.git
diff --git a/src/main/java/com/ning/http/client/AsyncHttpClient.java b/src/main/java/com/ning/http/client/AsyncHttpClient.java
index 90c6d5f2fc..ccc5285c17 100755
--- a/src/main/java/com/ning/http/client/AsyncHttpClient.java
+++ b/src/main/java/com/ning/http/client/AsyncHttpClient.java
@@ -25,10 +25,14 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -138,7 +142,7 @@
* An instance of this class will cache every HTTP 1.1 connections and close them when the {@link AsyncHttpClientConfig#getIdleConnectionTimeoutInMs()}
* expires. This object can hold many persistent connections to different host.
*/
-public class AsyncHttpClient {
+public class AsyncHttpClient implements Closeable {
private final static String DEFAULT_PROVIDER = "com.ning.http.client.providers.netty.NettyAsyncHttpProvider";
private final AsyncHttpProvider httpProvider;
@@ -369,11 +373,16 @@ public void close() {
* Asynchronous close the {@link AsyncHttpProvider} by spawning a thread and avoid blocking.
*/
public void closeAsynchronously() {
- config.applicationThreadPool.submit(new Runnable() {
-
+ final ExecutorService e = Executors.newSingleThreadExecutor();
+ e.submit(new Runnable() {
public void run() {
- httpProvider.close();
- isClosed.set(true);
+ try {
+ close();
+ } catch (Throwable t) {
+ logger.warn("", t);
+ } finally {
+ e.shutdown();
+ }
}
});
}
@@ -546,7 +555,7 @@ private FilterContext preProcessRequest(FilterContext fc) throws IOException {
}
Request request = fc.getRequest();
- if (ResumableAsyncHandler.class.isAssignableFrom(fc.getAsyncHandler().getClass())) {
+ if (fc.getAsyncHandler() instanceof ResumableAsyncHandler) {
request = ResumableAsyncHandler.class.cast(fc.getAsyncHandler()).adjustRequestRange(request);
}
@@ -568,6 +577,16 @@ private final static AsyncHttpProvider loadDefaultProvider(String className, Asy
new Class[]{AsyncHttpClientConfig.class}).newInstance(new Object[]{config});
} catch (Throwable t) {
+ if (t instanceof InvocationTargetException) {
+ final InvocationTargetException ite = (InvocationTargetException) t;
+ if (logger.isErrorEnabled()) {
+ logger.error(
+ "Unable to instantiate provider {}. Trying other providers.",
+ className);
+ logger.error(ite.getCause().toString(), ite.getCause());
+ }
+ }
+
// Let's try with another classloader
try {
Class providerClass = (Class)
diff --git a/src/main/java/com/ning/http/client/AsyncHttpClientConfig.java b/src/main/java/com/ning/http/client/AsyncHttpClientConfig.java
index b3fb5bac1c..e5b8a20736 100644
--- a/src/main/java/com/ning/http/client/AsyncHttpClientConfig.java
+++ b/src/main/java/com/ning/http/client/AsyncHttpClientConfig.java
@@ -67,7 +67,7 @@ public class AsyncHttpClientConfig {
protected boolean allowPoolingConnection;
protected ScheduledExecutorService reaper;
protected ExecutorService applicationThreadPool;
- protected ProxyServer proxyServer;
+ protected ProxyServerSelector proxyServerSelector;
protected SSLContext sslContext;
protected SSLEngineFactory sslEngineFactory;
protected AsyncHttpProviderConfig, ?> providerConfig;
@@ -84,6 +84,9 @@ public class AsyncHttpClientConfig {
protected HostnameVerifier hostnameVerifier;
protected int ioThreadMultiplier;
protected boolean strict302Handling;
+ protected boolean useRelativeURIsWithSSLProxies;
+ protected int maxConnectionLifeTimeInMs;
+ protected boolean rfc6265CookieEncoding;
protected AsyncHttpClientConfig() {
}
@@ -95,6 +98,7 @@ private AsyncHttpClientConfig(int maxTotalConnections,
int idleConnectionInPoolTimeoutInMs,
int idleConnectionTimeoutInMs,
int requestTimeoutInMs,
+ int connectionMaxLifeTimeInMs,
boolean redirectEnabled,
int maxDefaultRedirects,
boolean compressionEnabled,
@@ -102,7 +106,7 @@ private AsyncHttpClientConfig(int maxTotalConnections,
boolean keepAlive,
ScheduledExecutorService reaper,
ExecutorService applicationThreadPool,
- ProxyServer proxyServer,
+ ProxyServerSelector proxyServerSelector,
SSLContext sslContext,
SSLEngineFactory sslEngineFactory,
AsyncHttpProviderConfig, ?> providerConfig,
@@ -117,7 +121,9 @@ private AsyncHttpClientConfig(int maxTotalConnections,
boolean removeQueryParamOnRedirect,
HostnameVerifier hostnameVerifier,
int ioThreadMultiplier,
- boolean strict302Handling) {
+ boolean strict302Handling,
+ boolean useRelativeURIsWithSSLProxies,
+ boolean rfc6265CookieEncoding) {
this.maxTotalConnections = maxTotalConnections;
this.maxConnectionPerHost = maxConnectionPerHost;
@@ -126,6 +132,7 @@ private AsyncHttpClientConfig(int maxTotalConnections,
this.idleConnectionInPoolTimeoutInMs = idleConnectionInPoolTimeoutInMs;
this.idleConnectionTimeoutInMs = idleConnectionTimeoutInMs;
this.requestTimeoutInMs = requestTimeoutInMs;
+ this.maxConnectionLifeTimeInMs = connectionMaxLifeTimeInMs;
this.redirectEnabled = redirectEnabled;
this.maxDefaultRedirects = maxDefaultRedirects;
this.compressionEnabled = compressionEnabled;
@@ -147,13 +154,15 @@ private AsyncHttpClientConfig(int maxTotalConnections,
this.hostnameVerifier = hostnameVerifier;
this.ioThreadMultiplier = ioThreadMultiplier;
this.strict302Handling = strict302Handling;
+ this.useRelativeURIsWithSSLProxies = useRelativeURIsWithSSLProxies;
+ this.rfc6265CookieEncoding = rfc6265CookieEncoding;
if (applicationThreadPool == null) {
this.applicationThreadPool = Executors.newCachedThreadPool();
} else {
this.applicationThreadPool = applicationThreadPool;
}
- this.proxyServer = proxyServer;
+ this.proxyServerSelector = proxyServerSelector;
this.useRawUrl = useRawUrl;
}
@@ -301,8 +310,8 @@ public ExecutorService executorService() {
*
* @return instance of {@link com.ning.http.client.ProxyServer}
*/
- public ProxyServer getProxyServer() {
- return proxyServer;
+ public ProxyServerSelector getProxyServerSelector() {
+ return proxyServerSelector;
}
/**
@@ -438,9 +447,29 @@ public boolean isRemoveQueryParamOnRedirect() {
* Return true if one of the {@link java.util.concurrent.ExecutorService} has been shutdown.
*
* @return true if one of the {@link java.util.concurrent.ExecutorService} has been shutdown.
+ *
+ * @deprecated use #isValid
*/
public boolean isClosed() {
- return applicationThreadPool.isShutdown() || reaper.isShutdown();
+ return !isValid();
+ }
+
+ /**
+ * @return true
if both the application and reaper thread pools
+ * haven't yet been shutdown.
+ *
+ * @since 1.7.21
+ */
+ public boolean isValid() {
+ boolean atpRunning = true;
+ try {
+ atpRunning = applicationThreadPool.isShutdown();
+ } catch (Exception ignore) {
+ // isShutdown() will thrown an exception in an EE7 environment
+ // when using a ManagedExecutorService.
+ // When this is the case, we assume it's running.
+ }
+ return (atpRunning && !reaper.isShutdown());
}
/**
@@ -476,6 +505,35 @@ public boolean isStrict302Handling() {
return strict302Handling;
}
+ /**
+ * @returntrue
if AHC should use relative URIs instead of absolute ones when talking with a SSL proxy,
+ * otherwise false
.
+ *
+ * @since 1.7.12
+ */
+ public boolean isUseRelativeURIsWithSSLProxies() {
+ return useRelativeURIsWithSSLProxies;
+ }
+
+ /**
+ * Return the maximum time in millisecond an {@link com.ning.http.client.AsyncHttpClient} will keep connection in the pool, or -1 to keep connection while possible.
+ *
+ * @return the maximum time in millisecond an {@link com.ning.http.client.AsyncHttpClient} will keep connection in the pool, or -1 to keep connection while possible.
+ */
+ public int getMaxConnectionLifeTimeInMs() {
+ return maxConnectionLifeTimeInMs;
+ }
+
+ /**
+ * @returntrue
if AHC should use rfc6265 for encoding client side cookies,
+ * otherwise false
.
+ *
+ * @since 1.7.18
+ */
+ public boolean isRfc6265CookieEncoding() {
+ return rfc6265CookieEncoding;
+ }
+
/**
* Builder for an {@link AsyncHttpClient}
*/
@@ -487,27 +545,18 @@ public static class Builder {
private int defaultIdleConnectionInPoolTimeoutInMs = Integer.getInteger(ASYNC_CLIENT + "defaultIdleConnectionInPoolTimeoutInMS", 60 * 1000);
private int defaultIdleConnectionTimeoutInMs = Integer.getInteger(ASYNC_CLIENT + "defaultIdleConnectionTimeoutInMS", 60 * 1000);
private int defaultRequestTimeoutInMs = Integer.getInteger(ASYNC_CLIENT + "defaultRequestTimeoutInMS", 60 * 1000);
+ private int defaultMaxConnectionLifeTimeInMs = Integer.getInteger(ASYNC_CLIENT + "defaultMaxConnectionLifeTimeInMs", -1);
private boolean redirectEnabled = Boolean.getBoolean(ASYNC_CLIENT + "defaultRedirectsEnabled");
private int maxDefaultRedirects = Integer.getInteger(ASYNC_CLIENT + "defaultMaxRedirects", 5);
private boolean compressionEnabled = Boolean.getBoolean(ASYNC_CLIENT + "compressionEnabled");
private String userAgent = System.getProperty(ASYNC_CLIENT + "userAgent", "NING/1.0");
private boolean useProxyProperties = Boolean.getBoolean(ASYNC_CLIENT + "useProxyProperties");
+ private boolean useProxySelector = Boolean.getBoolean(ASYNC_CLIENT + "useProxySelector");
private boolean allowPoolingConnection = true;
- private ScheduledExecutorService reaper = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r, "AsyncHttpClient-Reaper");
- t.setDaemon(true);
- return t;
- }
- });
- private ExecutorService applicationThreadPool = Executors.newCachedThreadPool(new ThreadFactory() {
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r, "AsyncHttpClient-Callback");
- t.setDaemon(true);
- return t;
- }
- });
- private ProxyServer proxyServer = null;
+ private boolean useRelativeURIsWithSSLProxies = Boolean.getBoolean(ASYNC_CLIENT + "useRelativeURIsWithSSLProxies");
+ private ScheduledExecutorService reaper;
+ private ExecutorService applicationThreadPool;
+ private ProxyServerSelector proxyServerSelector = null;
private SSLContext sslContext;
private SSLEngineFactory sslEngineFactory;
private AsyncHttpProviderConfig, ?> providerConfig;
@@ -524,6 +573,7 @@ public Thread newThread(Runnable r) {
private HostnameVerifier hostnameVerifier = new AllowAllHostnameVerifier();
private int ioThreadMultiplier = 2;
private boolean strict302Handling;
+ private boolean rfc6265CookieEncoding;
public Builder() {
}
@@ -684,7 +734,6 @@ public Builder setKeepAlive(boolean allowPoolingConnection) {
* @return a {@link Builder}
*/
public Builder setScheduledExecutorService(ScheduledExecutorService reaper) {
- if (this.reaper != null) this.reaper.shutdown();
this.reaper = reaper;
return this;
}
@@ -698,19 +747,29 @@ public Builder setScheduledExecutorService(ScheduledExecutorService reaper) {
* @return a {@link Builder}
*/
public Builder setExecutorService(ExecutorService applicationThreadPool) {
- if (this.applicationThreadPool != null) this.applicationThreadPool.shutdown();
this.applicationThreadPool = applicationThreadPool;
return this;
}
/**
- * Set an instance of {@link com.ning.http.client.ProxyServer} used by an {@link AsyncHttpClient}
+ * Set an instance of {@link ProxyServerSelector} used by an {@link AsyncHttpClient}
+ *
+ * @param proxyServerSelector instance of {@link ProxyServerSelector}
+ * @return a {@link Builder}
+ */
+ public Builder setProxyServerSelector(ProxyServerSelector proxyServerSelector) {
+ this.proxyServerSelector = proxyServerSelector;
+ return this;
+ }
+
+ /**
+ * Set an instance of {@link ProxyServer} used by an {@link AsyncHttpClient}
*
* @param proxyServer instance of {@link com.ning.http.client.ProxyServer}
* @return a {@link Builder}
*/
public Builder setProxyServer(ProxyServer proxyServer) {
- this.proxyServer = proxyServer;
+ this.proxyServerSelector = ProxyUtils.createProxyServerSelector(proxyServer);
return this;
}
@@ -911,12 +970,27 @@ public Builder setRemoveQueryParamsOnRedirect(boolean removeQueryParamOnRedirect
return this;
}
+ /**
+ * Sets whether AHC should use the default JDK ProxySelector to select a proxy server.
+ *
+ * If useProxySelector is set to true
but {@link #setProxyServer(ProxyServer)}
+ * was used to explicitly set a proxy server, the latter is preferred.
+ *
+ * See http://docs.oracle.com/javase/7/docs/api/java/net/ProxySelector.html
+ */
+ public Builder setUseProxySelector(boolean useProxySelector) {
+ this.useProxySelector = useProxySelector;
+ return this;
+ }
+
/**
* Sets whether AHC should use the default http.proxy* system properties
- * to obtain proxy information.
+ * to obtain proxy information. This differs from {@link #setUseProxySelector(boolean)}
+ * in that AsyncHttpClient will use its own logic to handle the system properties,
+ * potentially supporting other protocols that the the JDK ProxySelector doesn't.
*
- * If useProxyProperties is set to true
but {@link #setProxyServer(ProxyServer)} was used
- * to explicitly set a proxy server, the latter is preferred.
+ * If useProxyProperties is set to true
but {@link #setUseProxySelector(boolean)}
+ * was also set to true, the latter is preferred.
*
* See http://download.oracle.com/javase/1.4.2/docs/guide/net/properties.html
*/
@@ -955,6 +1029,43 @@ public Builder setStrict302Handling(final boolean strict302Handling) {
this.strict302Handling = strict302Handling;
return this;
}
+
+ /**
+ * Configures this AHC instance to use relative URIs instead of absolute ones when talking with a SSL proxy.
+ *
+ * @param useRelativeURIsWithSSLProxies
+ * @return this
+ *
+ * @since 1.7.2
+ */
+ public Builder setUseRelativeURIsWithSSLProxies(boolean useRelativeURIsWithSSLProxies) {
+ this.useRelativeURIsWithSSLProxies = useRelativeURIsWithSSLProxies;
+ return this;
+ }
+
+ /**
+ * Set the maximum time in millisecond connection can be added to the pool for further reuse
+ *
+ * @param maxConnectionLifeTimeInMs the maximum time in millisecond connection can be added to the pool for further reuse
+ * @return a {@link Builder}
+ */
+ public Builder setMaxConnectionLifeTimeInMs(int maxConnectionLifeTimeInMs) {
+ this.defaultMaxConnectionLifeTimeInMs = maxConnectionLifeTimeInMs;
+ return this;
+ }
+
+ /**
+ * Configures this AHC instance to use RFC 6265 cookie encoding style
+ *
+ * @param rfc6265CookieEncoding
+ * @return this
+ *
+ * @since 1.7.18
+ */
+ public Builder setRfc6265CookieEncoding(boolean rfc6265CookieEncoding) {
+ this.rfc6265CookieEncoding = rfc6265CookieEncoding;
+ return this;
+ }
/**
* Create a config builder with values taken from the given prototype configuration.
@@ -969,9 +1080,10 @@ public Builder(AsyncHttpClientConfig prototype) {
defaultIdleConnectionInPoolTimeoutInMs = prototype.getIdleConnectionInPoolTimeoutInMs();
defaultIdleConnectionTimeoutInMs = prototype.getIdleConnectionTimeoutInMs();
defaultMaxConnectionPerHost = prototype.getMaxConnectionPerHost();
+ defaultMaxConnectionLifeTimeInMs = prototype.getMaxConnectionLifeTimeInMs();
maxDefaultRedirects = prototype.getMaxRedirects();
defaultMaxTotalConnections = prototype.getMaxTotalConnections();
- proxyServer = prototype.getProxyServer();
+ proxyServerSelector = prototype.getProxyServerSelector();
realm = prototype.getRealm();
defaultRequestTimeoutInMs = prototype.getRequestTimeoutInMs();
sslContext = prototype.getSSLContext();
@@ -998,6 +1110,7 @@ public Builder(AsyncHttpClientConfig prototype) {
removeQueryParamOnRedirect = prototype.isRemoveQueryParamOnRedirect();
hostnameVerifier = prototype.getHostnameVerifier();
strict302Handling = prototype.isStrict302Handling();
+ rfc6265CookieEncoding = prototype.isRfc6265CookieEncoding();
}
/**
@@ -1007,12 +1120,39 @@ public Builder(AsyncHttpClientConfig prototype) {
*/
public AsyncHttpClientConfig build() {
- if (applicationThreadPool.isShutdown()) {
- throw new IllegalStateException("ExecutorServices closed");
+ if (reaper == null) {
+ reaper = Executors.newScheduledThreadPool(Runtime.getRuntime()
+ .availableProcessors(), new ThreadFactory() {
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r, "AsyncHttpClient-Reaper");
+ t.setDaemon(true);
+ return t;
+ }
+ });
+ }
+
+ if (applicationThreadPool == null) {
+ applicationThreadPool = Executors
+ .newCachedThreadPool(new ThreadFactory() {
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r,
+ "AsyncHttpClient-Callback");
+ t.setDaemon(true);
+ return t;
+ }
+ });
+ }
+
+ if (proxyServerSelector == null && useProxySelector) {
+ proxyServerSelector = ProxyUtils.getJdkDefaultProxyServerSelector();
+ }
+
+ if (proxyServerSelector == null && useProxyProperties) {
+ proxyServerSelector = ProxyUtils.createProxyServerSelector(System.getProperties());
}
- if (proxyServer == null && useProxyProperties) {
- proxyServer = ProxyUtils.createProxy(System.getProperties());
+ if (proxyServerSelector == null) {
+ proxyServerSelector = ProxyServerSelector.NO_PROXY_SELECTOR;
}
return new AsyncHttpClientConfig(defaultMaxTotalConnections,
@@ -1022,6 +1162,7 @@ public AsyncHttpClientConfig build() {
defaultIdleConnectionInPoolTimeoutInMs,
defaultIdleConnectionTimeoutInMs,
defaultRequestTimeoutInMs,
+ defaultMaxConnectionLifeTimeInMs,
redirectEnabled,
maxDefaultRedirects,
compressionEnabled,
@@ -1029,7 +1170,7 @@ public AsyncHttpClientConfig build() {
allowPoolingConnection,
reaper,
applicationThreadPool,
- proxyServer,
+ proxyServerSelector,
sslContext,
sslEngineFactory,
providerConfig,
@@ -1045,7 +1186,9 @@ public AsyncHttpClientConfig build() {
removeQueryParamOnRedirect,
hostnameVerifier,
ioThreadMultiplier,
- strict302Handling);
+ strict302Handling,
+ useRelativeURIsWithSSLProxies,
+ rfc6265CookieEncoding);
}
}
}
diff --git a/src/main/java/com/ning/http/client/AsyncHttpClientConfigBean.java b/src/main/java/com/ning/http/client/AsyncHttpClientConfigBean.java
index 0924d4d760..8ea0b17459 100644
--- a/src/main/java/com/ning/http/client/AsyncHttpClientConfigBean.java
+++ b/src/main/java/com/ning/http/client/AsyncHttpClientConfigBean.java
@@ -54,10 +54,14 @@ void configureDefaults() {
maxDefaultRedirects = Integer.getInteger(ASYNC_CLIENT + "defaultMaxRedirects", 5);
compressionEnabled = Boolean.getBoolean(ASYNC_CLIENT + "compressionEnabled");
userAgent = System.getProperty(ASYNC_CLIENT + "userAgent", "NING/1.0");
+ ioThreadMultiplier = Integer.getInteger(ASYNC_CLIENT + "ioThreadMultiplier", 2);
+ boolean useProxySelector = Boolean.getBoolean(ASYNC_CLIENT + "useProxySelector");
boolean useProxyProperties = Boolean.getBoolean(ASYNC_CLIENT + "useProxyProperties");
- if (useProxyProperties) {
- proxyServer = ProxyUtils.createProxy(System.getProperties());
+ if (useProxySelector) {
+ proxyServerSelector = ProxyUtils.getJdkDefaultProxyServerSelector();
+ } else if (useProxyProperties) {
+ proxyServerSelector = ProxyUtils.createProxyServerSelector(System.getProperties());
}
allowPoolingConnection = true;
@@ -163,7 +167,12 @@ public AsyncHttpClientConfigBean setApplicationThreadPool(ExecutorService applic
}
public AsyncHttpClientConfigBean setProxyServer(ProxyServer proxyServer) {
- this.proxyServer = proxyServer;
+ this.proxyServerSelector = ProxyUtils.createProxyServerSelector(proxyServer);
+ return this;
+ }
+
+ public AsyncHttpClientConfigBean setProxyServerSelector(ProxyServerSelector proxyServerSelector) {
+ this.proxyServerSelector = proxyServerSelector;
return this;
}
diff --git a/src/main/java/com/ning/http/client/AsyncHttpProvider.java b/src/main/java/com/ning/http/client/AsyncHttpProvider.java
index 1689bc137e..1dfdb0474c 100644
--- a/src/main/java/com/ning/http/client/AsyncHttpProvider.java
+++ b/src/main/java/com/ning/http/client/AsyncHttpProvider.java
@@ -16,7 +16,7 @@
package com.ning.http.client;
import java.io.IOException;
-import java.util.Collection;
+import java.util.List;
/**
* Interface to be used when implementing custom asynchronous I/O HTTP client.
@@ -48,6 +48,6 @@ public interface AsyncHttpProvider {
*/
public Response prepareResponse(HttpResponseStatus status,
HttpResponseHeaders headers,
- Collection bodyParts);
+ List bodyParts);
}
diff --git a/src/main/java/com/ning/http/client/ConnectionPoolKeyStrategy.java b/src/main/java/com/ning/http/client/ConnectionPoolKeyStrategy.java
new file mode 100644
index 0000000000..d0e6643db1
--- /dev/null
+++ b/src/main/java/com/ning/http/client/ConnectionPoolKeyStrategy.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010 Ning, Inc.
+ *
+ * Ning 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 com.ning.http.client;
+
+import java.net.URI;
+
+public interface ConnectionPoolKeyStrategy {
+
+ String getKey(URI uri);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ning/http/client/Cookie.java b/src/main/java/com/ning/http/client/Cookie.java
index 26fd46920f..b3be657d5a 100644
--- a/src/main/java/com/ning/http/client/Cookie.java
+++ b/src/main/java/com/ning/http/client/Cookie.java
@@ -20,35 +20,93 @@
import java.util.Set;
import java.util.TreeSet;
-public class Cookie {
+public class Cookie implements Comparable {
private final String domain;
private final String name;
private final String value;
+ private final String rawValue;
private final String path;
private final int maxAge;
private final boolean secure;
private final int version;
+ private final boolean httpOnly;
+ private final boolean discard;
+ private final String comment;
+ private final String commentUrl;
+
private Set ports = Collections.emptySet();
private Set unmodifiablePorts = ports;
public Cookie(String domain, String name, String value, String path, int maxAge, boolean secure) {
- this.domain = domain;
- this.name = name;
- this.value = value;
- this.path = path;
- this.maxAge = maxAge;
- this.secure = secure;
- this.version = 1;
+ this(domain, name, value, path, maxAge, secure, 1);
}
public Cookie(String domain, String name, String value, String path, int maxAge, boolean secure, int version) {
- this.domain = domain;
+ this(domain, name, value, value, path, maxAge, secure, version, false, false, null, null, Collections. emptySet());
+ }
+
+ public Cookie(String domain, String name, String value, String rawValue, String path, int maxAge, boolean secure, int version, boolean httpOnly, boolean discard, String comment, String commentUrl, Iterable ports) {
+
+ if (name == null) {
+ throw new NullPointerException("name");
+ }
+ name = name.trim();
+ if (name.length() == 0) {
+ throw new IllegalArgumentException("empty name");
+ }
+
+ for (int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ if (c > 127) {
+ throw new IllegalArgumentException("name contains non-ascii character: " + name);
+ }
+
+ // Check prohibited characters.
+ switch (c) {
+ case '\t':
+ case '\n':
+ case 0x0b:
+ case '\f':
+ case '\r':
+ case ' ':
+ case ',':
+ case ';':
+ case '=':
+ throw new IllegalArgumentException("name contains one of the following prohibited characters: " + "=,; \\t\\r\\n\\v\\f: " + name);
+ }
+ }
+
+ if (name.charAt(0) == '$') {
+ throw new IllegalArgumentException("name starting with '$' not allowed: " + name);
+ }
+
+ if (value == null) {
+ throw new NullPointerException("value");
+ }
+
this.name = name;
this.value = value;
- this.path = path;
+ this.rawValue = rawValue;
+ this.domain = validateValue("domain", domain);
+ this.path = validateValue("path", path);
this.maxAge = maxAge;
this.secure = secure;
this.version = version;
+ this.httpOnly = httpOnly;
+
+ if (version > 0) {
+ this.comment = validateValue("comment", comment);
+ } else {
+ this.comment = null;
+ }
+ if (version > 1) {
+ this.discard = discard;
+ this.commentUrl = validateValue("commentUrl", commentUrl);
+ setPorts(ports);
+ } else {
+ this.discard = false;
+ this.commentUrl = null;
+ }
}
public String getDomain() {
@@ -63,6 +121,10 @@ public String getValue() {
return value == null ? "" : value;
}
+ public String getRawValue() {
+ return rawValue;
+ }
+
public String getPath() {
return path;
}
@@ -79,6 +141,22 @@ public int getVersion() {
return version;
}
+ public String getComment() {
+ return this.comment;
+ }
+
+ public String getCommentUrl() {
+ return this.commentUrl;
+ }
+
+ public boolean isHttpOnly() {
+ return httpOnly;
+ }
+
+ public boolean isDiscard() {
+ return discard;
+ }
+
public Set getPorts() {
if (unmodifiablePorts == null) {
unmodifiablePorts = Collections.unmodifiableSet(ports);
@@ -86,6 +164,8 @@ public Set getPorts() {
return unmodifiablePorts;
}
+ @Deprecated
+ // to be removed
public void setPorts(int... ports) {
if (ports == null) {
throw new NullPointerException("ports");
@@ -107,6 +187,8 @@ public void setPorts(int... ports) {
}
}
+ @Deprecated
+ // to become private
public void setPorts(Iterable ports) {
Set newPorts = new TreeSet();
for (int p : ports) {
@@ -128,4 +210,60 @@ public String toString() {
return String.format("Cookie: domain=%s, name=%s, value=%s, path=%s, maxAge=%d, secure=%s",
domain, name, value, path, maxAge, secure);
}
+
+ private String validateValue(String name, String value) {
+ if (value == null) {
+ return null;
+ }
+ value = value.trim();
+ if (value.length() == 0) {
+ return null;
+ }
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ switch (c) {
+ case '\r':
+ case '\n':
+ case '\f':
+ case 0x0b:
+ case ';':
+ throw new IllegalArgumentException(name + " contains one of the following prohibited characters: " + ";\\r\\n\\f\\v (" + value + ')');
+ }
+ }
+ return value;
+ }
+
+ public int compareTo(Cookie c) {
+ int v;
+ v = getName().compareToIgnoreCase(c.getName());
+ if (v != 0) {
+ return v;
+ }
+
+ if (getPath() == null) {
+ if (c.getPath() != null) {
+ return -1;
+ }
+ } else if (c.getPath() == null) {
+ return 1;
+ } else {
+ v = getPath().compareTo(c.getPath());
+ if (v != 0) {
+ return v;
+ }
+ }
+
+ if (getDomain() == null) {
+ if (c.getDomain() != null) {
+ return -1;
+ }
+ } else if (c.getDomain() == null) {
+ return 1;
+ } else {
+ v = getDomain().compareToIgnoreCase(c.getDomain());
+ return v;
+ }
+
+ return 0;
+ }
}
diff --git a/src/main/java/com/ning/http/client/DefaultConnectionPoolStrategy.java b/src/main/java/com/ning/http/client/DefaultConnectionPoolStrategy.java
new file mode 100644
index 0000000000..3d248e869f
--- /dev/null
+++ b/src/main/java/com/ning/http/client/DefaultConnectionPoolStrategy.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2010 Ning, Inc.
+ *
+ * Ning 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 com.ning.http.client;
+
+import java.net.URI;
+
+import com.ning.http.util.AsyncHttpProviderUtils;
+
+public enum DefaultConnectionPoolStrategy implements ConnectionPoolKeyStrategy {
+
+ INSTANCE;
+
+ public String getKey(URI uri) {
+ return AsyncHttpProviderUtils.getBaseUrl(uri);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ning/http/client/FluentCaseInsensitiveStringsMap.java b/src/main/java/com/ning/http/client/FluentCaseInsensitiveStringsMap.java
index 009af7e43f..16ad85572a 100644
--- a/src/main/java/com/ning/http/client/FluentCaseInsensitiveStringsMap.java
+++ b/src/main/java/com/ning/http/client/FluentCaseInsensitiveStringsMap.java
@@ -16,6 +16,8 @@
*/
package com.ning.http.client;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -24,6 +26,7 @@
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -66,7 +69,7 @@ public FluentCaseInsensitiveStringsMap(Map> src) {
* @return This object
*/
public FluentCaseInsensitiveStringsMap add(String key, String... values) {
- if ((values != null) && (values.length > 0)) {
+ if (isNonEmpty(values)) {
add(key, Arrays.asList(values));
}
return this;
@@ -103,7 +106,7 @@ public FluentCaseInsensitiveStringsMap add(String key, Collection values
List nonNullValues = fetchValues(values);
if (nonNullValues != null) {
- String lcKey = key.toLowerCase();
+ String lcKey = key.toLowerCase(Locale.ENGLISH);
String realKey = keyLookup.get(lcKey);
List curValues = null;
@@ -175,7 +178,7 @@ public FluentCaseInsensitiveStringsMap replace(final String key, final String...
public FluentCaseInsensitiveStringsMap replace(final String key, final Collection values) {
if (key != null) {
List nonNullValues = fetchValues(values);
- String lcKkey = key.toLowerCase();
+ String lcKkey = key.toLowerCase(Locale.ENGLISH);
String realKey = keyLookup.get(lcKkey);
if (nonNullValues == null) {
@@ -257,7 +260,7 @@ public void putAll(Map extends String, ? extends List> values) {
*/
public FluentCaseInsensitiveStringsMap delete(String key) {
if (key != null) {
- String lcKey = key.toLowerCase();
+ String lcKey = key.toLowerCase(Locale.ENGLISH);
String realKey = keyLookup.remove(lcKey);
if (realKey != null) {
@@ -366,7 +369,7 @@ public boolean isEmpty() {
*/
/* @Override */
public boolean containsKey(Object key) {
- return key == null ? false : keyLookup.containsKey(key.toString().toLowerCase());
+ return key == null ? false : keyLookup.containsKey(key.toString().toLowerCase(Locale.ENGLISH));
}
/**
@@ -431,7 +434,7 @@ public List get(Object key) {
return null;
}
- String lcKey = key.toString().toLowerCase();
+ String lcKey = key.toString().toLowerCase(Locale.ENGLISH);
String realKey = keyLookup.get(lcKey);
if (realKey == null) {
diff --git a/src/main/java/com/ning/http/client/FluentStringsMap.java b/src/main/java/com/ning/http/client/FluentStringsMap.java
index 5c71428614..22a2ccb6bb 100644
--- a/src/main/java/com/ning/http/client/FluentStringsMap.java
+++ b/src/main/java/com/ning/http/client/FluentStringsMap.java
@@ -16,6 +16,8 @@
*/
package com.ning.http.client;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -61,30 +63,12 @@ public FluentStringsMap(Map> src) {
* @return This object
*/
public FluentStringsMap add(String key, String... values) {
- if ((values != null) && (values.length > 0)) {
+ if (isNonEmpty(values)) {
add(key, Arrays.asList(values));
}
return this;
}
- private List fetchValues(Collection values) {
- List result = null;
-
- if (values != null) {
- for (String value : values) {
- if (value == null) {
- value = "";
- }
- if (result == null) {
- // lazy initialization
- result = new ArrayList();
- }
- result.add(value);
- }
- }
- return result;
- }
-
/**
* Adds the specified values and returns this object.
*
@@ -94,17 +78,13 @@ private List fetchValues(Collection values) {
* @return This object
*/
public FluentStringsMap add(String key, Collection values) {
- if (key != null) {
- List nonNullValues = fetchValues(values);
-
- if (nonNullValues != null) {
- List curValues = this.values.get(key);
+ if (key != null && isNonEmpty(values)) {
+ List curValues = this.values.get(key);
- if (curValues == null) {
- curValues = new ArrayList();
- this.values.put(key, curValues);
- }
- curValues.addAll(nonNullValues);
+ if (curValues == null) {
+ this.values.put(key, new ArrayList(values));
+ } else {
+ curValues.addAll(values);
}
}
return this;
@@ -160,12 +140,10 @@ public FluentStringsMap replace(final String key, final String... values) {
*/
public FluentStringsMap replace(final String key, final Collection values) {
if (key != null) {
- List nonNullValues = fetchValues(values);
-
- if (nonNullValues == null) {
+ if (values == null) {
this.values.remove(key);
} else {
- this.values.put(key, nonNullValues);
+ this.values.put(key, new ArrayList(values));
}
}
return this;
diff --git a/src/main/java/com/ning/http/client/HttpResponseBodyPartsInputStream.java b/src/main/java/com/ning/http/client/HttpResponseBodyPartsInputStream.java
index 1f6667cd2b..7b0f76db5b 100644
--- a/src/main/java/com/ning/http/client/HttpResponseBodyPartsInputStream.java
+++ b/src/main/java/com/ning/http/client/HttpResponseBodyPartsInputStream.java
@@ -14,26 +14,27 @@
import java.io.IOException;
import java.io.InputStream;
+import java.util.List;
/**
* An {@link InputStream} that reads all the elements in an array of {@link HttpResponseBodyPart}s.
*/
public class HttpResponseBodyPartsInputStream extends InputStream {
- private final HttpResponseBodyPart[] parts;
+ private final List parts;
private int currentPos = 0;
private int bytePos = -1;
private byte[] active;
private int available = 0;
- public HttpResponseBodyPartsInputStream(HttpResponseBodyPart[] parts) {
+ public HttpResponseBodyPartsInputStream(List parts) {
this.parts = parts;
- active = parts[0].getBodyPartBytes();
+ active = parts.get(0).getBodyPartBytes();
computeLength(parts);
}
- private void computeLength(HttpResponseBodyPart[] parts) {
+ private void computeLength(List parts) {
if (available == 0) {
for (HttpResponseBodyPart p : parts) {
available += p.getBodyPartBytes().length;
@@ -50,12 +51,12 @@ public int available() throws IOException {
public int read() throws IOException {
if (++bytePos >= active.length) {
// No more bytes, so step to the next array.
- if (++currentPos >= parts.length) {
+ if (++currentPos >= parts.size()) {
return -1;
}
bytePos = 0;
- active = parts[currentPos].getBodyPartBytes();
+ active = parts.get(currentPos).getBodyPartBytes();
}
return active[bytePos] & 0xFF;
diff --git a/src/main/java/com/ning/http/client/ListenableFuture.java b/src/main/java/com/ning/http/client/ListenableFuture.java
index 74dfcb70f0..b119f26597 100755
--- a/src/main/java/com/ning/http/client/ListenableFuture.java
+++ b/src/main/java/com/ning/http/client/ListenableFuture.java
@@ -30,7 +30,6 @@
*/
package com.ning.http.client;
-import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
@@ -42,11 +41,11 @@
public interface ListenableFuture extends Future {
/**
- * Execute a {@link Callable} and if there is no exception, mark this Future as done and release the internal lock.
+ * Terminate and if there is no exception, mark this Future as done and release the internal lock.
*
* @param callable
*/
- void done(Callable callable);
+ void done();
/**
* Abort the current processing, and propagate the {@link Throwable} to the {@link AsyncHandler} or {@link Future}
diff --git a/src/main/java/com/ning/http/client/ProxyServer.java b/src/main/java/com/ning/http/client/ProxyServer.java
index 79cc5e1eee..784ba97e44 100644
--- a/src/main/java/com/ning/http/client/ProxyServer.java
+++ b/src/main/java/com/ning/http/client/ProxyServer.java
@@ -16,10 +16,13 @@
*/
package com.ning.http.client;
+import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import com.ning.http.util.AsyncHttpProviderUtils;
+
/**
* Represents a proxy server.
*/
@@ -44,13 +47,14 @@ public String toString() {
}
}
- private String encoding = "UTF-8";
private final List nonProxyHosts = new ArrayList();
private final Protocol protocol;
private final String host;
private final String principal;
private final String password;
- private int port;
+ private final int port;
+ private final URI uri;
+ private String encoding = "UTF-8";
private String ntlmDomain = System.getProperty("http.auth.ntlm.domain", "");
public ProxyServer(final Protocol protocol, final String host, final int port, String principal, String password) {
@@ -59,6 +63,7 @@ public ProxyServer(final Protocol protocol, final String host, final int port, S
this.port = port;
this.principal = principal;
this.password = password;
+ uri = AsyncHttpProviderUtils.createUri(toString());
}
public ProxyServer(final String host, final int port, String principal, String password) {
@@ -97,6 +102,10 @@ public String getPassword() {
return password;
}
+ public URI getURI() {
+ return uri;
+ }
+
public ProxyServer setEncoding(String encoding) {
this.encoding = encoding;
return this;
@@ -131,7 +140,7 @@ public String getNtlmDomain() {
@Override
public String toString() {
- return String.format("%s://%s:%d", protocol.toString(), host, port);
+ return protocol + "://" + host + ":" + port;
}
}
diff --git a/src/main/java/com/ning/http/client/ProxyServerSelector.java b/src/main/java/com/ning/http/client/ProxyServerSelector.java
new file mode 100644
index 0000000000..8544e7e617
--- /dev/null
+++ b/src/main/java/com/ning/http/client/ProxyServerSelector.java
@@ -0,0 +1,26 @@
+package com.ning.http.client;
+
+import java.net.URI;
+
+/**
+ * Selector for a proxy server
+ */
+public interface ProxyServerSelector {
+
+ /**
+ * Select a proxy server to use for the given URI.
+ *
+ * @param uri The URI to select a proxy server for.
+ * @return The proxy server to use, if any. May return null.
+ */
+ ProxyServer select(URI uri);
+
+ /**
+ * A selector that always selects no proxy.
+ */
+ static final ProxyServerSelector NO_PROXY_SELECTOR = new ProxyServerSelector() {
+ public ProxyServer select(URI uri) {
+ return null;
+ }
+ };
+}
diff --git a/src/main/java/com/ning/http/client/Realm.java b/src/main/java/com/ning/http/client/Realm.java
index 07c84bfa3a..8e68142c98 100644
--- a/src/main/java/com/ning/http/client/Realm.java
+++ b/src/main/java/com/ning/http/client/Realm.java
@@ -16,6 +16,8 @@
*/
package com.ning.http.client;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -579,7 +581,7 @@ private static String toBase16(byte[] bytes) {
public Realm build() {
// Avoid generating
- if (nonce != null && !nonce.equals("")) {
+ if (isNonEmpty(nonce)) {
newCnonce();
try {
newResponse();
diff --git a/src/main/java/com/ning/http/client/Request.java b/src/main/java/com/ning/http/client/Request.java
index 10576405bb..8871dd6cc8 100644
--- a/src/main/java/com/ning/http/client/Request.java
+++ b/src/main/java/com/ning/http/client/Request.java
@@ -21,6 +21,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
+import java.net.URI;
import java.util.Collection;
import java.util.List;
@@ -66,6 +67,10 @@ public static interface EntityWriter {
*/
public String getUrl();
+ public URI getOriginalURI();
+ public URI getURI();
+ public URI getRawURI();
+
/**
* Return the InetAddress to override
*
@@ -232,4 +237,5 @@ public static interface EntityWriter {
public boolean isUseRawUrl();
+ ConnectionPoolKeyStrategy getConnectionPoolKeyStrategy();
}
diff --git a/src/main/java/com/ning/http/client/RequestBuilder.java b/src/main/java/com/ning/http/client/RequestBuilder.java
index 50aaad585f..7bf55ee1a5 100644
--- a/src/main/java/com/ning/http/client/RequestBuilder.java
+++ b/src/main/java/com/ning/http/client/RequestBuilder.java
@@ -23,6 +23,8 @@
/**
* Builder for a {@link Request}.
+ * Warning: mutable and not thread-safe! Beware that it holds a reference on the Request instance it builds,
+ * so modifying the builder will modify the request even after it has been built.
*/
public class RequestBuilder extends RequestBuilderBase {
diff --git a/src/main/java/com/ning/http/client/RequestBuilderBase.java b/src/main/java/com/ning/http/client/RequestBuilderBase.java
index 9cc5ec40e0..6749cfa767 100644
--- a/src/main/java/com/ning/http/client/RequestBuilderBase.java
+++ b/src/main/java/com/ning/http/client/RequestBuilderBase.java
@@ -15,8 +15,12 @@
*/
package com.ning.http.client;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
+
import com.ning.http.client.Request.EntityWriter;
+import com.ning.http.util.AsyncHttpProviderUtils;
import com.ning.http.util.UTF8UrlEncoder;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,17 +40,21 @@
/**
* Builder for {@link Request}
- *
+ *
* @param
*/
public abstract class RequestBuilderBase> {
private final static Logger logger = LoggerFactory.getLogger(RequestBuilderBase.class);
+ private static final URI DEFAULT_REQUEST_URL = URI.create("http://localhost/");
+
private static final class RequestImpl implements Request {
private String method;
- private String url = null;
- private InetAddress address = null;
- private InetAddress localAddress = null;
+ private URI originalUri;
+ private URI uri;
+ private URI rawUri;
+ private InetAddress address;
+ private InetAddress localAddress;
private FluentCaseInsensitiveStringsMap headers = new FluentCaseInsensitiveStringsMap();
private Collection cookies = new ArrayList();
private byte[] byteData;
@@ -64,9 +72,10 @@ private static final class RequestImpl implements Request {
private File file;
private Boolean followRedirects;
private PerRequestConfig perRequestConfig;
- private long rangeOffset = 0;
+ private long rangeOffset;
public String charset;
- private boolean useRawUrl = false;
+ private boolean useRawUrl;
+ private ConnectionPoolKeyStrategy connectionPoolKeyStrategy = DefaultConnectionPoolStrategy.INSTANCE;
public RequestImpl(boolean useRawUrl) {
this.useRawUrl = useRawUrl;
@@ -75,8 +84,7 @@ public RequestImpl(boolean useRawUrl) {
public RequestImpl(Request prototype) {
if (prototype != null) {
this.method = prototype.getMethod();
- int pos = prototype.getUrl().indexOf("?");
- this.url = pos > 0 ? prototype.getUrl().substring(0, pos) : prototype.getUrl();
+ this.originalUri = prototype.getOriginalURI();
this.address = prototype.getInetAddress();
this.localAddress = prototype.getLocalAddress();
this.headers = new FluentCaseInsensitiveStringsMap(prototype.getHeaders());
@@ -86,19 +94,20 @@ public RequestImpl(Request prototype) {
this.streamData = prototype.getStreamData();
this.entityWriter = prototype.getEntityWriter();
this.bodyGenerator = prototype.getBodyGenerator();
- this.params = (prototype.getParams() == null ? null : new FluentStringsMap(prototype.getParams()));
- this.queryParams = (prototype.getQueryParams() == null ? null : new FluentStringsMap(prototype.getQueryParams()));
- this.parts = (prototype.getParts() == null ? null : new ArrayList(prototype.getParts()));
+ this.params = prototype.getParams() == null ? null : new FluentStringsMap(prototype.getParams());
+ this.queryParams = prototype.getQueryParams() == null ? null : new FluentStringsMap(prototype.getQueryParams());
+ this.parts = prototype.getParts() == null ? null : new ArrayList(prototype.getParts());
this.virtualHost = prototype.getVirtualHost();
this.length = prototype.getContentLength();
this.proxyServer = prototype.getProxyServer();
this.realm = prototype.getRealm();
this.file = prototype.getFile();
- this.followRedirects = prototype.isRedirectOverrideSet()? prototype.isRedirectEnabled() : null;
+ this.followRedirects = prototype.isRedirectOverrideSet() ? prototype.isRedirectEnabled() : null;
this.perRequestConfig = prototype.getPerRequestConfig();
this.rangeOffset = prototype.getRangeOffset();
this.charset = prototype.getBodyEncoding();
this.useRawUrl = prototype.isUseRawUrl();
+ this.connectionPoolKeyStrategy = prototype.getConnectionPoolKeyStrategy();
}
}
@@ -112,12 +121,6 @@ public String getMethod() {
return method;
}
- /* @Override */
-
- public String getUrl() {
- return toUrl(true);
- }
-
public InetAddress getInetAddress() {
return address;
}
@@ -125,42 +128,74 @@ public InetAddress getInetAddress() {
public InetAddress getLocalAddress() {
return localAddress;
}
-
- private String toUrl(boolean encode) {
- if (url == null) {
+ private String removeTrailingSlash(URI uri) {
+ String uriString = uri.toString();
+ if (uriString.endsWith("/")) {
+ return uriString.substring(0, uriString.length() - 1);
+ } else {
+ return uriString;
+ }
+ }
+
+ /* @Override */
+ public String getUrl() {
+ return removeTrailingSlash(getURI());
+ }
+
+ /* @Override */
+ public String getRawUrl() {
+ return removeTrailingSlash(getRawURI());
+ }
+
+ public URI getOriginalURI() {
+ return originalUri;
+ }
+
+ public URI getURI() {
+ if (uri == null)
+ uri = toURI(true);
+ return uri;
+ }
+
+ public URI getRawURI() {
+ if (rawUri == null)
+ rawUri = toURI(false);
+ return rawUri;
+ }
+
+ private URI toURI(boolean encode) {
+
+ if (originalUri == null) {
logger.debug("setUrl hasn't been invoked. Using http://localhost");
- url = "http://localhost/";
+ originalUri = DEFAULT_REQUEST_URL;
}
- String uri = url;
- if (!uri.startsWith("ws")) {
- try {
- uri = URI.create(url).toURL().toString();
- } catch (Throwable e) {
- throw new IllegalArgumentException("Illegal URL: " + url, e);
- }
+ AsyncHttpProviderUtils.validateSupportedScheme(originalUri);
+
+ StringBuilder builder = new StringBuilder();
+ builder.append(originalUri.getScheme()).append("://").append(originalUri.getAuthority());
+ if (isNonEmpty(originalUri.getRawPath())) {
+ builder.append(originalUri.getRawPath());
+ } else {
+ builder.append("/");
}
- if (queryParams != null && !queryParams.isEmpty()) {
+ if (isNonEmpty(queryParams)) {
- StringBuilder builder = new StringBuilder();
- if (!url.substring(8).contains("/")) { // no other "/" than http[s]:// -> http://localhost:1234
- builder.append("/");
- }
builder.append("?");
- for (Iterator>> i = queryParams.iterator(); i.hasNext(); ) {
+ for (Iterator>> i = queryParams.iterator(); i.hasNext();) {
Map.Entry> param = i.next();
String name = param.getKey();
- for (Iterator j = param.getValue().iterator(); j.hasNext(); ) {
+ for (Iterator j = param.getValue().iterator(); j.hasNext();) {
String value = j.next();
if (encode) {
UTF8UrlEncoder.appendEncoded(builder, name);
} else {
builder.append(name);
}
- if (value != null && !value.equals("")) {
+ if (value != null) {
builder.append('=');
if (encode) {
UTF8UrlEncoder.appendEncoded(builder, value);
@@ -176,14 +211,9 @@ private String toUrl(boolean encode) {
builder.append('&');
}
}
- uri += builder.toString();
}
- return uri;
- }
- /* @Override */
- public String getRawUrl() {
- return toUrl(false);
+ return URI.create(builder.toString());
}
/* @Override */
@@ -270,7 +300,7 @@ public boolean isRedirectEnabled() {
return (followRedirects != null && followRedirects);
}
- public boolean isRedirectOverrideSet(){
+ public boolean isRedirectOverrideSet() {
return followRedirects != null;
}
@@ -286,17 +316,33 @@ public String getBodyEncoding() {
return charset;
}
+ public ConnectionPoolKeyStrategy getConnectionPoolKeyStrategy() {
+ return connectionPoolKeyStrategy;
+ }
+
@Override
public String toString() {
- StringBuilder sb = new StringBuilder(url);
+ StringBuilder sb = new StringBuilder(getURI().toString());
sb.append("\t");
sb.append(method);
- for (String name : headers.keySet()) {
- sb.append("\t");
- sb.append(name);
- sb.append(":");
- sb.append(headers.getJoinedValue(name, ", "));
+ sb.append("\theaders:");
+ if (isNonEmpty(headers)) {
+ for (String name : headers.keySet()) {
+ sb.append("\t");
+ sb.append(name);
+ sb.append(":");
+ sb.append(headers.getJoinedValue(name, ", "));
+ }
+ }
+ if (isNonEmpty(params)) {
+ sb.append("\tparams:");
+ for (String name : params.keySet()) {
+ sb.append("\t");
+ sb.append(name);
+ sb.append(":");
+ sb.append(params.getJoinedValue(name, ", "));
+ }
}
return sb.toString();
@@ -325,47 +371,31 @@ protected RequestBuilderBase(Class derived, Request prototype) {
}
public T setUrl(String url) {
- request.url = buildUrl(url);
+ return setURI(URI.create(url));
+ }
+
+ public T setURI(URI uri) {
+ if (uri.getPath() == null)
+ throw new IllegalArgumentException("Unsupported uri format: " + uri);
+ request.originalUri = uri;
+ addQueryParameters(request.originalUri);
+ request.uri = null;
+ request.rawUri = null;
return derived.cast(this);
}
public T setInetAddress(InetAddress address) {
- request.address = address;
- return derived.cast(this);
+ request.address = address;
+ return derived.cast(this);
}
-
+
public T setLocalInetAddress(InetAddress address) {
request.localAddress = address;
return derived.cast(this);
}
- private String buildUrl(String url) {
- URI uri = URI.create(url);
- StringBuilder buildedUrl = new StringBuilder();
-
- if (uri.getScheme() != null) {
- buildedUrl.append(uri.getScheme());
- buildedUrl.append("://");
- }
-
- if (uri.getAuthority() != null) {
- buildedUrl.append(uri.getAuthority());
- }
- if (uri.getRawPath() != null) {
- buildedUrl.append(uri.getRawPath());
- } else {
- // AHC-96
- // Let's try to derive it
- if (url.indexOf("://") == -1) {
- String s = buildedUrl.toString();
- url = s + url.substring(uri.getScheme().length() + 1);
- return buildUrl(url);
- } else {
- throw new IllegalArgumentException("Invalid url " + uri.toString());
- }
- }
-
- if (uri.getRawQuery() != null && !uri.getRawQuery().equals("")) {
+ private void addQueryParameters(URI uri) {
+ if (isNonEmpty(uri.getRawQuery())) {
String[] queries = uri.getRawQuery().split("&");
int pos;
for (String query : queries) {
@@ -374,7 +404,7 @@ private String buildUrl(String url) {
addQueryParameter(query, null);
} else {
try {
- if (this.useRawUrl) {
+ if (useRawUrl) {
addQueryParameter(query.substring(0, pos), query.substring(pos + 1));
} else {
addQueryParameter(URLDecoder.decode(query.substring(0, pos), "UTF-8"), URLDecoder.decode(query.substring(pos + 1), "UTF-8"));
@@ -385,10 +415,8 @@ private String buildUrl(String url) {
}
}
}
- return buildedUrl.toString();
}
-
public T setVirtualHost(String virtualHost) {
request.virtualHost = virtualHost;
return derived.cast(this);
@@ -446,8 +474,8 @@ private void resetMultipartData() {
}
private void checkIfBodyAllowed() {
- if ("GET".equals(request.method) || "HEAD".equals(request.method)) {
- throw new IllegalArgumentException("Can NOT set Body on HTTP Request Method GET nor HEAD.");
+ if ("HEAD".equals(request.method)) {
+ throw new IllegalArgumentException("Can NOT set Body on HTTP Request Method HEAD.");
}
}
@@ -590,6 +618,11 @@ public T setBodyEncoding(String charset) {
return derived.cast(this);
}
+ public T setConnectionPoolKeyStrategy(ConnectionPoolKeyStrategy connectionPoolKeyStrategy) {
+ request.connectionPoolKeyStrategy = connectionPoolKeyStrategy;
+ return derived.cast(this);
+ }
+
public Request build() {
if ((request.length < 0) && (request.streamData == null) && allowBody(request.getMethod())) {
// can't concatenate content-length
@@ -607,13 +640,7 @@ public Request build() {
}
private boolean allowBody(String method) {
- if (method.equalsIgnoreCase("GET") || method.equalsIgnoreCase("OPTIONS")
- && method.equalsIgnoreCase("TRACE")
- && method.equalsIgnoreCase("HEAD")) {
- return false;
- } else {
- return true;
- }
+ return !(method.equalsIgnoreCase("GET") || method.equalsIgnoreCase("OPTIONS") || method.equalsIgnoreCase("TRACE") || method.equalsIgnoreCase("HEAD"));
}
public T addOrReplaceCookie(Cookie cookie) {
diff --git a/src/main/java/com/ning/http/client/Response.java b/src/main/java/com/ning/http/client/Response.java
index 17da422110..a4b98464b5 100644
--- a/src/main/java/com/ning/http/client/Response.java
+++ b/src/main/java/com/ning/http/client/Response.java
@@ -20,8 +20,8 @@
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -51,6 +51,14 @@ public interface Response {
*/
public byte[] getResponseBodyAsBytes() throws IOException;
+ /**
+ * Return the entire response body as a ByteBuffer.
+ *
+ * @return the entire response body as a ByteBuffer.
+ * @throws IOException
+ */
+ public ByteBuffer getResponseBodyAsByteBuffer() throws IOException;
+
/**
* Returns an input stream for the response body. Note that you should not try to get this more than once,
* and that you should not close the stream.
@@ -178,8 +186,8 @@ public interface Response {
public static class ResponseBuilder {
- private final Collection bodies =
- Collections.synchronizedCollection(new ArrayList());
+ private final List bodies =
+ Collections.synchronizedList(new ArrayList());
private HttpResponseStatus status;
private HttpResponseHeaders headers;
diff --git a/src/main/java/com/ning/http/client/SimpleAsyncHttpClient.java b/src/main/java/com/ning/http/client/SimpleAsyncHttpClient.java
index ea03362bad..45eb19794f 100644
--- a/src/main/java/com/ning/http/client/SimpleAsyncHttpClient.java
+++ b/src/main/java/com/ning/http/client/SimpleAsyncHttpClient.java
@@ -68,8 +68,9 @@ public class SimpleAsyncHttpClient {
private final ErrorDocumentBehaviour errorDocumentBehaviour;
private final SimpleAHCTransferListener listener;
private final boolean derived;
+ private String providerClass;
- private SimpleAsyncHttpClient(AsyncHttpClientConfig config, RequestBuilder requestBuilder, ThrowableHandler defaultThrowableHandler, ErrorDocumentBehaviour errorDocumentBehaviour, boolean resumeEnabled, AsyncHttpClient ahc, SimpleAHCTransferListener listener) {
+ private SimpleAsyncHttpClient(AsyncHttpClientConfig config, RequestBuilder requestBuilder, ThrowableHandler defaultThrowableHandler, ErrorDocumentBehaviour errorDocumentBehaviour, boolean resumeEnabled, AsyncHttpClient ahc, SimpleAHCTransferListener listener, String providerClass) {
this.config = config;
this.requestBuilder = requestBuilder;
this.defaultThrowableHandler = defaultThrowableHandler;
@@ -77,6 +78,7 @@ private SimpleAsyncHttpClient(AsyncHttpClientConfig config, RequestBuilder reque
this.errorDocumentBehaviour = errorDocumentBehaviour;
this.asyncHttpClient = ahc;
this.listener = listener;
+ this.providerClass = providerClass;
this.derived = ahc != null;
}
@@ -287,7 +289,10 @@ private Future execute(RequestBuilder rb, BodyConsumer bodyConsumer, T
private AsyncHttpClient asyncHttpClient() {
synchronized (config) {
if (asyncHttpClient == null) {
- asyncHttpClient = new AsyncHttpClient(config);
+ if (providerClass == null)
+ asyncHttpClient = new AsyncHttpClient(config);
+ else
+ asyncHttpClient = new AsyncHttpClient(providerClass, config);
}
}
return asyncHttpClient;
@@ -400,6 +405,7 @@ public final static class Builder implements DerivedBuilder {
private ErrorDocumentBehaviour errorDocumentBehaviour = ErrorDocumentBehaviour.WRITE;
private AsyncHttpClient ahc = null;
private SimpleAHCTransferListener listener = null;
+ private String providerClass = null;
public Builder() {
requestBuilder = new RequestBuilder("GET", false);
@@ -659,6 +665,11 @@ public Builder setMaxRequestRetry(int maxRequestRetry) {
return this;
}
+ public Builder setProviderClass(String providerClass) {
+ this.providerClass = providerClass;
+ return this;
+ }
+
public SimpleAsyncHttpClient build() {
if (realmBuilder != null) {
@@ -671,7 +682,7 @@ public SimpleAsyncHttpClient build() {
configBuilder.addIOExceptionFilter(new ResumableIOExceptionFilter());
- SimpleAsyncHttpClient sc = new SimpleAsyncHttpClient(configBuilder.build(), requestBuilder, defaultThrowableHandler, errorDocumentBehaviour, enableResumableDownload, ahc, listener);
+ SimpleAsyncHttpClient sc = new SimpleAsyncHttpClient(configBuilder.build(), requestBuilder, defaultThrowableHandler, errorDocumentBehaviour, enableResumableDownload, ahc, listener, providerClass);
return sc;
}
diff --git a/src/main/java/com/ning/http/client/consumers/AppendableBodyConsumer.java b/src/main/java/com/ning/http/client/consumers/AppendableBodyConsumer.java
index ef04f9b8da..a1e9dc5ade 100644
--- a/src/main/java/com/ning/http/client/consumers/AppendableBodyConsumer.java
+++ b/src/main/java/com/ning/http/client/consumers/AppendableBodyConsumer.java
@@ -41,7 +41,10 @@ public AppendableBodyConsumer(Appendable appendable) {
*/
/* @Override */
public void consume(ByteBuffer byteBuffer) throws IOException {
- appendable.append(new String(byteBuffer.array(), encoding));
+ appendable.append(new String(byteBuffer.array(),
+ byteBuffer.arrayOffset() + byteBuffer.position(),
+ byteBuffer.remaining(),
+ encoding));
}
/**
@@ -49,7 +52,7 @@ public void consume(ByteBuffer byteBuffer) throws IOException {
*/
/* @Override */
public void close() throws IOException {
- if (Closeable.class.isAssignableFrom(appendable.getClass())) {
+ if (appendable instanceof Closeable) {
Closeable.class.cast(appendable).close();
}
}
diff --git a/src/main/java/com/ning/http/client/consumers/FileBodyConsumer.java b/src/main/java/com/ning/http/client/consumers/FileBodyConsumer.java
index ad8b7e288f..02a12d65fd 100644
--- a/src/main/java/com/ning/http/client/consumers/FileBodyConsumer.java
+++ b/src/main/java/com/ning/http/client/consumers/FileBodyConsumer.java
@@ -35,7 +35,9 @@ public FileBodyConsumer(RandomAccessFile file) {
/* @Override */
public void consume(ByteBuffer byteBuffer) throws IOException {
// TODO: Channel.transferFrom may be a good idea to investigate.
- file.write(byteBuffer.array());
+ file.write(byteBuffer.array(),
+ byteBuffer.arrayOffset() + byteBuffer.position(),
+ byteBuffer.remaining());
}
/**
diff --git a/src/main/java/com/ning/http/client/consumers/OutputStreamBodyConsumer.java b/src/main/java/com/ning/http/client/consumers/OutputStreamBodyConsumer.java
index d1e806ca43..9f8c93aec1 100644
--- a/src/main/java/com/ning/http/client/consumers/OutputStreamBodyConsumer.java
+++ b/src/main/java/com/ning/http/client/consumers/OutputStreamBodyConsumer.java
@@ -34,7 +34,9 @@ public OutputStreamBodyConsumer(OutputStream outputStream) {
*/
/* @Override */
public void consume(ByteBuffer byteBuffer) throws IOException {
- outputStream.write(byteBuffer.array());
+ outputStream.write(byteBuffer.array(),
+ byteBuffer.arrayOffset() + byteBuffer.position(),
+ byteBuffer.remaining());
}
/**
diff --git a/src/main/java/com/ning/http/client/extra/ListenableFutureAdapter.java b/src/main/java/com/ning/http/client/extra/ListenableFutureAdapter.java
new file mode 100644
index 0000000000..7d32343fca
--- /dev/null
+++ b/src/main/java/com/ning/http/client/extra/ListenableFutureAdapter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2010-2012 Sonatype, Inc. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+package com.ning.http.client.extra;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import com.ning.http.client.ListenableFuture;
+
+public final class ListenableFutureAdapter {
+
+ /**
+ * @param future an AHC ListenableFuture
+ * @return a Guava ListenableFuture
+ */
+ public static com.google.common.util.concurrent.ListenableFuture asGuavaFuture(final ListenableFuture future) {
+
+ return new com.google.common.util.concurrent.ListenableFuture() {
+
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return future.cancel(mayInterruptIfRunning);
+ }
+
+ public V get() throws InterruptedException, ExecutionException {
+ return future.get();
+ }
+
+ public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ return future.get(timeout, unit);
+ }
+
+ public boolean isCancelled() {
+ return future.isCancelled();
+ }
+
+ public boolean isDone() {
+ return future.isDone();
+ }
+
+ public void addListener(final Runnable runnable, final Executor executor) {
+ future.addListener(runnable, executor);
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/ning/http/client/generators/InputStreamBodyGenerator.java b/src/main/java/com/ning/http/client/generators/InputStreamBodyGenerator.java
index 12660ca438..799a74615a 100644
--- a/src/main/java/com/ning/http/client/generators/InputStreamBodyGenerator.java
+++ b/src/main/java/com/ning/http/client/generators/InputStreamBodyGenerator.java
@@ -43,7 +43,7 @@ public InputStreamBodyGenerator(InputStream inputStream) {
if (inputStream.markSupported()) {
inputStream.mark(0);
} else {
- logger.warn("inputStream.markSupported() not supported. Some features will not works");
+ logger.info("inputStream.markSupported() not supported. Some features will not work.");
}
}
@@ -79,7 +79,7 @@ public long read(ByteBuffer buffer) throws IOException {
if (patchNettyChunkingIssue) {
if (read == -1) {
- // Since we are chuncked, we must output extra bytes before considering the input stream closed.
+ // Since we are chunked, we must output extra bytes before considering the input stream closed.
// chunking requires to end the chunking:
// - A Terminating chunk of "0\r\n".getBytes(),
// - Then a separate packet of "\r\n".getBytes()
@@ -117,6 +117,10 @@ public long read(ByteBuffer buffer) throws IOException {
} else {
if (read > 0) {
buffer.put(chunk, 0, read);
+ } else {
+ if (inputStream.markSupported()) {
+ inputStream.reset();
+ }
}
}
return read;
diff --git a/src/main/java/com/ning/http/client/listenable/AbstractListenableFuture.java b/src/main/java/com/ning/http/client/listenable/AbstractListenableFuture.java
index a0f9575e6a..16b2f94352 100644
--- a/src/main/java/com/ning/http/client/listenable/AbstractListenableFuture.java
+++ b/src/main/java/com/ning/http/client/listenable/AbstractListenableFuture.java
@@ -63,7 +63,7 @@ public ListenableFuture addListener(Runnable listener, Executor exec) {
/*
* Override the done method to execute the execution list.
*/
- protected void done() {
+ protected void runListeners() {
executionList.run();
}
}
diff --git a/src/main/java/com/ning/http/client/listener/TransferCompletionHandler.java b/src/main/java/com/ning/http/client/listener/TransferCompletionHandler.java
index 6d71cbd238..5088f12577 100644
--- a/src/main/java/com/ning/http/client/listener/TransferCompletionHandler.java
+++ b/src/main/java/com/ning/http/client/listener/TransferCompletionHandler.java
@@ -26,6 +26,8 @@
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicLong;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
+
/**
* A {@link com.ning.http.client.AsyncHandler} that can be used to notify a set of {@link com.ning.http.client.listener.TransferListener}
*
@@ -144,7 +146,7 @@ public Response onCompleted(Response response) throws Exception {
*/
public STATE onHeaderWriteCompleted() {
List list = transferAdapter.getHeaders().get("Content-Length");
- if (list != null && list.size() > 0 && list.get(0) != "") {
+ if (isNonEmpty(list) && list.get(0) != "") {
totalBytesToTransfer.set(Long.valueOf(list.get(0)));
}
diff --git a/src/main/java/com/ning/http/client/ntlm/NTLMEngine.java b/src/main/java/com/ning/http/client/ntlm/NTLMEngine.java
index a56c9bf141..00023369ef 100644
--- a/src/main/java/com/ning/http/client/ntlm/NTLMEngine.java
+++ b/src/main/java/com/ning/http/client/ntlm/NTLMEngine.java
@@ -38,16 +38,19 @@
package com.ning.http.client.ntlm;
-import com.ning.http.util.Base64;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
-import javax.crypto.Cipher;
-import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Locale;
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+
+import com.ning.http.util.Base64;
+
/**
* Provides an implementation for NTLMv1, NTLMv2, and NTLM2 Session forms of the NTLM
* authentication protocol.
@@ -123,12 +126,12 @@ final String getResponseFor(String message, String username, String password,
String host, String domain) throws NTLMEngineException {
final String response;
- if (message == null || message.trim().equals("")) {
- response = getType1Message(host, domain);
- } else {
+ if (isNonEmpty(message)) {
Type2Message t2m = new Type2Message(message);
response = getType3Message(username, password, host, domain, t2m.getChallenge(), t2m
.getFlags(), t2m.getTarget(), t2m.getTargetInfo());
+ } else {
+ response = getType1Message(host, domain);
}
return response;
}
diff --git a/src/main/java/com/ning/http/client/oauth/OAuthSignatureCalculator.java b/src/main/java/com/ning/http/client/oauth/OAuthSignatureCalculator.java
index e33ad87c73..4e363745e0 100644
--- a/src/main/java/com/ning/http/client/oauth/OAuthSignatureCalculator.java
+++ b/src/main/java/com/ning/http/client/oauth/OAuthSignatureCalculator.java
@@ -16,7 +16,6 @@
*/
package com.ning.http.client.oauth;
-
import com.ning.http.client.FluentStringsMap;
import com.ning.http.client.Request;
import com.ning.http.client.RequestBuilderBase;
diff --git a/src/main/java/com/ning/http/client/oauth/ThreadSafeHMAC.java b/src/main/java/com/ning/http/client/oauth/ThreadSafeHMAC.java
index 0157a9df77..7ba72dd1a6 100644
--- a/src/main/java/com/ning/http/client/oauth/ThreadSafeHMAC.java
+++ b/src/main/java/com/ning/http/client/oauth/ThreadSafeHMAC.java
@@ -17,6 +17,7 @@
package com.ning.http.client.oauth;
import com.ning.http.util.UTF8Codec;
+import com.ning.http.util.UTF8UrlEncoder;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@@ -36,7 +37,7 @@ public class ThreadSafeHMAC {
private final Mac mac;
public ThreadSafeHMAC(ConsumerKey consumerAuth, RequestToken userAuth) {
- byte[] keyBytes = UTF8Codec.toUTF8(consumerAuth.getSecret() + "&" + userAuth.getSecret());
+ byte[] keyBytes = UTF8Codec.toUTF8(UTF8UrlEncoder.encode(consumerAuth.getSecret()) + "&" + UTF8UrlEncoder.encode(userAuth.getSecret()));
SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_SHA1_ALGORITHM);
// Get an hmac_sha1 instance and initialize with the signing key
diff --git a/src/main/java/com/ning/http/client/providers/apache/ApacheAsyncHttpProvider.java b/src/main/java/com/ning/http/client/providers/apache/ApacheAsyncHttpProvider.java
index 78abf16415..a125d1ba9d 100644
--- a/src/main/java/com/ning/http/client/providers/apache/ApacheAsyncHttpProvider.java
+++ b/src/main/java/com/ning/http/client/providers/apache/ApacheAsyncHttpProvider.java
@@ -12,6 +12,8 @@
*/
package com.ning.http.client.providers.apache;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
+
import com.ning.http.client.AsyncHandler;
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.AsyncHttpProvider;
@@ -47,7 +49,6 @@
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.Header;
-import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
@@ -102,7 +103,6 @@
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
-import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
@@ -158,7 +158,7 @@ public ApacheAsyncHttpProvider(AsyncHttpClientConfig config) {
params.setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());
AsyncHttpProviderConfig, ?> providerConfig = config.getAsyncHttpProviderConfig();
- if (providerConfig != null && ApacheAsyncHttpProvider.class.isAssignableFrom(providerConfig.getClass())) {
+ if (providerConfig instanceof ApacheAsyncHttpProvider) {
configure(ApacheAsyncHttpProviderConfig.class.cast(providerConfig));
}
}
@@ -171,7 +171,7 @@ public ListenableFuture execute(Request request, AsyncHandler handler)
throw new IOException("Closed");
}
- if (ResumableAsyncHandler.class.isAssignableFrom(handler.getClass())) {
+ if (handler instanceof ResumableAsyncHandler) {
request = ResumableAsyncHandler.class.cast(handler).adjustRequestRange(request);
}
@@ -224,7 +224,7 @@ public void close() {
}
}
- public Response prepareResponse(HttpResponseStatus status, HttpResponseHeaders headers, Collection bodyParts) {
+ public Response prepareResponse(HttpResponseStatus status, HttpResponseHeaders headers, List bodyParts) {
return new ApacheResponse(status, headers, bodyParts);
}
@@ -340,13 +340,12 @@ private HttpMethodBase createMethod(HttpClient client, Request request) throws I
throw new IllegalStateException(String.format("Invalid Method", methodName));
}
- ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer();
- boolean avoidProxy = ProxyUtils.avoidProxy(proxyServer, request);
- if (!avoidProxy) {
+ ProxyServer proxyServer = ProxyUtils.getProxyServer(config, request);
+ if (proxyServer != null) {
if (proxyServer.getPrincipal() != null) {
Credentials defaultcreds = new UsernamePasswordCredentials(proxyServer.getPrincipal(), proxyServer.getPassword());
- client.getState().setCredentials(new AuthScope(null, -1, AuthScope.ANY_REALM), defaultcreds);
+ client.getState().setProxyCredentials(new AuthScope(null, -1, AuthScope.ANY_REALM), defaultcreds);
}
ProxyHost proxyHost = proxyServer == null ? null : new ProxyHost(proxyServer.getHost(), proxyServer.getPort());
@@ -357,7 +356,7 @@ private HttpMethodBase createMethod(HttpClient client, Request request) throws I
}
method.setFollowRedirects(false);
- if ((request.getCookies() != null) && !request.getCookies().isEmpty()) {
+ if (isNonEmpty(request.getCookies())) {
for (Cookie cookie : request.getCookies()) {
method.setRequestHeader("Cookie", AsyncHttpProviderUtils.encodeCookies(request.getCookies()));
}
@@ -461,7 +460,7 @@ public T call() {
future.setReaperFuture(reaperFuture);
}
- if (TransferCompletionHandler.class.isAssignableFrom(asyncHandler.getClass())) {
+ if (asyncHandler instanceof TransferCompletionHandler) {
throw new IllegalStateException(TransferCompletionHandler.class.getName() + "not supported by this provider");
}
@@ -579,9 +578,10 @@ public T call() {
}
}
- if (ProgressAsyncHandler.class.isAssignableFrom(asyncHandler.getClass())) {
- ProgressAsyncHandler.class.cast(asyncHandler).onHeaderWriteCompleted();
- ProgressAsyncHandler.class.cast(asyncHandler).onContentWriteCompleted();
+ if (asyncHandler instanceof ProgressAsyncHandler) {
+ ProgressAsyncHandler progressAsyncHandler = (ProgressAsyncHandler) asyncHandler;
+ progressAsyncHandler.onHeaderWriteCompleted();
+ progressAsyncHandler.onContentWriteCompleted();
}
try {
@@ -593,7 +593,7 @@ public T call() {
}
} catch (Throwable t) {
- if (IOException.class.isAssignableFrom(t.getClass()) && config.getIOExceptionFilters().size() > 0) {
+ if (t instanceof IOException && !config.getIOExceptionFilters().isEmpty()) {
FilterContext fc = new FilterContext.FilterContextBuilder().asyncHandler(asyncHandler)
.request(future.getRequest()).ioException(IOException.class.cast(t)).build();
@@ -603,7 +603,7 @@ public T call() {
if (config.getMaxTotalConnections() != -1) {
maxConnections.decrementAndGet();
}
- future.done(null);
+ future.done();
method.releaseConnection();
}
@@ -629,7 +629,7 @@ public T call() {
if (config.getMaxTotalConnections() != -1) {
maxConnections.decrementAndGet();
}
- future.done(null);
+ future.done();
// Crappy Apache HttpClient who blocks forever here with large files.
config.executorService().submit(new Runnable() {
@@ -644,20 +644,18 @@ public void run() {
}
private Throwable filterException(Throwable t) {
- if (UnknownHostException.class.isAssignableFrom(t.getClass())) {
+ if (t instanceof UnknownHostException) {
t = new ConnectException(t.getMessage());
- }
- if (NoHttpResponseException.class.isAssignableFrom(t.getClass())) {
+ } else if (t instanceof NoHttpResponseException) {
int responseTimeoutInMs = config.getRequestTimeoutInMs();
if (request.getPerRequestConfig() != null && request.getPerRequestConfig().getRequestTimeoutInMs() != -1) {
responseTimeoutInMs = request.getPerRequestConfig().getRequestTimeoutInMs();
}
t = new TimeoutException(String.format("No response received after %s", responseTimeoutInMs));
- }
- if (SSLHandshakeException.class.isAssignableFrom(t.getClass())) {
+ } else if (t instanceof SSLHandshakeException) {
Throwable t2 = new ConnectException();
t2.initCause(t);
t = t2;
diff --git a/src/main/java/com/ning/http/client/providers/apache/ApacheResponse.java b/src/main/java/com/ning/http/client/providers/apache/ApacheResponse.java
index 9516f89ee4..988962dbdd 100644
--- a/src/main/java/com/ning/http/client/providers/apache/ApacheResponse.java
+++ b/src/main/java/com/ning/http/client/providers/apache/ApacheResponse.java
@@ -12,40 +12,40 @@
*/
package com.ning.http.client.providers.apache;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
+
+import com.ning.org.jboss.netty.handler.codec.http.CookieDecoder;
import com.ning.http.client.Cookie;
import com.ning.http.client.FluentCaseInsensitiveStringsMap;
import com.ning.http.client.HttpResponseBodyPart;
-import com.ning.http.client.HttpResponseBodyPartsInputStream;
import com.ning.http.client.HttpResponseHeaders;
import com.ning.http.client.HttpResponseStatus;
import com.ning.http.client.Response;
import com.ning.http.util.AsyncHttpProviderUtils;
-import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
-
+import java.util.Set;
public class ApacheResponse implements Response {
private final static String DEFAULT_CHARSET = "ISO-8859-1";
- private final static String HEADERS_NOT_COMPUTED = "Response's headers hasn't been computed by your AsyncHandler.";
private final URI uri;
- private final Collection bodyParts;
+ private final List bodyParts;
private final HttpResponseHeaders headers;
private final HttpResponseStatus status;
- private final List cookies = new ArrayList();
+ private List cookies;
public ApacheResponse(HttpResponseStatus status,
HttpResponseHeaders headers,
- Collection bodyParts) {
+ List bodyParts) {
this.bodyParts = bodyParts;
this.headers = headers;
@@ -71,31 +71,22 @@ public byte[] getResponseBodyAsBytes() throws IOException {
return AsyncHttpProviderUtils.contentToByte(bodyParts);
}
+ public ByteBuffer getResponseBodyAsByteBuffer() throws IOException {
+ return ByteBuffer.wrap(getResponseBodyAsBytes());
+ }
+
/* @Override */
public String getResponseBody() throws IOException {
return getResponseBody(DEFAULT_CHARSET);
}
public String getResponseBody(String charset) throws IOException {
- String contentType = getContentType();
- if (contentType != null && charset == null) {
- charset = AsyncHttpProviderUtils.parseCharset(contentType);
- }
-
- if (charset == null) {
- charset = DEFAULT_CHARSET;
- }
-
- return AsyncHttpProviderUtils.contentToString(bodyParts, charset);
+ return AsyncHttpProviderUtils.contentToString(bodyParts, computeCharset(charset));
}
-
+
/* @Override */
public InputStream getResponseBodyAsStream() throws IOException {
- if (bodyParts.size() > 0) {
- return new HttpResponseBodyPartsInputStream(bodyParts.toArray(new HttpResponseBodyPart[bodyParts.size()]));
- } else {
- return new ByteArrayInputStream("".getBytes());
- }
+ return AsyncHttpProviderUtils.contentToInputStream(bodyParts);
}
/* @Override */
@@ -107,18 +98,20 @@ public String getResponseBodyExcerpt(int maxLength) throws IOException {
/* @Override */
public String getResponseBodyExcerpt(int maxLength, String charset) throws IOException {
- String contentType = getContentType();
- if (contentType != null && charset == null) {
- charset = AsyncHttpProviderUtils.parseCharset(contentType);
- }
-
- if (charset == null) {
- charset = DEFAULT_CHARSET;
- }
+ charset = computeCharset(charset);
String response = AsyncHttpProviderUtils.contentToString(bodyParts, charset);
return response.length() <= maxLength ? response : response.substring(0, maxLength);
}
+
+ private String computeCharset(String charset) {
+ if (charset == null) {
+ String contentType = getContentType();
+ if (contentType != null)
+ charset = AsyncHttpProviderUtils.parseCharset(contentType); // parseCharset can return null
+ }
+ return charset != null? charset: DEFAULT_CHARSET;
+ }
/* @Override */
@@ -129,64 +122,63 @@ public URI getUri() throws MalformedURLException {
/* @Override */
public String getContentType() {
- if (headers == null) {
- throw new IllegalStateException(HEADERS_NOT_COMPUTED);
- }
- return headers.getHeaders().getFirstValue("Content-Type");
+ return getHeader("Content-Type");
}
/* @Override */
public String getHeader(String name) {
- if (headers == null) {
- throw new IllegalStateException();
- }
- return headers.getHeaders().getFirstValue(name);
+ return headers != null? headers.getHeaders().getFirstValue(name): null;
}
/* @Override */
public List getHeaders(String name) {
- if (headers == null) {
- throw new IllegalStateException(HEADERS_NOT_COMPUTED);
- }
- return headers.getHeaders().get(name);
+ return headers != null? headers.getHeaders().get(name): Collections. emptyList();
}
/* @Override */
public FluentCaseInsensitiveStringsMap getHeaders() {
- if (headers == null) {
- throw new IllegalStateException(HEADERS_NOT_COMPUTED);
- }
- return headers.getHeaders();
+ return headers != null? headers.getHeaders(): new FluentCaseInsensitiveStringsMap();
}
/* @Override */
public boolean isRedirected() {
- return (status.getStatusCode() >= 300) && (status.getStatusCode() <= 399);
+ switch (status.getStatusCode()) {
+ case 301:
+ case 302:
+ case 303:
+ case 307:
+ case 308:
+ return true;
+ default:
+ return false;
+ }
}
/* @Override */
public List getCookies() {
if (headers == null) {
- throw new IllegalStateException(HEADERS_NOT_COMPUTED);
+ return Collections.emptyList();
}
- if (cookies.isEmpty()) {
+ if (cookies == null) {
+ List localCookies = new ArrayList();
for (Map.Entry> header : headers.getHeaders().entrySet()) {
if (header.getKey().equalsIgnoreCase("Set-Cookie")) {
// TODO: ask for parsed header
List v = header.getValue();
for (String value : v) {
- Cookie cookie = AsyncHttpProviderUtils.parseCookie(value);
- cookies.add(cookie);
+ Set cookies = CookieDecoder.decode(value);
+ localCookies.addAll(cookies);
}
}
}
+ cookies = Collections.unmodifiableList(localCookies);
}
- return Collections.unmodifiableList(cookies);
+ return cookies;
}
/**
@@ -194,7 +186,7 @@ public List getCookies() {
*/
/* @Override */
public boolean hasResponseStatus() {
- return (bodyParts != null ? true : false);
+ return bodyParts != null;
}
/**
@@ -202,7 +194,7 @@ public boolean hasResponseStatus() {
*/
/* @Override */
public boolean hasResponseHeaders() {
- return (headers != null ? true : false);
+ return headers != null;
}
/**
@@ -210,6 +202,6 @@ public boolean hasResponseHeaders() {
*/
/* @Override */
public boolean hasResponseBody() {
- return (bodyParts != null && bodyParts.size() > 0 ? true : false);
+ return isNonEmpty(bodyParts);
}
}
diff --git a/src/main/java/com/ning/http/client/providers/apache/ApacheResponseFuture.java b/src/main/java/com/ning/http/client/providers/apache/ApacheResponseFuture.java
index 0b8abff3e9..cdbe106c92 100644
--- a/src/main/java/com/ning/http/client/providers/apache/ApacheResponseFuture.java
+++ b/src/main/java/com/ning/http/client/providers/apache/ApacheResponseFuture.java
@@ -12,6 +12,8 @@
*/
package com.ning.http.client.providers.apache;
+import static com.ning.http.util.DateUtil.millisTime;
+
import com.ning.http.client.AsyncHandler;
import com.ning.http.client.Request;
import com.ning.http.client.listenable.AbstractListenableFuture;
@@ -19,7 +21,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@@ -41,7 +42,7 @@ public class ApacheResponseFuture extends AbstractListenableFuture {
private final AtomicBoolean timedOut = new AtomicBoolean(false);
private final AtomicBoolean isDone = new AtomicBoolean(false);
private final AtomicReference exception = new AtomicReference();
- private final AtomicLong touch = new AtomicLong(System.currentTimeMillis());
+ private final AtomicLong touch = new AtomicLong(millisTime());
private final AtomicBoolean contentProcessed = new AtomicBoolean(false);
private final Request request;
private final HttpMethodBase method;
@@ -62,12 +63,12 @@ protected void setInnerFuture(Future innerFuture) {
this.innerFuture = innerFuture;
}
- public void done(Callable callable) {
+ public void done() {
isDone.set(true);
if (reaperFuture != null) {
reaperFuture.cancel(true);
}
- super.done();
+ runListeners();
}
/**
@@ -123,7 +124,7 @@ public void abort(Throwable t) {
logger.debug("asyncHandler.onThrowable", t2);
}
}
- super.done();
+ runListeners();
}
public boolean cancel(boolean mayInterruptIfRunning) {
@@ -138,10 +139,10 @@ public boolean cancel(boolean mayInterruptIfRunning) {
if (reaperFuture != null) {
reaperFuture.cancel(true);
}
- super.done();
+ runListeners();
return innerFuture.cancel(mayInterruptIfRunning);
} else {
- super.done();
+ runListeners();
return false;
}
}
@@ -174,7 +175,7 @@ public V get(long timeout, TimeUnit unit) throws InterruptedException, Execution
content = innerFuture.get(timeout, unit);
}
} catch (TimeoutException t) {
- if (!contentProcessed.get() && timeout != -1 && ((System.currentTimeMillis() - touch.get()) <= responseTimeoutInMs)) {
+ if (!contentProcessed.get() && timeout != -1 && ((millisTime() - touch.get()) <= responseTimeoutInMs)) {
return get(timeout, unit);
}
@@ -197,11 +198,11 @@ public V get(long timeout, TimeUnit unit) throws InterruptedException, Execution
* @return true
if response has expired and should be terminated.
*/
public boolean hasExpired() {
- return responseTimeoutInMs != -1 && ((System.currentTimeMillis() - touch.get()) >= responseTimeoutInMs);
+ return responseTimeoutInMs != -1 && ((millisTime() - touch.get()) >= responseTimeoutInMs);
}
public void touch() {
- touch.set(System.currentTimeMillis());
+ touch.set(millisTime());
}
public Request getRequest() {
diff --git a/src/main/java/com/ning/http/client/providers/grizzly/FeedableBodyGenerator.java b/src/main/java/com/ning/http/client/providers/grizzly/FeedableBodyGenerator.java
index 4e509964db..e9d826b651 100644
--- a/src/main/java/com/ning/http/client/providers/grizzly/FeedableBodyGenerator.java
+++ b/src/main/java/com/ning/http/client/providers/grizzly/FeedableBodyGenerator.java
@@ -14,73 +14,233 @@
import com.ning.http.client.Body;
import com.ning.http.client.BodyGenerator;
+
import java.io.IOException;
import java.nio.ByteBuffer;
-import java.util.Queue;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.ExecutionException;
+
import org.glassfish.grizzly.Buffer;
+import org.glassfish.grizzly.CompletionHandler;
+import org.glassfish.grizzly.Connection;
+import org.glassfish.grizzly.WriteHandler;
+import org.glassfish.grizzly.WriteResult;
+import org.glassfish.grizzly.filterchain.FilterChain;
import org.glassfish.grizzly.filterchain.FilterChainContext;
import org.glassfish.grizzly.http.HttpContent;
import org.glassfish.grizzly.http.HttpRequestPacket;
+import org.glassfish.grizzly.impl.FutureImpl;
+import org.glassfish.grizzly.nio.NIOConnection;
+import org.glassfish.grizzly.nio.SelectorRunner;
+import org.glassfish.grizzly.ssl.SSLBaseFilter;
+import org.glassfish.grizzly.ssl.SSLFilter;
+import org.glassfish.grizzly.utils.Futures;
+
+import static com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProvider.getHttpTransactionContext;
+import static java.lang.Boolean.TRUE;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.glassfish.grizzly.ssl.SSLUtils.getSSLEngine;
+import static org.glassfish.grizzly.utils.Exceptions.*;
/**
- * {@link BodyGenerator} which may return just part of the payload at the time
- * handler is requesting it. If it happens - PartialBodyGenerator becomes responsible
- * for finishing payload transferring asynchronously.
+ * A Grizzly-specific {@link BodyGenerator} that allows data to be fed to the
+ * connection in blocking or non-blocking fashion via the use of a {@link Feeder}.
+ *
+ * This class provides two {@link Feeder} implementations for rapid prototyping.
+ * First is the {@link SimpleFeeder} which is simply a listener that asynchronous
+ * data transferring has been initiated. The second is the {@link NonBlockingFeeder}
+ * which allows reading and feeding data in a non-blocking fashion.
*
* @author The Grizzly Team
* @since 1.7.0
*/
public class FeedableBodyGenerator implements BodyGenerator {
- private final Queue queue = new ConcurrentLinkedQueue();
- private final AtomicInteger queueSize = new AtomicInteger();
-
+
+ /**
+ * There is no limit on bytes waiting to be written. This configuration
+ * value should be used with caution as it could lead to out-of-memory
+ * conditions.
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public static final int UNBOUND = -1;
+
+ /**
+ * Defer to whatever the connection has been configured for max pending bytes.
+ */
+ public static final int DEFAULT = -2;
+
private volatile HttpRequestPacket requestPacket;
private volatile FilterChainContext context;
-
+ private volatile HttpContent.Builder contentBuilder;
+
+ private final EmptyBody EMPTY_BODY = new EmptyBody();
+
+ private Feeder feeder;
+ private int origMaxPendingBytes;
+ private int configuredMaxPendingBytes = DEFAULT;
+ private boolean asyncTransferInitiated;
+
+
+ // ---------------------------------------------- Methods from BodyGenerator
+
+
+ /**
+ * {@inheritDoc}
+ */
@Override
public Body createBody() throws IOException {
- return new EmptyBody();
+ return EMPTY_BODY;
}
-
- public void feed(final Buffer buffer, final boolean isLast)
- throws IOException {
- queue.offer(new BodyPart(buffer, isLast));
- queueSize.incrementAndGet();
-
- if (context != null) {
- flushQueue();
+
+
+ // ---------------------------------------------------------- Public Methods
+
+
+ /**
+ * Configured the maximum number of bytes that may be pending to be written
+ * to the wire. If not explicitly configured, the connection's current
+ * configuration will be used instead.
+ *
+ * Once all data has been fed, the connection's max pending bytes configuration
+ * will be restored to its original value.
+ *
+ * @param maxPendingBytes maximum number of bytes that may be queued to
+ * be written to the wire.
+ *
+ * @throws IllegalStateException if called after {@link #initializeAsynchronousTransfer(FilterChainContext, HttpRequestPacket)}
+ * has been called by the {@link GrizzlyAsyncHttpProvider}.
+ * @throws IllegalArgumentException if maxPendingBytes is less than zero and is
+ * not {@link #UNBOUND} or {@link #DEFAULT}.
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public synchronized void setMaxPendingBytes(final int maxPendingBytes) {
+ if (maxPendingBytes < DEFAULT) {
+ throw new IllegalArgumentException("Invalid maxPendingBytes value: " + maxPendingBytes);
+ }
+ if (asyncTransferInitiated) {
+ throw new IllegalStateException("Unable to set max pending bytes after async data transfer has been initiated.");
}
+ configuredMaxPendingBytes = maxPendingBytes;
}
+
+
+ /**
+ * Add a {@link Feeder} implementation that will be invoked when writing
+ * without blocking is possible. This method must be set before dispatching
+ * the request this feeder is associated with.
+ *
+ * @param feeder the {@link Feeder} responsible for providing data.
+ *
+ * @throws IllegalStateException if called after {@link #initializeAsynchronousTransfer(FilterChainContext, HttpRequestPacket)}
+ * has been called by the {@link GrizzlyAsyncHttpProvider}.
+ * @throws IllegalArgumentException if feeder
is null
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public synchronized void setFeeder(final Feeder feeder) {
+ if (asyncTransferInitiated) {
+ throw new IllegalStateException("Unable to set Feeder after async data transfer has been initiated.");
+ }
+ if (feeder == null) {
+ throw new IllegalArgumentException("Feeder argument cannot be null.");
+ }
+ this.feeder = feeder;
+ }
+
+
+ // ------------------------------------------------- Package Private Methods
+
- void initializeAsynchronousTransfer(final FilterChainContext context,
- final HttpRequestPacket requestPacket) throws IOException {
- this.context = context;
+ synchronized void initializeAsynchronousTransfer(final FilterChainContext context,
+ final HttpRequestPacket requestPacket)
+ throws IOException {
+
+ if (asyncTransferInitiated) {
+ throw new IllegalStateException("Async transfer has already been initiated.");
+ }
+ if (feeder == null) {
+ throw new IllegalStateException("No feeder available to perform the transfer.");
+ }
+ assert (context != null);
+ assert (requestPacket != null);
+
this.requestPacket = requestPacket;
- flushQueue();
+ this.contentBuilder = HttpContent.builder(requestPacket);
+ final Connection c = context.getConnection();
+ origMaxPendingBytes = c.getMaxAsyncWriteQueueSize();
+ if (configuredMaxPendingBytes != DEFAULT) {
+ c.setMaxAsyncWriteQueueSize(configuredMaxPendingBytes);
+ }
+ this.context = context;
+ asyncTransferInitiated = true;
+ final Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (requestPacket.isSecure() &&
+ (getSSLEngine(context.getConnection()) == null)) {
+ flushOnSSLHandshakeComplete();
+ } else {
+ feeder.flush();
+ }
+ } catch (IOException ioe) {
+ GrizzlyAsyncHttpProvider.HttpTransactionContext ctx =
+ GrizzlyAsyncHttpProvider.getHttpTransactionContext(
+ c);
+ ctx.abort(ioe);
+ }
+ }
+ };
+
+ // If the current thread is a selector thread, we need to execute
+ // the remainder of the task on the worker thread to prevent
+ // it from being blocked.
+ if (isCurrentThreadSelectorRunner()) {
+ c.getTransport().getWorkerThreadPool().execute(r);
+ } else {
+ r.run();
+ }
}
- private void flushQueue() throws IOException {
- if (queueSize.get() > 0) {
- synchronized(this) {
- while(queueSize.get() > 0) {
- final BodyPart bodyPart = queue.poll();
- queueSize.decrementAndGet();
- final HttpContent content =
- requestPacket.httpContentBuilder()
- .content(bodyPart.buffer)
- .last(bodyPart.isLast)
- .build();
- context.write(content, ((!requestPacket.isCommitted()) ?
- context.getTransportContext().getCompletionHandler() :
- null));
-
+
+ // --------------------------------------------------------- Private Methods
+
+
+ private boolean isCurrentThreadSelectorRunner() {
+ final NIOConnection c = (NIOConnection) context.getConnection();
+ final SelectorRunner runner = c.getSelectorRunner();
+ return (Thread.currentThread() == runner.getRunnerThread());
+ }
+
+
+ private void flushOnSSLHandshakeComplete() throws IOException {
+ final FilterChain filterChain = context.getFilterChain();
+ final int idx = filterChain.indexOfType(SSLFilter.class);
+ assert (idx != -1);
+ final SSLFilter filter = (SSLFilter) filterChain.get(idx);
+ final Connection c = context.getConnection();
+ filter.addHandshakeListener(new SSLBaseFilter.HandshakeListener() {
+ public void onStart(Connection connection) {
+ }
+
+ public void onComplete(Connection connection) {
+ if (c.equals(connection)) {
+ filter.removeHandshakeListener(this);
+ try {
+ feeder.flush();
+ } catch (IOException ioe) {
+ GrizzlyAsyncHttpProvider.HttpTransactionContext ctx =
+ GrizzlyAsyncHttpProvider.getHttpTransactionContext(c);
+ ctx.abort(ioe);
+ }
}
}
- }
+ });
+ filter.handshake(context.getConnection(), null);
}
-
+
+
+ // ----------------------------------------------------------- Inner Classes
+
+
private final class EmptyBody implements Body {
@Override
@@ -98,16 +258,406 @@ public void close() throws IOException {
context.completeAndRecycle();
context = null;
requestPacket = null;
+ contentBuilder = null;
}
- }
-
- private final static class BodyPart {
- private final boolean isLast;
- private final Buffer buffer;
- public BodyPart(final Buffer buffer, final boolean isLast) {
- this.buffer = buffer;
- this.isLast = isLast;
+ } // END EmptyBody
+
+
+ // ---------------------------------------------------------- Nested Classes
+
+
+ /**
+ * Specifies the functionality all Feeders must implement. Typically,
+ * developers need not worry about implementing this interface directly.
+ * It should be sufficient, for most use-cases, to simply use the {@link NonBlockingFeeder}
+ * or {@link SimpleFeeder} implementations.
+ */
+ public interface Feeder {
+
+ /**
+ * This method will be invoked when it's possible to begin feeding
+ * data downstream. Implementations of this method must use {@link #feed(Buffer, boolean)}
+ * to perform the actual write.
+ *
+ * @throws IOException if an I/O error occurs.
+ */
+ void flush() throws IOException;
+
+ /**
+ * This method will write the specified {@link Buffer} to the connection.
+ * Be aware that this method may block depending if data is being fed
+ * faster than it can write. How much data may be queued is dictated
+ * by {@link #setMaxPendingBytes(int)}. Once this threshold is exceeded,
+ * the method will block until the write queue length drops below the
+ * aforementioned threshold.
+ *
+ * @param buffer the {@link Buffer} to write.
+ * @param last flag indicating if this is the last buffer to send.
+ *
+ * @throws IOException if an I/O error occurs.
+ * @throws java.lang.IllegalArgumentException if buffer
+ * is null
.
+ * @throws java.lang.IllegalStateException if this method is invoked
+ * before asynchronous transferring has been initiated.
+ *
+ * @see #setMaxPendingBytes(int)
+ */
+ void feed(final Buffer buffer, final boolean last) throws IOException;
+
+ } // END Feeder
+
+
+ /**
+ * Base class for {@link Feeder} implementations. This class provides
+ * an implementation for the contract defined by the {@link #feed} method.
+ */
+ public static abstract class BaseFeeder implements Feeder {
+
+ protected final FeedableBodyGenerator feedableBodyGenerator;
+
+
+ // -------------------------------------------------------- Constructors
+
+
+ protected BaseFeeder(FeedableBodyGenerator feedableBodyGenerator) {
+ this.feedableBodyGenerator = feedableBodyGenerator;
}
- }
+
+
+ // --------------------------------------------- Package Private Methods
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public final synchronized void feed(final Buffer buffer, final boolean last)
+ throws IOException {
+ if (buffer == null) {
+ throw new IllegalArgumentException(
+ "Buffer argument cannot be null.");
+ }
+ if (!feedableBodyGenerator.asyncTransferInitiated) {
+ throw new IllegalStateException("Asynchronous transfer has not been initiated.");
+ }
+ blockUntilQueueFree(feedableBodyGenerator.context.getConnection());
+ final HttpContent content =
+ feedableBodyGenerator.contentBuilder.content(buffer).last(last).build();
+ final CompletionHandler handler =
+ ((last) ? new LastPacketCompletionHandler() : null);
+ feedableBodyGenerator.context.write(content, handler);
+ }
+
+ /**
+ * This method will block if the async write queue is currently larger
+ * than the configured maximum. The amount of time that this method
+ * will block is dependent on the write timeout of the transport
+ * associated with the specified connection.
+ */
+ private static void blockUntilQueueFree(final Connection c) {
+ if (!c.canWrite()) {
+ final FutureImpl future =
+ Futures.createSafeFuture();
+ // Connection may be obtained by calling FilterChainContext.getConnection().
+ c.notifyCanWrite(new WriteHandler() {
+
+ @Override
+ public void onWritePossible() throws Exception {
+ future.result(TRUE);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ future.failure(makeIOException(t));
+ }
+ });
+
+ block(c, future);
+ }
+ }
+
+ private static void block(final Connection c,
+ final FutureImpl future) {
+ try {
+ final long writeTimeout =
+ c.getTransport().getWriteTimeout(MILLISECONDS);
+ if (writeTimeout != -1) {
+ future.get(writeTimeout, MILLISECONDS);
+ } else {
+ future.get();
+ }
+ } catch (ExecutionException e) {
+ GrizzlyAsyncHttpProvider.HttpTransactionContext httpCtx =
+ getHttpTransactionContext(c);
+ httpCtx.abort(e.getCause());
+ } catch (Exception e) {
+ GrizzlyAsyncHttpProvider.HttpTransactionContext httpCtx =
+ getHttpTransactionContext(c);
+ httpCtx.abort(e);
+ }
+ }
+
+
+ // ------------------------------------------------------- Inner Classes
+
+
+ private final class LastPacketCompletionHandler
+ implements CompletionHandler {
+
+ private final CompletionHandler delegate;
+ private final Connection c;
+ private final int origMaxPendingBytes;
+
+ // -------------------------------------------------------- Constructors
+
+
+ @SuppressWarnings("unchecked")
+ private LastPacketCompletionHandler() {
+ delegate = ((!feedableBodyGenerator.requestPacket.isCommitted())
+ ? feedableBodyGenerator.context.getTransportContext().getCompletionHandler()
+ : null);
+ c = feedableBodyGenerator.context.getConnection();
+ origMaxPendingBytes = feedableBodyGenerator.origMaxPendingBytes;
+ }
+
+
+ // -------------------------------------- Methods from CompletionHandler
+
+
+ @Override
+ public void cancelled() {
+ c.setMaxAsyncWriteQueueSize(origMaxPendingBytes);
+ if (delegate != null) {
+ delegate.cancelled();
+ }
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+ c.setMaxAsyncWriteQueueSize(origMaxPendingBytes);
+ if (delegate != null) {
+ delegate.failed(throwable);
+ }
+
+ }
+
+ @Override
+ public void completed(WriteResult result) {
+ c.setMaxAsyncWriteQueueSize(origMaxPendingBytes);
+ if (delegate != null) {
+ delegate.completed(result);
+ }
+
+ }
+
+ @Override
+ public void updated(WriteResult result) {
+ if (delegate != null) {
+ delegate.updated(result);
+ }
+ }
+
+ } // END LastPacketCompletionHandler
+
+ } // END Feeder
+
+
+ /**
+ * Implementations of this class provide the framework to read data from
+ * some source and feed data to the {@link FeedableBodyGenerator}
+ * without blocking.
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public static abstract class NonBlockingFeeder extends BaseFeeder {
+
+
+ // -------------------------------------------------------- Constructors
+
+
+ /**
+ * Constructs the NonBlockingFeeder
with the associated
+ * {@link com.ning.http.client.providers.grizzly.FeedableBodyGenerator}.
+ */
+ public NonBlockingFeeder(final FeedableBodyGenerator feedableBodyGenerator) {
+ super(feedableBodyGenerator);
+ }
+
+
+ // ------------------------------------------------------ Public Methods
+
+
+ /**
+ * Notification that it's possible to send another block of data via
+ * {@link #feed(org.glassfish.grizzly.Buffer, boolean)}.
+ *
+ * It's important to only invoke {@link #feed(Buffer, boolean)}
+ * once per invocation of {@link #canFeed()}.
+ */
+ public abstract void canFeed();
+
+ /**
+ * @return true
if all data has been fed by this feeder,
+ * otherwise returns false
.
+ */
+ public abstract boolean isDone();
+
+ /**
+ * @return true
if data is available to be fed, otherwise
+ * returns false
. When this method returns false
,
+ * the {@link FeedableBodyGenerator} will call {@link #notifyReadyToFeed(ReadyToFeedListener)}
+ * by which this {@link NonBlockingFeeder} implementation may signal data is once
+ * again available to be fed.
+ */
+ public abstract boolean isReady();
+
+ /**
+ * Callback registration to signal the {@link FeedableBodyGenerator} that
+ * data is available once again to continue feeding. Once this listener
+ * has been invoked, the NonBlockingFeeder implementation should no longer maintain
+ * a reference to the listener.
+ */
+ public abstract void notifyReadyToFeed(final ReadyToFeedListener listener);
+
+
+ // ------------------------------------------------- Methods from Feeder
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public synchronized void flush() {
+ final Connection c = feedableBodyGenerator.context.getConnection();
+ if (isReady()) {
+ writeUntilFullOrDone(c);
+ if (!isDone()) {
+ if (!isReady()) {
+ notifyReadyToFeed(new ReadyToFeedListenerImpl());
+ }
+ if (!c.canWrite()) {
+ // write queue is full, leverage WriteListener to let us know
+ // when it is safe to write again.
+ c.notifyCanWrite(new WriteHandlerImpl());
+ }
+ }
+ } else {
+ notifyReadyToFeed(new ReadyToFeedListenerImpl());
+ }
+ }
+
+
+ // ----------------------------------------------------- Private Methods
+
+
+ private void writeUntilFullOrDone(final Connection c) {
+ while (c.canWrite()) {
+ if (isReady()) {
+ canFeed();
+ }
+ if (!isReady()) {
+ break;
+ }
+ }
+ }
+
+
+ // ------------------------------------------------------- Inner Classes
+
+
+ /**
+ * Listener to signal that data is available to be fed.
+ */
+ public interface ReadyToFeedListener {
+
+ /**
+ * Data is once again ready to be fed.
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ void ready();
+
+ } // END ReadyToFeedListener
+
+
+ private final class WriteHandlerImpl implements WriteHandler {
+
+
+ private final Connection c;
+
+
+ // -------------------------------------------------------- Constructors
+
+
+ private WriteHandlerImpl() {
+ this.c = feedableBodyGenerator.context.getConnection();
+ }
+
+
+ // ------------------------------------------ Methods from WriteListener
+
+ @Override
+ public void onWritePossible() throws Exception {
+ writeUntilFullOrDone(c);
+ if (!isDone()) {
+ if (!isReady()) {
+ notifyReadyToFeed(new ReadyToFeedListenerImpl());
+ }
+ if (!c.canWrite()) {
+ // write queue is full, leverage WriteListener to let us know
+ // when it is safe to write again.
+ c.notifyCanWrite(this);
+ }
+ }
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ c.setMaxAsyncWriteQueueSize(feedableBodyGenerator.origMaxPendingBytes);
+ GrizzlyAsyncHttpProvider.HttpTransactionContext ctx =
+ GrizzlyAsyncHttpProvider.getHttpTransactionContext(c);
+ ctx.abort(t);
+ }
+
+ } // END WriteHandlerImpl
+
+
+ private final class ReadyToFeedListenerImpl
+ implements NonBlockingFeeder.ReadyToFeedListener {
+
+
+ // ------------------------------------ Methods from ReadyToFeedListener
+
+
+ @Override
+ public void ready() {
+ flush();
+ }
+
+ } // END ReadToFeedListenerImpl
+
+ } // END NonBlockingFeeder
+
+
+ /**
+ * This simple {@link Feeder} implementation allows the implementation to
+ * feed data in whatever fashion is deemed appropriate.
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ public abstract static class SimpleFeeder extends BaseFeeder {
+
+
+ // -------------------------------------------------------- Constructors
+
+
+ /**
+ * Constructs the SimpleFeeder
with the associated
+ * {@link com.ning.http.client.providers.grizzly.FeedableBodyGenerator}.
+ */
+ public SimpleFeeder(FeedableBodyGenerator feedableBodyGenerator) {
+ super(feedableBodyGenerator);
+ }
+
+
+ } // END SimpleFeeder
+
}
diff --git a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProvider.java b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProvider.java
index 3cfa92c9e3..66828f7c91 100644
--- a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProvider.java
+++ b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProvider.java
@@ -13,9 +13,14 @@
package com.ning.http.client.providers.grizzly;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
+
+import com.ning.http.client.AsyncHttpClient;
+import com.ning.org.jboss.netty.handler.codec.http.CookieDecoder;
import com.ning.http.client.AsyncHandler;
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.AsyncHttpProvider;
+import com.ning.http.client.AsyncHttpProviderConfig;
import com.ning.http.client.Body;
import com.ning.http.client.BodyGenerator;
import com.ning.http.client.ConnectionsPool;
@@ -27,7 +32,6 @@
import com.ning.http.client.HttpResponseStatus;
import com.ning.http.client.ListenableFuture;
import com.ning.http.client.MaxRedirectException;
-import com.ning.http.client.Part;
import com.ning.http.client.PerRequestConfig;
import com.ning.http.client.ProxyServer;
import com.ning.http.client.Realm;
@@ -51,7 +55,6 @@
import com.ning.http.util.AuthenticatorUtils;
import com.ning.http.util.ProxyUtils;
import com.ning.http.util.SslUtils;
-
import org.glassfish.grizzly.Buffer;
import org.glassfish.grizzly.CompletionHandler;
import org.glassfish.grizzly.Connection;
@@ -59,9 +62,11 @@
import org.glassfish.grizzly.FileTransfer;
import org.glassfish.grizzly.Grizzly;
import org.glassfish.grizzly.WriteResult;
+import org.glassfish.grizzly.asyncqueue.AsyncQueueWriter;
import org.glassfish.grizzly.attributes.Attribute;
import org.glassfish.grizzly.attributes.AttributeStorage;
import org.glassfish.grizzly.filterchain.BaseFilter;
+import org.glassfish.grizzly.filterchain.FilterChain;
import org.glassfish.grizzly.filterchain.FilterChainBuilder;
import org.glassfish.grizzly.filterchain.FilterChainContext;
import org.glassfish.grizzly.filterchain.FilterChainEvent;
@@ -77,13 +82,12 @@
import org.glassfish.grizzly.http.HttpResponsePacket;
import org.glassfish.grizzly.http.Method;
import org.glassfish.grizzly.http.Protocol;
-import org.glassfish.grizzly.impl.FutureImpl;
-import org.glassfish.grizzly.utils.Charsets;
import org.glassfish.grizzly.http.util.CookieSerializerUtils;
import org.glassfish.grizzly.http.util.DataChunk;
import org.glassfish.grizzly.http.util.Header;
import org.glassfish.grizzly.http.util.HttpStatus;
import org.glassfish.grizzly.http.util.MimeHeaders;
+import org.glassfish.grizzly.impl.FutureImpl;
import org.glassfish.grizzly.impl.SafeFutureImpl;
import org.glassfish.grizzly.memory.Buffers;
import org.glassfish.grizzly.memory.MemoryManager;
@@ -95,21 +99,24 @@
import org.glassfish.grizzly.strategies.SameThreadIOStrategy;
import org.glassfish.grizzly.strategies.WorkerThreadIOStrategy;
import org.glassfish.grizzly.utils.BufferOutputStream;
+import org.glassfish.grizzly.utils.Charsets;
import org.glassfish.grizzly.utils.DelayedExecutor;
import org.glassfish.grizzly.utils.Futures;
import org.glassfish.grizzly.utils.IdleTimeoutFilter;
+import org.glassfish.grizzly.websockets.ClosingFrame;
import org.glassfish.grizzly.websockets.DataFrame;
-import org.glassfish.grizzly.websockets.DefaultWebSocket;
import org.glassfish.grizzly.websockets.HandShake;
+import org.glassfish.grizzly.websockets.HandshakeException;
import org.glassfish.grizzly.websockets.ProtocolHandler;
+import org.glassfish.grizzly.websockets.SimpleWebSocket;
import org.glassfish.grizzly.websockets.Version;
-import org.glassfish.grizzly.websockets.WebSocketEngine;
import org.glassfish.grizzly.websockets.WebSocketFilter;
-import org.glassfish.grizzly.websockets.draft06.ClosingFrame;
+import org.glassfish.grizzly.websockets.WebSocketHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
+import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
@@ -121,11 +128,12 @@
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
-import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;
@@ -134,6 +142,8 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
+import static com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProviderConfig.Property.BUFFER_WEBSOCKET_FRAGMENTS;
+import static com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProviderConfig.Property.MAX_HTTP_PACKET_HEADER_SIZE;
import static com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProviderConfig.Property.TRANSPORT_CUSTOMIZER;
/**
@@ -147,9 +157,9 @@ public class GrizzlyAsyncHttpProvider implements AsyncHttpProvider {
private final static Logger LOGGER = LoggerFactory.getLogger(GrizzlyAsyncHttpProvider.class);
private static final boolean SEND_FILE_SUPPORT;
static {
- SEND_FILE_SUPPORT = configSendFileSupport();
+ SEND_FILE_SUPPORT = /*configSendFileSupport()*/ false;
}
- private final Attribute REQUEST_STATE_ATTR =
+ private static final Attribute REQUEST_STATE_ATTR =
Grizzly.DEFAULT_ATTRIBUTE_BUILDER.createAttribute(HttpTransactionContext.class.getName());
private final BodyHandlerFactory bodyHandlerFactory = new BodyHandlerFactory();
@@ -193,8 +203,11 @@ public GrizzlyAsyncHttpProvider(final AsyncHttpClientConfig clientConfig) {
public ListenableFuture execute(final Request request,
final AsyncHandler handler) throws IOException {
- final GrizzlyResponseFuture future =
- new GrizzlyResponseFuture(this, request, handler);
+ if (clientTransport.isStopped()) {
+ throw new IOException("AsyncHttpClient has been closed.");
+ }
+ final ProxyServer proxy = ProxyUtils.getProxyServer(clientConfig, request);
+ final GrizzlyResponseFuture future = new GrizzlyResponseFuture(this, request, handler, proxy);
future.setDelegate(SafeFutureImpl.create());
final CompletionHandler connectHandler = new CompletionHandler() {
@Override
@@ -252,13 +265,14 @@ public void close() {
try {
connectionManager.destroy();
- clientTransport.stop();
+ clientTransport.shutdownNow();
final ExecutorService service = clientConfig.executorService();
if (service != null) {
service.shutdown();
}
if (timeoutExecutor != null) {
timeoutExecutor.stop();
+ timeoutExecutor.getThreadPool().shutdownNow();
}
} catch (IOException ignored) { }
@@ -270,7 +284,7 @@ public void close() {
*/
public Response prepareResponse(HttpResponseStatus status,
HttpResponseHeaders headers,
- Collection bodyParts) {
+ List bodyParts) {
return new GrizzlyResponse(status, headers, bodyParts);
@@ -369,8 +383,12 @@ public void onTimeout(Connection connection) {
false);
final SwitchingSSLFilter filter = new SwitchingSSLFilter(configurator, defaultSecState);
fcb.add(filter);
+ final GrizzlyAsyncHttpProviderConfig providerConfig =
+ clientConfig.getAsyncHttpProviderConfig() instanceof GrizzlyAsyncHttpProviderConfig ?
+ (GrizzlyAsyncHttpProviderConfig) clientConfig.getAsyncHttpProviderConfig()
+ : new GrizzlyAsyncHttpProviderConfig();
final AsyncHttpClientEventFilter eventFilter = new
- AsyncHttpClientEventFilter(this);
+ AsyncHttpClientEventFilter(this, (Integer) providerConfig.getProperty(MAX_HTTP_PACKET_HEADER_SIZE));
final AsyncHttpClientFilter clientFilter =
new AsyncHttpClientFilter(clientConfig);
ContentEncoding[] encodings = eventFilter.getContentEncodings();
@@ -387,22 +405,17 @@ public void onTimeout(Connection connection) {
}
fcb.add(eventFilter);
fcb.add(clientFilter);
-
- GrizzlyAsyncHttpProviderConfig providerConfig =
- (GrizzlyAsyncHttpProviderConfig) clientConfig.getAsyncHttpProviderConfig();
- if (providerConfig != null) {
- final TransportCustomizer customizer = (TransportCustomizer)
- providerConfig.getProperty(TRANSPORT_CUSTOMIZER);
- if (customizer != null) {
- customizer.customize(clientTransport, fcb);
- } else {
- doDefaultTransportConfig();
- }
+ clientTransport.getAsyncQueueIO().getWriter()
+ .setMaxPendingBytesPerConnection(AsyncQueueWriter.AUTO_SIZE);
+ final TransportCustomizer customizer = (TransportCustomizer)
+ providerConfig.getProperty(TRANSPORT_CUSTOMIZER);
+ if (customizer != null) {
+ customizer.customize(clientTransport, fcb);
} else {
doDefaultTransportConfig();
}
fcb.add(new WebSocketFilter());
- clientTransport.getAsyncQueueIO().getWriter().setMaxPendingBytesPerConnection(-1);
+
clientTransport.setProcessor(fcb.build());
}
@@ -492,7 +505,7 @@ public void updated(WriteResult result) {
}
- void setHttpTransactionContext(final AttributeStorage storage,
+ static void setHttpTransactionContext(final AttributeStorage storage,
final HttpTransactionContext httpTransactionState) {
if (httpTransactionState == null) {
@@ -503,7 +516,7 @@ void setHttpTransactionContext(final AttributeStorage storage,
}
- HttpTransactionContext getHttpTransactionContext(final AttributeStorage storage) {
+ static HttpTransactionContext getHttpTransactionContext(final AttributeStorage storage) {
return REQUEST_STATE_ATTR.get(storage);
@@ -521,7 +534,7 @@ void timeout(final Connection c) {
static int getPort(final URI uri, final int p) {
int port = p;
if (port == -1) {
- final String protocol = uri.getScheme().toLowerCase();
+ final String protocol = uri.getScheme().toLowerCase(Locale.ENGLISH);
if ("http".equals(protocol) || "ws".equals(protocol)) {
port = 80;
} else if ("https".equals(protocol) || "wss".equals(protocol)) {
@@ -550,13 +563,17 @@ boolean sendRequest(final FilterChainContext ctx,
handler = new ExpectHandler(handler);
}
context.bodyHandler = handler;
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("REQUEST: " + requestPacket.toString());
+ }
isWriteComplete = handler.doHandle(ctx, request, requestPacket);
} else {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("REQUEST: " + requestPacket.toString());
+ }
ctx.write(requestPacket, ctx.getTransportContext().getCompletionHandler());
}
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("REQUEST: " + requestPacket.toString());
- }
+
return isWriteComplete;
}
@@ -619,6 +636,7 @@ final class HttpTransactionContext {
HandShake handshake;
ProtocolHandler protocolHandler;
WebSocket webSocket;
+ boolean establishingTunnel;
// -------------------------------------------------------- Constructors
@@ -663,9 +681,9 @@ void abort(final Throwable t) {
}
}
- void done(final Callable c) {
+ void done() {
if (future != null) {
- future.done(c);
+ future.done();
}
}
@@ -673,10 +691,19 @@ void done(final Callable c) {
void result(Object result) {
if (future != null) {
future.delegate.result(result);
- future.done(null);
+ future.done();
}
}
+ boolean isTunnelEstablished(final Connection c) {
+ return c.getAttributes().getAttribute("tunnel-established") != null;
+ }
+
+
+ void tunnelEstablished(final Connection c) {
+ c.getAttributes().setAttribute("tunnel-established", Boolean.TRUE);
+ }
+
} // END HttpTransactionContext
@@ -796,24 +823,6 @@ public NextAction handleEvent(final FilterChainContext ctx,
}
-// @Override
-// public NextAction handleRead(FilterChainContext ctx) throws IOException {
-// Object message = ctx.getMessage();
-// if (HttpPacket.isHttp(message)) {
-// final HttpPacket packet = (HttpPacket) message;
-// HttpResponsePacket responsePacket;
-// if (HttpContent.isContent(packet)) {
-// responsePacket = (HttpResponsePacket) ((HttpContent) packet).getHttpHeader();
-// } else {
-// responsePacket = (HttpResponsePacket) packet;
-// }
-// if (HttpStatus.SWITCHING_PROTOCOLS_101.statusMatches(responsePacket.getStatus())) {
-// return ctx.getStopAction();
-// }
-// }
-// return super.handleRead(ctx);
-// }
-
// ----------------------------------------------------- Private Methods
@@ -826,7 +835,7 @@ private boolean sendAsGrizzlyRequest(final Request request,
httpCtx.isWSRequest = true;
convertToUpgradeRequest(httpCtx);
}
- final URI uri = AsyncHttpProviderUtils.createUri(httpCtx.requestUrl);
+ final URI uri = httpCtx.request.getURI();
final HttpRequestPacket.Builder builder = HttpRequestPacket.builder();
boolean secure = "https".equals(uri.getScheme());
builder.method(request.getMethod());
@@ -841,12 +850,16 @@ private boolean sendAsGrizzlyRequest(final Request request,
builder.header(Header.Host, uri.getHost() + ':' + uri.getPort());
}
}
- final ProxyServer proxy = getProxyServer(request);
- final boolean useProxy = (proxy != null);
+ final ProxyServer proxy = ProxyUtils.getProxyServer(config, request);
+ final boolean useProxy = proxy != null;
if (useProxy) {
- if (secure) {
+ if ((secure || httpCtx.isWSRequest) && !httpCtx.isTunnelEstablished(ctx.getConnection())) {
+ secure = false;
+ httpCtx.establishingTunnel = true;
builder.method(Method.CONNECT);
builder.uri(AsyncHttpProviderUtils.getAuthority(uri));
+ } else if (secure && config.isUseRelativeURIsWithSSLProxies()){
+ builder.uri(uri.getPath());
} else {
builder.uri(uri.toString());
}
@@ -864,10 +877,10 @@ private boolean sendAsGrizzlyRequest(final Request request,
}
HttpRequestPacket requestPacket;
- if (httpCtx.isWSRequest) {
+ if (httpCtx.isWSRequest && !httpCtx.establishingTunnel) {
try {
final URI wsURI = new URI(httpCtx.wsRequestURI);
- httpCtx.protocolHandler = Version.DRAFT17.createHandler(true);
+ httpCtx.protocolHandler = Version.RFC6455.createHandler(true);
httpCtx.handshake = httpCtx.protocolHandler.createHandShake(wsURI);
requestPacket = (HttpRequestPacket)
httpCtx.handshake.composeHeaders().getHttpHeader();
@@ -877,7 +890,10 @@ private boolean sendAsGrizzlyRequest(final Request request,
} else {
requestPacket = builder.build();
}
- requestPacket.setSecure(true);
+ requestPacket.setSecure(secure);
+
+ ctx.notifyDownstream(new SwitchingSSLFilter.SSLSwitchingEvent(secure, ctx.getConnection()));
+
if (!useProxy && !httpCtx.isWSRequest) {
addQueryString(request, requestPacket);
}
@@ -885,25 +901,20 @@ private boolean sendAsGrizzlyRequest(final Request request,
addCookies(request, requestPacket);
if (useProxy) {
- boolean avoidProxy = ProxyUtils.avoidProxy(proxy, request);
- if (!avoidProxy) {
- if (!requestPacket.getHeaders().contains(Header.ProxyConnection)) {
- requestPacket.setHeader(Header.ProxyConnection, "keep-alive");
- }
+ if (!requestPacket.getHeaders().contains(Header.ProxyConnection)) {
+ requestPacket.setHeader(Header.ProxyConnection, "keep-alive");
+ }
- if (proxy.getPrincipal() != null) {
- requestPacket.setHeader(Header.ProxyAuthorization,
- AuthenticatorUtils.computeBasicAuthentication(proxy));
- }
+ if (proxy.getPrincipal() != null) {
+ requestPacket.setHeader(Header.ProxyAuthorization,
+ AuthenticatorUtils.computeBasicAuthentication(proxy));
}
}
final AsyncHandler h = httpCtx.handler;
- if (h != null) {
- if (TransferCompletionHandler.class.isAssignableFrom(h.getClass())) {
- final FluentCaseInsensitiveStringsMap map =
- new FluentCaseInsensitiveStringsMap(request.getHeaders());
- TransferCompletionHandler.class.cast(h).transferAdapter(new GrizzlyTransferAdapter(map));
- }
+ if (h instanceof TransferCompletionHandler) {
+ final FluentCaseInsensitiveStringsMap map =
+ new FluentCaseInsensitiveStringsMap(request.getHeaders());
+ TransferCompletionHandler.class.cast(h).transferAdapter(new GrizzlyTransferAdapter(map));
}
return sendRequest(ctx, request, requestPacket);
@@ -933,27 +944,15 @@ private void convertToUpgradeRequest(final HttpTransactionContext ctx) {
ctx.requestUrl = sb.toString();
}
-
- private ProxyServer getProxyServer(Request request) {
-
- ProxyServer proxyServer = request.getProxyServer();
- if (proxyServer == null) {
- proxyServer = config.getProxyServer();
- }
- return proxyServer;
-
- }
-
-
private void addHeaders(final Request request,
final HttpRequestPacket requestPacket) {
final FluentCaseInsensitiveStringsMap map = request.getHeaders();
- if (map != null && !map.isEmpty()) {
+ if (isNonEmpty(map)) {
for (final Map.Entry> entry : map.entrySet()) {
final String headerName = entry.getKey();
final List headerValues = entry.getValue();
- if (headerValues != null && !headerValues.isEmpty()) {
+ if (isNonEmpty(headerValues)) {
for (final String headerValue : headerValues) {
requestPacket.addHeader(headerName, headerValue);
}
@@ -982,7 +981,7 @@ private void addCookies(final Request request,
final HttpRequestPacket requestPacket) {
final Collection cookies = request.getCookies();
- if (cookies != null && !cookies.isEmpty()) {
+ if (isNonEmpty(cookies)) {
StringBuilder sb = new StringBuilder(128);
org.glassfish.grizzly.http.Cookie[] gCookies =
new org.glassfish.grizzly.http.Cookie[cookies.size()];
@@ -1016,16 +1015,16 @@ private void addQueryString(final Request request,
final HttpRequestPacket requestPacket) {
final FluentStringsMap map = request.getQueryParams();
- if (map != null && !map.isEmpty()) {
+ if (isNonEmpty(map)) {
StringBuilder sb = new StringBuilder(128);
for (final Map.Entry> entry : map.entrySet()) {
final String name = entry.getKey();
final List values = entry.getValue();
- if (values != null && !values.isEmpty()) {
+ if (isNonEmpty(values)) {
try {
for (int i = 0, len = values.size(); i < len; i++) {
final String value = values.get(i);
- if (value != null && value.length() > 0) {
+ if (isNonEmpty(value)) {
sb.append(URLEncoder.encode(name, "UTF-8")).append('=')
.append(URLEncoder.encode(values.get(i), "UTF-8")).append('&');
} else {
@@ -1036,11 +1035,11 @@ private void addQueryString(final Request request,
}
}
}
- String queryString = sb.deleteCharAt((sb.length() - 1)).toString();
+ sb.setLength(sb.length() - 1);
+ String queryString = sb.toString();
requestPacket.setQueryString(queryString);
}
-
}
} // END AsyncHttpClientFiler
@@ -1053,15 +1052,15 @@ private static final class AsyncHttpClientEventFilter extends HttpClientFilter {
private final GrizzlyAsyncHttpProvider provider;
-
// -------------------------------------------------------- Constructors
- AsyncHttpClientEventFilter(final GrizzlyAsyncHttpProvider provider) {
+ AsyncHttpClientEventFilter(final GrizzlyAsyncHttpProvider provider, int maxHerdersSizeProperty) {
+ super(maxHerdersSizeProperty);
this.provider = provider;
HANDLER_MAP.put(HttpStatus.UNAUTHORIZED_401.getStatusCode(),
- AuthorizationHandler.INSTANCE);
+ AuthorizationHandler.INSTANCE);
HANDLER_MAP.put(HttpStatus.MOVED_PERMANENTLY_301.getStatusCode(),
RedirectHandler.INSTANCE);
HANDLER_MAP.put(HttpStatus.FOUND_302.getStatusCode(),
@@ -1108,10 +1107,8 @@ protected void onHttpContentParsed(HttpContent content,
protected void onHttpHeadersEncoded(HttpHeader httpHeader, FilterChainContext ctx) {
final HttpTransactionContext context = provider.getHttpTransactionContext(ctx.getConnection());
final AsyncHandler handler = context.handler;
- if (handler != null) {
- if (TransferCompletionHandler.class.isAssignableFrom(handler.getClass())) {
- ((TransferCompletionHandler) handler).onHeaderWriteCompleted();
- }
+ if (handler instanceof TransferCompletionHandler) {
+ ((TransferCompletionHandler) handler).onHeaderWriteCompleted();
}
}
@@ -1119,15 +1116,13 @@ protected void onHttpHeadersEncoded(HttpHeader httpHeader, FilterChainContext ct
protected void onHttpContentEncoded(HttpContent content, FilterChainContext ctx) {
final HttpTransactionContext context = provider.getHttpTransactionContext(ctx.getConnection());
final AsyncHandler handler = context.handler;
- if (handler != null) {
- if (TransferCompletionHandler.class.isAssignableFrom(handler.getClass())) {
- final int written = content.getContent().remaining();
- final long total = context.totalBodyWritten.addAndGet(written);
- ((TransferCompletionHandler) handler).onContentWriteProgress(
- written,
- total,
- content.getHttpHeader().getContentLength());
- }
+ if (handler instanceof TransferCompletionHandler) {
+ final int written = content.getContent().remaining();
+ final long total = context.totalBodyWritten.addAndGet(written);
+ ((TransferCompletionHandler) handler).onContentWriteProgress(
+ written,
+ total,
+ content.getHttpHeader().getContentLength());
}
}
@@ -1139,14 +1134,19 @@ protected void onInitialLineParsed(HttpHeader httpHeader,
if (httpHeader.isSkipRemainder()) {
return;
}
+ final Connection connection = ctx.getConnection();
final HttpTransactionContext context =
- provider.getHttpTransactionContext(ctx.getConnection());
+ provider.getHttpTransactionContext(connection);
final int status = ((HttpResponsePacket) httpHeader).getStatus();
+ if (context.establishingTunnel && HttpStatus.OK_200.statusMatches(status)) {
+ return;
+ }
if (HttpStatus.CONINTUE_100.statusMatches(status)) {
ctx.notifyUpstream(new ContinueEvent(context));
return;
}
+
if (context.statusHandler != null && !context.statusHandler.handlesStatus(status)) {
context.statusHandler = null;
context.invocationStatus = StatusHandler.InvocationStatus.CONTINUE;
@@ -1180,9 +1180,9 @@ protected void onInitialLineParsed(HttpHeader httpHeader,
}
}
final GrizzlyResponseStatus responseStatus =
- new GrizzlyResponseStatus((HttpResponsePacket) httpHeader,
- getURI(context.requestUrl),
- provider);
+ new GrizzlyResponseStatus((HttpResponsePacket) httpHeader,
+ context.request.getURI(),
+ provider);
context.responseStatus = responseStatus;
if (context.statusHandler != null) {
return;
@@ -1193,6 +1193,10 @@ protected void onInitialLineParsed(HttpHeader httpHeader,
final AsyncHandler handler = context.handler;
if (handler != null) {
context.currentState = handler.onStatusReceived(responseStatus);
+ if (context.isWSRequest && context.currentState == AsyncHandler.STATE.ABORT) {
+ httpHeader.setSkipRemainder(true);
+ context.abort(new HandshakeException("Upgrade failed"));
+ }
}
} catch (Exception e) {
httpHeader.setSkipRemainder(true);
@@ -1221,24 +1225,23 @@ protected void onHttpHeadersParsed(HttpHeader httpHeader,
FilterChainContext ctx) {
super.onHttpHeadersParsed(httpHeader, ctx);
- if (LOGGER.isDebugEnabled()) {
- LOGGER.debug("RESPONSE: " + httpHeader.toString());
- }
+ LOGGER.debug("RESPONSE: {}", httpHeader);
if (httpHeader.containsHeader(Header.Connection)) {
if ("close".equals(httpHeader.getHeader(Header.Connection))) {
ConnectionManager.markConnectionAsDoNotCache(ctx.getConnection());
}
}
- if (httpHeader.isSkipRemainder()) {
- return;
- }
final HttpTransactionContext context =
provider.getHttpTransactionContext(ctx.getConnection());
+ if (httpHeader.isSkipRemainder() || context.establishingTunnel) {
+ return;
+ }
+
final AsyncHandler handler = context.handler;
final List filters = context.provider.clientConfig.getResponseFilters();
final GrizzlyResponseHeaders responseHeaders = new GrizzlyResponseHeaders((HttpResponsePacket) httpHeader,
- null,
- provider);
+ null,
+ provider);
if (!filters.isEmpty()) {
FilterContext fc = new FilterContext.FilterContextBuilder()
.asyncHandler(handler).request(context.request)
@@ -1260,16 +1263,16 @@ protected void onHttpHeadersParsed(HttpHeader httpHeader,
context.provider.connectionManager;
final Connection c =
m.obtainConnection(newRequest,
- context.future);
+ context.future);
final HttpTransactionContext newContext =
context.copy();
context.future = null;
provider.setHttpTransactionContext(c, newContext);
try {
context.provider.execute(c,
- newRequest,
- newHandler,
- context.future);
+ newRequest,
+ newHandler,
+ context.future);
} catch (IOException ioe) {
newContext.abort(ioe);
}
@@ -1281,8 +1284,8 @@ protected void onHttpHeadersParsed(HttpHeader httpHeader,
}
if (context.statusHandler != null && context.invocationStatus == StatusHandler.InvocationStatus.CONTINUE) {
final boolean result = context.statusHandler.handleStatus(((HttpResponsePacket) httpHeader),
- context,
- ctx);
+ context,
+ ctx);
if (!result) {
httpHeader.setSkipRemainder(true);
return;
@@ -1291,14 +1294,15 @@ protected void onHttpHeadersParsed(HttpHeader httpHeader,
if (context.isWSRequest) {
try {
context.protocolHandler.setConnection(ctx.getConnection());
- DefaultWebSocket ws = new DefaultWebSocket(context.protocolHandler);
- context.webSocket = new GrizzlyWebSocketAdapter(ws);
+ final GrizzlyWebSocketAdapter webSocketAdapter = createWebSocketAdapter(context);
+ context.webSocket = webSocketAdapter;
+ SimpleWebSocket ws = webSocketAdapter.gWebSocket;
if (context.currentState == AsyncHandler.STATE.UPGRADE) {
httpHeader.setChunked(false);
ws.onConnect();
- WebSocketEngine.getEngine().setWebSocketHolder(ctx.getConnection(),
- context.protocolHandler,
- ws);
+ WebSocketHolder.set(ctx.getConnection(),
+ context.protocolHandler,
+ ws);
((WebSocketUpgradeHandler) context.handler).onSuccess(context.webSocket);
final int wsTimeout = context.provider.clientConfig.getWebSocketIdleTimeoutInMs();
IdleTimeoutFilter.setCustomTimeout(ctx.getConnection(),
@@ -1347,25 +1351,53 @@ protected boolean onHttpPacketParsed(HttpHeader httpHeader, FilterChainContext c
result = super.onHttpPacketParsed(httpHeader, ctx);
- final HttpTransactionContext context = cleanup(ctx, provider);
-
- final AsyncHandler handler = context.handler;
- if (handler != null) {
+ final HttpTransactionContext context = provider.getHttpTransactionContext(ctx.getConnection());
+ if (context.establishingTunnel
+ && HttpStatus.OK_200.statusMatches(
+ ((HttpResponsePacket) httpHeader).getStatus())) {
+ context.establishingTunnel = false;
+ final Connection c = ctx.getConnection();
+ context.tunnelEstablished(c);
try {
- context.result(handler.onCompleted());
- } catch (Exception e) {
+ context.provider.execute(c,
+ context.request,
+ context.handler,
+ context.future);
+ return result;
+ } catch (IOException e) {
context.abort(e);
+ return result;
}
} else {
- context.done(null);
- }
+ cleanup(ctx, provider);
+ final AsyncHandler handler = context.handler;
+ if (handler != null) {
+ try {
+ context.result(handler.onCompleted());
+ } catch (Exception e) {
+ context.abort(e);
+ }
+ } else {
+ context.done();
+ }
- return result;
+ return result;
+ }
}
// ----------------------------------------------------- Private Methods
+ private static GrizzlyWebSocketAdapter createWebSocketAdapter(final HttpTransactionContext context) {
+ SimpleWebSocket ws = new SimpleWebSocket(context.protocolHandler);
+ AsyncHttpProviderConfig config = context.provider.clientConfig.getAsyncHttpProviderConfig();
+ boolean bufferFragments = true;
+ if (config instanceof GrizzlyAsyncHttpProviderConfig) {
+ bufferFragments = (Boolean) ((GrizzlyAsyncHttpProviderConfig) config).getProperty(BUFFER_WEBSOCKET_FRAGMENTS);
+ }
+
+ return new GrizzlyWebSocketAdapter(ws, bufferFragments);
+ }
private static boolean isRedirectAllowed(final HttpTransactionContext ctx) {
boolean allowed = ctx.request.isRedirectEnabled();
@@ -1388,8 +1420,8 @@ private static HttpTransactionContext cleanup(final FilterChainContext ctx,
if (!context.provider.connectionManager.canReturnConnection(c)) {
context.abort(new IOException("Maximum pooled connections exceeded"));
} else {
- if (!context.provider.connectionManager.returnConnection(context.requestUrl, c)) {
- ctx.getConnection().close().markForRecycle(true);
+ if (!context.provider.connectionManager.returnConnection(context.request, c)) {
+ ctx.getConnection().close();
}
}
@@ -1397,14 +1429,6 @@ private static HttpTransactionContext cleanup(final FilterChainContext ctx,
}
-
- private static URI getURI(String url) {
-
- return AsyncHttpProviderUtils.createUri(url);
-
- }
-
-
private static boolean redirectCountExceeded(final HttpTransactionContext context) {
return (context.redirectCount.get() > context.maxRedirectCount);
@@ -1453,6 +1477,13 @@ public boolean handleStatus(final HttpResponsePacket responsePacket,
}
if (realm == null) {
httpTransactionContext.invocationStatus = InvocationStatus.STOP;
+ if (httpTransactionContext.handler != null) {
+ try {
+ httpTransactionContext.handler.onStatusReceived(httpTransactionContext.responseStatus);
+ } catch (Exception e) {
+ httpTransactionContext.abort(e);
+ }
+ }
return true;
}
@@ -1461,19 +1492,20 @@ public boolean handleStatus(final HttpResponsePacket responsePacket,
final Request req = httpTransactionContext.request;
realm = new Realm.RealmBuilder().clone(realm)
.setScheme(realm.getAuthScheme())
- .setUri(URI.create(httpTransactionContext.requestUrl).getPath())
+ .setUri(httpTransactionContext.request.getURI().getPath())
.setMethodName(req.getMethod())
.setUsePreemptiveAuth(true)
.parseWWWAuthenticateHeader(auth)
.build();
- if (auth.toLowerCase().startsWith("basic")) {
+ String lowerCaseAuth = auth.toLowerCase(Locale.ENGLISH);
+ if (lowerCaseAuth.startsWith("basic")) {
req.getHeaders().remove(Header.Authorization.toString());
try {
req.getHeaders().add(Header.Authorization.toString(),
AuthenticatorUtils.computeBasicAuthentication(realm));
} catch (UnsupportedEncodingException ignored) {
}
- } else if (auth.toLowerCase().startsWith("digest")) {
+ } else if (lowerCaseAuth.startsWith("digest")) {
req.getHeaders().remove(Header.Authorization.toString());
try {
req.getHeaders().add(Header.Authorization.toString(),
@@ -1540,9 +1572,9 @@ public boolean handleStatus(final HttpResponsePacket responsePacket,
URI orig;
if (httpTransactionContext.lastRedirectURI == null) {
- orig = AsyncHttpProviderUtils.createUri(httpTransactionContext.requestUrl);
+ orig = httpTransactionContext.request.getURI();
} else {
- orig = AsyncHttpProviderUtils.getRedirectUri(AsyncHttpProviderUtils.createUri(httpTransactionContext.requestUrl),
+ orig = AsyncHttpProviderUtils.getRedirectUri(httpTransactionContext.request.getURI(),
httpTransactionContext.lastRedirectURI);
}
httpTransactionContext.lastRedirectURI = redirectURL;
@@ -1644,8 +1676,9 @@ private static Request newRequest(final URI uri,
builder.setQueryParameters(null);
}
for (String cookieStr : response.getHeaders().values(Header.Cookie)) {
- Cookie c = AsyncHttpProviderUtils.parseCookie(cookieStr);
- builder.addOrReplaceCookie(c);
+ for (Cookie c : CookieDecoder.decode(cookieStr)) {
+ builder.addOrReplaceCookie(c);
+ }
}
return builder.build();
@@ -1664,7 +1697,7 @@ private static final class ClientEncodingFilter implements EncodingFilter {
public boolean applyEncoding(HttpHeader httpPacket) {
httpPacket.addHeader(Header.AcceptEncoding, "gzip");
- return true;
+ return false;
}
@@ -1803,7 +1836,7 @@ public boolean doHandle(final FilterChainContext ctx,
String charset = request.getBodyEncoding();
if (charset == null) {
- charset = Charsets.DEFAULT_CHARACTER_ENCODING;
+ charset = Charsets.ASCII_CHARSET.name();
}
final byte[] data = new String(request.getByteData(), charset).getBytes(charset);
final MemoryManager mm = ctx.getMemoryManager();
@@ -1839,7 +1872,7 @@ public boolean doHandle(final FilterChainContext ctx,
String charset = request.getBodyEncoding();
if (charset == null) {
- charset = Charsets.DEFAULT_CHARACTER_ENCODING;
+ charset = Charsets.ASCII_CHARSET.name();
}
final byte[] data = request.getStringData().getBytes(charset);
final MemoryManager mm = ctx.getMemoryManager();
@@ -1890,8 +1923,7 @@ private final class ParamsBodyHandler implements BodyHandler {
public boolean handlesBodyType(final Request request) {
- final FluentStringsMap params = request.getParams();
- return (params != null && !params.isEmpty());
+ return isNonEmpty(request.getParams());
}
@SuppressWarnings({"unchecked"})
@@ -1906,14 +1938,14 @@ public boolean doHandle(final FilterChainContext ctx,
StringBuilder sb = null;
String charset = request.getBodyEncoding();
if (charset == null) {
- charset = Charsets.DEFAULT_CHARACTER_ENCODING;
+ charset = Charsets.ASCII_CHARSET.name();
}
final FluentStringsMap params = request.getParams();
if (!params.isEmpty()) {
for (Map.Entry> entry : params.entrySet()) {
String name = entry.getKey();
List values = entry.getValue();
- if (values != null && !values.isEmpty()) {
+ if (isNonEmpty(values)) {
if (sb == null) {
sb = new StringBuilder(128);
}
@@ -2037,8 +2069,7 @@ private static final class PartsBodyHandler implements BodyHandler {
public boolean handlesBodyType(final Request request) {
- final List parts = request.getParts();
- return (parts != null && !parts.isEmpty());
+ return isNonEmpty(request.getParts());
}
@SuppressWarnings({"unchecked"})
@@ -2050,7 +2081,7 @@ public boolean doHandle(final FilterChainContext ctx,
MultipartRequestEntity mre =
AsyncHttpProviderUtils.createMultipartRequestEntity(
request.getParts(),
- request.getParams());
+ request.getHeaders());
requestPacket.setContentLengthLong(mre.getContentLength());
requestPacket.setContentType(mre.getContentType());
final MemoryManager mm = ctx.getMemoryManager();
@@ -2126,15 +2157,13 @@ public boolean doHandle(final FilterChainContext ctx,
@Override
public void updated(WriteResult result) {
final AsyncHandler handler = context.handler;
- if (handler != null) {
- if (TransferCompletionHandler.class.isAssignableFrom(handler.getClass())) {
- final long written = result.getWrittenSize();
- final long total = context.totalBodyWritten.addAndGet(written);
- ((TransferCompletionHandler) handler).onContentWriteProgress(
- written,
- total,
- requestPacket.getContentLength());
- }
+ if (handler instanceof TransferCompletionHandler) {
+ final long written = result.getWrittenSize();
+ final long total = context.totalBodyWritten.addAndGet(written);
+ ((TransferCompletionHandler) handler).onContentWriteProgress(
+ written,
+ total,
+ requestPacket.getContentLength());
}
}
});
@@ -2262,13 +2291,12 @@ void doAsyncTrackedConnection(final Request request,
final GrizzlyResponseFuture requestFuture,
final CompletionHandler connectHandler)
throws IOException, ExecutionException, InterruptedException {
- final String url = request.getUrl();
- Connection c = pool.poll(AsyncHttpProviderUtils.getBaseUrl(url));
+ Connection c = pool.poll(getPoolKey(request, requestFuture.getProxy()));
if (c == null) {
if (!connectionMonitor.acquire()) {
throw new IOException("Max connections exceeded");
}
- doAsyncConnect(url, request, requestFuture, connectHandler);
+ doAsyncConnect(request, requestFuture, connectHandler);
} else {
provider.touchConnection(c, request);
connectHandler.completed(c);
@@ -2280,25 +2308,19 @@ Connection obtainConnection(final Request request,
final GrizzlyResponseFuture requestFuture)
throws IOException, ExecutionException, InterruptedException, TimeoutException {
- final Connection c = (obtainConnection0(request.getUrl(),
- request,
- requestFuture));
+ final Connection c = obtainConnection0(request, requestFuture);
DO_NOT_CACHE.set(c, Boolean.TRUE);
return c;
}
- void doAsyncConnect(final String url,
- final Request request,
+ void doAsyncConnect(final Request request,
final GrizzlyResponseFuture requestFuture,
final CompletionHandler connectHandler)
throws IOException, ExecutionException, InterruptedException {
- final URI uri = AsyncHttpProviderUtils.createUri(url);
- ProxyServer proxy = getProxyServer(request);
- if (ProxyUtils.avoidProxy(proxy, request)) {
- proxy = null;
- }
+ ProxyServer proxy = requestFuture.getProxy();
+ final URI uri = request.getURI();
String host = ((proxy != null) ? proxy.getHost() : uri.getHost());
int port = ((proxy != null) ? proxy.getPort() : uri.getPort());
if(request.getLocalAddress()!=null) {
@@ -2311,18 +2333,14 @@ void doAsyncConnect(final String url,
}
- private Connection obtainConnection0(final String url,
- final Request request,
+ private Connection obtainConnection0(final Request request,
final GrizzlyResponseFuture requestFuture)
throws IOException, ExecutionException, InterruptedException, TimeoutException {
- final URI uri = AsyncHttpProviderUtils.createUri(url);
- ProxyServer proxy = getProxyServer(request);
- if (ProxyUtils.avoidProxy(proxy, request)) {
- proxy = null;
- }
- String host = ((proxy != null) ? proxy.getHost() : uri.getHost());
- int port = ((proxy != null) ? proxy.getPort() : uri.getPort());
+ final URI uri = request.getURI();
+ final ProxyServer proxy = requestFuture.getProxy();
+ String host = (proxy != null) ? proxy.getHost() : uri.getHost();
+ int port = (proxy != null) ? proxy.getPort() : uri.getPort();
int cTimeout = provider.clientConfig.getConnectionTimeoutInMs();
FutureImpl future = Futures.createSafeFuture();
CompletionHandler ch = Futures.toCompletionHandler(future,
@@ -2338,26 +2356,16 @@ private Connection obtainConnection0(final String url,
}
}
- private ProxyServer getProxyServer(Request request) {
-
- ProxyServer proxyServer = request.getProxyServer();
- if (proxyServer == null) {
- proxyServer = provider.clientConfig.getProxyServer();
- }
- return proxyServer;
-
- }
-
- boolean returnConnection(final String url, final Connection c) {
+ boolean returnConnection(final Request request, final Connection c) {
+ ProxyServer proxyServer = ProxyUtils.getProxyServer(provider.clientConfig, request);
final boolean result = (DO_NOT_CACHE.get(c) == null
- && pool.offer(AsyncHttpProviderUtils.getBaseUrl(url), c));
+ && pool.offer(getPoolKey(request, proxyServer), c));
if (result) {
if (provider.resolver != null) {
provider.resolver.setTimeoutMillis(c, IdleTimeoutFilter.FOREVER);
}
}
return result;
-
}
@@ -2411,6 +2419,11 @@ public void updated(Connection result) {
};
}
+ private static String getPoolKey(Request request, ProxyServer proxyServer) {
+ URI uri = proxyServer != null? proxyServer.getURI(): request.getURI();
+ return request.getConnectionPoolKeyStrategy().getKey(uri);
+ }
+
// ------------------------------------------------------ Nested Classes
private static class ConnectionMonitor implements Connection.CloseListener {
@@ -2503,6 +2516,11 @@ public NextAction handleWrite(FilterChainContext ctx) throws IOException {
}
+ @Override
+ public void onFilterChainChanged(FilterChain filterChain) {
+ // no-op
+ }
+
// ----------------------------------------------------- Private Methods
@@ -2571,13 +2589,16 @@ public void getBytes(byte[] bytes) {
private static final class GrizzlyWebSocketAdapter implements WebSocket {
- private final org.glassfish.grizzly.websockets.WebSocket gWebSocket;
+ final SimpleWebSocket gWebSocket;
+ final boolean bufferFragments;
// -------------------------------------------------------- Constructors
- GrizzlyWebSocketAdapter(final org.glassfish.grizzly.websockets.WebSocket gWebSocket) {
- this.gWebSocket = gWebSocket;
+ GrizzlyWebSocketAdapter(final SimpleWebSocket gWebSocket,
+ final boolean bufferFragments) {
+ this.gWebSocket = gWebSocket;
+ this.bufferFragments = bufferFragments;
}
@@ -2592,7 +2613,7 @@ public WebSocket sendMessage(byte[] message) {
@Override
public WebSocket stream(byte[] fragment, boolean last) {
- if (fragment != null && fragment.length > 0) {
+ if (isNonEmpty(fragment)) {
gWebSocket.stream(last, fragment, 0, fragment.length);
}
return this;
@@ -2600,7 +2621,7 @@ public WebSocket stream(byte[] fragment, boolean last) {
@Override
public WebSocket stream(byte[] fragment, int offset, int len, boolean last) {
- if (fragment != null && fragment.length > 0) {
+ if (isNonEmpty(fragment)) {
gWebSocket.stream(last, fragment, offset, len);
}
return this;
@@ -2658,14 +2679,25 @@ public void close() {
private static final class AHCWebSocketListenerAdapter implements org.glassfish.grizzly.websockets.WebSocketListener {
private final WebSocketListener ahcListener;
- private final WebSocket webSocket;
+ private final GrizzlyWebSocketAdapter webSocket;
+ private final StringBuilder stringBuffer;
+ private final ByteArrayOutputStream byteArrayOutputStream;
+
// -------------------------------------------------------- Constructors
- AHCWebSocketListenerAdapter(final WebSocketListener ahcListener, WebSocket webSocket) {
+ AHCWebSocketListenerAdapter(final WebSocketListener ahcListener,
+ final GrizzlyWebSocketAdapter webSocket) {
this.ahcListener = ahcListener;
this.webSocket = webSocket;
+ if (webSocket.bufferFragments) {
+ stringBuffer = new StringBuilder();
+ byteArrayOutputStream = new ByteArrayOutputStream();
+ } else {
+ stringBuffer = null;
+ byteArrayOutputStream = null;
+ }
}
@@ -2675,7 +2707,7 @@ private static final class AHCWebSocketListenerAdapter implements org.glassfish.
@Override
public void onClose(org.glassfish.grizzly.websockets.WebSocket gWebSocket, DataFrame dataFrame) {
try {
- if (WebSocketCloseCodeReasonListener.class.isAssignableFrom(ahcListener.getClass())) {
+ if (ahcListener instanceof WebSocketCloseCodeReasonListener) {
ClosingFrame cf = ClosingFrame.class.cast(dataFrame);
WebSocketCloseCodeReasonListener.class.cast(ahcListener).onClose(webSocket, cf.getCode(), cf.getReason());
} else {
@@ -2698,7 +2730,7 @@ public void onConnect(org.glassfish.grizzly.websockets.WebSocket gWebSocket) {
@Override
public void onMessage(org.glassfish.grizzly.websockets.WebSocket webSocket, String s) {
try {
- if (WebSocketTextListener.class.isAssignableFrom(ahcListener.getClass())) {
+ if (ahcListener instanceof WebSocketTextListener) {
WebSocketTextListener.class.cast(ahcListener).onMessage(s);
}
} catch (Throwable e) {
@@ -2709,7 +2741,7 @@ public void onMessage(org.glassfish.grizzly.websockets.WebSocket webSocket, Stri
@Override
public void onMessage(org.glassfish.grizzly.websockets.WebSocket webSocket, byte[] bytes) {
try {
- if (WebSocketByteListener.class.isAssignableFrom(ahcListener.getClass())) {
+ if (ahcListener instanceof WebSocketByteListener) {
WebSocketByteListener.class.cast(ahcListener).onMessage(bytes);
}
} catch (Throwable e) {
@@ -2720,7 +2752,7 @@ public void onMessage(org.glassfish.grizzly.websockets.WebSocket webSocket, byte
@Override
public void onPing(org.glassfish.grizzly.websockets.WebSocket webSocket, byte[] bytes) {
try {
- if (WebSocketPingListener.class.isAssignableFrom(ahcListener.getClass())) {
+ if (ahcListener instanceof WebSocketPingListener) {
WebSocketPingListener.class.cast(ahcListener).onPing(bytes);
}
} catch (Throwable e) {
@@ -2731,7 +2763,7 @@ public void onPing(org.glassfish.grizzly.websockets.WebSocket webSocket, byte[]
@Override
public void onPong(org.glassfish.grizzly.websockets.WebSocket webSocket, byte[] bytes) {
try {
- if (WebSocketPongListener.class.isAssignableFrom(ahcListener.getClass())) {
+ if (ahcListener instanceof WebSocketPongListener) {
WebSocketPongListener.class.cast(ahcListener).onPong(bytes);
}
} catch (Throwable e) {
@@ -2740,10 +2772,23 @@ public void onPong(org.glassfish.grizzly.websockets.WebSocket webSocket, byte[]
}
@Override
- public void onFragment(org.glassfish.grizzly.websockets.WebSocket webSocket, String s, boolean b) {
+ public void onFragment(org.glassfish.grizzly.websockets.WebSocket webSocket, String s, boolean last) {
try {
- if (WebSocketTextListener.class.isAssignableFrom(ahcListener.getClass())) {
- WebSocketTextListener.class.cast(ahcListener).onFragment(s, b);
+ if (this.webSocket.bufferFragments) {
+ synchronized (this.webSocket) {
+ stringBuffer.append(s);
+ if (last) {
+ if (ahcListener instanceof WebSocketTextListener) {
+ final String message = stringBuffer.toString();
+ stringBuffer.setLength(0);
+ WebSocketTextListener.class.cast(ahcListener).onMessage(message);
+ }
+ }
+ }
+ } else {
+ if (ahcListener instanceof WebSocketTextListener) {
+ WebSocketTextListener.class.cast(ahcListener).onFragment(s, last);
+ }
}
} catch (Throwable e) {
ahcListener.onError(e);
@@ -2751,10 +2796,23 @@ public void onFragment(org.glassfish.grizzly.websockets.WebSocket webSocket, Str
}
@Override
- public void onFragment(org.glassfish.grizzly.websockets.WebSocket webSocket, byte[] bytes, boolean b) {
+ public void onFragment(org.glassfish.grizzly.websockets.WebSocket webSocket, byte[] bytes, boolean last) {
try {
- if (WebSocketByteListener.class.isAssignableFrom(ahcListener.getClass())) {
- WebSocketByteListener.class.cast(ahcListener).onFragment(bytes, b);
+ if (this.webSocket.bufferFragments) {
+ synchronized (this.webSocket) {
+ byteArrayOutputStream.write(bytes);
+ if (last) {
+ if (ahcListener instanceof WebSocketByteListener) {
+ final byte[] bytesLocal = byteArrayOutputStream.toByteArray();
+ byteArrayOutputStream.reset();
+ WebSocketByteListener.class.cast(ahcListener).onMessage(bytesLocal);
+ }
+ }
+ }
+ } else {
+ if (ahcListener instanceof WebSocketByteListener) {
+ WebSocketByteListener.class.cast(ahcListener).onFragment(bytes, last);
+ }
}
} catch (Throwable e) {
ahcListener.onError(e);
@@ -2783,7 +2841,35 @@ public int hashCode() {
return result;
}
} // END AHCWebSocketListenerAdapter
-
+
+
+ public static void main(String[] args) {
+ SecureRandom secureRandom = new SecureRandom();
+ SSLContext sslContext = null;
+ try {
+ sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, null, secureRandom);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ AsyncHttpClientConfig config = new AsyncHttpClientConfig.Builder()
+ .setConnectionTimeoutInMs(5000)
+ .setSSLContext(sslContext).build();
+ AsyncHttpClient client = new AsyncHttpClient(new GrizzlyAsyncHttpProvider(config), config);
+ try {
+ long start = System.currentTimeMillis();
+ try {
+ client.executeRequest(client.prepareGet("http://www.google.com/").build()).get();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ System.out.println("COMPLETE: " + (System.currentTimeMillis() - start) + "ms");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
}
diff --git a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProviderConfig.java b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProviderConfig.java
index 70b7425391..066e1a959d 100644
--- a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProviderConfig.java
+++ b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProviderConfig.java
@@ -14,6 +14,7 @@
package com.ning.http.client.providers.grizzly;
import com.ning.http.client.AsyncHttpProviderConfig;
+import org.glassfish.grizzly.http.HttpCodecFilter;
import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
import java.util.HashMap;
@@ -49,7 +50,24 @@ public static enum Property {
*
* @see TransportCustomizer
*/
- TRANSPORT_CUSTOMIZER(TransportCustomizer.class);
+ TRANSPORT_CUSTOMIZER(TransportCustomizer.class),
+
+
+ /**
+ * Defines the maximum HTTP packet header size.
+ */
+ MAX_HTTP_PACKET_HEADER_SIZE(Integer.class, HttpCodecFilter.DEFAULT_MAX_HTTP_PACKET_HEADER_SIZE),
+
+
+ /**
+ * By default, Websocket messages that are fragmented will be buffered. Once all
+ * fragments have been accumulated, the appropriate onMessage() call back will be
+ * invoked with the complete message. If this functionality is not desired, set
+ * this property to false.
+ */
+ BUFFER_WEBSOCKET_FRAGMENTS(Boolean.class, true)
+
+ ;
final Object defaultValue;
diff --git a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyConnectionsPool.java b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyConnectionsPool.java
index e3478e5769..8859fb821c 100644
--- a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyConnectionsPool.java
+++ b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyConnectionsPool.java
@@ -13,14 +13,18 @@
package com.ning.http.client.providers.grizzly;
+import static com.ning.http.util.DateUtil.millisTime;
+
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.ConnectionsPool;
+import org.glassfish.grizzly.CloseListener;
+import org.glassfish.grizzly.CloseType;
import org.glassfish.grizzly.Connection;
import org.glassfish.grizzly.Grizzly;
import org.glassfish.grizzly.attributes.Attribute;
-import org.glassfish.grizzly.attributes.NullaryFunction;
import org.glassfish.grizzly.utils.DataStructures;
+import org.glassfish.grizzly.utils.NullaryFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -55,34 +59,68 @@ public class GrizzlyConnectionsPool implements ConnectionsPool() {
+ public void onClosed(Connection connection, CloseType closeType)
+ throws IOException {
+ if (closeType == CloseType.REMOTELY) {
+ if (LOG.isInfoEnabled()) {
+ LOG.info("Remote closed connection ({}). Removing from cache",
+ connection.toString());
+ }
+ }
+ GrizzlyConnectionsPool.this.removeAll(connection);
+ }
+ };
// ------------------------------------------------------------ Constructors
+ @SuppressWarnings("UnusedDeclaration")
+ public GrizzlyConnectionsPool(final boolean cacheSSLConnections,
+ final int timeout,
+ final int maxConnectionLifeTimeInMs,
+ final int maxConnectionsPerHost,
+ final int maxConnections,
+ final DelayedExecutor delayedExecutor) {
+ this.cacheSSLConnections = cacheSSLConnections;
+ this.timeout = timeout;
+ this.maxConnectionLifeTimeInMs = maxConnectionLifeTimeInMs;
+ this.maxConnectionsPerHost = maxConnectionsPerHost;
+ this.maxConnections = maxConnections;
+ unlimitedConnections = (maxConnections == -1);
+ if (delayedExecutor != null) {
+ this.delayedExecutor = delayedExecutor;
+ ownsDelayedExecutor = false;
+ } else {
+ this.delayedExecutor =
+ new DelayedExecutor(Executors.newSingleThreadExecutor(),
+ this);
+ ownsDelayedExecutor = true;
+ }
+ if (!this.delayedExecutor.isStarted) {
+ this.delayedExecutor.start();
+ }
+ }
+
+
public GrizzlyConnectionsPool(final AsyncHttpClientConfig config) {
cacheSSLConnections = config.isSslConnectionPoolEnabled();
timeout = config.getIdleConnectionInPoolTimeoutInMs();
+ maxConnectionLifeTimeInMs = config.getMaxConnectionLifeTimeInMs();
maxConnectionsPerHost = config.getMaxConnectionPerHost();
maxConnections = config.getMaxTotalConnections();
unlimitedConnections = (maxConnections == -1);
- delayedExecutor = new DelayedExecutor(Executors.newSingleThreadExecutor());
+ delayedExecutor = new DelayedExecutor(Executors.newSingleThreadExecutor(), this);
delayedExecutor.start();
- listener = new Connection.CloseListener() {
- @Override
- public void onClosed(Connection connection, Connection.CloseType closeType) throws IOException {
- if (closeType == Connection.CloseType.REMOTELY) {
- if (LOG.isInfoEnabled()) {
- LOG.info("Remote closed connection ({}). Removing from cache", connection.toString());
- }
- }
- GrizzlyConnectionsPool.this.removeAll(connection);
- }
- };
-
+ ownsDelayedExecutor = true;
}
@@ -94,7 +132,7 @@ public void onClosed(Connection connection, Connection.CloseType closeType) thro
*/
public boolean offer(String uri, Connection connection) {
- if (cacheSSLConnections && isSecure(uri)) {
+ if (isSecure(uri) && !cacheSSLConnections) {
return false;
}
@@ -105,7 +143,7 @@ public boolean offer(String uri, Connection connection) {
new Object[]{uri, connection});
}
DelayedExecutor.IdleConnectionQueue newPool =
- delayedExecutor.createIdleConnectionQueue(timeout);
+ delayedExecutor.createIdleConnectionQueue(timeout, maxConnectionLifeTimeInMs);
conQueue = connectionsPool.putIfAbsent(uri, newPool);
if (conQueue == null) {
conQueue = newPool;
@@ -119,13 +157,14 @@ public boolean offer(String uri, Connection connection) {
final int total = totalCachedConnections.incrementAndGet();
if (LOG.isDebugEnabled()) {
LOG.debug("[offer] Pooling connection [{}] for uri [{}]. Current size (for host; before pooling): [{}]. Max size (for host): [{}]. Total number of cached connections: [{}].",
- new Object[]{connection, uri, size, maxConnectionsPerHost, total});
+ connection, uri, size, maxConnectionsPerHost, total);
}
return true;
}
if (LOG.isDebugEnabled()) {
LOG.debug("[offer] Unable to pool connection [{}] for uri [{}]. Current size (for host): [{}]. Max size (for host): [{}]. Total number of cached connections: [{}].",
- new Object[]{connection, uri, size, maxConnectionsPerHost, totalCachedConnections.get()});
+ connection, uri, size, maxConnectionsPerHost,
+ totalCachedConnections.get());
}
return false;
@@ -190,6 +229,9 @@ public boolean removeAll(Connection connection) {
boolean removed = entry.getValue().remove(connection);
isRemoved |= removed;
}
+ if (isRemoved) {
+ totalCachedConnections.decrementAndGet();
+ }
return isRemoved;
}
@@ -219,8 +261,10 @@ public void destroy() {
entry.getValue().destroy();
}
connectionsPool.clear();
- delayedExecutor.stop();
- delayedExecutor.getThreadPool().shutdownNow();
+ if (ownsDelayedExecutor) {
+ delayedExecutor.stop();
+ delayedExecutor.getThreadPool().shutdownNow();
+ }
}
@@ -230,7 +274,7 @@ public void destroy() {
private boolean isSecure(String uri) {
- return (uri.charAt(0) == 'h' && uri.charAt(4) == 's');
+ return (uri.startsWith("https") || uri.startsWith("wss"));
}
@@ -238,7 +282,7 @@ private boolean isSecure(String uri) {
// ---------------------------------------------------------- Nested Classes
- private static final class DelayedExecutor {
+ public static final class DelayedExecutor {
public final static long UNSET_TIMEOUT = -1;
private final ExecutorService threadPool;
@@ -248,25 +292,31 @@ private static final class DelayedExecutor {
private final Object sync = new Object();
private volatile boolean isStarted;
private final long checkIntervalMs;
+ private final AtomicInteger totalCachedConnections;
// -------------------------------------------------------- Constructors
- private DelayedExecutor(final ExecutorService threadPool) {
- this(threadPool, 1000, TimeUnit.MILLISECONDS);
+ public DelayedExecutor(final ExecutorService threadPool,
+ final GrizzlyConnectionsPool connectionsPool) {
+ this(threadPool, 1000, TimeUnit.MILLISECONDS, connectionsPool);
}
- // ----------------------------------------------------- Private Methods
-
- private DelayedExecutor(final ExecutorService threadPool,
+ public DelayedExecutor(final ExecutorService threadPool,
final long checkInterval,
- final TimeUnit timeunit) {
+ final TimeUnit timeunit,
+ final GrizzlyConnectionsPool connectionsPool) {
this.threadPool = threadPool;
this.checkIntervalMs = TimeUnit.MILLISECONDS.convert(checkInterval, timeunit);
+ totalCachedConnections = connectionsPool.totalCachedConnections;
}
+
+ // ----------------------------------------------------- Private Methods
+
+
private void start() {
synchronized (sync) {
if (!isStarted) {
@@ -289,15 +339,15 @@ private ExecutorService getThreadPool() {
return threadPool;
}
- private IdleConnectionQueue createIdleConnectionQueue(final long timeout) {
- final IdleConnectionQueue queue = new IdleConnectionQueue(timeout);
+ private IdleConnectionQueue createIdleConnectionQueue(final long timeout, final long maxConnectionLifeTimeInMs) {
+ final IdleConnectionQueue queue = new IdleConnectionQueue(timeout, maxConnectionLifeTimeInMs);
queues.add(queue);
return queue;
}
@SuppressWarnings({"NumberEquality"})
- private static boolean wasModified(final Long l1, final Long l2) {
- return l1 != l2 && (l1 != null ? !l1.equals(l2) : !l2.equals(l1));
+ private static boolean wasModified(final long l1, final long l2) {
+ return l1 != l2;
}
@@ -310,7 +360,7 @@ private class DelayedRunnable implements Runnable {
@Override
public void run() {
while (isStarted) {
- final long currentTimeMs = System.currentTimeMillis();
+ final long currentTimeMs = millisTime();
for (final IdleConnectionQueue delayQueue : queues) {
if (delayQueue.queue.isEmpty()) continue;
@@ -321,7 +371,7 @@ public void run() {
final Connection element = it.next();
final Long timeoutMs = resolver.getTimeoutMs(element);
- if (timeoutMs == null || timeoutMs == UNSET_TIMEOUT) {
+ if (timeoutMs == UNSET_TIMEOUT) {
it.remove();
if (wasModified(timeoutMs,
resolver.getTimeoutMs(element))) {
@@ -337,7 +387,8 @@ public void run() {
if (LOG.isDebugEnabled()) {
LOG.debug("Idle connection ({}) detected. Removing from cache.", element.toString());
}
- element.close().markForRecycle(true);
+ totalCachedConnections.decrementAndGet();
+ element.close();
} catch (Exception ignored) {
}
}
@@ -367,12 +418,14 @@ final class IdleConnectionQueue {
final TimeoutResolver resolver = new TimeoutResolver();
final long timeout;
final AtomicInteger count = new AtomicInteger(0);
+ final long maxConnectionLifeTimeInMs;
// ---------------------------------------------------- Constructors
- public IdleConnectionQueue(final long timeout) {
+ public IdleConnectionQueue(final long timeout, final long maxConnectionLifeTimeInMs) {
this.timeout = timeout;
+ this.maxConnectionLifeTimeInMs = maxConnectionLifeTimeInMs;
}
@@ -380,9 +433,25 @@ public IdleConnectionQueue(final long timeout) {
void offer(final Connection c) {
- if (timeout >= 0) {
- resolver.setTimeoutMs(c, System.currentTimeMillis() + timeout);
+ long timeoutMs = UNSET_TIMEOUT;
+ long currentTime = millisTime();
+ if (maxConnectionLifeTimeInMs < 0 && timeout >= 0) {
+ timeoutMs = currentTime + timeout;
+ } else if (maxConnectionLifeTimeInMs >= 0) {
+ long t = resolver.getTimeoutMs(c);
+ if (t == UNSET_TIMEOUT) {
+ if (timeout >= 0) {
+ timeoutMs = currentTime + Math.min(maxConnectionLifeTimeInMs, timeout);
+ } else {
+ timeoutMs = currentTime + maxConnectionLifeTimeInMs;
+ }
+ } else {
+ if (timeout >= 0) {
+ timeoutMs = Math.min(t, currentTime + timeout);
+ }
+ }
}
+ resolver.setTimeoutMs(c, timeoutMs);
queue.offer(c);
count.incrementAndGet();
}
@@ -411,7 +480,7 @@ boolean isEmpty() {
void destroy() {
for (Connection c : queue) {
- c.close().markForRecycle(true);
+ c.close();
}
queue.clear();
queues.remove(this);
@@ -445,7 +514,7 @@ boolean removeTimeout(final Connection c) {
return true;
}
- Long getTimeoutMs(final Connection c) {
+ long getTimeoutMs(final Connection c) {
return IDLE_ATTR.get(c).timeoutMs;
}
@@ -458,7 +527,7 @@ void setTimeoutMs(final Connection c, final long timeoutMs) {
static final class IdleRecord {
- volatile long timeoutMs;
+ volatile long timeoutMs = UNSET_TIMEOUT;
} // END IdleRecord
diff --git a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyResponse.java b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyResponse.java
index 52f3fece36..8df137e885 100644
--- a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyResponse.java
+++ b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyResponse.java
@@ -13,6 +13,8 @@
package com.ning.http.client.providers.grizzly;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
+
import com.ning.http.client.Cookie;
import com.ning.http.client.FluentCaseInsensitiveStringsMap;
import com.ning.http.client.HttpResponseBodyPart;
@@ -33,6 +35,7 @@
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
+import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
@@ -61,26 +64,24 @@ public class GrizzlyResponse implements Response {
public GrizzlyResponse(final HttpResponseStatus status,
final HttpResponseHeaders headers,
- final Collection bodyParts) {
+ final List bodyParts) {
this.status = status;
this.headers = headers;
this.bodyParts = bodyParts;
- if (bodyParts != null && !bodyParts.isEmpty()) {
- HttpResponseBodyPart[] parts =
- bodyParts.toArray(new HttpResponseBodyPart[bodyParts.size()]);
- if (parts.length == 1) {
- responseBody = ((GrizzlyResponseBodyPart) parts[0]).getBodyBuffer();
+ if (isNonEmpty(bodyParts)) {
+ if (bodyParts.size() == 1) {
+ responseBody = ((GrizzlyResponseBodyPart) bodyParts.get(0)).getBodyBuffer();
} else {
- final Buffer firstBuffer = ((GrizzlyResponseBodyPart) parts[0]).getBodyBuffer();
+ final Buffer firstBuffer = ((GrizzlyResponseBodyPart) bodyParts.get(0)).getBodyBuffer();
final MemoryManager mm = MemoryManager.DEFAULT_MEMORY_MANAGER;
Buffer constructedBodyBuffer = firstBuffer;
- for (int i = 1, len = parts.length; i < len; i++) {
+ for (int i = 1, len = bodyParts.size(); i < len; i++) {
constructedBodyBuffer =
Buffers.appendBuffers(mm,
constructedBodyBuffer,
- ((GrizzlyResponseBodyPart) parts[i]).getBodyBuffer());
+ ((GrizzlyResponseBodyPart) bodyParts.get(i)).getBodyBuffer());
}
responseBody = constructedBodyBuffer;
}
@@ -162,7 +163,7 @@ public String getResponseBodyExcerpt(int maxLength) throws IOException {
*/
public String getResponseBody() throws IOException {
- return getResponseBody(Charsets.DEFAULT_CHARACTER_ENCODING);
+ return getResponseBody(null);
}
@@ -171,9 +172,25 @@ public String getResponseBody() throws IOException {
* {@inheritDoc}
*/
public byte[] getResponseBodyAsBytes() throws IOException {
+ final byte[] responseBodyBytes = new byte[responseBody.remaining()];
+ final int origPos = responseBody.position();
+ responseBody.get(responseBodyBytes);
+ responseBody.position(origPos);
+ return responseBodyBytes;
+ }
- return getResponseBody().getBytes(Charsets.DEFAULT_CHARACTER_ENCODING);
+ public ByteBuffer getResponseBodyAsByteBuffer() throws IOException {
+ return responseBody.toByteBuffer();
+ }
+ /**
+ * @return the response body as a Grizzly {@link Buffer}.
+ *
+ * @since 1.7.11.
+ */
+ @SuppressWarnings("UnusedDeclaration")
+ private Buffer getResponseBodyAsBuffer() {
+ return responseBody;
}
@@ -231,9 +248,16 @@ public FluentCaseInsensitiveStringsMap getHeaders() {
* {@inheritDoc}
*/
public boolean isRedirected() {
-
- return between(status.getStatusCode(), 300, 399);
-
+ switch (status.getStatusCode()) {
+ case 301:
+ case 302:
+ case 303:
+ case 307:
+ case 308:
+ return true;
+ default:
+ return false;
+ }
}
@@ -248,16 +272,16 @@ public List getCookies() {
if (cookies == null) {
List values = headers.getHeaders().get("set-cookie");
- if (values != null && !values.isEmpty()) {
+ if (isNonEmpty(values)) {
CookiesBuilder.ServerCookiesBuilder builder =
- new CookiesBuilder.ServerCookiesBuilder(false);
+ new CookiesBuilder.ServerCookiesBuilder(false, true);
for (String header : values) {
builder.parse(header);
}
cookies = convertCookies(builder.build());
} else {
- cookies = Collections.unmodifiableList(Collections.emptyList());
+ cookies = Collections.emptyList();
}
}
return cookies;
@@ -277,7 +301,7 @@ public boolean hasResponseStatus() {
* {@inheritDoc}
*/
public boolean hasResponseHeaders() {
- return (headers != null && !headers.getHeaders().isEmpty());
+ return headers != null && !headers.getHeaders().isEmpty();
}
@@ -285,7 +309,7 @@ public boolean hasResponseHeaders() {
* {@inheritDoc}
*/
public boolean hasResponseBody() {
- return (bodyParts != null && !bodyParts.isEmpty());
+ return isNonEmpty(bodyParts);
}
@@ -328,14 +352,4 @@ private Charset getCharset(final String charset) {
return Charsets.lookupCharset(charsetLocal);
}
-
-
- private boolean between(final int value,
- final int lowerBound,
- final int upperBound) {
-
- return (value >= lowerBound && value <= upperBound);
-
- }
-
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyResponseFuture.java b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyResponseFuture.java
index 239b677206..fb68580678 100644
--- a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyResponseFuture.java
+++ b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyResponseFuture.java
@@ -14,14 +14,13 @@
package com.ning.http.client.providers.grizzly;
import com.ning.http.client.AsyncHandler;
+import com.ning.http.client.ProxyServer;
import com.ning.http.client.Request;
import com.ning.http.client.listenable.AbstractListenableFuture;
import org.glassfish.grizzly.Connection;
import org.glassfish.grizzly.impl.FutureImpl;
-import java.io.IOException;
-import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -38,10 +37,11 @@
public class GrizzlyResponseFuture extends AbstractListenableFuture {
private final AtomicBoolean done = new AtomicBoolean(false);
+ private final AtomicBoolean cancelled = new AtomicBoolean(false);
private final AsyncHandler handler;
private final GrizzlyAsyncHttpProvider provider;
private final Request request;
-
+ private final ProxyServer proxy;
private Connection connection;
FutureImpl delegate;
@@ -52,34 +52,44 @@ public class GrizzlyResponseFuture extends AbstractListenableFuture {
GrizzlyResponseFuture(final GrizzlyAsyncHttpProvider provider,
final Request request,
- final AsyncHandler handler) {
+ final AsyncHandler handler,
+ final ProxyServer proxy) {
this.provider = provider;
this.request = request;
this.handler = handler;
-
+ this.proxy = proxy;
}
// ----------------------------------- Methods from AbstractListenableFuture
- public void done(Callable callable) {
-
- done.compareAndSet(false, true);
- super.done();
+ public void done() {
+ if (!done.compareAndSet(false, true) || cancelled.get()) {
+ return;
+ }
+ runListeners();
}
public void abort(Throwable t) {
+ if (done.get() || !cancelled.compareAndSet(false, true)) {
+ return;
+ }
+
delegate.failure(t);
if (handler != null) {
- handler.onThrowable(t);
+ try {
+ handler.onThrowable(t);
+ } catch (Throwable ignore) {
+ }
+
}
closeConnection();
- done();
+ runListeners();
}
@@ -121,8 +131,16 @@ public boolean getAndSetWriteBody(boolean writeBody) {
public boolean cancel(boolean mayInterruptIfRunning) {
- handler.onThrowable(new CancellationException());
- done();
+ if (done.get() || !cancelled.compareAndSet(false, true)) {
+ return false;
+ }
+ if (handler != null) {
+ try {
+ handler.onThrowable(new CancellationException());
+ } catch (Throwable ignore) {
+ }
+ }
+ runListeners();
return delegate.cancel(mayInterruptIfRunning);
}
@@ -182,10 +200,13 @@ void setDelegate(final FutureImpl delegate) {
private void closeConnection() {
- if (connection != null && !connection.isOpen()) {
+ if (connection != null && connection.isOpen()) {
connection.close().markForRecycle(true);
}
}
+ public ProxyServer getProxy() {
+ return proxy;
+ }
}
diff --git a/src/main/java/com/ning/http/client/providers/jdk/JDKAsyncHttpProvider.java b/src/main/java/com/ning/http/client/providers/jdk/JDKAsyncHttpProvider.java
index 37c8af7748..011884c171 100644
--- a/src/main/java/com/ning/http/client/providers/jdk/JDKAsyncHttpProvider.java
+++ b/src/main/java/com/ning/http/client/providers/jdk/JDKAsyncHttpProvider.java
@@ -12,6 +12,8 @@
*/
package com.ning.http.client.providers.jdk;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
+
import com.ning.http.client.AsyncHandler;
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.AsyncHttpProvider;
@@ -41,6 +43,7 @@
import com.ning.http.util.ProxyUtils;
import com.ning.http.util.SslUtils;
import com.ning.http.util.UTF8UrlEncoder;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -68,7 +71,6 @@
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
-import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
@@ -102,7 +104,7 @@ public JDKAsyncHttpProvider(AsyncHttpClientConfig config) {
this.config = config;
AsyncHttpProviderConfig, ?> providerConfig = config.getAsyncHttpProviderConfig();
- if (providerConfig != null && JDKAsyncHttpProviderConfig.class.isAssignableFrom(providerConfig.getClass())) {
+ if (providerConfig instanceof JDKAsyncHttpProviderConfig) {
configure(JDKAsyncHttpProviderConfig.class.cast(providerConfig));
}
}
@@ -130,11 +132,10 @@ public ListenableFuture execute(Request request, AsyncHandler handler,
throw new IOException(String.format("Too many connections %s", config.getMaxTotalConnections()));
}
- ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer();
+ ProxyServer proxyServer = ProxyUtils.getProxyServer(config, request);
Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm();
- boolean avoidProxy = ProxyUtils.avoidProxy(proxyServer, request);
Proxy proxy = null;
- if (!avoidProxy && (proxyServer != null || realm != null)) {
+ if (proxyServer != null || realm != null) {
try {
proxy = configureProxyAndAuth(proxyServer, realm);
} catch (AuthenticationException e) {
@@ -163,11 +164,10 @@ public ListenableFuture execute(Request request, AsyncHandler handler,
}
private HttpURLConnection createUrlConnection(Request request) throws IOException {
- ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer();
+ ProxyServer proxyServer = ProxyUtils.getProxyServer(config, request);
Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm();
- boolean avoidProxy = ProxyUtils.avoidProxy(proxyServer, request);
Proxy proxy = null;
- if (!avoidProxy && proxyServer != null || realm != null) {
+ if (proxyServer != null || realm != null) {
try {
proxy = configureProxyAndAuth(proxyServer, realm);
} catch (AuthenticationException e) {
@@ -175,13 +175,8 @@ private HttpURLConnection createUrlConnection(Request request) throws IOExceptio
}
}
- HttpURLConnection urlConnection = null;
- if (proxy == null) {
- urlConnection =
- (HttpURLConnection) AsyncHttpProviderUtils.createUri(request.getUrl()).toURL().openConnection(Proxy.NO_PROXY);
- } else {
- urlConnection = (HttpURLConnection) AsyncHttpProviderUtils.createUri(request.getUrl()).toURL().openConnection(proxy);
- }
+ HttpURLConnection urlConnection = (HttpURLConnection)
+ request.getURI().toURL().openConnection(proxy == null ? Proxy.NO_PROXY : proxy);
if (request.getUrl().startsWith("https")) {
HttpsURLConnection secure = (HttpsURLConnection) urlConnection;
@@ -205,7 +200,7 @@ public void close() {
isClose.set(true);
}
- public Response prepareResponse(HttpResponseStatus status, HttpResponseHeaders headers, Collection bodyParts) {
+ public Response prepareResponse(HttpResponseStatus status, HttpResponseHeaders headers, List bodyParts) {
return new JDKResponse(status, headers, bodyParts);
}
@@ -243,7 +238,7 @@ public T call() throws Exception {
configure(uri, urlConnection, request);
urlConnection.connect();
- if (TransferCompletionHandler.class.isAssignableFrom(asyncHandler.getClass())) {
+ if (asyncHandler instanceof TransferCompletionHandler) {
throw new IllegalStateException(TransferCompletionHandler.class.getName() + "not supported by this provider");
}
@@ -358,14 +353,15 @@ public T call() throws Exception {
}
}
- if (ProgressAsyncHandler.class.isAssignableFrom(asyncHandler.getClass())) {
- ProgressAsyncHandler.class.cast(asyncHandler).onHeaderWriteCompleted();
- ProgressAsyncHandler.class.cast(asyncHandler).onContentWriteCompleted();
+ if (asyncHandler instanceof ProgressAsyncHandler) {
+ ProgressAsyncHandler progressAsyncHandler = (ProgressAsyncHandler) asyncHandler;
+ progressAsyncHandler.onHeaderWriteCompleted();
+ progressAsyncHandler.onContentWriteCompleted();
}
try {
T t = asyncHandler.onCompleted();
future.content(t);
- future.done(null);
+ future.done();
return t;
} catch (Throwable t) {
RuntimeException ex = new RuntimeException();
@@ -375,7 +371,7 @@ public T call() throws Exception {
} catch (Throwable t) {
logger.debug(t.getMessage(), t);
- if (IOException.class.isAssignableFrom(t.getClass()) && config.getIOExceptionFilters().size() > 0) {
+ if (t instanceof IOException && !config.getIOExceptionFilters().isEmpty()) {
FilterContext fc = new FilterContext.FilterContextBuilder().asyncHandler(asyncHandler)
.request(request).ioException(IOException.class.cast(t)).build();
@@ -385,7 +381,7 @@ public T call() throws Exception {
if (config.getMaxTotalConnections() != -1) {
maxConnections.decrementAndGet();
}
- future.done(null);
+ future.done();
}
if (fc.replayRequest()) {
@@ -426,20 +422,18 @@ private FilterContext handleIoException(FilterContext fc) throws FilterException
}
private Throwable filterException(Throwable t) {
- if (UnknownHostException.class.isAssignableFrom(t.getClass())) {
+ if (t instanceof UnknownHostException) {
t = new ConnectException(t.getMessage());
- }
- if (SocketTimeoutException.class.isAssignableFrom(t.getClass())) {
+ } else if (t instanceof SocketTimeoutException) {
int responseTimeoutInMs = config.getRequestTimeoutInMs();
if (request.getPerRequestConfig() != null && request.getPerRequestConfig().getRequestTimeoutInMs() != -1) {
responseTimeoutInMs = request.getPerRequestConfig().getRequestTimeoutInMs();
}
t = new TimeoutException(String.format("No response received after %s", responseTimeoutInMs));
- }
- if (SSLHandshakeException.class.isAssignableFrom(t.getClass())) {
+ } else if (t instanceof SSLHandshakeException) {
Throwable t2 = new ConnectException();
t2.initCause(t);
t = t2;
@@ -494,9 +488,9 @@ private void configure(URI uri, HttpURLConnection urlConnection, Request request
}
}
- String ka = config.getAllowPoolingConnection() ? "keep-alive" : "close";
+ String ka = AsyncHttpProviderUtils.keepAliveHeaderValue(config);
urlConnection.setRequestProperty("Connection", ka);
- ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer();
+ ProxyServer proxyServer = ProxyUtils.getProxyServer(config, request);
boolean avoidProxy = ProxyUtils.avoidProxy(proxyServer, uri.getHost());
if (!avoidProxy) {
urlConnection.setRequestProperty("Proxy-Connection", ka);
@@ -518,7 +512,7 @@ private void configure(URI uri, HttpURLConnection urlConnection, Request request
AuthenticatorUtils.computeBasicAuthentication(realm));
break;
case DIGEST:
- if (realm.getNonce() != null && !realm.getNonce().equals("")) {
+ if (isNonEmpty(realm.getNonce())) {
try {
urlConnection.setRequestProperty("Authorization",
AuthenticatorUtils.computeDigestAuthentication(realm));
@@ -552,7 +546,7 @@ private void configure(URI uri, HttpURLConnection urlConnection, Request request
urlConnection.setRequestProperty("User-Agent", AsyncHttpProviderUtils.constructUserAgent(JDKAsyncHttpProvider.class));
}
- if (request.getCookies() != null && !request.getCookies().isEmpty()) {
+ if (isNonEmpty(request.getCookies())) {
urlConnection.setRequestProperty("Cookie", AsyncHttpProviderUtils.encodeCookies(request.getCookies()));
}
@@ -619,7 +613,7 @@ private void configure(URI uri, HttpURLConnection urlConnection, Request request
lenght = MAX_BUFFERED_BYTES;
}
- MultipartRequestEntity mre = AsyncHttpProviderUtils.createMultipartRequestEntity(request.getParts(), request.getParams());
+ MultipartRequestEntity mre = AsyncHttpProviderUtils.createMultipartRequestEntity(request.getParts(), request.getHeaders());
urlConnection.setRequestProperty("Content-Type", mre.getContentType());
urlConnection.setRequestProperty("Content-Length", String.valueOf(mre.getContentLength()));
diff --git a/src/main/java/com/ning/http/client/providers/jdk/JDKDelegateFuture.java b/src/main/java/com/ning/http/client/providers/jdk/JDKDelegateFuture.java
index 9553e02152..b1dbcc3a3a 100644
--- a/src/main/java/com/ning/http/client/providers/jdk/JDKDelegateFuture.java
+++ b/src/main/java/com/ning/http/client/providers/jdk/JDKDelegateFuture.java
@@ -12,11 +12,12 @@
*/
package com.ning.http.client.providers.jdk;
+import static com.ning.http.util.DateUtil.millisTime;
+
import com.ning.http.client.AsyncHandler;
import com.ning.http.client.ListenableFuture;
import java.net.HttpURLConnection;
-import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -30,9 +31,9 @@ public JDKDelegateFuture(AsyncHandler asyncHandler, int responseTimeoutInMs,
this.delegateFuture = delegateFuture;
}
- public void done(Callable callable) {
- delegateFuture.done(callable);
- super.done(callable);
+ public void done() {
+ delegateFuture.done();
+ super.done();
}
public void abort(Throwable t) {
@@ -66,7 +67,7 @@ public V get(long timeout, TimeUnit unit) throws InterruptedException, Execution
content = innerFuture.get(timeout, unit);
}
} catch (Throwable t) {
- if (!contentProcessed.get() && timeout != -1 && ((System.currentTimeMillis() - touch.get()) <= responseTimeoutInMs)) {
+ if (!contentProcessed.get() && timeout != -1 && ((millisTime() - touch.get()) <= responseTimeoutInMs)) {
return get(timeout, unit);
}
timedOut.set(true);
@@ -77,7 +78,7 @@ public V get(long timeout, TimeUnit unit) throws InterruptedException, Execution
delegateFuture.abort(new ExecutionException(exception.get()));
}
delegateFuture.content(content);
- delegateFuture.done(null);
+ delegateFuture.done();
return content;
}
}
diff --git a/src/main/java/com/ning/http/client/providers/jdk/JDKFuture.java b/src/main/java/com/ning/http/client/providers/jdk/JDKFuture.java
index 4666459dc9..0ec695ae9f 100644
--- a/src/main/java/com/ning/http/client/providers/jdk/JDKFuture.java
+++ b/src/main/java/com/ning/http/client/providers/jdk/JDKFuture.java
@@ -12,13 +12,14 @@
*/
package com.ning.http.client.providers.jdk;
+import static com.ning.http.util.DateUtil.millisTime;
+
import com.ning.http.client.AsyncHandler;
import com.ning.http.client.listenable.AbstractListenableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.HttpURLConnection;
-import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@@ -40,7 +41,7 @@ public class JDKFuture extends AbstractListenableFuture {
protected final AtomicBoolean timedOut = new AtomicBoolean(false);
protected final AtomicBoolean isDone = new AtomicBoolean(false);
protected final AtomicReference exception = new AtomicReference();
- protected final AtomicLong touch = new AtomicLong(System.currentTimeMillis());
+ protected final AtomicLong touch = new AtomicLong(millisTime());
protected final AtomicBoolean contentProcessed = new AtomicBoolean(false);
protected final HttpURLConnection urlConnection;
private boolean writeHeaders;
@@ -58,9 +59,9 @@ protected void setInnerFuture(Future innerFuture) {
this.innerFuture = innerFuture;
}
- public void done(Callable callable) {
+ public void done() {
isDone.set(true);
- super.done();
+ runListeners();
}
public void abort(Throwable t) {
@@ -75,7 +76,7 @@ public void abort(Throwable t) {
logger.debug("asyncHandler.onThrowable", te);
}
}
- super.done();
+ runListeners();
}
public void content(V v) {
@@ -90,10 +91,10 @@ public boolean cancel(boolean mayInterruptIfRunning) {
logger.debug("asyncHandler.onThrowable", te);
}
cancelled.set(true);
- super.done();
+ runListeners();
return innerFuture.cancel(mayInterruptIfRunning);
} else {
- super.done();
+ runListeners();
return false;
}
}
@@ -126,7 +127,7 @@ public V get(long timeout, TimeUnit unit) throws InterruptedException, Execution
content = innerFuture.get(timeout, unit);
}
} catch (TimeoutException t) {
- if (!contentProcessed.get() && timeout != -1 && ((System.currentTimeMillis() - touch.get()) <= responseTimeoutInMs)) {
+ if (!contentProcessed.get() && timeout != -1 && ((millisTime() - touch.get()) <= responseTimeoutInMs)) {
return get(timeout, unit);
}
@@ -149,7 +150,7 @@ public V get(long timeout, TimeUnit unit) throws InterruptedException, Execution
* @return true
if response has expired and should be terminated.
*/
public boolean hasExpired() {
- return responseTimeoutInMs != -1 && ((System.currentTimeMillis() - touch.get()) > responseTimeoutInMs);
+ return responseTimeoutInMs != -1 && ((millisTime() - touch.get()) > responseTimeoutInMs);
}
/**
@@ -157,7 +158,7 @@ public boolean hasExpired() {
*/
/* @Override */
public void touch() {
- touch.set(System.currentTimeMillis());
+ touch.set(millisTime());
}
/**
diff --git a/src/main/java/com/ning/http/client/providers/jdk/JDKResponse.java b/src/main/java/com/ning/http/client/providers/jdk/JDKResponse.java
index f1fd4d2ce1..00cae65b0a 100644
--- a/src/main/java/com/ning/http/client/providers/jdk/JDKResponse.java
+++ b/src/main/java/com/ning/http/client/providers/jdk/JDKResponse.java
@@ -12,10 +12,12 @@
*/
package com.ning.http.client.providers.jdk;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
+
+import com.ning.org.jboss.netty.handler.codec.http.CookieDecoder;
import com.ning.http.client.Cookie;
import com.ning.http.client.FluentCaseInsensitiveStringsMap;
import com.ning.http.client.HttpResponseBodyPart;
-import com.ning.http.client.HttpResponseBodyPartsInputStream;
import com.ning.http.client.HttpResponseHeaders;
import com.ning.http.client.HttpResponseStatus;
import com.ning.http.client.Response;
@@ -26,29 +28,29 @@
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
public class JDKResponse implements Response {
private final static String DEFAULT_CHARSET = "ISO-8859-1";
- private final static String HEADERS_NOT_COMPUTED = "Response's headers hasn't been computed by your AsyncHandler.";
private final URI uri;
- private final Collection bodyParts;
+ private final List bodyParts;
private final HttpResponseHeaders headers;
private final HttpResponseStatus status;
- private final List cookies = new ArrayList();
+ private List cookies;
private AtomicBoolean contentComputed = new AtomicBoolean(false);
private String content;
public JDKResponse(HttpResponseStatus status,
HttpResponseHeaders headers,
- Collection bodyParts) {
+ List bodyParts) {
this.bodyParts = bodyParts;
this.headers = headers;
@@ -80,33 +82,25 @@ public byte[] getResponseBodyAsBytes() throws IOException {
return AsyncHttpProviderUtils.contentToByte(bodyParts);
}
+ public ByteBuffer getResponseBodyAsByteBuffer() throws IOException {
+ return ByteBuffer.wrap(getResponseBodyAsBytes());
+ }
+
public String getResponseBody(String charset) throws IOException {
- String contentType = getContentType();
- if (contentType != null && charset == null) {
- charset = AsyncHttpProviderUtils.parseCharset(contentType);
- }
- if (charset == null) {
- charset = DEFAULT_CHARSET;
- }
-
- if (!contentComputed.get()) {
- content = AsyncHttpProviderUtils.contentToString(bodyParts, charset);
+ if (!contentComputed.get()) {
+ content = AsyncHttpProviderUtils.contentToString(bodyParts, computeCharset(charset));
}
return content;
}
-
+
/* @Override */
public InputStream getResponseBodyAsStream() throws IOException {
if (contentComputed.get()) {
return new ByteArrayInputStream(content.getBytes(DEFAULT_CHARSET));
}
- if (bodyParts.size() > 0) {
- return new HttpResponseBodyPartsInputStream(bodyParts.toArray(new HttpResponseBodyPart[bodyParts.size()]));
- } else {
- return new ByteArrayInputStream("".getBytes());
- }
+ return AsyncHttpProviderUtils.contentToInputStream(bodyParts);
}
/* @Override */
@@ -116,14 +110,7 @@ public String getResponseBodyExcerpt(int maxLength) throws IOException {
}
public String getResponseBodyExcerpt(int maxLength, String charset) throws IOException {
- String contentType = getContentType();
- if (contentType != null && charset == null) {
- charset = AsyncHttpProviderUtils.parseCharset(contentType);
- }
-
- if (charset == null) {
- charset = DEFAULT_CHARSET;
- }
+ charset = computeCharset(charset);
if (!contentComputed.get()) {
content = AsyncHttpProviderUtils.contentToString(bodyParts, charset == null ? DEFAULT_CHARSET : charset);
@@ -131,6 +118,15 @@ public String getResponseBodyExcerpt(int maxLength, String charset) throws IOExc
return content.length() <= maxLength ? content : content.substring(0, maxLength);
}
+
+ private String computeCharset(String charset) {
+ if (charset == null) {
+ String contentType = getContentType();
+ if (contentType != null)
+ charset = AsyncHttpProviderUtils.parseCharset(contentType); // parseCharset can return null
+ }
+ return charset != null? charset: DEFAULT_CHARSET;
+ }
/* @Override */
@@ -141,64 +137,63 @@ public URI getUri() throws MalformedURLException {
/* @Override */
public String getContentType() {
- if (headers == null) {
- throw new IllegalStateException(HEADERS_NOT_COMPUTED);
- }
- return headers.getHeaders().getFirstValue("Content-Type");
+ return getHeader("Content-Type");
}
/* @Override */
public String getHeader(String name) {
- if (headers == null) {
- throw new IllegalStateException();
- }
- return headers.getHeaders().getFirstValue(name);
+ return headers != null? headers.getHeaders().getFirstValue(name): null;
}
/* @Override */
public List getHeaders(String name) {
- if (headers == null) {
- throw new IllegalStateException(HEADERS_NOT_COMPUTED);
- }
- return headers.getHeaders().get(name);
+ return headers != null? headers.getHeaders().get(name): Collections. emptyList();
}
/* @Override */
public FluentCaseInsensitiveStringsMap getHeaders() {
- if (headers == null) {
- throw new IllegalStateException(HEADERS_NOT_COMPUTED);
- }
- return headers.getHeaders();
+ return headers != null? headers.getHeaders(): new FluentCaseInsensitiveStringsMap();
}
/* @Override */
public boolean isRedirected() {
- return (status.getStatusCode() >= 300) && (status.getStatusCode() <= 399);
+ switch (status.getStatusCode()) {
+ case 301:
+ case 302:
+ case 303:
+ case 307:
+ case 308:
+ return true;
+ default:
+ return false;
+ }
}
/* @Override */
public List getCookies() {
if (headers == null) {
- throw new IllegalStateException(HEADERS_NOT_COMPUTED);
+ return Collections.emptyList();
}
- if (cookies.isEmpty()) {
+ if (!isNonEmpty(cookies)) {
+ List localCookies = new ArrayList();
for (Map.Entry> header : headers.getHeaders().entrySet()) {
if (header.getKey().equalsIgnoreCase("Set-Cookie")) {
// TODO: ask for parsed header
List v = header.getValue();
for (String value : v) {
- Cookie cookie = AsyncHttpProviderUtils.parseCookie(value);
- cookies.add(cookie);
+ Set cookies = CookieDecoder.decode(value);
+ localCookies.addAll(cookies);
}
}
}
+ cookies = Collections.unmodifiableList(localCookies);
}
- return Collections.unmodifiableList(cookies);
+ return cookies;
}
/**
@@ -206,7 +201,7 @@ public List getCookies() {
*/
/* @Override */
public boolean hasResponseStatus() {
- return (bodyParts != null ? true : false);
+ return bodyParts != null;
}
/**
@@ -214,7 +209,7 @@ public boolean hasResponseStatus() {
*/
/* @Override */
public boolean hasResponseHeaders() {
- return (headers != null ? true : false);
+ return headers != null;
}
/**
@@ -222,6 +217,6 @@ public boolean hasResponseHeaders() {
*/
/* @Override */
public boolean hasResponseBody() {
- return (bodyParts != null && bodyParts.size() > 0 ? true : false);
+ return isNonEmpty(bodyParts);
}
}
diff --git a/src/main/java/com/ning/http/client/providers/netty/BodyChunkedInput.java b/src/main/java/com/ning/http/client/providers/netty/BodyChunkedInput.java
index 9ea1de6609..1cf8282b2c 100644
--- a/src/main/java/com/ning/http/client/providers/netty/BodyChunkedInput.java
+++ b/src/main/java/com/ning/http/client/providers/netty/BodyChunkedInput.java
@@ -16,68 +16,61 @@
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.handler.stream.ChunkedInput;
-import java.io.IOException;
import java.nio.ByteBuffer;
/**
* Adapts a {@link Body} to Netty's {@link ChunkedInput}.
*/
-class BodyChunkedInput
- implements ChunkedInput {
+class BodyChunkedInput implements ChunkedInput {
- private final Body body;
-
- private final int chunkSize = 1024 * 8;
+ private static final int DEFAULT_CHUNK_SIZE = 8 * 1024;
- private ByteBuffer nextChunk;
+ private final Body body;
+ private final int contentLength;
+ private final int chunkSize;
- private static final ByteBuffer EOF = ByteBuffer.allocate(0);
+ private boolean endOfInput;
public BodyChunkedInput(Body body) {
if (body == null) {
throw new IllegalArgumentException("no body specified");
}
this.body = body;
+ contentLength = (int) body.getContentLength();
+ if (contentLength <= 0)
+ chunkSize = DEFAULT_CHUNK_SIZE;
+ else
+ chunkSize = Math.min(contentLength, DEFAULT_CHUNK_SIZE);
}
- private ByteBuffer peekNextChuck()
- throws IOException {
+ public boolean hasNextChunk() throws Exception {
+ // unused
+ throw new UnsupportedOperationException();
+ }
- if (nextChunk == null) {
+ public Object nextChunk() throws Exception {
+ if (endOfInput) {
+ return null;
+ } else {
ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
- if (body.read(buffer) < 0) {
- nextChunk = EOF;
+ long r = body.read(buffer);
+ if (r < 0L) {
+ endOfInput = true;
+ return null;
} else {
+ endOfInput = r == contentLength || r < chunkSize && contentLength > 0;
buffer.flip();
- nextChunk = buffer;
+ return ChannelBuffers.wrappedBuffer(buffer);
}
}
- return nextChunk;
- }
-
- public boolean hasNextChunk()
- throws Exception {
- return !isEndOfInput();
- }
-
- public Object nextChunk()
- throws Exception {
- ByteBuffer buffer = peekNextChuck();
- if (buffer == EOF) {
- return null;
- }
- nextChunk = null;
- return ChannelBuffers.wrappedBuffer(buffer);
}
- public boolean isEndOfInput()
- throws Exception {
- return peekNextChuck() == EOF;
+ public boolean isEndOfInput() throws Exception {
+ // called by ChunkedWriteHandler AFTER nextChunk
+ return endOfInput;
}
- public void close()
- throws Exception {
+ public void close() throws Exception {
body.close();
}
-
}
diff --git a/src/main/java/com/ning/http/client/providers/netty/ChannelBufferUtil.java b/src/main/java/com/ning/http/client/providers/netty/ChannelBufferUtil.java
new file mode 100644
index 0000000000..e2707f6dfe
--- /dev/null
+++ b/src/main/java/com/ning/http/client/providers/netty/ChannelBufferUtil.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010 Ning, Inc.
+ *
+ * Ning 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 com.ning.http.client.providers.netty;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+
+public class ChannelBufferUtil {
+
+ public static byte[] channelBuffer2bytes(ChannelBuffer b) {
+ int readable = b.readableBytes();
+ int readerIndex = b.readerIndex();
+ if (b.hasArray()) {
+ byte[] array = b.array();
+ if (b.arrayOffset() == 0 && readerIndex == 0 && array.length == readable) {
+ return array;
+ }
+ }
+ byte[] array = new byte[readable];
+ b.getBytes(readerIndex, array);
+ return array;
+ }
+}
diff --git a/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProvider.java b/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProvider.java
index da017b1679..1722c6e0cf 100644
--- a/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProvider.java
+++ b/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProvider.java
@@ -21,6 +21,7 @@
import com.ning.http.client.AsyncHttpProvider;
import com.ning.http.client.Body;
import com.ning.http.client.BodyGenerator;
+import com.ning.http.client.ConnectionPoolKeyStrategy;
import com.ning.http.client.ConnectionsPool;
import com.ning.http.client.Cookie;
import com.ning.http.client.FluentCaseInsensitiveStringsMap;
@@ -55,6 +56,8 @@
import com.ning.http.util.ProxyUtils;
import com.ning.http.util.SslUtils;
import com.ning.http.util.UTF8UrlEncoder;
+import com.ning.org.jboss.netty.handler.codec.http.CookieDecoder;
+import com.ning.org.jboss.netty.handler.codec.http.CookieEncoder;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBufferOutputStream;
@@ -75,8 +78,7 @@
import org.jboss.netty.channel.socket.ClientSocketChannelFactory;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.channel.socket.oio.OioClientSocketChannelFactory;
-import org.jboss.netty.handler.codec.http.CookieEncoder;
-import org.jboss.netty.handler.codec.http.DefaultCookie;
+import org.jboss.netty.handler.codec.PrematureChannelClosureException;
import org.jboss.netty.handler.codec.http.DefaultHttpChunkTrailer;
import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
import org.jboss.netty.handler.codec.http.HttpChunk;
@@ -91,7 +93,9 @@
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseDecoder;
import org.jboss.netty.handler.codec.http.HttpVersion;
+import org.jboss.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import org.jboss.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
+import org.jboss.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.jboss.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder;
import org.jboss.netty.handler.codec.http.websocketx.WebSocket08FrameEncoder;
import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame;
@@ -113,12 +117,12 @@
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
+import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Iterator;
import java.util.List;
+import java.util.Locale;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@@ -131,11 +135,24 @@
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
+import static com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig.BOSS_EXECUTOR_SERVICE;
+import static com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig.DISABLE_NESTED_REQUEST;
+import static com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig.EXECUTE_ASYNC_CONNECT;
+import static com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig.HTTPS_CLIENT_CODEC_MAX_CHUNK_SIZE;
+import static com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig.HTTPS_CLIENT_CODEC_MAX_HEADER_SIZE;
+import static com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig.HTTPS_CLIENT_CODEC_MAX_INITIAL_LINE_LENGTH;
+import static com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig.HTTP_CLIENT_CODEC_MAX_CHUNK_SIZE;
+import static com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig.HTTP_CLIENT_CODEC_MAX_HEADER_SIZE;
+import static com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig.HTTP_CLIENT_CODEC_MAX_INITIAL_LINE_LENGTH;
+import static com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig.REUSE_ADDRESS;
+import static com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig.SOCKET_CHANNEL_FACTORY;
+import static com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig.USE_BLOCKING_IO;
import static com.ning.http.util.AsyncHttpProviderUtils.DEFAULT_CHARSET;
+import static com.ning.http.util.DateUtil.millisTime;
+import static com.ning.http.util.MiscUtil.isNonEmpty;
import static org.jboss.netty.channel.Channels.pipeline;
public class NettyAsyncHttpProvider extends SimpleChannelUpstreamHandler implements AsyncHttpProvider {
- private final static String WEBSOCKET_KEY = "Sec-WebSocket-Key";
private final static String HTTP_HANDLER = "httpHandler";
protected final static String SSL_HANDLER = "sslHandler";
private final static String HTTPS = "https";
@@ -143,6 +160,7 @@ public class NettyAsyncHttpProvider extends SimpleChannelUpstreamHandler impleme
private static final String WEBSOCKET = "ws";
private static final String WEBSOCKET_SSL = "wss";
private final static Logger log = LoggerFactory.getLogger(NettyAsyncHttpProvider.class);
+ private final static Charset UTF8 = Charset.forName("UTF-8");
private final ClientBootstrap plainBootstrap;
private final ClientBootstrap secureBootstrap;
private final ClientBootstrap webSocketBootstrap;
@@ -151,18 +169,24 @@ public class NettyAsyncHttpProvider extends SimpleChannelUpstreamHandler impleme
private final AsyncHttpClientConfig config;
private final AtomicBoolean isClose = new AtomicBoolean(false);
private final ClientSocketChannelFactory socketChannelFactory;
-
- private final ChannelGroup openChannels = new
- CleanupChannelGroup("asyncHttpClient") {
- @Override
- public boolean remove(Object o) {
- boolean removed = super.remove(o);
- if (removed && trackConnections) {
- freeConnections.release();
- }
- return removed;
- }
- };
+ private final boolean allowReleaseSocketChannelFactory;
+ private int httpClientCodecMaxInitialLineLength = 4096;
+ private int httpClientCodecMaxHeaderSize = 8192;
+ private int httpClientCodecMaxChunkSize = 8192;
+ private int httpsClientCodecMaxInitialLineLength = 4096;
+ private int httpsClientCodecMaxHeaderSize = 8192;
+ private int httpsClientCodecMaxChunkSize = 8192;
+
+ private final ChannelGroup openChannels = new CleanupChannelGroup("asyncHttpClient") {
+ @Override
+ public boolean remove(Object o) {
+ boolean removed = super.remove(o);
+ if (removed && trackConnections) {
+ freeConnections.release();
+ }
+ return removed;
+ }
+ };
private final ConnectionsPool connectionsPool;
private Semaphore freeConnections = null;
private final NettyAsyncHttpProviderConfig asyncHttpProviderConfig;
@@ -171,41 +195,55 @@ public boolean remove(Object o) {
private final boolean trackConnections;
private final boolean useRawUrl;
private final static NTLMEngine ntlmEngine = new NTLMEngine();
- private final static SpnegoEngine spnegoEngine = new SpnegoEngine();
+ private static SpnegoEngine spnegoEngine = null;
private final Protocol httpProtocol = new HttpProtocol();
private final Protocol webSocketProtocol = new WebSocketProtocol();
+ private static boolean isNTLM(List auth) {
+ return isNonEmpty(auth) && auth.get(0).startsWith("NTLM");
+ }
+
public NettyAsyncHttpProvider(AsyncHttpClientConfig config) {
- if (config.getAsyncHttpProviderConfig() != null
- && NettyAsyncHttpProviderConfig.class.isAssignableFrom(config.getAsyncHttpProviderConfig().getClass())) {
+ if (config.getAsyncHttpProviderConfig() instanceof NettyAsyncHttpProviderConfig) {
asyncHttpProviderConfig = NettyAsyncHttpProviderConfig.class.cast(config.getAsyncHttpProviderConfig());
} else {
asyncHttpProviderConfig = new NettyAsyncHttpProviderConfig();
}
- if (asyncHttpProviderConfig.getProperty(NettyAsyncHttpProviderConfig.USE_BLOCKING_IO) != null) {
+ if (asyncHttpProviderConfig.getProperty(USE_BLOCKING_IO) != null) {
socketChannelFactory = new OioClientSocketChannelFactory(config.executorService());
+ this.allowReleaseSocketChannelFactory = true;
} else {
- ExecutorService e;
- Object o = asyncHttpProviderConfig.getProperty(NettyAsyncHttpProviderConfig.BOSS_EXECUTOR_SERVICE);
- if (o != null && ExecutorService.class.isAssignableFrom(o.getClass())) {
- e = ExecutorService.class.cast(o);
+ // check if external NioClientSocketChannelFactory is defined
+ Object oo = asyncHttpProviderConfig.getProperty(SOCKET_CHANNEL_FACTORY);
+ if (oo instanceof NioClientSocketChannelFactory) {
+ this.socketChannelFactory = NioClientSocketChannelFactory.class.cast(oo);
+
+ // cannot allow releasing shared channel factory
+ this.allowReleaseSocketChannelFactory = false;
} else {
- e = Executors.newCachedThreadPool();
+ ExecutorService e;
+ Object o = asyncHttpProviderConfig.getProperty(BOSS_EXECUTOR_SERVICE);
+ if (o instanceof ExecutorService) {
+ e = ExecutorService.class.cast(o);
+ } else {
+ e = Executors.newCachedThreadPool();
+ }
+ int numWorkers = config.getIoThreadMultiplier() * Runtime.getRuntime().availableProcessors();
+ log.debug("Number of application's worker threads is {}", numWorkers);
+ socketChannelFactory = new NioClientSocketChannelFactory(e, config.executorService(), numWorkers);
+ this.allowReleaseSocketChannelFactory = true;
}
- int numWorkers = config.getIoThreadMultiplier() * Runtime.getRuntime().availableProcessors();
- log.debug("Number of application's worker threads is {}", numWorkers);
- socketChannelFactory = new NioClientSocketChannelFactory(e, config.executorService(), numWorkers);
}
plainBootstrap = new ClientBootstrap(socketChannelFactory);
secureBootstrap = new ClientBootstrap(socketChannelFactory);
webSocketBootstrap = new ClientBootstrap(socketChannelFactory);
secureWebSocketBootstrap = new ClientBootstrap(socketChannelFactory);
- configureNetty();
-
this.config = config;
+ configureNetty();
+
// This is dangerous as we can't catch a wrong typed ConnectionsPool
ConnectionsPool cp = (ConnectionsPool) config.getConnectionsPool();
if (cp == null && config.getAllowPoolingConnection()) {
@@ -227,9 +265,9 @@ public NettyAsyncHttpProvider(AsyncHttpClientConfig config) {
@Override
public String toString() {
- return String.format("NettyAsyncHttpProvider:\n\t- maxConnections: %d\n\t- openChannels: %s\n\t- connectionPools: %s",
- config.getMaxTotalConnections() - freeConnections.availablePermits(),
- openChannels.toString(),
+ return String.format("NettyAsyncHttpProvider:\n\t- maxConnections: %d\n\t- openChannels: %s\n\t- connectionPools: %s",//
+ config.getMaxTotalConnections() - freeConnections.availablePermits(),//
+ openChannels.toString(),//
connectionsPool.toString());
}
@@ -238,6 +276,8 @@ void configureNetty() {
for (Entry entry : asyncHttpProviderConfig.propertiesSet()) {
plainBootstrap.setOption(entry.getKey(), entry.getValue());
}
+ configureHttpClientCodec();
+ configureHttpsClientCodec();
}
plainBootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@@ -246,7 +286,7 @@ void configureNetty() {
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = pipeline();
- pipeline.addLast(HTTP_HANDLER, new HttpClientCodec());
+ pipeline.addLast(HTTP_HANDLER, createHttpClientCodec());
if (config.getRequestCompressionLevel() > 0) {
pipeline.addLast("deflater", new HttpContentCompressor(config.getRequestCompressionLevel()));
@@ -263,10 +303,10 @@ public ChannelPipeline getPipeline() throws Exception {
DefaultChannelFuture.setUseDeadLockChecker(false);
if (asyncHttpProviderConfig != null) {
- Object value = asyncHttpProviderConfig.getProperty(NettyAsyncHttpProviderConfig.EXECUTE_ASYNC_CONNECT);
- if (value != null && Boolean.class.isAssignableFrom(value.getClass())) {
+ Object value = asyncHttpProviderConfig.getProperty(EXECUTE_ASYNC_CONNECT);
+ if (value instanceof Boolean) {
executeConnectAsync = Boolean.class.cast(value);
- } else if (asyncHttpProviderConfig.getProperty(NettyAsyncHttpProviderConfig.DISABLE_NESTED_REQUEST) != null) {
+ } else if (asyncHttpProviderConfig.getProperty(DISABLE_NESTED_REQUEST) != null) {
DefaultChannelFuture.setUseDeadLockChecker(true);
}
}
@@ -276,14 +316,26 @@ public ChannelPipeline getPipeline() throws Exception {
/* @Override */
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = pipeline();
- pipeline.addLast("ws-decoder", new HttpResponseDecoder());
- pipeline.addLast("ws-encoder", new HttpRequestEncoder());
+ pipeline.addLast("http-decoder", new HttpResponseDecoder());
+ pipeline.addLast("http-encoder", new HttpRequestEncoder());
pipeline.addLast("httpProcessor", NettyAsyncHttpProvider.this);
return pipeline;
}
});
}
+ protected void configureHttpClientCodec() {
+ httpClientCodecMaxInitialLineLength = asyncHttpProviderConfig.getProperty(HTTP_CLIENT_CODEC_MAX_INITIAL_LINE_LENGTH, Integer.class, httpClientCodecMaxInitialLineLength);
+ httpClientCodecMaxHeaderSize = asyncHttpProviderConfig.getProperty(HTTP_CLIENT_CODEC_MAX_HEADER_SIZE, Integer.class, httpClientCodecMaxHeaderSize);
+ httpClientCodecMaxChunkSize = asyncHttpProviderConfig.getProperty(HTTP_CLIENT_CODEC_MAX_CHUNK_SIZE, Integer.class, httpClientCodecMaxChunkSize);
+ }
+
+ protected void configureHttpsClientCodec() {
+ httpsClientCodecMaxInitialLineLength = asyncHttpProviderConfig.getProperty(HTTPS_CLIENT_CODEC_MAX_INITIAL_LINE_LENGTH, Integer.class, httpsClientCodecMaxInitialLineLength);
+ httpsClientCodecMaxHeaderSize = asyncHttpProviderConfig.getProperty(HTTPS_CLIENT_CODEC_MAX_HEADER_SIZE, Integer.class, httpsClientCodecMaxHeaderSize);
+ httpsClientCodecMaxChunkSize = asyncHttpProviderConfig.getProperty(HTTPS_CLIENT_CODEC_MAX_CHUNK_SIZE, Integer.class, httpsClientCodecMaxChunkSize);
+ }
+
void constructSSLPipeline(final NettyConnectListener> cl) {
secureBootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@@ -298,7 +350,7 @@ public ChannelPipeline getPipeline() throws Exception {
abort(cl.future(), ex);
}
- pipeline.addLast(HTTP_HANDLER, new HttpClientCodec());
+ pipeline.addLast(HTTP_HANDLER, createHttpsClientCodec());
if (config.isCompressionEnabled()) {
pipeline.addLast("inflater", new HttpContentDecompressor());
@@ -321,8 +373,8 @@ public ChannelPipeline getPipeline() throws Exception {
abort(cl.future(), ex);
}
- pipeline.addLast("ws-decoder", new HttpResponseDecoder());
- pipeline.addLast("ws-encoder", new HttpRequestEncoder());
+ pipeline.addLast("http-decoder", new HttpResponseDecoder());
+ pipeline.addLast("http-encoder", new HttpRequestEncoder());
pipeline.addLast("httpProcessor", NettyAsyncHttpProvider.this);
return pipeline;
@@ -337,15 +389,15 @@ public ChannelPipeline getPipeline() throws Exception {
}
}
- private Channel lookupInCache(URI uri) {
- final Channel channel = connectionsPool.poll(AsyncHttpProviderUtils.getBaseUrl(uri));
+ private Channel lookupInCache(URI uri, ConnectionPoolKeyStrategy connectionPoolKeyStrategy) {
+ final Channel channel = connectionsPool.poll(connectionPoolKeyStrategy.getKey(uri));
if (channel != null) {
log.debug("Using cached Channel {}\n for uri {}\n", channel, uri);
try {
// Always make sure the channel who got cached support the proper protocol. It could
- // only occurs when a HttpMethod.CONNECT is used agains a proxy that require upgrading from http to
+ // only occurs when a HttpMethod.CONNECT is used against a proxy that require upgrading from http to
// https.
return verifyChannelPipeline(channel, uri.getScheme());
} catch (Exception ex) {
@@ -363,6 +415,14 @@ private SSLEngine createSSLEngine() throws IOException, GeneralSecurityException
return sslEngine;
}
+ private HttpClientCodec createHttpClientCodec() {
+ return new HttpClientCodec(httpClientCodecMaxInitialLineLength, httpClientCodecMaxHeaderSize, httpClientCodecMaxChunkSize);
+ }
+
+ private HttpClientCodec createHttpsClientCodec() {
+ return new HttpClientCodec(httpsClientCodecMaxInitialLineLength, httpsClientCodecMaxHeaderSize, httpsClientCodecMaxChunkSize);
+ }
+
private Channel verifyChannelPipeline(Channel channel, String scheme) throws IOException, GeneralSecurityException {
if (channel.getPipeline().get(SSL_HANDLER) != null && HTTP.equalsIgnoreCase(scheme)) {
@@ -375,25 +435,25 @@ private Channel verifyChannelPipeline(Channel channel, String scheme) throws IOE
return channel;
}
- protected final void writeRequest(final Channel channel,
- final AsyncHttpClientConfig config,
- final NettyResponseFuture future,
- final HttpRequest nettyRequest) {
+ protected final void writeRequest(final Channel channel, final AsyncHttpClientConfig config, final NettyResponseFuture future) {
+
+ HttpRequest nettyRequest = future.getNettyRequest();
+ boolean ssl = channel.getPipeline().get(SslHandler.class) != null;
+
try {
/**
- * If the channel is dead because it was pooled and the remote server decided to close it,
- * we just let it go and the closeChannel do it's work.
+ * If the channel is dead because it was pooled and the remote server decided to close it, we just let it go and the closeChannel do it's work.
*/
if (!channel.isOpen() || !channel.isConnected()) {
return;
}
Body body = null;
- if (!future.getNettyRequest().getMethod().equals(HttpMethod.CONNECT)) {
+ if (!nettyRequest.getMethod().equals(HttpMethod.CONNECT)) {
BodyGenerator bg = future.getRequest().getBodyGenerator();
if (bg != null) {
// Netty issue with chunking.
- if (InputStreamBodyGenerator.class.isAssignableFrom(bg.getClass())) {
+ if (bg instanceof InputStreamBodyGenerator) {
InputStreamBodyGenerator.class.cast(bg).patchNettyChunkingIssue(true);
}
@@ -408,22 +468,24 @@ protected final void writeRequest(final Channel channel,
} else {
nettyRequest.setHeader(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);
}
- } else {
- body = null;
+
+ } else if (future.getRequest().getParts() != null) {
+ String contentType = nettyRequest.getHeader(HttpHeaders.Names.CONTENT_TYPE);
+ String length = nettyRequest.getHeader(HttpHeaders.Names.CONTENT_LENGTH);
+ body = new MultipartBody(future.getRequest().getParts(), contentType, length);
}
}
- if (TransferCompletionHandler.class.isAssignableFrom(future.getAsyncHandler().getClass())) {
+ if (future.getAsyncHandler() instanceof TransferCompletionHandler) {
FluentCaseInsensitiveStringsMap h = new FluentCaseInsensitiveStringsMap();
- for (String s : future.getNettyRequest().getHeaderNames()) {
- for (String header : future.getNettyRequest().getHeaders(s)) {
+ for (String s : nettyRequest.getHeaderNames()) {
+ for (String header : nettyRequest.getHeaders(s)) {
h.add(s, header);
}
}
- TransferCompletionHandler.class.cast(future.getAsyncHandler()).transferAdapter(
- new NettyTransferAdapter(h, nettyRequest.getContent(), future.getRequest().getFile()));
+ TransferCompletionHandler.class.cast(future.getAsyncHandler()).transferAdapter(new NettyTransferAdapter(h, nettyRequest.getContent(), future.getRequest().getFile()));
}
// Leave it to true.
@@ -442,7 +504,7 @@ protected final void writeRequest(final Channel channel,
}
if (future.getAndSetWriteBody(true)) {
- if (!future.getNettyRequest().getMethod().equals(HttpMethod.CONNECT)) {
+ if (!nettyRequest.getMethod().equals(HttpMethod.CONNECT)) {
if (future.getRequest().getFile() != null) {
final File file = future.getRequest().getFile();
@@ -453,13 +515,22 @@ protected final void writeRequest(final Channel channel,
fileLength = raf.length();
ChannelFuture writeFuture;
- if (channel.getPipeline().get(SslHandler.class) != null) {
- writeFuture = channel.write(new ChunkedFile(raf, 0, fileLength, 8192));
+ if (ssl) {
+ writeFuture = channel.write(new ChunkedFile(raf, 0, fileLength, MAX_BUFFERED_BYTES));
} else {
final FileRegion region = new OptimizedFileRegion(raf, 0, fileLength);
writeFuture = channel.write(region);
}
- writeFuture.addListener(new ProgressListener(false, future.getAsyncHandler(), future));
+ writeFuture.addListener(new ProgressListener(false, future.getAsyncHandler(), future) {
+ public void operationComplete(ChannelFuture cf) {
+ try {
+ raf.close();
+ } catch (IOException e) {
+ log.warn("Failed to close request body: {}", e.getMessage(), e);
+ }
+ super.operationComplete(cf);
+ }
+ });
} catch (IOException ex) {
if (raf != null) {
try {
@@ -469,18 +540,10 @@ protected final void writeRequest(final Channel channel,
}
throw ex;
}
- } else if (body != null || future.getRequest().getParts() != null) {
- /**
- * TODO: AHC-78: SSL + zero copy isn't supported by the MultiPart class and pretty complex to implements.
- */
- if (future.getRequest().getParts() != null) {
- String boundary = future.getNettyRequest().getHeader("Content-Type");
- String length = future.getNettyRequest().getHeader("Content-Length");
- body = new MultipartBody(future.getRequest().getParts(), boundary, length);
- }
+ } else if (body != null) {
ChannelFuture writeFuture;
- if (channel.getPipeline().get(SslHandler.class) == null && (body instanceof RandomAccessBody)) {
+ if (!ssl && body instanceof RandomAccessBody) {
BodyFileRegion bodyFileRegion = new BodyFileRegion((RandomAccessBody) body);
writeFuture = channel.write(bodyFileRegion);
} else {
@@ -512,10 +575,11 @@ public void operationComplete(ChannelFuture cf) {
try {
future.touch();
- int delay = requestTimeout(config, future.getRequest().getPerRequestConfig());
- if (delay != -1 && !future.isDone() && !future.isCancelled()) {
+ int requestTimeout = requestTimeoutInMs(config, future.getRequest().getPerRequestConfig());
+ int schedulePeriod = requestTimeout != -1 ? (config.getIdleConnectionTimeoutInMs() != -1 ? Math.min(requestTimeout, config.getIdleConnectionTimeoutInMs()) : requestTimeout) : config.getIdleConnectionTimeoutInMs();
+ if (schedulePeriod != -1 && !future.isDone() && !future.isCancelled()) {
ReaperFuture reaperFuture = new ReaperFuture(future);
- Future scheduledFuture = config.reaper().scheduleAtFixedRate(reaperFuture, 0, delay, TimeUnit.MILLISECONDS);
+ Future> scheduledFuture = config.reaper().scheduleAtFixedRate(reaperFuture, 0, schedulePeriod, TimeUnit.MILLISECONDS);
reaperFuture.setScheduledFuture(scheduledFuture);
future.setReaperFuture(reaperFuture);
}
@@ -525,63 +589,55 @@ public void operationComplete(ChannelFuture cf) {
}
- private static boolean isProxyServer(AsyncHttpClientConfig config, Request request) {
- return request.getProxyServer() != null || config.getProxyServer() != null;
- }
-
- protected final static HttpRequest buildRequest(AsyncHttpClientConfig config, Request request, URI uri,
- boolean allowConnect, ChannelBuffer buffer) throws IOException {
+ protected final static HttpRequest buildRequest(AsyncHttpClientConfig config, Request request, URI uri, boolean allowConnect, ChannelBuffer buffer, ProxyServer proxyServer) throws IOException {
String method = request.getMethod();
- if (allowConnect && (isProxyServer(config, request) && isSecure(uri))) {
+ if (allowConnect && proxyServer != null && isSecure(uri)) {
method = HttpMethod.CONNECT.toString();
}
- return construct(config, request, new HttpMethod(method), uri, buffer);
+ return construct(config, request, new HttpMethod(method), uri, buffer, proxyServer);
}
- @SuppressWarnings("deprecation")
- private static HttpRequest construct(AsyncHttpClientConfig config,
- Request request,
- HttpMethod m,
- URI uri,
- ChannelBuffer buffer) throws IOException {
+ private static SpnegoEngine getSpnegoEngine() {
+ if (spnegoEngine == null)
+ spnegoEngine = new SpnegoEngine();
+ return spnegoEngine;
+ }
- String host = AsyncHttpProviderUtils.getHost(uri);
- boolean webSocket = isWebSocket(uri);
+ private static HttpRequest construct(AsyncHttpClientConfig config, Request request, HttpMethod m, URI uri, ChannelBuffer buffer, ProxyServer proxyServer) throws IOException {
+
+ String host = null;
if (request.getVirtualHost() != null) {
host = request.getVirtualHost();
+ } else {
+ host = AsyncHttpProviderUtils.getHost(uri);
}
HttpRequest nettyRequest;
if (m.equals(HttpMethod.CONNECT)) {
nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_0, m, AsyncHttpProviderUtils.getAuthority(uri));
} else {
- StringBuilder path = null;
- if (isProxyServer(config, request))
- path = new StringBuilder(uri.toString());
- else {
- path = new StringBuilder(uri.getRawPath());
- if (uri.getQuery() != null) {
- path.append("?").append(uri.getRawQuery());
- }
- }
- nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, m, path.toString());
+ String path = null;
+ if (proxyServer != null && !(isSecure(uri) && config.isUseRelativeURIsWithSSLProxies()))
+ path = uri.toString();
+ else if (uri.getRawQuery() != null)
+ path = uri.getRawPath() + "?" + uri.getRawQuery();
+ else
+ path = uri.getRawPath();
+ nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, m, path);
}
-
+ boolean webSocket = isWebSocket(uri);
if (webSocket) {
nettyRequest.addHeader(HttpHeaders.Names.UPGRADE, HttpHeaders.Values.WEBSOCKET);
nettyRequest.addHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.UPGRADE);
- nettyRequest.addHeader("Origin", "http://" + uri.getHost() + ":"
- + (uri.getPort() == -1 ? isSecure(uri.getScheme()) ? 443 : 80 : uri.getPort()));
- nettyRequest.addHeader(WEBSOCKET_KEY, WebSocketUtil.getKey());
- nettyRequest.addHeader("Sec-WebSocket-Version", "13");
+ nettyRequest.addHeader(HttpHeaders.Names.ORIGIN, "http://" + uri.getHost() + ":" + uri.getPort());
+ nettyRequest.addHeader(HttpHeaders.Names.SEC_WEBSOCKET_KEY, WebSocketUtil.getKey());
+ nettyRequest.addHeader(HttpHeaders.Names.SEC_WEBSOCKET_VERSION, "13");
}
if (host != null) {
- if (uri.getPort() == -1) {
- nettyRequest.setHeader(HttpHeaders.Names.HOST, host);
- } else if (request.getVirtualHost() != null) {
+ if (request.getVirtualHost() != null || uri.getPort() == -1) {
nettyRequest.setHeader(HttpHeaders.Names.HOST, host);
} else {
nettyRequest.setHeader(HttpHeaders.Names.HOST, host + ":" + uri.getPort());
@@ -591,13 +647,11 @@ private static HttpRequest construct(AsyncHttpClientConfig config,
}
if (!m.equals(HttpMethod.CONNECT)) {
- FluentCaseInsensitiveStringsMap h = request.getHeaders();
- if (h != null) {
- for (String name : h.keySet()) {
- if (!"host".equalsIgnoreCase(name)) {
- for (String value : h.get(name)) {
- nettyRequest.addHeader(name, value);
- }
+ for (Entry> header : request.getHeaders()) {
+ String name = header.getKey();
+ if (!HttpHeaders.Names.HOST.equalsIgnoreCase(name)) {
+ for (String value : header.getValue()) {
+ nettyRequest.addHeader(name, value);
}
}
}
@@ -607,11 +661,10 @@ private static HttpRequest construct(AsyncHttpClientConfig config,
}
} else {
List auth = request.getHeaders().get(HttpHeaders.Names.PROXY_AUTHORIZATION);
- if (auth != null && auth.size() > 0 && auth.get(0).startsWith("NTLM")) {
+ if (isNTLM(auth)) {
nettyRequest.addHeader(HttpHeaders.Names.PROXY_AUTHORIZATION, auth.get(0));
}
}
- ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer();
Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm();
if (realm != null && realm.getUsePreemptiveAuth()) {
@@ -627,68 +680,63 @@ private static HttpRequest construct(AsyncHttpClientConfig config,
}
switch (realm.getAuthScheme()) {
- case BASIC:
- nettyRequest.setHeader(HttpHeaders.Names.AUTHORIZATION,
- AuthenticatorUtils.computeBasicAuthentication(realm));
- break;
- case DIGEST:
- if (realm.getNonce() != null && !realm.getNonce().equals("")) {
- try {
- nettyRequest.setHeader(HttpHeaders.Names.AUTHORIZATION,
- AuthenticatorUtils.computeDigestAuthentication(realm));
- } catch (NoSuchAlgorithmException e) {
- throw new SecurityException(e);
- }
- }
- break;
- case NTLM:
- try {
- nettyRequest.setHeader(HttpHeaders.Names.AUTHORIZATION,
- ntlmEngine.generateType1Msg("NTLM " + domain, authHost));
- } catch (NTLMEngineException e) {
- IOException ie = new IOException();
- ie.initCause(e);
- throw ie;
- }
- break;
- case KERBEROS:
- case SPNEGO:
- String challengeHeader = null;
- String server = proxyServer == null ? host : proxyServer.getHost();
+ case BASIC:
+ nettyRequest.setHeader(HttpHeaders.Names.AUTHORIZATION, AuthenticatorUtils.computeBasicAuthentication(realm));
+ break;
+ case DIGEST:
+ if (isNonEmpty(realm.getNonce())) {
try {
- challengeHeader = spnegoEngine.generateToken(server);
- } catch (Throwable e) {
- IOException ie = new IOException();
- ie.initCause(e);
- throw ie;
+ nettyRequest.setHeader(HttpHeaders.Names.AUTHORIZATION, AuthenticatorUtils.computeDigestAuthentication(realm));
+ } catch (NoSuchAlgorithmException e) {
+ throw new SecurityException(e);
}
- nettyRequest.setHeader(HttpHeaders.Names.AUTHORIZATION, "Negotiate " + challengeHeader);
- break;
- case NONE:
- break;
- default:
- throw new IllegalStateException(String.format("Invalid Authentication %s", realm.toString()));
+ }
+ break;
+ case NTLM:
+ try {
+ nettyRequest.setHeader(HttpHeaders.Names.AUTHORIZATION, ntlmEngine.generateType1Msg("NTLM " + domain, authHost));
+ } catch (NTLMEngineException e) {
+ IOException ie = new IOException();
+ ie.initCause(e);
+ throw ie;
+ }
+ break;
+ case KERBEROS:
+ case SPNEGO:
+ String challengeHeader = null;
+ String server = proxyServer == null ? host : proxyServer.getHost();
+ try {
+ challengeHeader = getSpnegoEngine().generateToken(server);
+ } catch (Throwable e) {
+ IOException ie = new IOException();
+ ie.initCause(e);
+ throw ie;
+ }
+ nettyRequest.setHeader(HttpHeaders.Names.AUTHORIZATION, "Negotiate " + challengeHeader);
+ break;
+ case NONE:
+ break;
+ default:
+ throw new IllegalStateException(String.format("Invalid Authentication %s", realm.toString()));
}
}
if (!webSocket && !request.getHeaders().containsKey(HttpHeaders.Names.CONNECTION)) {
- nettyRequest.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive");
+ nettyRequest.setHeader(HttpHeaders.Names.CONNECTION, AsyncHttpProviderUtils.keepAliveHeaderValue(config));
}
- boolean avoidProxy = ProxyUtils.avoidProxy(proxyServer, request);
- if (!avoidProxy) {
+ if (proxyServer != null) {
if (!request.getHeaders().containsKey("Proxy-Connection")) {
- nettyRequest.setHeader("Proxy-Connection", "keep-alive");
+ nettyRequest.setHeader("Proxy-Connection", AsyncHttpProviderUtils.keepAliveHeaderValue(config));
}
if (proxyServer.getPrincipal() != null) {
- if (proxyServer.getNtlmDomain() != null && proxyServer.getNtlmDomain().length() > 0) {
+ if (isNonEmpty(proxyServer.getNtlmDomain())) {
List auth = request.getHeaders().get(HttpHeaders.Names.PROXY_AUTHORIZATION);
- if (!(auth != null && auth.size() > 0 && auth.get(0).startsWith("NTLM"))) {
+ if (!isNTLM(auth)) {
try {
- String msg = ntlmEngine.generateType1Msg(proxyServer.getNtlmDomain(),
- proxyServer.getHost());
+ String msg = ntlmEngine.generateType1Msg(proxyServer.getNtlmDomain(), proxyServer.getHost());
nettyRequest.setHeader(HttpHeaders.Names.PROXY_AUTHORIZATION, "NTLM " + msg);
} catch (NTLMEngineException e) {
IOException ie = new IOException();
@@ -697,40 +745,28 @@ private static HttpRequest construct(AsyncHttpClientConfig config,
}
}
} else {
- nettyRequest.setHeader(HttpHeaders.Names.PROXY_AUTHORIZATION,
- AuthenticatorUtils.computeBasicAuthentication(proxyServer));
+ nettyRequest.setHeader(HttpHeaders.Names.PROXY_AUTHORIZATION, AuthenticatorUtils.computeBasicAuthentication(proxyServer));
}
}
}
// Add default accept headers.
- if (request.getHeaders().getFirstValue("Accept") == null) {
+ if (!request.getHeaders().containsKey(HttpHeaders.Names.ACCEPT)) {
nettyRequest.setHeader(HttpHeaders.Names.ACCEPT, "*/*");
}
- if (request.getHeaders().getFirstValue("User-Agent") != null) {
- nettyRequest.setHeader("User-Agent", request.getHeaders().getFirstValue("User-Agent"));
+ String userAgentHeader = request.getHeaders().getFirstValue(HttpHeaders.Names.USER_AGENT);
+ if (userAgentHeader != null) {
+ nettyRequest.setHeader(HttpHeaders.Names.USER_AGENT, userAgentHeader);
} else if (config.getUserAgent() != null) {
- nettyRequest.setHeader("User-Agent", config.getUserAgent());
+ nettyRequest.setHeader(HttpHeaders.Names.USER_AGENT, config.getUserAgent());
} else {
- nettyRequest.setHeader("User-Agent", AsyncHttpProviderUtils.constructUserAgent(NettyAsyncHttpProvider.class));
+ nettyRequest.setHeader(HttpHeaders.Names.USER_AGENT, AsyncHttpProviderUtils.constructUserAgent(NettyAsyncHttpProvider.class));
}
if (!m.equals(HttpMethod.CONNECT)) {
- if (request.getCookies() != null && !request.getCookies().isEmpty()) {
- CookieEncoder httpCookieEncoder = new CookieEncoder(false);
- Iterator ic = request.getCookies().iterator();
- Cookie c;
- org.jboss.netty.handler.codec.http.Cookie cookie;
- while (ic.hasNext()) {
- c = ic.next();
- cookie = new DefaultCookie(c.getName(), c.getValue());
- cookie.setPath(c.getPath());
- cookie.setMaxAge(c.getMaxAge());
- cookie.setDomain(c.getDomain());
- httpCookieEncoder.addCookie(cookie);
- }
- nettyRequest.setHeader(HttpHeaders.Names.COOKIE, httpCookieEncoder.encode());
+ if (isNonEmpty(request.getCookies())) {
+ nettyRequest.setHeader(HttpHeaders.Names.COOKIE, CookieEncoder.encodeClientSide(request.getCookies(), config.isRfc6265CookieEncoding()));
}
String reqType = request.getMethod();
@@ -746,15 +782,16 @@ private static HttpRequest construct(AsyncHttpClientConfig config,
nettyRequest.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(request.getByteData().length));
nettyRequest.setContent(ChannelBuffers.wrappedBuffer(request.getByteData()));
} else if (request.getStringData() != null) {
- nettyRequest.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(request.getStringData().getBytes(bodyCharset).length));
- nettyRequest.setContent(ChannelBuffers.wrappedBuffer(request.getStringData().getBytes(bodyCharset)));
+ byte[] bytes = request.getStringData().getBytes(bodyCharset);
+ nettyRequest.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(bytes.length));
+ nettyRequest.setContent(ChannelBuffers.wrappedBuffer(bytes));
} else if (request.getStreamData() != null) {
int[] lengthWrapper = new int[1];
byte[] bytes = AsyncHttpProviderUtils.readFully(request.getStreamData(), lengthWrapper);
int length = lengthWrapper[0];
nettyRequest.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(length));
nettyRequest.setContent(ChannelBuffers.wrappedBuffer(bytes, 0, length));
- } else if (request.getParams() != null && !request.getParams().isEmpty()) {
+ } else if (isNonEmpty(request.getParams())) {
StringBuilder sb = new StringBuilder();
for (final Entry> paramEntry : request.getParams()) {
final String key = paramEntry.getKey();
@@ -771,38 +808,23 @@ private static HttpRequest construct(AsyncHttpClientConfig config,
nettyRequest.setContent(ChannelBuffers.wrappedBuffer(sb.toString().getBytes(bodyCharset)));
if (!request.getHeaders().containsKey(HttpHeaders.Names.CONTENT_TYPE)) {
- nettyRequest.setHeader(HttpHeaders.Names.CONTENT_TYPE, "application/x-www-form-urlencoded");
+ nettyRequest.setHeader(HttpHeaders.Names.CONTENT_TYPE, HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED);
}
} else if (request.getParts() != null) {
- int lenght = computeAndSetContentLength(request, nettyRequest);
-
- if (lenght == -1) {
- lenght = MAX_BUFFERED_BYTES;
- }
-
- MultipartRequestEntity mre = AsyncHttpProviderUtils.createMultipartRequestEntity(request.getParts(), request.getParams());
+ MultipartRequestEntity mre = AsyncHttpProviderUtils.createMultipartRequestEntity(request.getParts(), request.getHeaders());
nettyRequest.setHeader(HttpHeaders.Names.CONTENT_TYPE, mre.getContentType());
nettyRequest.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(mre.getContentLength()));
- /**
- * TODO: AHC-78: SSL + zero copy isn't supported by the MultiPart class and pretty complex to implements.
- */
-
- if (isSecure(uri)) {
- ChannelBuffer b = ChannelBuffers.dynamicBuffer(lenght);
- mre.writeRequest(new ChannelBufferOutputStream(b));
- nettyRequest.setContent(b);
- }
} else if (request.getEntityWriter() != null) {
- int lenght = computeAndSetContentLength(request, nettyRequest);
+ int length = (int) request.getContentLength();
- if (lenght == -1) {
- lenght = MAX_BUFFERED_BYTES;
+ if (length == -1) {
+ length = MAX_BUFFERED_BYTES;
}
- ChannelBuffer b = ChannelBuffers.dynamicBuffer(lenght);
+ ChannelBuffer b = ChannelBuffers.dynamicBuffer(length);
request.getEntityWriter().writeEntity(new ChannelBufferOutputStream(b));
nettyRequest.setHeader(HttpHeaders.Names.CONTENT_LENGTH, b.writerIndex());
nettyRequest.setContent(b);
@@ -834,11 +856,13 @@ public void close() {
config.executorService().shutdown();
config.reaper().shutdown();
- socketChannelFactory.releaseExternalResources();
- plainBootstrap.releaseExternalResources();
- secureBootstrap.releaseExternalResources();
- webSocketBootstrap.releaseExternalResources();
- secureWebSocketBootstrap.releaseExternalResources();
+ if (this.allowReleaseSocketChannelFactory) {
+ socketChannelFactory.releaseExternalResources();
+ plainBootstrap.releaseExternalResources();
+ secureBootstrap.releaseExternalResources();
+ webSocketBootstrap.releaseExternalResources();
+ secureWebSocketBootstrap.releaseExternalResources();
+ }
} catch (Throwable t) {
log.warn("Unexpected error on close", t);
}
@@ -846,9 +870,7 @@ public void close() {
/* @Override */
- public Response prepareResponse(final HttpResponseStatus status,
- final HttpResponseHeaders headers,
- final Collection bodyParts) {
+ public Response prepareResponse(final HttpResponseStatus status, final HttpResponseHeaders headers, final List bodyParts) {
return new NettyResponse(status, headers, bodyParts);
}
@@ -858,16 +880,11 @@ public ListenableFuture execute(Request request, final AsyncHandler as
return doConnect(request, asyncHandler, null, true, executeConnectAsync, false);
}
- private void execute(final Request request, final NettyResponseFuture f, boolean useCache, boolean asyncConnect) throws IOException {
- doConnect(request, f.getAsyncHandler(), f, useCache, asyncConnect, false);
- }
-
private void execute(final Request request, final NettyResponseFuture f, boolean useCache, boolean asyncConnect, boolean reclaimCache) throws IOException {
doConnect(request, f.getAsyncHandler(), f, useCache, asyncConnect, reclaimCache);
}
- private ListenableFuture doConnect(final Request request, final AsyncHandler asyncHandler, NettyResponseFuture f,
- boolean useCache, boolean asyncConnect, boolean reclaimCache) throws IOException {
+ private ListenableFuture doConnect(final Request request, final AsyncHandler asyncHandler, NettyResponseFuture f, boolean useCache, boolean asyncConnect, boolean reclaimCache) throws IOException {
if (isClose.get()) {
throw new IOException("Closed");
@@ -877,38 +894,40 @@ private ListenableFuture doConnect(final Request request, final AsyncHand
throw new IOException("WebSocket method must be a GET");
}
- ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer();
- String requestUrl;
+ ProxyServer proxyServer = ProxyUtils.getProxyServer(config, request);
+ boolean useProxy = proxyServer != null;
+
+ URI uri;
if (useRawUrl) {
- requestUrl = request.getRawUrl();
+ uri = request.getRawURI();
} else {
- requestUrl = request.getUrl();
+ uri = request.getURI();
}
- URI uri = AsyncHttpProviderUtils.createUri(requestUrl);
Channel channel = null;
if (useCache) {
if (f != null && f.reuseChannel() && f.channel() != null) {
channel = f.channel();
} else {
- channel = lookupInCache(uri);
+ URI connectionKeyUri = useProxy ? proxyServer.getURI() : uri;
+ channel = lookupInCache(connectionKeyUri, request.getConnectionPoolKeyStrategy());
}
}
ChannelBuffer bufferedBytes = null;
- if (f != null && f.getRequest().getFile() == null &&
- !f.getNettyRequest().getMethod().getName().equals(HttpMethod.CONNECT.getName())) {
+ if (f != null && f.getRequest().getFile() == null && !f.getNettyRequest().getMethod().getName().equals(HttpMethod.CONNECT.getName())) {
bufferedBytes = f.getNettyRequest().getContent();
}
- boolean useSSl = isSecure(uri) && proxyServer == null;
+ boolean useSSl = isSecure(uri) && !useProxy;
if (channel != null && channel.isOpen() && channel.isConnected()) {
- HttpRequest nettyRequest = buildRequest(config, request, uri, f == null ? false : f.isConnectAllowed(), bufferedBytes);
+ HttpRequest nettyRequest = null;
if (f == null) {
- f = newFuture(uri, request, asyncHandler, nettyRequest, config, this);
+ nettyRequest = buildRequest(config, request, uri, false, bufferedBytes, proxyServer);
+ f = newFuture(uri, request, asyncHandler, nettyRequest, config, this, proxyServer);
} else {
- nettyRequest = buildRequest(config, request, uri, f.isConnectAllowed(), bufferedBytes);
+ nettyRequest = buildRequest(config, request, uri, f.isConnectAllowed(), bufferedBytes, proxyServer);
f.setNettyRequest(nettyRequest);
}
f.setState(NettyResponseFuture.STATE.POOLED);
@@ -918,7 +937,7 @@ private ListenableFuture doConnect(final Request request, final AsyncHand
channel.getPipeline().getContext(NettyAsyncHttpProvider.class).setAttachment(f);
try {
- writeRequest(channel, config, f, nettyRequest);
+ writeRequest(channel, config, f);
} catch (Exception ex) {
log.debug("writeRequest failure", ex);
if (useSSl && ex.getMessage() != null && ex.getMessage().contains("SSLEngine")) {
@@ -968,7 +987,6 @@ private ListenableFuture doConnect(final Request request, final AsyncHand
}
NettyConnectListener c = new NettyConnectListener.Builder(config, request, asyncHandler, f, this, bufferedBytes).build(uri);
- boolean avoidProxy = ProxyUtils.avoidProxy(proxyServer, uri.getHost());
if (useSSl) {
constructSSLPipeline(c);
@@ -979,23 +997,23 @@ private ListenableFuture doConnect(final Request request, final AsyncHand
bootstrap.setOption("connectTimeoutMillis", config.getConnectionTimeoutInMs());
// Do no enable this with win.
- if (System.getProperty("os.name").toLowerCase().indexOf("win") == -1) {
- bootstrap.setOption("reuseAddress", asyncHttpProviderConfig.getProperty(NettyAsyncHttpProviderConfig.REUSE_ADDRESS));
+ if (!System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win")) {
+ bootstrap.setOption("reuseAddress", asyncHttpProviderConfig.getProperty(REUSE_ADDRESS));
}
try {
InetSocketAddress remoteAddress;
if (request.getInetAddress() != null) {
remoteAddress = new InetSocketAddress(request.getInetAddress(), AsyncHttpProviderUtils.getPort(uri));
- } else if (proxyServer == null || avoidProxy) {
+ } else if (!useProxy) {
remoteAddress = new InetSocketAddress(AsyncHttpProviderUtils.getHost(uri), AsyncHttpProviderUtils.getPort(uri));
} else {
remoteAddress = new InetSocketAddress(proxyServer.getHost(), proxyServer.getPort());
}
- if(request.getLocalAddress() != null){
+ if (request.getLocalAddress() != null) {
channelFuture = bootstrap.connect(remoteAddress, new InetSocketAddress(request.getLocalAddress(), 0));
- }else{
+ } else {
channelFuture = bootstrap.connect(remoteAddress);
}
@@ -1007,10 +1025,7 @@ private ListenableFuture doConnect(final Request request, final AsyncHand
return c.future();
}
- boolean directInvokation = true;
- if (IN_IO_THREAD.get() && DefaultChannelFuture.isUseDeadLockChecker()) {
- directInvokation = false;
- }
+ boolean directInvokation = !(IN_IO_THREAD.get() && DefaultChannelFuture.isUseDeadLockChecker());
if (directInvokation && !asyncConnect && request.getFile() == null) {
int timeOut = config.getConnectionTimeoutInMs() > 0 ? config.getConnectionTimeoutInMs() : Integer.MAX_VALUE;
@@ -1050,7 +1065,7 @@ private ListenableFuture doConnect(final Request request, final AsyncHand
return c.future();
}
- protected static int requestTimeout(AsyncHttpClientConfig config, PerRequestConfig perRequestConfig) {
+ protected static int requestTimeoutInMs(AsyncHttpClientConfig config, PerRequestConfig perRequestConfig) {
int result;
if (perRequestConfig != null) {
int prRequestTimeout = perRequestConfig.getRequestTimeoutInMs();
@@ -1076,7 +1091,6 @@ private void finishChannel(final ChannelHandlerContext ctx) {
log.debug("Closing Channel {} ", ctx.getChannel());
-
try {
ctx.getChannel().close();
} catch (Throwable t) {
@@ -1091,7 +1105,7 @@ private void finishChannel(final ChannelHandlerContext ctx) {
@Override
public void messageReceived(final ChannelHandlerContext ctx, MessageEvent e) throws Exception {
- //call super to reset the read timeout
+ // call super to reset the read timeout
super.messageReceived(ctx, e);
IN_IO_THREAD.set(Boolean.TRUE);
if (ctx.getAttachment() == null) {
@@ -1128,18 +1142,13 @@ public void messageReceived(final ChannelHandlerContext ctx, MessageEvent e) thr
p.handle(ctx, e);
}
- private Realm kerberosChallenge(List proxyAuth,
- Request request,
- ProxyServer proxyServer,
- FluentCaseInsensitiveStringsMap headers,
- Realm realm,
- NettyResponseFuture> future) throws NTLMEngineException {
+ private Realm kerberosChallenge(List proxyAuth, Request request, ProxyServer proxyServer, FluentCaseInsensitiveStringsMap headers, Realm realm, NettyResponseFuture> future) throws NTLMEngineException {
- URI uri = URI.create(request.getUrl());
+ URI uri = request.getURI();
String host = request.getVirtualHost() == null ? AsyncHttpProviderUtils.getHost(uri) : request.getVirtualHost();
String server = proxyServer == null ? host : proxyServer.getHost();
try {
- String challengeHeader = spnegoEngine.generateToken(server);
+ String challengeHeader = getSpnegoEngine().generateToken(server);
headers.remove(HttpHeaders.Names.AUTHORIZATION);
headers.add(HttpHeaders.Names.AUTHORIZATION, "Negotiate " + challengeHeader);
@@ -1149,12 +1158,9 @@ private Realm kerberosChallenge(List proxyAuth,
} else {
realmBuilder = new Realm.RealmBuilder();
}
- return realmBuilder.setUri(uri.getPath())
- .setMethodName(request.getMethod())
- .setScheme(Realm.AuthScheme.KERBEROS)
- .build();
+ return realmBuilder.setUri(uri.getRawPath()).setMethodName(request.getMethod()).setScheme(Realm.AuthScheme.KERBEROS).build();
} catch (Throwable throwable) {
- if (proxyAuth.contains("NTLM")) {
+ if (isNTLM(proxyAuth)) {
return ntlmChallenge(proxyAuth, request, proxyServer, headers, realm, future);
}
abort(future, throwable);
@@ -1162,12 +1168,24 @@ private Realm kerberosChallenge(List proxyAuth,
}
}
- private Realm ntlmChallenge(List wwwAuth,
- Request request,
- ProxyServer proxyServer,
- FluentCaseInsensitiveStringsMap headers,
- Realm realm,
- NettyResponseFuture> future) throws NTLMEngineException {
+ private void addType3NTLMAuthorizationHeader(
+ List auth,
+ FluentCaseInsensitiveStringsMap headers,
+ String username,
+ String password,
+ String domain,
+ String workstation) throws NTLMEngineException {
+ headers.remove(HttpHeaders.Names.AUTHORIZATION);
+
+ if (isNTLM(auth)) {
+ String serverChallenge = auth.get(0).trim().substring("NTLM ".length());
+ String challengeHeader = ntlmEngine.generateType3Msg(username, password, domain, workstation, serverChallenge);
+
+ headers.add(HttpHeaders.Names.AUTHORIZATION, "NTLM " + challengeHeader);
+ }
+ }
+
+ private Realm ntlmChallenge(List wwwAuth, Request request, ProxyServer proxyServer, FluentCaseInsensitiveStringsMap headers, Realm realm, NettyResponseFuture> future) throws NTLMEngineException {
boolean useRealm = (proxyServer == null && realm != null);
@@ -1180,23 +1198,12 @@ private Realm ntlmChallenge(List wwwAuth,
if (realm != null && !realm.isNtlmMessageType2Received()) {
String challengeHeader = ntlmEngine.generateType1Msg(ntlmDomain, ntlmHost);
+ URI uri = request.getURI();
headers.add(HttpHeaders.Names.AUTHORIZATION, "NTLM " + challengeHeader);
- newRealm = new Realm.RealmBuilder().clone(realm).setScheme(realm.getAuthScheme())
- .setUri(URI.create(request.getUrl()).getPath())
- .setMethodName(request.getMethod())
- .setNtlmMessageType2Received(true)
- .build();
+ newRealm = new Realm.RealmBuilder().clone(realm).setScheme(realm.getAuthScheme()).setUri(uri.getRawPath()).setMethodName(request.getMethod()).setNtlmMessageType2Received(true).build();
future.getAndSetAuth(false);
} else {
- headers.remove(HttpHeaders.Names.AUTHORIZATION);
-
- if (wwwAuth.get(0).startsWith("NTLM ")) {
- String serverChallenge = wwwAuth.get(0).trim().substring("NTLM ".length());
- String challengeHeader = ntlmEngine.generateType3Msg(principal, password,
- ntlmDomain, ntlmHost, serverChallenge);
-
- headers.add(HttpHeaders.Names.AUTHORIZATION, "NTLM " + challengeHeader);
- }
+ addType3NTLMAuthorizationHeader(wwwAuth, headers, principal, password, ntlmDomain, ntlmHost);
Realm.RealmBuilder realmBuilder;
Realm.AuthScheme authScheme;
@@ -1207,52 +1214,39 @@ private Realm ntlmChallenge(List wwwAuth,
realmBuilder = new Realm.RealmBuilder();
authScheme = Realm.AuthScheme.NTLM;
}
- newRealm = realmBuilder.setScheme(authScheme)
- .setUri(URI.create(request.getUrl()).getPath())
- .setMethodName(request.getMethod())
- .build();
+ newRealm = realmBuilder.setScheme(authScheme).setUri(request.getURI().getPath()).setMethodName(request.getMethod()).build();
}
return newRealm;
}
- private Realm ntlmProxyChallenge(List wwwAuth,
- Request request,
- ProxyServer proxyServer,
- FluentCaseInsensitiveStringsMap headers,
- Realm realm,
- NettyResponseFuture> future) throws NTLMEngineException {
+ private Realm ntlmProxyChallenge(List wwwAuth, Request request, ProxyServer proxyServer, FluentCaseInsensitiveStringsMap headers, Realm realm, NettyResponseFuture> future) throws NTLMEngineException {
future.getAndSetAuth(false);
- headers.remove(HttpHeaders.Names.PROXY_AUTHORIZATION);
-
- if (wwwAuth.get(0).startsWith("NTLM ")) {
- String serverChallenge = wwwAuth.get(0).trim().substring("NTLM ".length());
- String challengeHeader = ntlmEngine.generateType3Msg(proxyServer.getPrincipal(),
- proxyServer.getPassword(),
- proxyServer.getNtlmDomain(),
- proxyServer.getHost(),
- serverChallenge);
- headers.add(HttpHeaders.Names.PROXY_AUTHORIZATION, "NTLM " + challengeHeader);
- }
+
+ addType3NTLMAuthorizationHeader(wwwAuth, headers, proxyServer.getPrincipal(), proxyServer.getPassword(), proxyServer.getNtlmDomain(), proxyServer.getHost());
Realm newRealm;
+
Realm.RealmBuilder realmBuilder;
if (realm != null) {
realmBuilder = new Realm.RealmBuilder().clone(realm);
} else {
realmBuilder = new Realm.RealmBuilder();
}
- newRealm = realmBuilder//.setScheme(realm.getAuthScheme())
- .setUri(URI.create(request.getUrl()).getPath())
- .setMethodName(request.getMethod())
- .build();
+ newRealm = realmBuilder.setUri(request.getURI().getPath()).setMethodName(request.getMethod()).build();
return newRealm;
}
- private void drainChannel(final ChannelHandlerContext ctx, final NettyResponseFuture> future, final boolean keepAlive, final URI uri) {
+ private String getPoolKey(NettyResponseFuture> future) throws MalformedURLException {
+ URI uri = future.getProxyServer() != null ? future.getProxyServer().getURI() : future.getURI();
+ return future.getConnectionPoolKeyStrategy().getKey(uri);
+ }
+
+ private void drainChannel(final ChannelHandlerContext ctx, final NettyResponseFuture> future) {
ctx.setAttachment(new AsyncCallable(future) {
public Object call() throws Exception {
- if (keepAlive && ctx.getChannel().isReadable() && connectionsPool.offer(AsyncHttpProviderUtils.getBaseUrl(uri), ctx.getChannel())) {
+
+ if (future.getKeepAlive() && ctx.getChannel().isReadable() && connectionsPool.offer(getPoolKey(future), ctx.getChannel())) {
return null;
}
@@ -1288,7 +1282,7 @@ private void replayRequest(final NettyResponseFuture> future, FilterContext fc
future.touch();
log.debug("\n\nReplaying Request {}\n for Future {}\n", newRequest, future);
- drainChannel(ctx, future, future.getKeepAlive(), future.getURI());
+ drainChannel(ctx, future);
nextRequest(newRequest, future);
return;
}
@@ -1333,14 +1327,14 @@ private void upgradeProtocol(ChannelPipeline p, String scheme) throws IOExceptio
if (isSecure(scheme)) {
if (p.get(SSL_HANDLER) == null) {
- p.addFirst(HTTP_HANDLER, new HttpClientCodec());
+ p.addFirst(HTTP_HANDLER, createHttpClientCodec());
p.addFirst(SSL_HANDLER, new SslHandler(createSSLEngine()));
} else {
- p.addAfter(SSL_HANDLER, HTTP_HANDLER, new HttpClientCodec());
+ p.addAfter(SSL_HANDLER, HTTP_HANDLER, createHttpClientCodec());
}
} else {
- p.addFirst(HTTP_HANDLER, new HttpClientCodec());
+ p.addFirst(HTTP_HANDLER, createHttpClientCodec());
}
}
@@ -1370,9 +1364,8 @@ public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws
NettyResponseFuture> future = (NettyResponseFuture>) ctx.getAttachment();
future.touch();
- if (config.getIOExceptionFilters().size() > 0) {
- FilterContext> fc = new FilterContext.FilterContextBuilder().asyncHandler(future.getAsyncHandler())
- .request(future.getRequest()).ioException(new IOException("Channel Closed")).build();
+ if (!config.getIOExceptionFilters().isEmpty()) {
+ FilterContext> fc = new FilterContext.FilterContextBuilder().asyncHandler(future.getAsyncHandler()).request(future.getRequest()).ioException(new IOException("Channel Closed")).build();
fc = handleIoException(fc, future);
if (fc.replayRequest() && !future.cannotBeReplay()) {
@@ -1385,8 +1378,8 @@ public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws
p.onClose(ctx, e);
if (future != null && !future.isDone() && !future.isCancelled()) {
- if (!remotelyClosed(ctx.getChannel(), future)) {
- abort(future, new IOException("Remotely Closed " + ctx.getChannel()));
+ if (remotelyClosed(ctx.getChannel(), future)) {
+ abort(future, new IOException("Remotely Closed"));
}
} else {
closeChannel(ctx);
@@ -1397,21 +1390,20 @@ public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws
protected boolean remotelyClosed(Channel channel, NettyResponseFuture> future) {
if (isClose.get()) {
- return false;
+ return true;
}
connectionsPool.removeAll(channel);
- if (future == null && channel.getPipeline().getContext(NettyAsyncHttpProvider.class).getAttachment() != null
- && NettyResponseFuture.class.isAssignableFrom(
- channel.getPipeline().getContext(NettyAsyncHttpProvider.class).getAttachment().getClass())) {
- future = (NettyResponseFuture>)
- channel.getPipeline().getContext(NettyAsyncHttpProvider.class).getAttachment();
+ if (future == null) {
+ Object attachment = channel.getPipeline().getContext(NettyAsyncHttpProvider.class).getAttachment();
+ if (attachment instanceof NettyResponseFuture)
+ future = (NettyResponseFuture>) attachment;
}
if (future == null || future.cannotBeReplay()) {
log.debug("Unable to recover future {}\n", future);
- return false;
+ return true;
}
future.setState(NettyResponseFuture.STATE.RECONNECTED);
@@ -1420,19 +1412,19 @@ protected boolean remotelyClosed(Channel channel, NettyResponseFuture> future)
try {
nextRequest(future.getRequest(), future);
- return true;
+ return false;
} catch (IOException iox) {
future.setState(NettyResponseFuture.STATE.CLOSED);
future.abort(iox);
log.error("Remotely Closed, unable to recover", iox);
+ return true;
}
- return false;
}
private void markAsDone(final NettyResponseFuture> future, final ChannelHandlerContext ctx) throws MalformedURLException {
// We need to make sure everything is OK before adding the connection back to the pool.
try {
- future.done(null);
+ future.done();
} catch (Throwable t) {
// Never propagate exception once we know we are done.
log.debug(t.getMessage(), t);
@@ -1445,10 +1437,9 @@ private void markAsDone(final NettyResponseFuture> future, final ChannelHandle
private void finishUpdate(final NettyResponseFuture> future, final ChannelHandlerContext ctx, boolean lastValidChunk) throws IOException {
if (lastValidChunk && future.getKeepAlive()) {
- drainChannel(ctx, future, future.getKeepAlive(), future.getURI());
+ drainChannel(ctx, future);
} else {
- if (future.getKeepAlive() && ctx.getChannel().isReadable() &&
- connectionsPool.offer(AsyncHttpProviderUtils.getBaseUrl(future.getURI()), ctx.getChannel())) {
+ if (future.getKeepAlive() && ctx.getChannel().isReadable() && connectionsPool.offer(getPoolKey(future), ctx.getChannel())) {
markAsDone(future, ctx);
return;
}
@@ -1476,24 +1467,18 @@ private final boolean updateBodyAndInterrupt(final NettyResponseFuture> future
return state;
}
- //Simple marker for stopping publishing bytes.
+ // Simple marker for stopping publishing bytes.
final static class DiscardEvent {
}
@Override
- public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
- throws Exception {
+ public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
Channel channel = e.getChannel();
Throwable cause = e.getCause();
NettyResponseFuture> future = null;
- /** Issue 81
- if (e.getCause() != null && e.getCause().getClass().isAssignableFrom(PrematureChannelClosureException.class)) {
- return;
- }
- */
- if (e.getCause() != null && e.getCause().getClass().getSimpleName().equals("PrematureChannelClosureException")) {
+ if (e.getCause() instanceof PrematureChannelClosureException) {
return;
}
@@ -1503,7 +1488,7 @@ public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
try {
- if (cause != null && ClosedChannelException.class.isAssignableFrom(cause.getClass())) {
+ if (cause instanceof ClosedChannelException) {
return;
}
@@ -1512,11 +1497,10 @@ public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
future.attachChannel(null, false);
future.touch();
- if (IOException.class.isAssignableFrom(cause.getClass())) {
+ if (cause instanceof IOException) {
- if (config.getIOExceptionFilters().size() > 0) {
- FilterContext> fc = new FilterContext.FilterContextBuilder().asyncHandler(future.getAsyncHandler())
- .request(future.getRequest()).ioException(new IOException("Channel Closed")).build();
+ if (!config.getIOExceptionFilters().isEmpty()) {
+ FilterContext> fc = new FilterContext.FilterContextBuilder().asyncHandler(future.getAsyncHandler()).request(future.getRequest()).ioException(new IOException("Channel Closed")).build();
fc = handleIoException(fc, future);
if (fc.replayRequest()) {
@@ -1564,8 +1548,7 @@ public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
protected static boolean abortOnConnectCloseException(Throwable cause) {
try {
for (StackTraceElement element : cause.getStackTrace()) {
- if (element.getClassName().equals("sun.nio.ch.SocketChannelImpl")
- && element.getMethodName().equals("checkConnect")) {
+ if (element.getClassName().equals("sun.nio.ch.SocketChannelImpl") && element.getMethodName().equals("checkConnect")) {
return true;
}
}
@@ -1582,8 +1565,7 @@ protected static boolean abortOnConnectCloseException(Throwable cause) {
protected static boolean abortOnDisconnectException(Throwable cause) {
try {
for (StackTraceElement element : cause.getStackTrace()) {
- if (element.getClassName().equals("org.jboss.netty.handler.ssl.SslHandler")
- && element.getMethodName().equals("channelDisconnected")) {
+ if (element.getClassName().equals("org.jboss.netty.handler.ssl.SslHandler") && element.getMethodName().equals("channelDisconnected")) {
return true;
}
}
@@ -1600,8 +1582,7 @@ protected static boolean abortOnDisconnectException(Throwable cause) {
protected static boolean abortOnReadCloseException(Throwable cause) {
for (StackTraceElement element : cause.getStackTrace()) {
- if (element.getClassName().equals("sun.nio.ch.SocketDispatcher")
- && element.getMethodName().equals("read")) {
+ if (element.getClassName().equals("sun.nio.ch.SocketDispatcher") && element.getMethodName().equals("read")) {
return true;
}
}
@@ -1616,8 +1597,7 @@ protected static boolean abortOnReadCloseException(Throwable cause) {
protected static boolean abortOnWriteCloseException(Throwable cause) {
for (StackTraceElement element : cause.getStackTrace()) {
- if (element.getClassName().equals("sun.nio.ch.SocketDispatcher")
- && element.getMethodName().equals("write")) {
+ if (element.getClassName().equals("sun.nio.ch.SocketDispatcher") && element.getMethodName().equals("write")) {
return true;
}
}
@@ -1629,30 +1609,20 @@ protected static boolean abortOnWriteCloseException(Throwable cause) {
return false;
}
- private final static int computeAndSetContentLength(Request request, HttpRequest r) {
- int length = (int) request.getContentLength();
- if (length == -1 && r.getHeader(HttpHeaders.Names.CONTENT_LENGTH) != null) {
- length = Integer.valueOf(r.getHeader(HttpHeaders.Names.CONTENT_LENGTH));
- }
-
- if (length >= 0) {
- r.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(length));
- }
- return length;
- }
-
- public static NettyResponseFuture newFuture(URI uri,
- Request request,
- AsyncHandler asyncHandler,
- HttpRequest nettyRequest,
- AsyncHttpClientConfig config,
- NettyAsyncHttpProvider provider) {
+ public static NettyResponseFuture newFuture(URI uri, Request request, AsyncHandler asyncHandler, HttpRequest nettyRequest, AsyncHttpClientConfig config, NettyAsyncHttpProvider provider, ProxyServer proxyServer) {
- NettyResponseFuture f = new NettyResponseFuture(uri, request, asyncHandler, nettyRequest,
- requestTimeout(config, request.getPerRequestConfig()), config.getIdleConnectionTimeoutInMs(), provider);
+ NettyResponseFuture f = new NettyResponseFuture(uri,//
+ request,//
+ asyncHandler,//
+ nettyRequest,//
+ requestTimeoutInMs(config, request.getPerRequestConfig()),//
+ config.getIdleConnectionTimeoutInMs(),//
+ provider,//
+ request.getConnectionPoolKeyStrategy(),//
+ proxyServer);
- if (request.getHeaders().getFirstValue("Expect") != null
- && request.getHeaders().getFirstValue("Expect").equalsIgnoreCase("100-Continue")) {
+ String expectHeader = request.getHeaders().getFirstValue(HttpHeaders.Names.EXPECT);
+ if (expectHeader != null && expectHeader.equalsIgnoreCase(HttpHeaders.Values.CONTINUE)) {
f.getAndSetWriteBody(false);
}
return f;
@@ -1676,7 +1646,7 @@ public void operationComplete(ChannelFuture cf) {
Throwable cause = cf.getCause();
if (cause != null && future.getState() != NettyResponseFuture.STATE.NEW) {
- if (IllegalStateException.class.isAssignableFrom(cause.getClass())) {
+ if (cause instanceof IllegalStateException) {
log.debug(cause.getMessage(), cause);
try {
cf.getChannel().close();
@@ -1686,9 +1656,7 @@ public void operationComplete(ChannelFuture cf) {
return;
}
- if (ClosedChannelException.class.isAssignableFrom(cause.getClass())
- || abortOnReadCloseException(cause)
- || abortOnWriteCloseException(cause)) {
+ if (cause instanceof ClosedChannelException || abortOnReadCloseException(cause) || abortOnWriteCloseException(cause)) {
if (log.isDebugEnabled()) {
log.debug(cf.getCause() == null ? "" : cf.getCause().getMessage(), cf.getCause());
@@ -1708,15 +1676,12 @@ public void operationComplete(ChannelFuture cf) {
future.touch();
/**
- * We need to make sure we aren't in the middle of an authorization process before publishing events
- * as we will re-publish again the same event after the authorization, causing unpredictable behavior.
+ * We need to make sure we aren't in the middle of an authorization process before publishing events as we will re-publish again the same event after the authorization, causing unpredictable behavior.
*/
Realm realm = future.getRequest().getRealm() != null ? future.getRequest().getRealm() : NettyAsyncHttpProvider.this.getConfig().getRealm();
- boolean startPublishing = future.isInAuth()
- || realm == null
- || realm.getUsePreemptiveAuth() == true;
+ boolean startPublishing = future.isInAuth() || realm == null || realm.getUsePreemptiveAuth() == true;
- if (startPublishing && ProgressAsyncHandler.class.isAssignableFrom(asyncHandler.getClass())) {
+ if (startPublishing && asyncHandler instanceof ProgressAsyncHandler) {
if (notifyHeaders) {
ProgressAsyncHandler.class.cast(asyncHandler).onHeaderWriteCompleted();
} else {
@@ -1727,17 +1692,15 @@ public void operationComplete(ChannelFuture cf) {
public void operationProgressed(ChannelFuture cf, long amount, long current, long total) {
future.touch();
- if (ProgressAsyncHandler.class.isAssignableFrom(asyncHandler.getClass())) {
+ if (asyncHandler instanceof ProgressAsyncHandler) {
ProgressAsyncHandler.class.cast(asyncHandler).onContentWriteProgress(amount, current, total);
}
}
}
/**
- * Because some implementation of the ThreadSchedulingService do not clean up cancel task until they try to run
- * them, we wrap the task with the future so the when the NettyResponseFuture cancel the reaper future
- * this wrapper will release the references to the channel and the nettyResponseFuture immediately. Otherwise,
- * the memory referenced this way will only be released after the request timeout period which can be arbitrary long.
+ * Because some implementation of the ThreadSchedulingService do not clean up cancel task until they try to run them, we wrap the task with the future so the when the NettyResponseFuture cancel the reaper future this wrapper will release the references to the channel and the
+ * nettyResponseFuture immediately. Otherwise, the memory referenced this way will only be released after the request timeout period which can be arbitrary long.
*/
private final class ReaperFuture implements Future, Runnable {
private Future scheduledFuture;
@@ -1787,6 +1750,12 @@ public boolean isDone() {
return scheduledFuture.isDone();
}
+ private void expire(String message) {
+ log.debug("{} for {}", message, nettyResponseFuture);
+ abort(nettyResponseFuture, new TimeoutException(message));
+ nettyResponseFuture = null;
+ }
+
/**
* @Override
*/
@@ -1796,22 +1765,22 @@ public synchronized void run() {
return;
}
- if (nettyResponseFuture != null && nettyResponseFuture.hasExpired()
- && !nettyResponseFuture.isDone() && !nettyResponseFuture.isCancelled()) {
- log.debug("Request Timeout expired for {}\n", nettyResponseFuture);
+ boolean futureDone = nettyResponseFuture.isDone();
+ boolean futureCanceled = nettyResponseFuture.isCancelled();
- int requestTimeout = config.getRequestTimeoutInMs();
- PerRequestConfig p = nettyResponseFuture.getRequest().getPerRequestConfig();
- if (p != null && p.getRequestTimeoutInMs() != -1) {
- requestTimeout = p.getRequestTimeoutInMs();
- }
+ if (nettyResponseFuture != null && !futureDone && !futureCanceled) {
- abort(nettyResponseFuture, new TimeoutException(String.format("No response received after %s", requestTimeout)));
+ long now = millisTime();
+ if (nettyResponseFuture.hasRequestTimedOut(now)) {
+ long age = now - nettyResponseFuture.getStart();
+ expire("Request reached time out of " + nettyResponseFuture.getRequestTimeoutInMs() + " ms after " + age + " ms");
- nettyResponseFuture = null;
- }
+ } else if (nettyResponseFuture.hasConnectionIdleTimedOut(now)) {
+ long age = now - nettyResponseFuture.getStart();
+ expire("Request reached idle time out of " + nettyResponseFuture.getIdleConnectionTimeoutInMs() + " ms after " + age + " ms");
+ }
- if (nettyResponseFuture == null || nettyResponseFuture.isDone() || nettyResponseFuture.isCancelled()) {
+ } else if (nettyResponseFuture == null || futureDone || futureCanceled) {
cancel(true);
}
}
@@ -1876,9 +1845,7 @@ public long getCount() {
public long transferTo(WritableByteChannel target, long position) throws IOException {
long count = this.count - position;
if (count < 0 || position < 0) {
- throw new IllegalArgumentException(
- "position out of range: " + position +
- " (expected: 0 - " + (this.count - 1) + ")");
+ throw new IllegalArgumentException("position out of range: " + position + " (expected: 0 - " + (this.count - 1) + ")");
}
if (count == 0) {
return 0L;
@@ -1965,23 +1932,17 @@ public void destroy() {
}
private static final boolean validateWebSocketRequest(Request request, AsyncHandler> asyncHandler) {
- if (request.getMethod() != "GET" || !WebSocketUpgradeHandler.class.isAssignableFrom(asyncHandler.getClass())) {
+ if (request.getMethod() != "GET" || !(asyncHandler instanceof WebSocketUpgradeHandler)) {
return false;
}
return true;
}
- private boolean redirect(Request request,
- NettyResponseFuture> future,
- HttpResponse response,
- final ChannelHandlerContext ctx) throws Exception {
+ private boolean redirect(Request request, NettyResponseFuture> future, HttpResponse response, final ChannelHandlerContext ctx) throws Exception {
int statusCode = response.getStatus().getCode();
boolean redirectEnabled = request.isRedirectOverrideSet() ? request.isRedirectEnabled() : config.isRedirectEnabled();
- if (redirectEnabled && (statusCode == 302
- || statusCode == 301
- || statusCode == 303
- || statusCode == 307)) {
+ if (redirectEnabled && (statusCode == 302 || statusCode == 301 || statusCode == 303 || statusCode == 307)) {
if (future.incrementAndGetCurrentRedirectCount() < config.getMaxRedirects()) {
// We must allow 401 handling again.
@@ -1990,18 +1951,14 @@ private boolean redirect(Request request,
String location = response.getHeader(HttpHeaders.Names.LOCATION);
URI uri = AsyncHttpProviderUtils.getRedirectUri(future.getURI(), location);
boolean stripQueryString = config.isRemoveQueryParamOnRedirect();
- if (!uri.toString().equalsIgnoreCase(future.getURI().toString())) {
- final RequestBuilder nBuilder = stripQueryString ?
- new RequestBuilder(future.getRequest()).setQueryParameters(null)
- : new RequestBuilder(future.getRequest());
-
- if (!(statusCode < 302 || statusCode > 303)
- && !(statusCode == 302
- && config.isStrict302Handling())) {
+ if (!uri.toString().equals(future.getURI().toString())) {
+ final RequestBuilder nBuilder = stripQueryString ? new RequestBuilder(future.getRequest()).setQueryParameters(null) : new RequestBuilder(future.getRequest());
+
+ if (!(statusCode < 302 || statusCode > 303) && !(statusCode == 302 && config.isStrict302Handling())) {
nBuilder.setMethod("GET");
}
- final URI initialConnectionUri = future.getURI();
final boolean initialConnectionKeepAlive = future.getKeepAlive();
+ final String initialPoolKey = getPoolKey(future);
future.setURI(uri);
String newUrl = uri.toString();
if (request.getUrl().startsWith(WEBSOCKET)) {
@@ -2010,19 +1967,20 @@ private boolean redirect(Request request,
log.debug("Redirecting to {}", newUrl);
for (String cookieStr : future.getHttpResponse().getHeaders(HttpHeaders.Names.SET_COOKIE)) {
- Cookie c = AsyncHttpProviderUtils.parseCookie(cookieStr);
- nBuilder.addOrReplaceCookie(c);
+ for (Cookie c : CookieDecoder.decode(cookieStr)) {
+ nBuilder.addOrReplaceCookie(c);
+ }
}
for (String cookieStr : future.getHttpResponse().getHeaders(HttpHeaders.Names.SET_COOKIE2)) {
- Cookie c = AsyncHttpProviderUtils.parseCookie(cookieStr);
- nBuilder.addOrReplaceCookie(c);
+ for (Cookie c : CookieDecoder.decode(cookieStr)) {
+ nBuilder.addOrReplaceCookie(c);
+ }
}
AsyncCallable ac = new AsyncCallable(future) {
public Object call() throws Exception {
- if (initialConnectionKeepAlive && ctx.getChannel().isReadable() &&
- connectionsPool.offer(AsyncHttpProviderUtils.getBaseUrl(initialConnectionUri), ctx.getChannel())) {
+ if (initialConnectionKeepAlive && ctx.getChannel().isReadable() && connectionsPool.offer(initialPoolKey, ctx.getChannel())) {
return null;
}
finishChannel(ctx);
@@ -2061,6 +2019,7 @@ public void handle(final ChannelHandlerContext ctx, final MessageEvent e) throws
HttpRequest nettyRequest = future.getNettyRequest();
AsyncHandler handler = future.getAsyncHandler();
Request request = future.getRequest();
+ ProxyServer proxyServer = future.getProxyServer();
HttpResponse response = null;
try {
if (e.getMessage() instanceof HttpResponse) {
@@ -2074,19 +2033,14 @@ public void handle(final ChannelHandlerContext ctx, final MessageEvent e) throws
int statusCode = response.getStatus().getCode();
String ka = response.getHeader(HttpHeaders.Names.CONNECTION);
- future.setKeepAlive(ka == null || ka.toLowerCase().equals("keep-alive"));
+ future.setKeepAlive(ka == null || ka.equalsIgnoreCase(HttpHeaders.Values.KEEP_ALIVE));
List wwwAuth = getAuthorizationToken(response.getHeaders(), HttpHeaders.Names.WWW_AUTHENTICATE);
Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm();
HttpResponseStatus status = new ResponseStatus(future.getURI(), response, NettyAsyncHttpProvider.this);
HttpResponseHeaders responseHeaders = new ResponseHeaders(future.getURI(), response, NettyAsyncHttpProvider.this);
- FilterContext fc = new FilterContext.FilterContextBuilder()
- .asyncHandler(handler)
- .request(request)
- .responseStatus(status)
- .responseHeaders(responseHeaders)
- .build();
+ FilterContext fc = new FilterContext.FilterContextBuilder().asyncHandler(handler).request(request).responseStatus(status).responseHeaders(responseHeaders).build();
for (ResponseFilter asyncFilter : config.getResponseFilters()) {
try {
@@ -2110,49 +2064,40 @@ public void handle(final ChannelHandlerContext ctx, final MessageEvent e) throws
}
Realm newRealm = null;
- ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer();
final FluentCaseInsensitiveStringsMap headers = request.getHeaders();
final RequestBuilder builder = new RequestBuilder(future.getRequest());
- //if (realm != null && !future.getURI().getPath().equalsIgnoreCase(realm.getUri())) {
- // builder.setUrl(future.getURI().toString());
- //}
+ // if (realm != null && !future.getURI().getPath().equalsIgnoreCase(realm.getUri())) {
+ // builder.setUrl(future.getURI().toString());
+ // }
- if (statusCode == 401
- && wwwAuth.size() > 0
- && !future.getAndSetAuth(true)) {
+ if (statusCode == 401 && realm != null && !wwwAuth.isEmpty() && !future.getAndSetAuth(true)) {
future.setState(NettyResponseFuture.STATE.NEW);
// NTLM
- if (!wwwAuth.contains("Kerberos") && (wwwAuth.contains("NTLM") || (wwwAuth.contains("Negotiate")))) {
+ if (!wwwAuth.contains("Kerberos") && (isNTLM(wwwAuth) || (wwwAuth.contains("Negotiate")))) {
newRealm = ntlmChallenge(wwwAuth, request, proxyServer, headers, realm, future);
// SPNEGO KERBEROS
} else if (wwwAuth.contains("Negotiate")) {
newRealm = kerberosChallenge(wwwAuth, request, proxyServer, headers, realm, future);
- if (newRealm == null) return;
+ if (newRealm == null)
+ return;
} else {
Realm.RealmBuilder realmBuilder;
if (realm != null) {
- realmBuilder = new Realm.RealmBuilder().clone(realm).setScheme(realm.getAuthScheme())
- ;
+ realmBuilder = new Realm.RealmBuilder().clone(realm).setScheme(realm.getAuthScheme());
} else {
realmBuilder = new Realm.RealmBuilder();
}
- newRealm = realmBuilder
- .setUri(URI.create(request.getUrl()).getPath())
- .setMethodName(request.getMethod())
- .setUsePreemptiveAuth(true)
- .parseWWWAuthenticateHeader(wwwAuth.get(0))
- .build();
+ newRealm = realmBuilder.setUri(request.getURI().getPath()).setMethodName(request.getMethod()).setUsePreemptiveAuth(true).parseWWWAuthenticateHeader(wwwAuth.get(0)).build();
}
- final Realm nr = new Realm.RealmBuilder().clone(newRealm)
- .setUri(request.getUrl()).build();
+ final Realm nr = new Realm.RealmBuilder().clone(newRealm).setUri(request.getUrl()).build();
log.debug("Sending authentication to {}", request.getUrl());
AsyncCallable ac = new AsyncCallable(future) {
public Object call() throws Exception {
- drainChannel(ctx, future, future.getKeepAlive(), future.getURI());
+ drainChannel(ctx, future);
nextRequest(builder.setHeaders(headers).setRealm(nr).build(), future);
return null;
}
@@ -2170,25 +2115,24 @@ public Object call() throws Exception {
if (statusCode == 100) {
future.getAndSetWriteHeaders(false);
future.getAndSetWriteBody(true);
- writeRequest(ctx.getChannel(), config, future, nettyRequest);
+ writeRequest(ctx.getChannel(), config, future);
return;
}
List proxyAuth = getAuthorizationToken(response.getHeaders(), HttpHeaders.Names.PROXY_AUTHENTICATE);
- if (statusCode == 407
- && proxyAuth.size() > 0
- && !future.getAndSetAuth(true)) {
+ if (statusCode == 407 && realm != null && !proxyAuth.isEmpty() && !future.getAndSetAuth(true)) {
log.debug("Sending proxy authentication to {}", request.getUrl());
future.setState(NettyResponseFuture.STATE.NEW);
- if (!proxyAuth.contains("Kerberos") && (proxyAuth.get(0).contains("NTLM") || (proxyAuth.contains("Negotiate")))) {
+ if (!proxyAuth.contains("Kerberos") && (isNTLM(proxyAuth) || (proxyAuth.contains("Negotiate")))) {
newRealm = ntlmProxyChallenge(proxyAuth, request, proxyServer, headers, realm, future);
// SPNEGO KERBEROS
} else if (proxyAuth.contains("Negotiate")) {
newRealm = kerberosChallenge(proxyAuth, request, proxyServer, headers, realm, future);
- if (newRealm == null) return;
+ if (newRealm == null)
+ return;
} else {
newRealm = future.getRequest().getRealm();
}
@@ -2200,8 +2144,7 @@ public Object call() throws Exception {
return;
}
- if (future.getNettyRequest().getMethod().equals(HttpMethod.CONNECT)
- && statusCode == 200) {
+ if (future.getNettyRequest().getMethod().equals(HttpMethod.CONNECT) && statusCode == 200) {
log.debug("Connected to {}:{}", proxyServer.getHost(), proxyServer.getPort());
@@ -2211,7 +2154,7 @@ public Object call() throws Exception {
try {
log.debug("Connecting to proxy {} for scheme {}", proxyServer, request.getUrl());
- upgradeProtocol(ctx.getChannel().getPipeline(), URI.create(request.getUrl()).getScheme());
+ upgradeProtocol(ctx.getChannel().getPipeline(), request.getURI().getScheme());
} catch (Throwable ex) {
abort(future, ex);
}
@@ -2222,12 +2165,13 @@ public Object call() throws Exception {
return;
}
- if (redirect(request, future, response, ctx)) return;
+ if (redirect(request, future, response, ctx))
+ return;
if (!future.getAndSetStatusReceived(true) && updateStatusAndInterrupt(handler, status)) {
finishUpdate(future, ctx, response.isChunked());
return;
- } else if (updateHeadersAndInterrupt(handler, responseHeaders)) {
+ } else if (!response.getHeaders().isEmpty() && updateHeadersAndInterrupt(handler, responseHeaders)) {
finishUpdate(future, ctx, response.isChunked());
return;
} else if (!response.isChunked()) {
@@ -2241,27 +2185,24 @@ public Object call() throws Exception {
if (nettyRequest.getMethod().equals(HttpMethod.HEAD)) {
updateBodyAndInterrupt(future, handler, new ResponseBodyPart(future.getURI(), response, NettyAsyncHttpProvider.this, true));
markAsDone(future, ctx);
- drainChannel(ctx, future, future.getKeepAlive(), future.getURI());
+ drainChannel(ctx, future);
}
} else if (e.getMessage() instanceof HttpChunk) {
HttpChunk chunk = (HttpChunk) e.getMessage();
if (handler != null) {
- if (chunk.isLast() || updateBodyAndInterrupt(future, handler,
- new ResponseBodyPart(future.getURI(), null, NettyAsyncHttpProvider.this, chunk, chunk.isLast()))) {
+ if (chunk.isLast() || updateBodyAndInterrupt(future, handler, new ResponseBodyPart(future.getURI(), null, NettyAsyncHttpProvider.this, chunk, chunk.isLast()))) {
if (chunk instanceof DefaultHttpChunkTrailer) {
- updateHeadersAndInterrupt(handler, new ResponseHeaders(future.getURI(),
- future.getHttpResponse(), NettyAsyncHttpProvider.this, (HttpChunkTrailer) chunk));
+ updateHeadersAndInterrupt(handler, new ResponseHeaders(future.getURI(), future.getHttpResponse(), NettyAsyncHttpProvider.this, (HttpChunkTrailer) chunk));
}
finishUpdate(future, ctx, !chunk.isLast());
}
}
}
} catch (Exception t) {
- if (IOException.class.isAssignableFrom(t.getClass()) && config.getIOExceptionFilters().size() > 0) {
- FilterContext> fc = new FilterContext.FilterContextBuilder().asyncHandler(future.getAsyncHandler())
- .request(future.getRequest()).ioException(IOException.class.cast(t)).build();
+ if (t instanceof IOException && !config.getIOExceptionFilters().isEmpty()) {
+ FilterContext> fc = new FilterContext.FilterContextBuilder().asyncHandler(future.getAsyncHandler()).request(future.getRequest()).ioException(IOException.class.cast(t)).build();
fc = handleIoException(fc, future);
if (fc.replayRequest()) {
@@ -2289,6 +2230,22 @@ public void onClose(ChannelHandlerContext ctx, ChannelStateEvent e) {
}
private final class WebSocketProtocol implements Protocol {
+ private static final byte OPCODE_CONT = 0x0;
+ private static final byte OPCODE_TEXT = 0x1;
+ private static final byte OPCODE_BINARY = 0x2;
+ private static final byte OPCODE_UNKNOWN = -1;
+ protected byte pendingOpcode = OPCODE_UNKNOWN;
+
+ // We don't need to synchronize as replacing the "ws-decoder" will process using the same thread.
+ private void invokeOnSucces(ChannelHandlerContext ctx, WebSocketUpgradeHandler h) {
+ if (!h.touchSuccess()) {
+ try {
+ h.onSuccess(new NettyWebSocket(ctx.getChannel()));
+ } catch (Exception ex) {
+ NettyAsyncHttpProvider.this.log.warn("onSuccess unexexpected exception", ex);
+ }
+ }
+ }
// @Override
public void handle(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
@@ -2301,12 +2258,7 @@ public void handle(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
HttpResponseStatus s = new ResponseStatus(future.getURI(), response, NettyAsyncHttpProvider.this);
HttpResponseHeaders responseHeaders = new ResponseHeaders(future.getURI(), response, NettyAsyncHttpProvider.this);
- FilterContext> fc = new FilterContext.FilterContextBuilder()
- .asyncHandler(h)
- .request(request)
- .responseStatus(s)
- .responseHeaders(responseHeaders)
- .build();
+ FilterContext> fc = new FilterContext.FilterContextBuilder().asyncHandler(h).request(request).responseStatus(s).responseHeaders(responseHeaders).build();
for (ResponseFilter asyncFilter : config.getResponseFilters()) {
try {
fc = asyncFilter.filter(fc);
@@ -2329,10 +2281,10 @@ public void handle(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
}
future.setHttpResponse(response);
- if (redirect(request, future, response, ctx)) return;
+ if (redirect(request, future, response, ctx))
+ return;
- final org.jboss.netty.handler.codec.http.HttpResponseStatus status =
- new org.jboss.netty.handler.codec.http.HttpResponseStatus(101, "Web Socket Protocol Handshake");
+ final org.jboss.netty.handler.codec.http.HttpResponseStatus status = new org.jboss.netty.handler.codec.http.HttpResponseStatus(101, "Web Socket Protocol Handshake");
final boolean validStatus = response.getStatus().equals(status);
final boolean validUpgrade = response.getHeader(HttpHeaders.Names.UPGRADE) != null;
@@ -2346,31 +2298,35 @@ public void handle(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
s = new ResponseStatus(future.getURI(), response, NettyAsyncHttpProvider.this);
final boolean statusReceived = h.onStatusReceived(s) == STATE.UPGRADE;
- if (!statusReceived) {
- h.onClose(new NettyWebSocket(ctx.getChannel()), 1002, "Bad response status " + response.getStatus().getCode());
- future.done(null);
+ final boolean headerOK = h.onHeadersReceived(responseHeaders) == STATE.CONTINUE;
+ if (!headerOK || !validStatus || !validUpgrade || !validConnection || !statusReceived) {
+ abort(future, new IOException("Invalid handshake response"));
return;
}
- if (!validStatus || !validUpgrade || !validConnection) {
- throw new IOException("Invalid handshake response");
- }
-
- String accept = response.getHeader("Sec-WebSocket-Accept");
- String key = WebSocketUtil.getAcceptKey(future.getNettyRequest().getHeader(WEBSOCKET_KEY));
+ String accept = response.getHeader(HttpHeaders.Names.SEC_WEBSOCKET_ACCEPT);
+ String key = WebSocketUtil.getAcceptKey(future.getNettyRequest().getHeader(HttpHeaders.Names.SEC_WEBSOCKET_KEY));
if (accept == null || !accept.equals(key)) {
throw new IOException(String.format("Invalid challenge. Actual: %s. Expected: %s", accept, key));
}
- ctx.getPipeline().replace("ws-decoder", "ws-decoder", new WebSocket08FrameDecoder(false, false));
- ctx.getPipeline().replace("ws-encoder", "ws-encoder", new WebSocket08FrameEncoder(true));
- if (h.onHeadersReceived(responseHeaders) == STATE.CONTINUE) {
- h.onSuccess(new NettyWebSocket(ctx.getChannel()));
- }
- future.done(null);
+ ctx.getPipeline().replace("http-encoder", "ws-encoder", new WebSocket08FrameEncoder(true));
+ ctx.getPipeline().get(HttpResponseDecoder.class).replace("ws-decoder", new WebSocket08FrameDecoder(false, false));
+
+ invokeOnSucces(ctx, h);
+ future.done();
} else if (e.getMessage() instanceof WebSocketFrame) {
+
+ invokeOnSucces(ctx, h);
+
final WebSocketFrame frame = (WebSocketFrame) e.getMessage();
+ if (frame instanceof TextWebSocketFrame) {
+ pendingOpcode = OPCODE_TEXT;
+ } else if (frame instanceof BinaryWebSocketFrame) {
+ pendingOpcode = OPCODE_BINARY;
+ }
+
HttpChunk webSocketChunk = new HttpChunk() {
private ChannelBuffer content;
@@ -2396,16 +2352,27 @@ public void setContent(ChannelBuffer content) {
h.onBodyPartReceived(rp);
NettyWebSocket webSocket = NettyWebSocket.class.cast(h.onCompleted());
- webSocket.onMessage(rp.getBodyPartBytes());
- webSocket.onTextMessage(frame.getBinaryData().toString("UTF-8"));
- if (CloseWebSocketFrame.class.isAssignableFrom(frame.getClass())) {
- try {
- webSocket.onClose(CloseWebSocketFrame.class.cast(frame).getStatusCode(), CloseWebSocketFrame.class.cast(frame).getReasonText());
- } catch (Throwable t) {
- // Swallow any exception that may comes from a Netty version released before 3.4.0
- log.trace("", t);
+ if (webSocket != null) {
+ if (pendingOpcode == OPCODE_BINARY) {
+ webSocket.onBinaryFragment(rp.getBodyPartBytes(), frame.isFinalFragment());
+ } else {
+ webSocket.onTextFragment(frame.getBinaryData().toString(UTF8), frame.isFinalFragment());
}
+
+ if (frame instanceof CloseWebSocketFrame) {
+ try {
+ ctx.setAttachment(DiscardEvent.class);
+ webSocket.onClose(CloseWebSocketFrame.class.cast(frame).getStatusCode(), CloseWebSocketFrame.class.cast(frame).getReasonText());
+ } catch (Throwable t) {
+ // Swallow any exception that may comes from a Netty version released before 3.4.0
+ log.trace("", t);
+ } finally {
+ h.resetSuccess();
+ }
+ }
+ } else {
+ log.debug("UpgradeHandler returned a null NettyWebSocket ");
}
}
} else {
@@ -2413,11 +2380,11 @@ public void setContent(ChannelBuffer content) {
}
}
- //@Override
+ // @Override
public void onError(ChannelHandlerContext ctx, ExceptionEvent e) {
try {
log.warn("onError {}", e);
- if (ctx.getAttachment() == null || !NettyResponseFuture.class.isAssignableFrom(ctx.getAttachment().getClass())) {
+ if (!(ctx.getAttachment() instanceof NettyResponseFuture)) {
return;
}
@@ -2425,17 +2392,19 @@ public void onError(ChannelHandlerContext ctx, ExceptionEvent e) {
WebSocketUpgradeHandler h = WebSocketUpgradeHandler.class.cast(nettyResponse.getAsyncHandler());
NettyWebSocket webSocket = NettyWebSocket.class.cast(h.onCompleted());
- webSocket.onError(e.getCause());
- webSocket.close();
+ if (webSocket != null) {
+ webSocket.onError(e.getCause());
+ webSocket.close();
+ }
} catch (Throwable t) {
log.error("onError", t);
}
}
- //@Override
+ // @Override
public void onClose(ChannelHandlerContext ctx, ChannelStateEvent e) {
log.trace("onClose {}", e);
- if (ctx.getAttachment() == null || !NettyResponseFuture.class.isAssignableFrom(ctx.getAttachment().getClass())) {
+ if (!(ctx.getAttachment() instanceof NettyResponseFuture)) {
return;
}
@@ -2443,8 +2412,10 @@ public void onClose(ChannelHandlerContext ctx, ChannelStateEvent e) {
NettyResponseFuture> nettyResponse = NettyResponseFuture.class.cast(ctx.getAttachment());
WebSocketUpgradeHandler h = WebSocketUpgradeHandler.class.cast(nettyResponse.getAsyncHandler());
NettyWebSocket webSocket = NettyWebSocket.class.cast(h.onCompleted());
+ h.resetSuccess();
- webSocket.close();
+ if (!(ctx.getAttachment() instanceof DiscardEvent))
+ webSocket.close(1006, "Connection was closed abnormally (that is, with no close frame being sent).");
} catch (Throwable t) {
log.error("onError", t);
}
@@ -2463,4 +2434,3 @@ private static boolean isSecure(URI uri) {
return isSecure(uri.getScheme());
}
}
-
diff --git a/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProviderConfig.java b/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProviderConfig.java
index 7c976149e6..79bd0741b7 100644
--- a/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProviderConfig.java
+++ b/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProviderConfig.java
@@ -57,6 +57,25 @@ public class NettyAsyncHttpProviderConfig implements AsyncHttpProviderConfig properties = new ConcurrentHashMap();
public NettyAsyncHttpProviderConfig() {
@@ -85,6 +104,20 @@ public Object getProperty(String name) {
return properties.get(name);
}
+ /**
+ * Return the value associated with the property's name
+ *
+ * @param name
+ * @return this instance of AsyncHttpProviderConfig
+ */
+ public T getProperty(String name, Class type, T defaultValue) {
+ Object value = properties.get(name);
+ if (value != null && type.isAssignableFrom(value.getClass())) {
+ return type.cast(value);
+ }
+ return defaultValue;
+ }
+
/**
* Remove the value associated with the property's name
*
@@ -103,4 +136,4 @@ public Object removeProperty(String name) {
public Set