All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fi.evolver.basics.spring.http.HttpCommunicator Maven / Gradle / Ivy

There is a newer version: 6.5.1
Show newest version
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