package uk.ac.ox.zoo.seeg.abraid.mp.common.web;
import org.apache.http.*;
import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthState;
import org.apache.http.auth.Credentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.protocol.HttpContext;
import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import java.io.File;
import java.io.IOException;
/**
* Acts as a web service client.
*
* Copyright (c) 2014 University of Oxford
*/
public class WebServiceClient {
private static final String GET_WEB_SERVICE_MESSAGE = "Making GET request to web service URL \"%s\"";
private static final String POST_WEB_SERVICE_MESSAGE = "Making POST request to web service URL \"%s\" (%s %s)";
private static final String PUT_WEB_SERVICE_MESSAGE = "Making PUT request to web service URL \"%s\" (%s %s)";
private static final String CALLED_WEB_SERVICE_MESSAGE = "Call to web service URL \"%s\" took %d ms";
private static final String STATUS_UNSUCCESSFUL_MESSAGE =
"Web service returned status code %d (\"%s\"). Web service URL: \"%s\"";
private static final String INVALID_URL_MESSAGE =
"Error when accessing web service with URL \"%s\": Invalid URL - %s";
private static final String GENERAL_ERROR_MESSAGE =
"Error when accessing web service with URL \"%s\": %s";
private static final Logger LOGGER = Logger.getLogger(WebServiceClient.class);
private CloseableHttpClient httpClient;
private BasicResponseHandler responseHandler;
public WebServiceClient(int connectTimeoutMilliseconds, int readTimeoutMilliseconds) {
this.httpClient = createHttpClient(connectTimeoutMilliseconds, readTimeoutMilliseconds);
this.responseHandler = new BasicResponseHandler();
}
private CloseableHttpClient createHttpClient(int connectTimeoutMilliseconds, int readTimeoutMilliseconds) {
RequestConfig.Builder requestBuilder = RequestConfig.custom();
requestBuilder = requestBuilder.setConnectTimeout(connectTimeoutMilliseconds);
requestBuilder = requestBuilder.setSocketTimeout(readTimeoutMilliseconds);
RequestConfig requestConfig = requestBuilder.build();
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
clientBuilder = clientBuilder.setDefaultRequestConfig(requestConfig);
clientBuilder = clientBuilder.disableAutomaticRetries();
clientBuilder = clientBuilder.addInterceptorFirst(new PreemptiveAuthInterceptor());
clientBuilder = clientBuilder.setRedirectStrategy(new LaxRedirectStrategy());
return clientBuilder.build();
}
/**
* Calls a web service by making a GET request.
* @param url The web service URL to call.
* @return The web service response as a string.
* @throws WebServiceClientException If a response could not be obtained from the web service for whatever reason,
* or if a response status code other than "successful" is returned.
*/
public String makeGetRequest(String url) throws WebServiceClientException {
LOGGER.debug(String.format(GET_WEB_SERVICE_MESSAGE, url));
return request(createRequest(url, HttpMethod.GET).build());
}
/**
* Calls a web service by making a POST request.
* @param url The web service URL to call.
* @param body A string in JSON format that will be the body of the POST request.
* @return The web service response as a string.
* @throws WebServiceClientException If a response could not be obtained from the web service for whatever reason,
* or if a response status code other than "successful" is returned.
*/
public String makePostRequestWithJSON(String url, final String body) throws WebServiceClientException {
if (body == null) {
throw new IllegalArgumentException("POST body must be non-null");
}
LOGGER.debug(String.format(POST_WEB_SERVICE_MESSAGE, url, body.length(), "characters"));
return request(createRequest(url, HttpMethod.POST, body, ContentType.APPLICATION_JSON).build());
}
/**
* Calls a web service by making a POST request.
* @param url The web service URL to call.
* @param body The body as an array of bytes.
* @return The web service response as a string.
* @throws WebServiceClientException If a response could not be obtained from the web service for whatever reason,
* or if a response status code other than "successful" is returned.
*/
public String makePostRequestWithBinary(String url, final File body) throws WebServiceClientException {
if (body == null || body.length() == 0) {
throw new IllegalArgumentException("POST body must be non-null");
}
LOGGER.debug(String.format(POST_WEB_SERVICE_MESSAGE, url, body.length(), "bytes"));
RequestBuilder request = createRequest(url, HttpMethod.POST, body);
HttpUriRequest build = request.build();
return request(build);
}
/**
* Calls a web service by making a PUT request.
* @param url The web service URL to call.
* @param body The body of the request.
* @return The web service response as a string.
* @throws WebServiceClientException If a response could not be obtained from the web service for whatever reason,
* or if a response status code other than "successful" (or anything else in the 200 family) is returned.
*/
public String makePutRequest(String url, final String body) throws WebServiceClientException {
if (body == null) {
throw new IllegalArgumentException("PUT body must be non-null");
}
LOGGER.debug(String.format(PUT_WEB_SERVICE_MESSAGE, url, body.length(), "characters"));
return request(createRequest(url, HttpMethod.PUT, body, ContentType.TEXT_PLAIN).build());
}
/**
* Calls a web service by making a POST request with a "text/xml" "Content-type" header.
* @param url The web service URL to call.
* @param body The body of the request (should be xml).
* @return The web service response as a string.
* @throws WebServiceClientException If a response could not be obtained from the web service for whatever reason,
* or if a response status code other than "successful" (or anything else in the 200 family) is returned.
*/
public String makePostRequestWithXML(String url, final String body) throws WebServiceClientException {
if (body == null) {
throw new IllegalArgumentException("POST body must be non-null");
}
LOGGER.debug(String.format(POST_WEB_SERVICE_MESSAGE, url, body.length(), "characters"));
return request(createRequest(url, HttpMethod.POST, body, ContentType.APPLICATION_XML.withCharset("")).build());
}
/**
* Calls a web service by making a PUT request with a "text/xml" "Content-type" header.
* @param url The web service URL to call.
* @param body The body of the request (should be xml).
* @return The web service response as a string.
* @throws WebServiceClientException If a response could not be obtained from the web service for whatever reason,
* or if a response status code other than "successful" (or anything else in the 200 family) is returned.
*/
public String makePutRequestWithXML(String url, final String body) throws WebServiceClientException {
if (body == null) {
throw new IllegalArgumentException("PUT body must be non-null");
}
LOGGER.debug(String.format(PUT_WEB_SERVICE_MESSAGE, url, body.length(), "characters"));
return request(createRequest(url, HttpMethod.PUT, body, ContentType.APPLICATION_XML.withCharset("")).build());
}
private RequestBuilder createRequest(String url, HttpMethod method) {
try {
return RequestBuilder.create(method.name()).setUri(url);
} catch (IllegalArgumentException e) {
String message = String.format(INVALID_URL_MESSAGE, url, getInnermostExceptionMessage(e));
throw new WebServiceClientException(message, e);
}
}
private RequestBuilder createRequest(String url, HttpMethod method, HttpEntity body) {
return createRequest(url, method).setEntity(body);
}
private RequestBuilder createRequest(String url, HttpMethod method, String body, ContentType contentType) {
return createRequest(url, method, new StringEntity(body, contentType));
}
private RequestBuilder createRequest(String url, HttpMethod method, File body) {
return createRequest(url, method, MultipartEntityBuilder.create().addPart("file", new FileBody(body)).build());
}
private String request(HttpUriRequest request) {
try {
DateTime startDate = DateTime.now();
String response = httpClient.execute(request, responseHandler);
DateTime endDate = DateTime.now();
long callDuration = new Duration(startDate, endDate).getMillis();
LOGGER.debug(String.format(CALLED_WEB_SERVICE_MESSAGE, request.getURI(), callDuration));
return response;
} catch (HttpResponseException e) {
String status = HttpStatus.valueOf(e.getStatusCode()).getReasonPhrase();
String message = String.format(STATUS_UNSUCCESSFUL_MESSAGE, e.getStatusCode(), status, request.getURI());
throw new WebServiceClientException(message);
} catch (IOException e) {
// We convert this to our WebServiceClientException; as well as being consistent and friendly,
// it hides callers from the http client implementation library.
String message = String.format(GENERAL_ERROR_MESSAGE, request.getURI(), getInnermostExceptionMessage(e));
throw new WebServiceClientException(message, e);
}
}
private String getInnermostExceptionMessage(Throwable t) {
if (t.getCause() == null) {
return t.getMessage();
} else {
return getInnermostExceptionMessage(t.getCause());
}
}
/**
* A HttpRequestInterceptor to enable preemptive basic auth (ie 1 req, not 2) if credential specified in the url.
*/
private static class PreemptiveAuthInterceptor implements HttpRequestInterceptor {
@Override
public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
AuthState authState = (AuthState) context.getAttribute(HttpClientContext.TARGET_AUTH_STATE);
Credentials creds = getCredentials(context);
AuthScheme authScheme = getAuthScheme(authState);
if (creds != null && authScheme == null) {
// If credentials have been provided (i.e. basic auth in the URI), but there isn't an auth scheme setup
// then preemptively set up basic auth.
authState.update(new BasicScheme(), creds);
}
}
private AuthScheme getAuthScheme(AuthState authState) {
return authState == null ? null : authState.getAuthScheme();
}
private Credentials getCredentials(HttpContext context) {
CredentialsProvider credsProvider =
(CredentialsProvider) context.getAttribute(HttpClientContext.CREDS_PROVIDER);
HttpHost targetHost = (HttpHost) context.getAttribute(HttpClientContext.HTTP_TARGET_HOST);
return credsProvider.getCredentials(new AuthScope(targetHost.getHostName(), targetHost.getPort()));
}
}
}