fi.evolver.basics.spring.http.HttpCommunicator Maven / Gradle / Ivy
package fi.evolver.basics.spring.http;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.http.HttpClient;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import fi.evolver.basics.spring.http.LoggingHttpClient.LogMetadataCallback;
import fi.evolver.basics.spring.http.LoggingHttpClient.LogParameters;
import fi.evolver.basics.spring.log.MessageLogService;
import fi.evolver.basics.spring.log.entity.MessageLog.Direction;
@Component
public class HttpCommunicator {
private static final Logger LOG = LoggerFactory.getLogger(HttpCommunicator.class);
private static final Pattern REGEX_HTTP_OK = Pattern.compile("2\\d\\d");
private final LoggingHttpClient httpClient;
@Autowired
public HttpCommunicator(
MessageLogService messageLogService,
@Value("${evolver.http-communicator.connection.timeout.seconds:30}") int connectionTimeoutSeconds
) {
this.httpClient = new LoggingHttpClient(
messageLogService,
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(connectionTimeoutSeconds))
.build());
}
public HttpRequest createRequest(String messageType, String otherSystem, URL url, Direction dataFlowDirection) {
return new HttpRequest(messageType, otherSystem, url, dataFlowDirection);
}
public HttpRequest createGetRequest(String messageType, String otherSystem, URL url) {
return createRequest(messageType, otherSystem, url, Direction.INBOUND).setNoPayload().setMethod(HttpMethod.GET);
}
public HttpRequest createPostRequest(String messageType, String otherSystem, URL url, String payload, Charset charset) {
return createRequest(messageType, otherSystem, url, Direction.OUTBOUND).setPayload(payload, charset).setMethod(HttpMethod.POST);
}
public HttpRequest createPostRequest(String messageType, String otherSystem, URL url, byte[] payload) {
return createRequest(messageType, otherSystem, url, Direction.OUTBOUND).setPayload(payload).setMethod(HttpMethod.POST);
}
public HttpRequest createPostRequest(String messageType, String otherSystem, URL url, InputStream payload) {
return createRequest(messageType, otherSystem, url, Direction.OUTBOUND).setPayload(payload).setMethod(HttpMethod.POST);
}
public HttpRequest createPostRequest(String messageType, String otherSystem, URL url) {
return createRequest(messageType, otherSystem, url, Direction.OUTBOUND).setEmptyPayload().setMethod(HttpMethod.POST);
}
public class HttpRequest {
private final List metadataCallbacks = new ArrayList<>(1);
private final List metadataCallbacksForStream = new ArrayList<>(1);
private final Map headers = new LinkedHashMap<>();
private final Map metadata = new LinkedHashMap<>();
private final String messageType;
private final URI uri;
private final String otherSystem;
private final Direction direction;
private InputStream data;
private Duration readTimeout = Duration.ofSeconds(30);
private boolean payloadSet = false;
private boolean done = false;
private String method = "GET";
private Pattern okStatusRegex = REGEX_HTTP_OK;
private Optional failStatusRegex = Optional.empty();
private Optional okResponseRegex = Optional.empty();
private Optional failResponseRegex = Optional.empty();
private HttpRequest(String messageType, String otherSystem, URL url, Direction direction) {
this.messageType = messageType;
this.otherSystem = otherSystem;
try {
this.uri = url.toURI();
}
catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid URL", e);
}
this.direction = direction;
}
public HttpRequest addHeaders(Map headers) {
headers.forEach(this::addHeader);
return this;
}
public HttpRequest addHeader(String key, String value) {
if (key != null && value != null)
headers.put(key, value);
return this;
}
public HttpRequest setBasicAuthorization(String username, String password) {
if (username != null && password != null) {
byte[] encodedBytes = Base64.getUrlEncoder().encode(String.format("%s:%s", username, password).getBytes(StandardCharsets.UTF_8));
addHeader("Authorization", "Basic " + new String(encodedBytes));
}
return this;
}
public HttpRequest addMetadatas(Map metadata) {
metadata.forEach(this::addMetadata);
return this;
}
public HttpRequest addMetadata(String key, Object value) {
if (key != null && value != null)
metadata.put(key, value);
return this;
}
public HttpRequest setReadTimeout(Duration readTimeout) {
Objects.requireNonNull(readTimeout);
this.readTimeout = readTimeout;
return this;
}
public HttpRequest setReadTimeout(int readTimeoutMs) {
return setReadTimeout(Duration.ofMillis(readTimeoutMs));
}
public HttpRequest setPayload(InputStream data) {
if (payloadSet)
throw new IllegalStateException("Payload may not be set multiple times");
this.payloadSet = true;
this.data = data;
return this;
}
public HttpRequest setPayload(byte[] data) {
return setPayload(data == null ? null : new ByteArrayInputStream(data));
}
public HttpRequest setPayload(String data, Charset charset) {
return setPayload(data == null ? null : data.getBytes(charset));
}
public HttpRequest setEmptyPayload() {
return setPayload((InputStream)null);
}
public HttpRequest setNoPayload() {
if (payloadSet)
throw new IllegalStateException("Payload may not be set multiple times");
this.payloadSet = true;
this.data = null;
return this;
}
public HttpRequest setOkStatusRegex(Pattern okStatusRegex) {
if (okStatusRegex != null)
this.okStatusRegex = okStatusRegex;
return this;
}
public HttpRequest setOkStatusRegex(String okStatusRegex) {
if (okStatusRegex != null)
setOkStatusRegex(Pattern.compile(okStatusRegex));
return this;
}
public HttpRequest setFailStatusRegex(Pattern failStatusRegex) {
this.failStatusRegex = Optional.ofNullable(failStatusRegex);
return this;
}
public HttpRequest setFailStatusRegex(String failStatusRegex) {
if (failStatusRegex != null)
setFailStatusRegex(Pattern.compile(failStatusRegex));
return this;
}
public HttpRequest setOkResponseRegex(Pattern okResponseRegex) {
this.okResponseRegex = Optional.ofNullable(okResponseRegex);
return this;
}
public HttpRequest setOkResponseRegex(String okResponseRegex) {
if (okResponseRegex != null)
setOkResponseRegex(Pattern.compile("(?s)" + okResponseRegex));
return this;
}
public HttpRequest setFailResponseRegex(Pattern failResponseRegex) {
this.failResponseRegex = Optional.ofNullable(failResponseRegex);
return this;
}
public HttpRequest setFailResponseRegex(String failResponseRegex) {
if (failResponseRegex != null)
setFailResponseRegex(Pattern.compile("(?s)" + failResponseRegex));
return this;
}
/**
* For non-streaming character data
* @param callback
* @return
*/
public HttpRequest addMetadataCallback(MetadataCallback callback) {
metadataCallbacks.add(callback);
return this;
}
/**
* For streaming binary data
* @param callback
* @return
*/
public HttpRequest addMetadataCallbackForStream(MetadataCallbackForStream callback) {
metadataCallbacksForStream.add(callback);
return this;
}
public HttpRequest setMethod(HttpMethod method) {
Objects.requireNonNull(method, "Method may not be null");
this.method = method.name();
return this;
}
public HttpRequest setMethod(String method) {
Objects.requireNonNull(method, "Method may not be null");
this.method = method;
return this;
}
/**
* Send and get a streaming response body.
*
* @return A streaming HTTP response.
*/
public HttpStreamResponse sendForStream() {
if (done)
throw new IllegalStateException("Send may not be called multiple times");
done = true;
LogParameters logParameters = new LogParameters(messageType)
.setTargetSystem(otherSystem)
.setDirection(direction);
metadata.forEach(logParameters::addMetadata);
metadataCallbacksForStream.forEach(c -> logParameters.addMetadataCallback(new StreamingCallback(c, this::isSuccess)));
java.net.http.HttpRequest.Builder requestBuilder = java.net.http.HttpRequest.newBuilder()
.uri(uri)
.timeout(readTimeout);
requestBuilder.method(method, data != null ? BodyPublishers.ofInputStream(() -> data) : BodyPublishers.noBody());
headers.forEach(requestBuilder::header);
try {
java.net.http.HttpResponse httpResponse = httpClient.send(
requestBuilder.build(),
BodyHandlers.ofInputStream(),
logParameters);
return HttpStreamResponse.create(
httpResponse,
isSuccess(httpResponse.statusCode()));
}
catch (IOException | InterruptedException | RuntimeException e) {
LOG.error("HTTP send failed", e);
return HttpStreamResponse.create(e);
}
}
/**
* Send and get a textual response body.
*
* @return HttpResponse A HTTP response with textual output.
*/
public HttpResponse send() {
if (done)
throw new IllegalStateException("Send may not be called multiple times");
done = true;
LogParameters logParameters = new LogParameters(messageType)
.setTargetSystem(otherSystem)
.setDirection(direction);
metadata.forEach(logParameters::addMetadata);
metadataCallbacks.forEach(c -> logParameters.addMetadataCallback(new StringCallback(c, this::isSuccess)));
java.net.http.HttpRequest.Builder requestBuilder = java.net.http.HttpRequest.newBuilder()
.uri(uri)
.timeout(readTimeout);
requestBuilder.method(method, data != null ? BodyPublishers.ofInputStream(() -> data) : BodyPublishers.noBody());
headers.forEach(requestBuilder::header);
try {
java.net.http.HttpResponse httpResponse = httpClient.send(
requestBuilder.build(),
BodyHandlers.ofString(),
logParameters);
return HttpResponse.create(httpResponse, isSuccess(httpResponse.statusCode(), httpResponse.body()));
}
catch (IOException | InterruptedException | RuntimeException e) {
LOG.error("HTTP send failed", e);
return HttpResponse.create(e);
}
}
private boolean isSuccess(int statusCode, String responseMessage) {
return isSuccess(Integer.toString(statusCode), responseMessage);
}
private boolean isSuccess(String statusCode, String responseMessage) {
return isSuccess(statusCode) &&
okResponseRegex.map(r -> r.matcher(responseMessage)).map(Matcher::matches).orElse(true) &&
!failResponseRegex.map(r -> r.matcher(responseMessage)).map(Matcher::matches).orElse(false);
}
private boolean isSuccess(int statusCode) {
return isSuccess(Integer.toString(statusCode));
}
private boolean isSuccess(String statusCode) {
return okStatusRegex.matcher(statusCode).matches() &&
!failStatusRegex.map(r -> r.matcher(statusCode)).map(Matcher::matches).orElse(false);
}
}
public static interface MetadataCallback {
/**
* Only for non-binary character data
* @param response
* @return
*/
public Map call(HttpResponse response);
}
public static interface MetadataCallbackForStream {
/**
* For binary stream data
* @param response
* @return
*/
public Map call(HttpStreamResponse response);
}
private interface StringResponseValidator {
boolean validate(Integer statusCode, String response);
}
private static class StreamingCallback implements LogMetadataCallback {
private final MetadataCallbackForStream callback;
private final Function responseValidator;
public StreamingCallback(MetadataCallbackForStream callback, Function responseValidator) {
this.responseValidator = responseValidator;
this.callback = callback;
}
@Override
public Map onResponse(java.net.http.HttpResponse httpResponse) {
return callback.call(HttpStreamResponse.create(
httpResponse,
responseValidator.apply(httpResponse.statusCode())));
}
@Override
public Map onError(Throwable throwable) {
return callback.call(HttpStreamResponse.create(throwable));
}
}
private static class StringCallback implements LogMetadataCallback {
private final MetadataCallback callback;
private final StringResponseValidator responseValidator;
public StringCallback(MetadataCallback callback, StringResponseValidator responseValidator) {
this.responseValidator = responseValidator;
this.callback = callback;
}
@Override
public Map onResponse(java.net.http.HttpResponse httpResponse) {
return callback.call(HttpResponse.create(
httpResponse,
responseValidator.validate(httpResponse.statusCode(), httpResponse.body())));
}
@Override
public Map onError(Throwable throwable) {
return callback.call(HttpResponse.create(throwable));
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy