package io.dropwizard.client;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.MoreExecutors;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.VersionInfo;
import org.glassfish.jersey.apache.connector.LocalizationMessages;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.ClientRequest;
import org.glassfish.jersey.client.ClientResponse;
import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
import org.glassfish.jersey.client.spi.Connector;
import org.glassfish.jersey.message.internal.Statuses;
import javax.annotation.Nullable;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.core.Response;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Future;
import static com.google.common.base.MoreObjects.firstNonNull;
Dropwizard Apache Connector.
It's a custom version of Jersey's Connector
that uses Apache's HttpClient
as an HTTP transport implementation.
It uses a pre-configured HTTP client by HttpClientBuilder
rather then creates a client from the Jersey configuration.
This approach affords to use the extended configuration of
the Apache HttpClient in Dropwizard with a fluent interface
of JerseyClient.
/**
* Dropwizard Apache Connector.
* <p>
* It's a custom version of Jersey's {@link org.glassfish.jersey.client.spi.Connector}
* that uses Apache's {@link org.apache.http.client.HttpClient}
* as an HTTP transport implementation.
* </p>
* <p>
* It uses a pre-configured HTTP client by {@link io.dropwizard.client.HttpClientBuilder}
* rather then creates a client from the Jersey configuration.
* </p>
* <p>
* This approach affords to use the extended configuration of
* the Apache HttpClient in Dropwizard with a fluent interface
* of JerseyClient.
* </p>
*/
public class DropwizardApacheConnector implements Connector {
private static final String APACHE_HTTP_CLIENT_VERSION = VersionInfo
.loadVersionInfo("org.apache.http.client", DropwizardApacheConnector.class.getClassLoader())
.getRelease();
Actual HTTP client
/**
* Actual HTTP client
*/
private final CloseableHttpClient client;
Default HttpUriRequestConfig
/**
* Default HttpUriRequestConfig
*/
@Nullable
private final RequestConfig defaultRequestConfig;
Should a chunked encoding be used in POST requests
/**
* Should a chunked encoding be used in POST requests
*/
private final boolean chunkedEncodingEnabled;
public DropwizardApacheConnector(CloseableHttpClient client, @Nullable RequestConfig defaultRequestConfig,
boolean chunkedEncodingEnabled) {
this.client = client;
this.defaultRequestConfig = defaultRequestConfig;
this.chunkedEncodingEnabled = chunkedEncodingEnabled;
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public ClientResponse apply(ClientRequest jerseyRequest) {
try {
final HttpUriRequest apacheRequest = buildApacheRequest(jerseyRequest);
final CloseableHttpResponse apacheResponse = client.execute(apacheRequest);
final StatusLine statusLine = apacheResponse.getStatusLine();
final Response.StatusType status = Statuses.from(statusLine.getStatusCode(),
firstNonNull(statusLine.getReasonPhrase(), ""));
final ClientResponse jerseyResponse = new ClientResponse(status, jerseyRequest);
for (Header header : apacheResponse.getAllHeaders()) {
final List<String> headerValues = jerseyResponse.getHeaders().get(header.getName());
if (headerValues == null) {
jerseyResponse.getHeaders().put(header.getName(), Lists.newArrayList(header.getValue()));
} else {
headerValues.add(header.getValue());
}
}
final HttpEntity httpEntity = apacheResponse.getEntity();
jerseyResponse.setEntityStream(httpEntity != null ? httpEntity.getContent() :
new ByteArrayInputStream(new byte[0]));
return jerseyResponse;
} catch (Exception e) {
throw new ProcessingException(e);
}
}
Build a new Apache's HttpUriRequest
from Jersey's ClientRequest
Convert a method, URI, body, headers and override a user-agent if necessary
Params: - jerseyRequest – representation of an HTTP request in Jersey
Returns: a new HttpUriRequest
/**
* Build a new Apache's {@link org.apache.http.client.methods.HttpUriRequest}
* from Jersey's {@link org.glassfish.jersey.client.ClientRequest}
* <p>
* Convert a method, URI, body, headers and override a user-agent if necessary
* </p>
*
* @param jerseyRequest representation of an HTTP request in Jersey
* @return a new {@link org.apache.http.client.methods.HttpUriRequest}
*/
private HttpUriRequest buildApacheRequest(ClientRequest jerseyRequest) {
final RequestBuilder builder = RequestBuilder
.create(jerseyRequest.getMethod())
.setUri(jerseyRequest.getUri())
.setEntity(getHttpEntity(jerseyRequest));
for (String headerName : jerseyRequest.getHeaders().keySet()) {
builder.addHeader(headerName, jerseyRequest.getHeaderString(headerName));
}
final Optional<RequestConfig> requestConfig = addJerseyRequestConfig(jerseyRequest);
requestConfig.ifPresent(builder::setConfig);
return builder.build();
}
private Optional<RequestConfig> addJerseyRequestConfig(ClientRequest clientRequest) {
final Integer timeout = clientRequest.resolveProperty(ClientProperties.READ_TIMEOUT, Integer.class);
final Integer connectTimeout = clientRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, Integer.class);
final Boolean followRedirects = clientRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, Boolean.class);
if (timeout != null || connectTimeout != null || followRedirects != null) {
final RequestConfig.Builder requestConfig = RequestConfig.copy(defaultRequestConfig);
if (timeout != null) {
requestConfig.setSocketTimeout(timeout);
}
if (connectTimeout != null) {
requestConfig.setConnectTimeout(connectTimeout);
}
if (followRedirects != null) {
requestConfig.setRedirectsEnabled(followRedirects);
}
return Optional.of(requestConfig.build());
}
return Optional.empty();
}
Get an Apache's HttpEntity
from Jersey's ClientRequest
Create a custom HTTP entity, because Jersey doesn't provide
a request stream or a byte buffer.
Params: - jerseyRequest – representation of an HTTP request in Jersey
Returns: a correct HttpEntity
implementation
/**
* Get an Apache's {@link org.apache.http.HttpEntity}
* from Jersey's {@link org.glassfish.jersey.client.ClientRequest}
* <p>
* Create a custom HTTP entity, because Jersey doesn't provide
* a request stream or a byte buffer.
* </p>
*
* @param jerseyRequest representation of an HTTP request in Jersey
* @return a correct {@link org.apache.http.HttpEntity} implementation
*/
@Nullable
protected HttpEntity getHttpEntity(ClientRequest jerseyRequest) {
if (jerseyRequest.getEntity() == null) {
return null;
}
return chunkedEncodingEnabled ? new JerseyRequestHttpEntity(jerseyRequest) :
new BufferedJerseyRequestHttpEntity(jerseyRequest);
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public Future<?> apply(final ClientRequest request, final AsyncConnectorCallback callback) {
// Simulate an asynchronous execution
return MoreExecutors.newDirectExecutorService().submit(() -> {
try {
callback.response(apply(request));
} catch (Exception e) {
callback.failure(e);
}
});
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public String getName() {
return "Apache-HttpClient/" + APACHE_HTTP_CLIENT_VERSION;
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public void close() {
// Should not close the client here, because it's managed by the Dropwizard environment
}
A custom AbstractHttpEntity
that uses a Jersey request as a content source. It's chunked because we don't know the content length beforehand. /**
* A custom {@link org.apache.http.entity.AbstractHttpEntity} that uses
* a Jersey request as a content source. It's chunked because we don't
* know the content length beforehand.
*/
private static class JerseyRequestHttpEntity extends AbstractHttpEntity {
private ClientRequest clientRequest;
private JerseyRequestHttpEntity(ClientRequest clientRequest) {
this.clientRequest = clientRequest;
setChunked(true);
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public boolean isRepeatable() {
return false;
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public long getContentLength() {
return -1;
}
{@inheritDoc}
This method isn't supported at will throw an UnsupportedOperationException
if invoked.
/**
* {@inheritDoc}
* <p>
* This method isn't supported at will throw an {@link java.lang.UnsupportedOperationException}
* if invoked.
* </p>
*/
@Override
public InputStream getContent() throws IOException {
// Shouldn't be called
throw new UnsupportedOperationException("Reading from the entity is not supported");
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public void writeTo(final OutputStream outputStream) throws IOException {
clientRequest.setStreamProvider(contentLength -> outputStream);
clientRequest.writeEntity();
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public boolean isStreaming() {
return false;
}
}
A custom AbstractHttpEntity
that uses a Jersey request as a content source. In contrast to JerseyRequestHttpEntity
its contents are buffered on initialization.
/**
* A custom {@link org.apache.http.entity.AbstractHttpEntity} that uses
* a Jersey request as a content source.
* <p>
* In contrast to {@link io.dropwizard.client.DropwizardApacheConnector.JerseyRequestHttpEntity}
* its contents are buffered on initialization.
* </p>
*/
private static class BufferedJerseyRequestHttpEntity extends AbstractHttpEntity {
private static final int BUFFER_INITIAL_SIZE = 512;
private byte[] buffer;
private BufferedJerseyRequestHttpEntity(ClientRequest clientRequest) {
final ByteArrayOutputStream stream = new ByteArrayOutputStream(BUFFER_INITIAL_SIZE);
clientRequest.setStreamProvider(contentLength -> stream);
try {
clientRequest.writeEntity();
} catch (IOException e) {
throw new ProcessingException(LocalizationMessages.ERROR_BUFFERING_ENTITY(), e);
}
buffer = stream.toByteArray();
setChunked(false);
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public boolean isRepeatable() {
return true;
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public long getContentLength() {
return buffer.length;
}
{@inheritDoc}
This method isn't supported at will throw an UnsupportedOperationException
if invoked.
/**
* {@inheritDoc}
* <p>
* This method isn't supported at will throw an {@link java.lang.UnsupportedOperationException}
* if invoked.
* </p>
*/
@Override
public InputStream getContent() throws IOException {
// Shouldn't be called
throw new UnsupportedOperationException("Reading from the entity is not supported");
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public void writeTo(OutputStream outstream) throws IOException {
outstream.write(buffer);
outstream.flush();
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public boolean isStreaming() {
return false;
}
}
}