dev.fitko.fitconnect.core.http.DefaultHttpClient Maven / Gradle / Ivy
Show all versions of client Show documentation
package dev.fitko.fitconnect.core.http;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.fitko.fitconnect.api.config.ZBPCertConfig;
import dev.fitko.fitconnect.api.config.http.HttpConfig;
import dev.fitko.fitconnect.api.config.http.ProxyAuth;
import dev.fitko.fitconnect.api.config.http.ProxyConfig;
import dev.fitko.fitconnect.api.config.http.Timeouts;
import dev.fitko.fitconnect.api.exceptions.internal.RestApiException;
import dev.fitko.fitconnect.api.services.http.HttpClient;
import dev.fitko.fitconnect.api.services.http.HttpResponse;
import dev.fitko.fitconnect.core.http.ssl.SSLContextBuilder;
import dev.fitko.fitconnect.core.utils.Preconditions;
import okhttp3.Authenticator;
import okhttp3.Credentials;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.BufferedSink;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.Proxy;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class DefaultHttpClient implements HttpClient {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHttpClient.class);
private static final ObjectMapper MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private final OkHttpClient httpClient;
public DefaultHttpClient() {
this(new HttpConfig(), Collections.emptyList());
}
public DefaultHttpClient(HttpConfig httpConfig, List interceptors) {
this(httpConfig, interceptors, null);
}
public DefaultHttpClient(HttpConfig httpConfig, List interceptors, ZBPCertConfig zbpCertConfig) {
var builder = new OkHttpClient.Builder();
setInterceptors(interceptors, builder);
setTimeouts(httpConfig.getTimeouts(), builder);
setProxy(httpConfig.getProxyConfig(), builder);
if (zbpCertConfig != null) {
addSSLContext(zbpCertConfig, builder);
}
httpClient = builder.build();
}
private static void setProxy(final ProxyConfig proxyConfig, final OkHttpClient.Builder builder) {
if (!proxyConfig.isProxySet()) {
LOGGER.info("Creating HttpClient without proxy configuration.");
return;
}
final Proxy proxy = proxyConfig.getHttpProxy();
LOGGER.info("Creating HttpClient with proxy configuration: {}", proxy);
builder.proxy(proxy);
if (proxyConfig.hasBasicAuthentication()) {
LOGGER.info("Creating proxy with basic authentication");
final Authenticator proxyAuthenticator = getProxyAuthenticator(proxyConfig.getBasicAuth());
builder.proxyAuthenticator(proxyAuthenticator);
}
}
private static Authenticator getProxyAuthenticator(ProxyAuth auth) {
return (route, response) -> {
String credential = Credentials.basic(auth.getUsername(), auth.getPassword());
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
};
}
private static void setInterceptors(final List interceptors, final OkHttpClient.Builder builder) {
interceptors.forEach(builder::addInterceptor);
}
private static void setTimeouts(final Timeouts timeouts, final OkHttpClient.Builder builder) {
builder.readTimeout(timeouts.getReadTimeoutInSeconds());
builder.writeTimeout(timeouts.getWriteTimeoutInSeconds());
builder.connectTimeout(timeouts.getConnectionTimeoutInSeconds());
}
@Override
public HttpResponse get(final String url, final Map headers, final Class responseType) {
final Request request = new Request.Builder()
.url(url)
.get()
.headers(Headers.of(headers))
.build();
try (final Response response = httpClient.newCall(request).execute()) {
return evaluateStatusAndRespond(response, responseType);
} catch (final RestApiException restApiException) {
// rethrow to keep status code
throw restApiException;
} catch (Exception exception) {
throw new RestApiException("HTTP GET call to '" + url + "' failed.", exception);
}
}
@Override
public HttpResponse post(final String url, final Map headers, final P httpPayload, final Class responseType) {
try {
final RequestBody requestBody = createRequestBody(headers, httpPayload);
final Request request = new Request.Builder()
.url(url)
.post(requestBody)
.headers(Headers.of(headers))
.build();
try (final Response response = httpClient.newCall(request).execute()) {
return evaluateStatusAndRespond(response, responseType);
}
} catch (final RestApiException restApiException) {
// rethrow to keep status code
throw restApiException;
} catch (Exception exception) {
throw new RestApiException("HTTP POST call to '" + url + "' failed.", exception);
}
}
@Override
public HttpResponse put(final String url, final Map headers, final P httpPayload, final Class responseType) {
try {
final RequestBody requestBody = createRequestBody(headers, httpPayload);
final Request request = new Request.Builder()
.url(url)
.put(requestBody)
.headers(Headers.of(headers))
.build();
try (final Response response = httpClient.newCall(request).execute()) {
return evaluateStatusAndRespond(response, responseType);
}
} catch (final RestApiException restApiException) {
// rethrow to keep status code
throw restApiException;
} catch (Exception exception) {
throw new RestApiException("HTTP PUT call to '" + url + "' failed.", exception);
}
}
/**
* The methods {@link ObjectMapper#writeValueAsBytes(Object)} and
* {@link ObjectMapper#writeValueAsString(Object)} seem to have issues with writing
* a raw string into a new string, resulting in obsolete quotation marks at begin and end of the actual string.
* Therefore, this method contains a dedicated logic part for dealing with raw strings.
*
* Also, we have to do a little workaround with {@link String#getBytes(Charset)} due to OkHttp enforcing a standard
* value for the header "Content-Type" in case of raw strings. See
* https://github.com/square/okhttp/issues/2099
* for more information on that issue.
*/
RequestBody createRequestBody(final Map headers, final T httpPayload) throws JsonProcessingException {
if (httpPayload instanceof MultipartBody) {
return (MultipartBody) httpPayload;
}
if (httpPayload instanceof InputStream) {
return getRequestBodyWithStreamPayload(headers, (InputStream) httpPayload);
}
return RequestBody.create(buildPayloadBody(httpPayload), getContentType(headers));
}
HttpResponse evaluateStatusAndRespond(final Response response, final Class responseType) throws IOException {
if (!response.isSuccessful()) {
final String message = response.body() != null ? response.body().string() : "HTTP call failed.";
throw new RestApiException(message, response.code());
}
final ResponseBody responseBody = response.body();
if (responseBody == null) {
throw new RestApiException("Response body is null.", response.code());
}
if (responseType.equals(Void.class)) {
return new HttpResponse<>(response.code(), null);
}
if (responseType.equals(String.class)) {
return new HttpResponse<>(response.code(), (R) responseBody.string());
}
return new HttpResponse<>(response.code(), MAPPER.readValue(responseBody.string(), responseType));
}
private void addSSLContext(ZBPCertConfig config, OkHttpClient.Builder clientBuilder) {
Preconditions.checkArgumentAndThrow(!config.hasValidConfiguration(), "Required ZBP certificates are not set");
var sslContext = SSLContextBuilder.build(config);
clientBuilder.sslSocketFactory(sslContext.getSslSocketFactory(), sslContext.getTrustManager());
}
private static RequestBody getRequestBodyWithStreamPayload(final Map headers, final InputStream httpPayload) {
return new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse(headers.get(HttpHeaders.CONTENT_TYPE));
}
@Override
public void writeTo(final BufferedSink sink) throws IOException {
httpPayload.transferTo(sink.outputStream());
}
};
}
private static MediaType getContentType(final Map headers) {
if (headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
return MediaType.parse(headers.get(HttpHeaders.CONTENT_TYPE));
}
return MediaType.get(MimeTypes.APPLICATION_JSON);
}
private static byte[] buildPayloadBody(final T httpPayload) throws JsonProcessingException {
if (httpPayload instanceof String) {
return ((String) httpPayload).getBytes(StandardCharsets.UTF_8);
}
return MAPPER.writeValueAsBytes(httpPayload);
}
}