/* * Copyright (c) 2010 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 org.asynchttpclient; import org.asynchttpclient.resumable.ResumableAsyncHandler; import org.asynchttpclient.resumable.ResumableIOExceptionFilter; import org.asynchttpclient.simple.HeaderMap; import org.asynchttpclient.simple.SimpleAHCTransferListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; import java.io.Closeable; import java.io.IOException; import java.util.Collection; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; /** * Simple implementation of {@link AsyncHttpClient} and it's related builders ({@link AsyncHttpClientConfig}, * {@link Realm}, {@link ProxyServer} and {@link AsyncHandler}. You can * build powerful application by just using this class. *

* This class rely on {@link BodyGenerator} and {@link BodyConsumer} for handling the request and response body. No * {@link AsyncHandler} are required. As simple as: *

 * SimpleAsyncHttpClient client = new SimpleAsyncHttpClient.Builder()
 * .setIdleConnectionInPoolTimeoutInMs(100)
 * .setMaximumConnectionsTotal(50)
 * .setRequestTimeoutInMs(5 * 60 * 1000)
 * .setUrl(getTargetUrl())
 * .setHeader("Content-Type", "text/html").build();
 * 

* StringBuilder s = new StringBuilder(); * Future future = client.post(new InputStreamBodyGenerator(new ByteArrayInputStream(MY_MESSAGE.getBytes())), new AppendableBodyConsumer(s)); *

* or *
 * public void ByteArrayOutputStreamBodyConsumerTest() throws Throwable {
 * 

* SimpleAsyncHttpClient client = new SimpleAsyncHttpClient.Builder() * .setUrl(getTargetUrl()) * .build(); *

* ByteArrayOutputStream o = new ByteArrayOutputStream(10); * Future future = client.post(new FileodyGenerator(myFile), new OutputStreamBodyConsumer(o)); *

*/ public class SimpleAsyncHttpClient implements Closeable { private final static Logger logger = LoggerFactory.getLogger(SimpleAsyncHttpClient.class); private final AsyncHttpClientConfig config; private final RequestBuilder requestBuilder; private AsyncHttpClient asyncHttpClient; private final ThrowableHandler defaultThrowableHandler; private final boolean resumeEnabled; private final ErrorDocumentBehaviour errorDocumentBehaviour; private final SimpleAHCTransferListener listener; private final boolean derived; private final String providerClass; 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; this.resumeEnabled = resumeEnabled; this.errorDocumentBehaviour = errorDocumentBehaviour; this.asyncHttpClient = ahc; this.listener = listener; this.providerClass = providerClass; this.derived = ahc != null; } public Future post(Part... parts) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("POST"); for (Part part : parts) { r.addBodyPart(part); } return execute(r, null, null); } public Future post(BodyConsumer consumer, Part... parts) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("POST"); for (Part part : parts) { r.addBodyPart(part); } return execute(r, consumer, null); } public Future post(BodyGenerator bodyGenerator) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("POST"); r.setBody(bodyGenerator); return execute(r, null, null); } public Future post(BodyGenerator bodyGenerator, ThrowableHandler throwableHandler) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("POST"); r.setBody(bodyGenerator); return execute(r, null, throwableHandler); } public Future post(BodyGenerator bodyGenerator, BodyConsumer bodyConsumer) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("POST"); r.setBody(bodyGenerator); return execute(r, bodyConsumer, null); } public Future post(BodyGenerator bodyGenerator, BodyConsumer bodyConsumer, ThrowableHandler throwableHandler) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("POST"); r.setBody(bodyGenerator); return execute(r, bodyConsumer, throwableHandler); } public Future put(Part... parts) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("POST"); for (Part part : parts) { r.addBodyPart(part); } return execute(r, null, null); } public Future put(BodyConsumer consumer, Part... parts) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("POST"); for (Part part : parts) { r.addBodyPart(part); } return execute(r, consumer, null); } public Future put(BodyGenerator bodyGenerator, BodyConsumer bodyConsumer) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("PUT"); r.setBody(bodyGenerator); return execute(r, bodyConsumer, null); } public Future put(BodyGenerator bodyGenerator, BodyConsumer bodyConsumer, ThrowableHandler throwableHandler) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("PUT"); r.setBody(bodyGenerator); return execute(r, bodyConsumer, throwableHandler); } public Future put(BodyGenerator bodyGenerator) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("PUT"); r.setBody(bodyGenerator); return execute(r, null, null); } public Future put(BodyGenerator bodyGenerator, ThrowableHandler throwableHandler) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("PUT"); r.setBody(bodyGenerator); return execute(r, null, throwableHandler); } public Future get() throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); return execute(r, null, null); } public Future get(ThrowableHandler throwableHandler) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); return execute(r, null, throwableHandler); } public Future get(BodyConsumer bodyConsumer) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); return execute(r, bodyConsumer, null); } public Future get(BodyConsumer bodyConsumer, ThrowableHandler throwableHandler) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); return execute(r, bodyConsumer, throwableHandler); } public Future delete() throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("DELETE"); return execute(r, null, null); } public Future delete(ThrowableHandler throwableHandler) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("DELETE"); return execute(r, null, throwableHandler); } public Future delete(BodyConsumer bodyConsumer) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("DELETE"); return execute(r, bodyConsumer, null); } public Future delete(BodyConsumer bodyConsumer, ThrowableHandler throwableHandler) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("DELETE"); return execute(r, bodyConsumer, throwableHandler); } public Future head() throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("HEAD"); return execute(r, null, null); } public Future head(ThrowableHandler throwableHandler) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("HEAD"); return execute(r, null, throwableHandler); } public Future options() throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("OPTIONS"); return execute(r, null, null); } public Future options(ThrowableHandler throwableHandler) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("OPTIONS"); return execute(r, null, throwableHandler); } public Future options(BodyConsumer bodyConsumer) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("OPTIONS"); return execute(r, bodyConsumer, null); } public Future options(BodyConsumer bodyConsumer, ThrowableHandler throwableHandler) throws IOException { RequestBuilder r = rebuildRequest(requestBuilder.build()); r.setMethod("OPTIONS"); return execute(r, bodyConsumer, throwableHandler); } private RequestBuilder rebuildRequest(Request rb) { return new RequestBuilder(rb); } private Future execute(RequestBuilder rb, BodyConsumer bodyConsumer, ThrowableHandler throwableHandler) throws IOException { if (throwableHandler == null) { throwableHandler = defaultThrowableHandler; } Request request = rb.build(); ProgressAsyncHandler handler = new BodyConsumerAsyncHandler(bodyConsumer, throwableHandler, errorDocumentBehaviour, request.getUrl(), listener); if (resumeEnabled && request.getMethod().equals("GET") && bodyConsumer != null && bodyConsumer instanceof ResumableBodyConsumer) { ResumableBodyConsumer fileBodyConsumer = (ResumableBodyConsumer) bodyConsumer; long length = fileBodyConsumer.getTransferredBytes(); fileBodyConsumer.resume(); handler = new ResumableBodyConsumerAsyncHandler(length, handler); } return asyncHttpClient().executeRequest(request, handler); } private AsyncHttpClient asyncHttpClient() { synchronized (config) { if (asyncHttpClient == null) { if (providerClass == null) asyncHttpClient = new AsyncHttpClient(config); else asyncHttpClient = new AsyncHttpClient(providerClass, config); } } return asyncHttpClient; } /** * Close the underlying AsyncHttpClient for this instance. *

* If this instance is derived from another instance, this method does * nothing as the client instance is managed by the original * SimpleAsyncHttpClient. * * @see #derive() * @see AsyncHttpClient#close() */ public void close() { if (!derived && asyncHttpClient != null) { asyncHttpClient.close(); } } /** * Returns a Builder for a derived SimpleAsyncHttpClient that uses the same * instance of {@link AsyncHttpClient} to execute requests. *

*

*

* The original SimpleAsyncHttpClient is responsible for managing the * underlying AsyncHttpClient. For the derived instance, {@link #close()} is * a NOOP. If the original SimpleAsyncHttpClient is closed, all derived * instances become invalid. * * @return a Builder for a derived SimpleAsyncHttpClient that uses the same * instance of {@link AsyncHttpClient} to execute requests, never * {@code null}. */ public DerivedBuilder derive() { return new Builder(this); } public enum ErrorDocumentBehaviour { /** * Write error documents as usual via * {@link BodyConsumer#consume(java.nio.ByteBuffer)}. */ WRITE, /** * Accumulate error documents in memory but do not consume. */ ACCUMULATE, /** * Omit error documents. An error document will neither be available in * the response nor written via a {@link BodyConsumer}. */ OMIT; } /** * This interface contains possible configuration changes for a derived SimpleAsyncHttpClient. * * @see SimpleAsyncHttpClient#derive() */ public interface DerivedBuilder { DerivedBuilder setFollowRedirects(boolean followRedirects); DerivedBuilder setVirtualHost(String virtualHost); DerivedBuilder setUrl(String url); DerivedBuilder setParameters(FluentStringsMap parameters) throws IllegalArgumentException; DerivedBuilder setParameters(Map> parameters) throws IllegalArgumentException; DerivedBuilder setHeaders(Map> headers); DerivedBuilder setHeaders(FluentCaseInsensitiveStringsMap headers); DerivedBuilder setHeader(String name, String value); DerivedBuilder addQueryParameter(String name, String value); DerivedBuilder addParameter(String key, String value) throws IllegalArgumentException; DerivedBuilder addHeader(String name, String value); DerivedBuilder addCookie(Cookie cookie); DerivedBuilder addBodyPart(Part part) throws IllegalArgumentException; DerivedBuilder setResumableDownload(boolean resume); SimpleAsyncHttpClient build(); } public final static class Builder implements DerivedBuilder { private final RequestBuilder requestBuilder; private final AsyncHttpClientConfig.Builder configBuilder = new AsyncHttpClientConfig.Builder(); private Realm.RealmBuilder realmBuilder = null; private ProxyServer.Protocol proxyProtocol = null; private String proxyHost = null; private String proxyPrincipal = null; private String proxyPassword = null; private int proxyPort = 80; private ThrowableHandler defaultThrowableHandler = null; private boolean enableResumableDownload = false; private ErrorDocumentBehaviour errorDocumentBehaviour = ErrorDocumentBehaviour.WRITE; private AsyncHttpClient ahc = null; private SimpleAHCTransferListener listener = null; private String providerClass = null; public Builder() { requestBuilder = new RequestBuilder("GET", false); } private Builder(SimpleAsyncHttpClient client) { this.requestBuilder = new RequestBuilder(client.requestBuilder.build()); this.defaultThrowableHandler = client.defaultThrowableHandler; this.errorDocumentBehaviour = client.errorDocumentBehaviour; this.enableResumableDownload = client.resumeEnabled; this.ahc = client.asyncHttpClient(); this.listener = client.listener; } public Builder addBodyPart(Part part) throws IllegalArgumentException { requestBuilder.addBodyPart(part); return this; } public Builder addCookie(Cookie cookie) { requestBuilder.addCookie(cookie); return this; } public Builder addHeader(String name, String value) { requestBuilder.addHeader(name, value); return this; } public Builder addParameter(String key, String value) throws IllegalArgumentException { requestBuilder.addParameter(key, value); return this; } public Builder addQueryParameter(String name, String value) { requestBuilder.addQueryParameter(name, value); return this; } public Builder setHeader(String name, String value) { requestBuilder.setHeader(name, value); return this; } public Builder setHeaders(FluentCaseInsensitiveStringsMap headers) { requestBuilder.setHeaders(headers); return this; } public Builder setHeaders(Map> headers) { requestBuilder.setHeaders(headers); return this; } public Builder setParameters(Map> parameters) throws IllegalArgumentException { requestBuilder.setParameters(parameters); return this; } public Builder setParameters(FluentStringsMap parameters) throws IllegalArgumentException { requestBuilder.setParameters(parameters); return this; } public Builder setUrl(String url) { requestBuilder.setUrl(url); return this; } public Builder setVirtualHost(String virtualHost) { requestBuilder.setVirtualHost(virtualHost); return this; } public Builder setFollowRedirects(boolean followRedirects) { requestBuilder.setFollowRedirects(followRedirects); return this; } public Builder setMaximumConnectionsTotal(int defaultMaxTotalConnections) { configBuilder.setMaximumConnectionsTotal(defaultMaxTotalConnections); return this; } public Builder setMaximumConnectionsPerHost(int defaultMaxConnectionPerHost) { configBuilder.setMaximumConnectionsPerHost(defaultMaxConnectionPerHost); return this; } public Builder setConnectionTimeoutInMs(int connectionTimeuot) { configBuilder.setConnectionTimeoutInMs(connectionTimeuot); return this; } public Builder setIdleConnectionInPoolTimeoutInMs(int defaultIdleConnectionInPoolTimeoutInMs) { configBuilder.setIdleConnectionInPoolTimeoutInMs(defaultIdleConnectionInPoolTimeoutInMs); return this; } public Builder setRequestTimeoutInMs(int defaultRequestTimeoutInMs) { configBuilder.setRequestTimeoutInMs(defaultRequestTimeoutInMs); return this; } public Builder setMaximumNumberOfRedirects(int maxDefaultRedirects) { configBuilder.setMaximumNumberOfRedirects(maxDefaultRedirects); return this; } public Builder setCompressionEnabled(boolean compressionEnabled) { configBuilder.setCompressionEnabled(compressionEnabled); return this; } public Builder setUserAgent(String userAgent) { configBuilder.setUserAgent(userAgent); return this; } public Builder setAllowPoolingConnection(boolean allowPoolingConnection) { configBuilder.setAllowPoolingConnection(allowPoolingConnection); return this; } public Builder setScheduledExecutorService(ScheduledExecutorService reaper) { configBuilder.setScheduledExecutorService(reaper); return this; } public Builder setExecutorService(ExecutorService applicationThreadPool) { configBuilder.setExecutorService(applicationThreadPool); return this; } public Builder setSSLEngineFactory(SSLEngineFactory sslEngineFactory) { configBuilder.setSSLEngineFactory(sslEngineFactory); return this; } public Builder setSSLContext(final SSLContext sslContext) { configBuilder.setSSLContext(sslContext); return this; } public Builder setRequestCompressionLevel(int requestCompressionLevel) { configBuilder.setRequestCompressionLevel(requestCompressionLevel); return this; } public Builder setRealmDomain(String domain) { realm().setDomain(domain); return this; } public Builder setRealmPrincipal(String principal) { realm().setPrincipal(principal); return this; } public Builder setRealmPassword(String password) { realm().setPassword(password); return this; } public Builder setRealmScheme(Realm.AuthScheme scheme) { realm().setScheme(scheme); return this; } public Builder setRealmName(String realmName) { realm().setRealmName(realmName); return this; } public Builder setRealmUsePreemptiveAuth(boolean usePreemptiveAuth) { realm().setUsePreemptiveAuth(usePreemptiveAuth); return this; } public Builder setRealmEnconding(String enc) { realm().setEnconding(enc); return this; } public Builder setProxyProtocol(ProxyServer.Protocol protocol) { this.proxyProtocol = protocol; return this; } public Builder setProxyHost(String host) { this.proxyHost = host; return this; } public Builder setProxyPrincipal(String principal) { this.proxyPrincipal = principal; return this; } public Builder setProxyPassword(String password) { this.proxyPassword = password; return this; } public Builder setProxyPort(int port) { this.proxyPort = port; return this; } public Builder setDefaultThrowableHandler(ThrowableHandler throwableHandler) { this.defaultThrowableHandler = throwableHandler; return this; } /** * This setting controls whether an error document should be written via * the {@link BodyConsumer} after an error status code was received (e.g. * 404). Default is {@link ErrorDocumentBehaviour#WRITE}. */ public Builder setErrorDocumentBehaviour(ErrorDocumentBehaviour behaviour) { this.errorDocumentBehaviour = behaviour; return this; } /** * Enable resumable downloads for the SimpleAHC. Resuming downloads will only work for GET requests * with an instance of {@link ResumableBodyConsumer}. */ public Builder setResumableDownload(boolean enableResumableDownload) { this.enableResumableDownload = enableResumableDownload; return this; } private Realm.RealmBuilder realm() { if (realmBuilder == null) { realmBuilder = new Realm.RealmBuilder(); } return realmBuilder; } /** * Set the listener to notify about connection progress. */ public Builder setListener(SimpleAHCTransferListener listener) { this.listener = listener; return this; } /** * Set the number of time a request will be retried when an {@link java.io.IOException} occurs because of a Network exception. * * @param maxRequestRetry the number of time a request will be retried * @return this */ public Builder setMaxRequestRetry(int maxRequestRetry) { configBuilder.setMaxRequestRetry(maxRequestRetry); return this; } public Builder setProviderClass(String providerClass) { this.providerClass = providerClass; return this; } public SimpleAsyncHttpClient build() { if (realmBuilder != null) { configBuilder.setRealm(realmBuilder.build()); } if (proxyHost != null) { configBuilder.setProxyServer(new ProxyServer(proxyProtocol, proxyHost, proxyPort, proxyPrincipal, proxyPassword)); } configBuilder.addIOExceptionFilter(new ResumableIOExceptionFilter()); SimpleAsyncHttpClient sc = new SimpleAsyncHttpClient(configBuilder.build(), requestBuilder, defaultThrowableHandler, errorDocumentBehaviour, enableResumableDownload, ahc, listener, providerClass); return sc; } } private final static class ResumableBodyConsumerAsyncHandler extends ResumableAsyncHandler implements ProgressAsyncHandler { private final ProgressAsyncHandler delegate; public ResumableBodyConsumerAsyncHandler(long byteTransferred, ProgressAsyncHandler delegate) { super(byteTransferred, delegate); this.delegate = delegate; } public AsyncHandler.STATE onHeaderWriteCompleted() { return delegate.onHeaderWriteCompleted(); } public AsyncHandler.STATE onContentWriteCompleted() { return delegate.onContentWriteCompleted(); } public AsyncHandler.STATE onContentWriteProgress(long amount, long current, long total) { return delegate.onContentWriteProgress(amount, current, total); } } private final static class BodyConsumerAsyncHandler extends AsyncCompletionHandlerBase { private final BodyConsumer bodyConsumer; private final ThrowableHandler exceptionHandler; private final ErrorDocumentBehaviour errorDocumentBehaviour; private final String url; private final SimpleAHCTransferListener listener; private boolean accumulateBody = false; private boolean omitBody = false; private int amount = 0; private long total = -1; public BodyConsumerAsyncHandler(BodyConsumer bodyConsumer, ThrowableHandler exceptionHandler, ErrorDocumentBehaviour errorDocumentBehaviour, String url, SimpleAHCTransferListener listener) { this.bodyConsumer = bodyConsumer; this.exceptionHandler = exceptionHandler; this.errorDocumentBehaviour = errorDocumentBehaviour; this.url = url; this.listener = listener; } @Override public void onThrowable(Throwable t) { try { if (exceptionHandler != null) { exceptionHandler.onThrowable(t); } else { super.onThrowable(t); } } finally { closeConsumer(); } } /** * {@inheritDoc} */ public STATE onBodyPartReceived(final HttpResponseBodyPart content) throws Exception { fireReceived(content); if (omitBody) { return STATE.CONTINUE; } if (!accumulateBody && bodyConsumer != null) { bodyConsumer.consume(content.getBodyByteBuffer()); } else { return super.onBodyPartReceived(content); } return STATE.CONTINUE; } /** * {@inheritDoc} */ @Override public Response onCompleted(Response response) throws Exception { fireCompleted(response); closeConsumer(); return super.onCompleted(response); } private void closeConsumer() { try { if (bodyConsumer != null) { bodyConsumer.close(); } } catch (IOException ex) { logger.warn("Unable to close a BodyConsumer {}", bodyConsumer); } } @Override public STATE onStatusReceived(HttpResponseStatus status) throws Exception { fireStatus(status); if (isErrorStatus(status)) { switch (errorDocumentBehaviour) { case ACCUMULATE: accumulateBody = true; break; case OMIT: omitBody = true; break; default: break; } } return super.onStatusReceived(status); } private boolean isErrorStatus(HttpResponseStatus status) { return status.getStatusCode() >= 400; } @Override public STATE onHeadersReceived(HttpResponseHeaders headers) throws Exception { calculateTotal(headers); fireHeaders(headers); return super.onHeadersReceived(headers); } private void calculateTotal(HttpResponseHeaders headers) { String length = headers.getHeaders().getFirstValue("Content-Length"); try { total = Integer.valueOf(length); } catch (Exception e) { total = -1; } } @Override public STATE onContentWriteProgress(long amount, long current, long total) { fireSent(url, amount, current, total); return super.onContentWriteProgress(amount, current, total); } private void fireStatus(HttpResponseStatus status) { if (listener != null) { listener.onStatus(url, status.getStatusCode(), status.getStatusText()); } } private void fireReceived(HttpResponseBodyPart content) { int remaining = content.getBodyByteBuffer().remaining(); amount += remaining; if (listener != null) { listener.onBytesReceived(url, amount, remaining, total); } } private void fireHeaders(HttpResponseHeaders headers) { if (listener != null) { listener.onHeaders(url, new HeaderMap(headers.getHeaders())); } } private void fireSent(String url, long amount, long current, long total) { if (listener != null) { listener.onBytesSent(url, amount, current, total); } } private void fireCompleted(Response response) { if (listener != null) { listener.onCompleted(url, response.getStatusCode(), response.getStatusText()); } } } }