/* * 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 org.asynchttpclient; import org.asynchttpclient.filter.FilterContext; import org.asynchttpclient.filter.FilterException; import org.asynchttpclient.filter.RequestFilter; import org.asynchttpclient.resumable.ResumableAsyncHandler; import java.io.Closeable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; /** * This class support asynchronous and synchronous HTTP request. *

* To execute synchronous HTTP request, you just need to do *

 *    AsyncHttpClient c = new AsyncHttpClient();
 *    Future f = c.prepareGet("http://www.ning.com/").execute();
 * 
* The code above will block until the response is fully received. To execute asynchronous HTTP request, you * create an {@link AsyncHandler} or its abstract implementation, {@link AsyncCompletionHandler} *

*

 *       AsyncHttpClient c = new AsyncHttpClient();
 *       Future f = c.prepareGet("http://www.ning.com/").execute(new AsyncCompletionHandler() {
 * 

* @Override * public Response onCompleted(Response response) throws IOException { * // Do something * return response; * } *

* @Override * public void onThrowable(Throwable t) { * } * }); * Response response = f.get(); *

* // We are just interested to retrieve the status code. * Future f = c.prepareGet("http://www.ning.com/").execute(new AsyncCompletionHandler() { *

* @Override * public Integer onCompleted(Response response) throws IOException { * // Do something * return response.getStatusCode(); * } *

* @Override * public void onThrowable(Throwable t) { * } * }); * Integer statusCode = f.get(); *

* You can also have more control about the how the response is asynchronously processed by using a {@link AsyncHandler} *
 *      AsyncHttpClient c = new AsyncHttpClient();
 *      Future f = c.prepareGet("http://www.ning.com/").execute(new AsyncHandler() {
 *          private StringBuilder builder = new StringBuilder();
 * 

* @Override * public STATE onStatusReceived(HttpResponseStatus s) throws Exception { * // return STATE.CONTINUE or STATE.ABORT * return STATE.CONTINUE * } *

* @Override * public STATE onHeadersReceived(HttpResponseHeaders bodyPart) throws Exception { * // return STATE.CONTINUE or STATE.ABORT * return STATE.CONTINUE *

* } * @Override *

* public STATE onBodyPartReceived(HttpResponseBodyPart bodyPart) throws Exception { * builder.append(new String(bodyPart)); * // return STATE.CONTINUE or STATE.ABORT * return STATE.CONTINUE * } *

* @Override * public String onCompleted() throws Exception { * // Will be invoked once the response has been fully read or a ResponseComplete exception * // has been thrown. * return builder.toString(); * } *

* @Override * public void onThrowable(Throwable t) { * } * }); *

* String bodyResponse = f.get(); *

* This class can also be used without the need of {@link AsyncHandler}

*
 *      AsyncHttpClient c = new AsyncHttpClient();
 *      Future f = c.prepareGet(TARGET_URL).execute();
 *      Response r = f.get();
 * 
*

* Finally, you can configure the AsyncHttpClient using an {@link AsyncHttpClientConfig} instance

*
 *      AsyncHttpClient c = new AsyncHttpClient(new AsyncHttpClientConfig.Builder().setRequestTimeoutInMs(...).build());
 *      Future f = c.prepareGet(TARGET_URL).execute();
 *      Response r = f.get();
 * 
*

* 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 implements Closeable { /** * Providers that will be searched for, on the classpath, in order when no * provider is explicitly specified by the developer. */ private static final String[] DEFAULT_PROVIDERS = { "org.asynchttpclient.providers.netty.NettyAsyncHttpProvider", "org.asynchttpclient.providers.grizzly.GrizzlyAsyncHttpProvider", "org.asynchttpclient.providers.jdk.JDKAsyncHttpProvider" }; private final AsyncHttpProvider httpProvider; private final AsyncHttpClientConfig config; private final static Logger logger = LoggerFactory.getLogger(AsyncHttpClient.class); private final AtomicBoolean isClosed = new AtomicBoolean(false); /** * Default signature calculator to use for all requests constructed by this client instance. * * @since 1.1 */ protected SignatureCalculator signatureCalculator; /** * Create a new HTTP Asynchronous Client using the default {@link AsyncHttpClientConfig} configuration. The * default {@link AsyncHttpProvider} that will be used will be based on the classpath configuration. * * The default providers will be searched for in this order: *

* * If none of those providers are found, then the runtime will default to * the {@link org.asynchttpclient.providers.jdk.JDKAsyncHttpProvider}. */ public AsyncHttpClient() { this(new AsyncHttpClientConfig.Builder().build()); } /** * Create a new HTTP Asynchronous Client using an implementation of {@link AsyncHttpProvider} and * the default {@link AsyncHttpClientConfig} configuration. * * @param provider a {@link AsyncHttpProvider} */ public AsyncHttpClient(AsyncHttpProvider provider) { this(provider, new AsyncHttpClientConfig.Builder().build()); } /** * Create a new HTTP Asynchronous Client using the specified {@link AsyncHttpClientConfig} configuration. * This configuration will be passed to the default {@link AsyncHttpProvider} that will be selected based on * the classpath configuration. * * The default providers will be searched for in this order: * * * If none of those providers are found, then the runtime will default to * the {@link org.asynchttpclient.providers.jdk.JDKAsyncHttpProvider}. * * @param config a {@link AsyncHttpClientConfig} */ public AsyncHttpClient(AsyncHttpClientConfig config) { this(loadDefaultProvider(DEFAULT_PROVIDERS, config), config); } /** * Create a new HTTP Asynchronous Client using a {@link AsyncHttpClientConfig} configuration and * and a AsyncHttpProvider class' name. * * @param config a {@link AsyncHttpClientConfig} * @param providerClass a {@link AsyncHttpProvider} */ public AsyncHttpClient(String providerClass, AsyncHttpClientConfig config) { this(loadProvider(providerClass, config), new AsyncHttpClientConfig.Builder().build()); } /** * Create a new HTTP Asynchronous Client using a {@link AsyncHttpClientConfig} configuration and * and a {@link AsyncHttpProvider}. * * @param config a {@link AsyncHttpClientConfig} * @param httpProvider a {@link AsyncHttpProvider} */ public AsyncHttpClient(AsyncHttpProvider httpProvider, AsyncHttpClientConfig config) { this.config = config; this.httpProvider = httpProvider; } public class BoundRequestBuilder extends RequestBuilderBase { /** * Calculator used for calculating request signature for the request being * built, if any. */ protected SignatureCalculator signatureCalculator; /** * URL used as the base, not including possibly query parameters. Needed for * signature calculation */ protected String baseURL; private BoundRequestBuilder(String reqType, boolean useRawUrl) { super(BoundRequestBuilder.class, reqType, useRawUrl); } private BoundRequestBuilder(Request prototype) { super(BoundRequestBuilder.class, prototype); } public ListenableFuture execute(AsyncHandler handler) throws IOException { return AsyncHttpClient.this.executeRequest(build(), handler); } public ListenableFuture execute() throws IOException { return AsyncHttpClient.this.executeRequest(build(), new AsyncCompletionHandlerBase()); } // Note: For now we keep the delegates in place even though they are not needed // since otherwise Clojure (and maybe other languages) won't be able to // access these methods - see Clojure tickets 126 and 259 @Override public BoundRequestBuilder addBodyPart(Part part) throws IllegalArgumentException { return super.addBodyPart(part); } @Override public BoundRequestBuilder addCookie(Cookie cookie) { return super.addCookie(cookie); } @Override public BoundRequestBuilder addHeader(String name, String value) { return super.addHeader(name, value); } @Override public BoundRequestBuilder addParameter(String key, String value) throws IllegalArgumentException { return super.addParameter(key, value); } @Override public BoundRequestBuilder addQueryParameter(String name, String value) { return super.addQueryParameter(name, value); } @Override public Request build() { /* Let's first calculate and inject signature, before finalizing actual build * (order does not matter with current implementation but may in future) */ if (signatureCalculator != null) { String url = baseURL; // Should not include query parameters, ensure: int i = url.indexOf('?'); if (i >= 0) { url = url.substring(0, i); } signatureCalculator.calculateAndAddSignature(url, request, this); } return super.build(); } @Override public BoundRequestBuilder setBody(byte[] data) throws IllegalArgumentException { return super.setBody(data); } @Override public BoundRequestBuilder setBody(InputStream stream) throws IllegalArgumentException { return super.setBody(stream); } @Override public BoundRequestBuilder setBody(String data) throws IllegalArgumentException { return super.setBody(data); } @Override public BoundRequestBuilder setHeader(String name, String value) { return super.setHeader(name, value); } @Override public BoundRequestBuilder setHeaders(FluentCaseInsensitiveStringsMap headers) { return super.setHeaders(headers); } @Override public BoundRequestBuilder setHeaders(Map> headers) { return super.setHeaders(headers); } @Override public BoundRequestBuilder setParameters(Map> parameters) throws IllegalArgumentException { return super.setParameters(parameters); } @Override public BoundRequestBuilder setParameters(FluentStringsMap parameters) throws IllegalArgumentException { return super.setParameters(parameters); } @Override public BoundRequestBuilder setUrl(String url) { baseURL = url; return super.setUrl(url); } @Override public BoundRequestBuilder setVirtualHost(String virtualHost) { return super.setVirtualHost(virtualHost); } public BoundRequestBuilder setSignatureCalculator(SignatureCalculator signatureCalculator) { this.signatureCalculator = signatureCalculator; return this; } } /** * Return the asynchronous {@link AsyncHttpProvider} * * @return an {@link AsyncHttpProvider} */ @SuppressWarnings("UnusedDeclaration") public AsyncHttpProvider getProvider() { return httpProvider; } /** * Close the underlying connections. */ public void close() { httpProvider.close(); isClosed.set(true); } /** * Asynchronous close the {@link AsyncHttpProvider} by spawning a thread and avoid blocking. */ @SuppressWarnings("UnusedDeclaration") public void closeAsynchronously() { config.applicationThreadPool.submit(new Runnable() { public void run() { httpProvider.close(); isClosed.set(true); } }); } @Override protected void finalize() throws Throwable { try { if (!isClosed.get()) { logger.error("AsyncHttpClient.close() hasn't been invoked, which may produce file descriptor leaks"); } } finally { super.finalize(); } } /** * Return true if closed * * @return true if closed */ @SuppressWarnings("UnusedDeclaration") public boolean isClosed() { return isClosed.get(); } /** * Return the {@link AsyncHttpClientConfig} * * @return {@link AsyncHttpClientConfig} */ @SuppressWarnings("UnusedDeclaration") public AsyncHttpClientConfig getConfig() { return config; } /** * Set default signature calculator to use for requests build by this client instance */ @SuppressWarnings("UnusedDeclaration") public AsyncHttpClient setSignatureCalculator(SignatureCalculator signatureCalculator) { this.signatureCalculator = signatureCalculator; return this; } /** * Prepare an HTTP client GET request. * * @param url A well formed URL. * @return {@link RequestBuilder} */ public BoundRequestBuilder prepareGet(String url) { return requestBuilder("GET", url); } /** * Prepare an HTTP client CONNECT request. * * @param url A well formed URL. * @return {@link RequestBuilder} */ @SuppressWarnings("UnusedDeclaration") public BoundRequestBuilder prepareConnect(String url) { return requestBuilder("CONNECT", url); } /** * Prepare an HTTP client OPTIONS request. * * @param url A well formed URL. * @return {@link RequestBuilder} */ public BoundRequestBuilder prepareOptions(String url) { return requestBuilder("OPTIONS", url); } /** * Prepare an HTTP client HEAD request. * * @param url A well formed URL. * @return {@link RequestBuilder} */ public BoundRequestBuilder prepareHead(String url) { return requestBuilder("HEAD", url); } /** * Prepare an HTTP client POST request. * * @param url A well formed URL. * @return {@link RequestBuilder} */ public BoundRequestBuilder preparePost(String url) { return requestBuilder("POST", url); } /** * Prepare an HTTP client PUT request. * * @param url A well formed URL. * @return {@link RequestBuilder} */ public BoundRequestBuilder preparePut(String url) { return requestBuilder("PUT", url); } /** * Prepare an HTTP client DELETE request. * * @param url A well formed URL. * @return {@link RequestBuilder} */ @SuppressWarnings("UnusedDeclaration") public BoundRequestBuilder prepareDelete(String url) { return requestBuilder("DELETE", url); } /** * Prepare an HTTP client PATCH request. * * @param url A well formed URL. * @return {@link RequestBuilder} */ @SuppressWarnings("UnusedDeclaration") public BoundRequestBuilder preparePatch(String url) { return requestBuilder("PATCH", url); } /** * Prepare an HTTP client TRACE request. * * @param url A well formed URL. * @return {@link RequestBuilder} */ @SuppressWarnings("UnusedDeclaration") public BoundRequestBuilder prepareTrace(String url) { return requestBuilder("TRACE", url); } /** * Construct a {@link RequestBuilder} using a {@link Request} * * @param request a {@link Request} * @return {@link RequestBuilder} */ @SuppressWarnings("UnusedDeclaration") public BoundRequestBuilder prepareRequest(Request request) { return requestBuilder(request); } /** * Execute an HTTP request. * * @param request {@link Request} * @param handler an instance of {@link AsyncHandler} * @param Type of the value that will be returned by the associated {@link java.util.concurrent.Future} * @return a {@link Future} of type T * @throws IOException */ public ListenableFuture executeRequest(Request request, AsyncHandler handler) throws IOException { FilterContext fc = new FilterContext.FilterContextBuilder().asyncHandler(handler).request(request).build(); fc = preProcessRequest(fc); return httpProvider.execute(fc.getRequest(), fc.getAsyncHandler()); } /** * Execute an HTTP request. * * @param request {@link Request} * @return a {@link Future} of type Response * @throws IOException */ public ListenableFuture executeRequest(Request request) throws IOException { FilterContext fc = new FilterContext.FilterContextBuilder().asyncHandler(new AsyncCompletionHandlerBase()).request(request).build(); fc = preProcessRequest(fc); return httpProvider.execute(fc.getRequest(), fc.getAsyncHandler()); } /** * Configure and execute the associated {@link RequestFilter}. This class may decorate the {@link Request} and {@link AsyncHandler} * * @param fc {@link FilterContext} * @return {@link FilterContext} */ private FilterContext preProcessRequest(FilterContext fc) throws IOException { if (config.hasRequestFilters()) { final List requestFilters = config.getRequestFilters(); for (int i = 0, len = requestFilters.size(); i < len; i++) { final RequestFilter asyncFilter = requestFilters.get(i); try { fc = asyncFilter.filter(fc); if (fc == null) { throw new NullPointerException("FilterContext is null"); } } catch (FilterException e) { IOException ex = new IOException(); ex.initCause(e); throw ex; } } } Request request = fc.getRequest(); if (fc.getAsyncHandler() instanceof ResumableAsyncHandler) { request = ResumableAsyncHandler.class.cast(fc.getAsyncHandler()).adjustRequestRange(request); } if (request.getRangeOffset() != 0) { RequestBuilder builder = new RequestBuilder(request); builder.setHeader("Range", "bytes=" + request.getRangeOffset() + "-"); request = builder.build(); } fc = new FilterContext.FilterContextBuilder(fc).request(request).build(); return fc; } @SuppressWarnings("unchecked") private static AsyncHttpProvider loadProvider(final String className, final AsyncHttpClientConfig config) { try { Class providerClass = (Class) Thread.currentThread() .getContextClassLoader().loadClass(className); return providerClass.getDeclaredConstructor( new Class[]{AsyncHttpClientConfig.class}).newInstance(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) AsyncHttpClient.class.getClassLoader().loadClass(className); return providerClass.getDeclaredConstructor( new Class[]{AsyncHttpClientConfig.class}).newInstance(config); } catch (Throwable ignored) { } } return null; } @SuppressWarnings("unchecked") private static AsyncHttpProvider loadDefaultProvider(String[] providerClassNames, AsyncHttpClientConfig config) { AsyncHttpProvider provider; for (final String className : providerClassNames) { provider = loadProvider(className, config); if (provider != null) { return provider; } } throw new IllegalStateException("No providers found on the classpath"); } protected BoundRequestBuilder requestBuilder(String reqType, String url) { return new BoundRequestBuilder(reqType, config.isUseRawUrl()).setUrl(url).setSignatureCalculator(signatureCalculator); } protected BoundRequestBuilder requestBuilder(Request prototype) { return new BoundRequestBuilder(prototype).setSignatureCalculator(signatureCalculator); } }