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

fi.evolver.basics.spring.messaging.sender.AwsSender Maven / Gradle / Ivy

package fi.evolver.basics.spring.messaging.sender;

import static fi.evolver.basics.spring.messaging.util.SendUtils.PROPERTY_CONNECT_TIMEOUT_MS;
import static fi.evolver.basics.spring.messaging.util.SendUtils.PROPERTY_READ_TIMEOUT_MS;
import static fi.evolver.basics.spring.messaging.util.SendUtils.mapMetadata;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.net.URL;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Date;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.CountingInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.amazonaws.ClientConfiguration;
import com.amazonaws.SdkClientException;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.BasicSessionCredentials;
import com.amazonaws.auth.InstanceProfileCredentialsProvider;
import com.amazonaws.services.kinesis.AmazonKinesis;
import com.amazonaws.services.kinesis.AmazonKinesisClientBuilder;
import com.amazonaws.services.kinesis.model.PutRecordRequest;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.SSEAwsKeyManagementParams;
import com.amazonaws.services.securitytoken.AWSSecurityTokenService;
import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder;
import com.amazonaws.services.securitytoken.model.AssumeRoleRequest;
import com.amazonaws.services.securitytoken.model.Credentials;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.AmazonSNSClientBuilder;
import com.amazonaws.services.sns.model.PublishRequest;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
import com.amazonaws.services.sqs.model.SendMessageRequest;

import fi.evolver.basics.spring.log.MessageLogService;
import fi.evolver.basics.spring.log.entity.MessageLog.Direction;
import fi.evolver.basics.spring.messaging.SendResult;
import fi.evolver.basics.spring.messaging.entity.Message;
import fi.evolver.basics.spring.messaging.entity.MessageTargetConfig;
import fi.evolver.basics.spring.messaging.util.SendUtils;
import fi.evolver.utils.CommunicationException;
import fi.evolver.utils.TagUtils;
import fi.evolver.utils.stream.FinishingInputStream;


@Component
public class AwsSender implements Sender {
	private static final Logger LOG = LoggerFactory.getLogger(AwsSender.class);

	private static final String PROTOCOL_KINESIS = "kinesis";
	private static final String PROTOCOL_S3 = "s3";
	private static final String PROTOCOL_S3_SNS = "s3+sns";
	private static final String PROTOCOL_S3_SQS = "s3+sqs";
	private static final String PROTOCOL_SNS = "sns";
	private static final String PROTOCOL_SQS = "sqs";

	private static final int DEFAULT_TTL_MS = 1000 * 60 * 60;

	private static final String PROPERTY_DELAY_SECONDS = "DelayS";
	private static final String PROPERTY_ENCRYPTION = "Encryption";
	private static final String PROPERTY_GROUP_ID = "GroupId";
	private static final String PROPERTY_KMS_KEY_ARN = "KmsKeyArn";
	private static final String PROPERTY_QUEUE = "Queue";
	private static final String PROPERTY_SNS_TOPIC_ARN = "TopicArn";
	private static final String PROPERTY_KINESIS_STREAM_NAME = "KinesisStreamName";
	private static final String PROPERTY_KINESIS_PARTITION_KEY = "KinesisPartitionKey";
	private static final String PROPERTY_STS_ASSUME_ROLE_ARN = "StsRoleArn";
	private static final String PROPERTY_STS_ROLE_DURATION_SECONDS = "StsRoleDurationSeconds";
	private static final String PROPERTY_STS_SESSION_NAME = "StsSessionName";
	private static final String PROPERTY_TIME_TO_LIVE_MS = "TimeToLiveMs";
	private static final String PROPERTY_ACCESS_KEY = "AccessKey";
	private static final String PROPERTY_SECRET_ACCESS_KEY = "SecretAccessKey";

	private static final String AWS_V4_SIGNER_TYPE = "AWSS3V4SignerType";

	private static final String STATUS_FAILED = "FAILED";
	private static final String STATUS_OK = "OK";

	private static final Set PROTOCOLS = Set.of(PROTOCOL_S3, PROTOCOL_SNS, PROTOCOL_S3_SNS, PROTOCOL_SQS, PROTOCOL_S3_SQS, PROTOCOL_KINESIS);


	private final MessageLogService messageLogService;


	@Autowired
	public AwsSender(MessageLogService messageLogService) {
		this.messageLogService = messageLogService;
	}


	@Override
	public SendResult send(Message message, URI uri) {
		switch (uri.getScheme().toLowerCase()) {
			case PROTOCOL_S3:
				return uploadToS3(message, uri);
			case PROTOCOL_SNS:
				return publishMessageToSns(message, uri);
			case PROTOCOL_S3_SNS:
				return uploadToS3AndPublishUrlToSns(message, uri);
			case PROTOCOL_SQS:
				return sendMessageToSqs(message, uri);
			case PROTOCOL_S3_SQS:
				return uploadToS3AndSendUrlToSqs(message, uri);
			case PROTOCOL_KINESIS:
				return publishMessageToKinesis(message, uri);
			default:
				throw new IllegalArgumentException("Unsupported protocol: " + uri.getScheme());
		}
	}


	private SendResult uploadToS3(Message message, URI uri) {
		LocalDateTime start = LocalDateTime.now();
		String statusCode = STATUS_FAILED;
		long requestLength = -1L;

		try {
			AWSCredentialsProvider credentialsProvider = getCredentialsProvider(message, uri);
			ClientConfiguration config = buildClientConfiguration(message.getMessageTargetConfig());
			if (credentialsProvider instanceof AWSStaticCredentialsProvider)
				config.setSignerOverride(AWS_V4_SIGNER_TYPE);

			requestLength = getInputLength(message);
			upload(message, requestLength, uri, config, credentialsProvider, false);
			statusCode = STATUS_OK;
			return SendResult.success();
		}
		catch (IOException e) {
			LOG.warn("S3 upload failed", e);
			return SendResult.error("S3 upload failed: %s", e.getMessage());
		}
		finally {
			messageLogService.logZippedMessage(
					start,
					message.getMessageType(),
					PROTOCOL_S3,
					uri.toString(),
					messageLogService.getApplicationName(),
					message.getTargetSystem(),
					Direction.OUTBOUND,
					(int) requestLength,
					message.getCompressedData(),
					Collections.emptyMap(),
					0,
					null,
					null,
					statusCode,
					null,
					mapMetadata(message.getMetadata()));
		}
	}


	private SendResult publishMessageToSns(Message message, URI uri) {
		LocalDateTime start = LocalDateTime.now();
		String statusCode = STATUS_FAILED;
		String body = null;

		try (Reader reader = new InputStreamReader(message.getDataStream(), StandardCharsets.UTF_8)) {
			AWSCredentialsProvider credentialsProvider = getCredentialsProvider(message, uri);
			ClientConfiguration config = buildClientConfiguration(message.getMessageTargetConfig());

			body = IOUtils.toString(reader);
			if (body != null && !body.isEmpty())
				publishToSns(message, body, uri, config, credentialsProvider);
			statusCode = STATUS_OK;
			return SendResult.success();
		}
		catch (IOException e) {
			LOG.warn("SNS publish failed", e);
			return SendResult.error("SNS publish failed: %s", e.getMessage());
		}
		finally {
			messageLogService.logMessage(
					start,
					message.getMessageType(),
					PROTOCOL_SNS,
					uri.toString(),
					messageLogService.getApplicationName(),
					message.getTargetSystem(),
					Direction.OUTBOUND,
					body,
					Collections.emptyMap(),
					null,
					null,
					statusCode,
					null,
					mapMetadata(message.getMetadata()));
		}
	}


	private SendResult publishMessageToKinesis(Message message, URI uri) {
		LocalDateTime start = LocalDateTime.now();
		String statusCode = STATUS_FAILED;
		String body = null;

		try (Reader reader = new InputStreamReader(message.getDataStream(), StandardCharsets.UTF_8)) {
			body = IOUtils.toString(reader);
			AWSCredentialsProvider credentialsProvider = getCredentialsProvider(message, uri);
			ClientConfiguration config = buildClientConfiguration(message.getMessageTargetConfig());
			publishToKinesis(message, body, uri, config, credentialsProvider);
			statusCode = STATUS_OK;
			return SendResult.success();
		}
		catch (IOException e) {
			LOG.warn("Kinesis publish failed", e);
			return SendResult.error("Kinesis publish failed: %s", e.getMessage());
		}
		finally {
			messageLogService.logMessage(
					start,
					message.getMessageType(),
					PROTOCOL_KINESIS,
					uri.toString(),
					messageLogService.getApplicationName(),
					message.getTargetSystem(),
					Direction.OUTBOUND,
					body,
					Collections.emptyMap(),
					null,
					null,
					statusCode,
					null,
					mapMetadata(message.getMetadata())
			);
		}
	}


	private SendResult sendMessageToSqs(Message message, URI uri) {
		LocalDateTime start = LocalDateTime.now();
		String statusCode = STATUS_FAILED;
		String body = null;

		try (Reader reader = new InputStreamReader(message.getDataStream(), StandardCharsets.UTF_8)) {
			body = IOUtils.toString(reader);
			AWSCredentialsProvider credentialsProvider = getCredentialsProvider(message, uri);
			ClientConfiguration config = buildClientConfiguration(message.getMessageTargetConfig());

			sendToSqs(message, body, uri, config, credentialsProvider);
			statusCode = STATUS_OK;
			return SendResult.success();
		}
		catch (IOException e) {
			LOG.warn("SQS send failed", e);
			return SendResult.error("SQS send failed: %s", e.getMessage());
		}
		finally {
			messageLogService.logMessage(
					start,
					message.getMessageType(),
					PROTOCOL_SQS,
					uri.toString(),
					messageLogService.getApplicationName(),
					message.getTargetSystem(),
					Direction.OUTBOUND,
					body,
					Collections.emptyMap(),
					null,
					null,
					statusCode,
					null,
					mapMetadata(message.getMetadata()));
		}
	}


	private SendResult uploadToS3AndPublishUrlToSns(Message message, URI uri) {
		LocalDateTime start = LocalDateTime.now();
		String statusCode = STATUS_FAILED;
		long requestLength = -1L;

		try {
			AWSCredentialsProvider credentialsProvider = getCredentialsProvider(message, uri);
			ClientConfiguration config = buildClientConfiguration(message.getMessageTargetConfig());
			if (credentialsProvider instanceof AWSStaticCredentialsProvider)
				config.setSignerOverride(AWS_V4_SIGNER_TYPE);

			requestLength = getInputLength(message);
			URL preSignedUrl = upload(message, requestLength, uri, config, credentialsProvider, true);
			if (preSignedUrl == null)
				throw new IllegalStateException("Did not get presigned S3 uri from upload");

			config.setSignerOverride(null);
			publishToSns(message, preSignedUrl.toString(), uri, config, credentialsProvider);
			statusCode = STATUS_OK;
			return SendResult.success();
		}
		catch (IOException e) {
			LOG.warn("S3 upload + SNS publish failed", e);
			return SendResult.error("S3 upload + SNS publish failed: %s", e.getMessage());
		}
		finally {
			messageLogService.logZippedMessage(
					start,
					message.getMessageType(),
					PROTOCOL_S3_SNS,
					uri.toString(),
					messageLogService.getApplicationName(),
					message.getTargetSystem(),
					Direction.OUTBOUND,
					(int) requestLength,
					message.getCompressedData(),
					Collections.emptyMap(),
					0,
					null,
					null,
					statusCode,
					null,
					mapMetadata(message.getMetadata()));
		}
	}


	private SendResult uploadToS3AndSendUrlToSqs(Message message, URI uri) {
		LocalDateTime start = LocalDateTime.now();
		String statusCode = STATUS_FAILED;
		long requestLength = -1L;

		try {
			AWSCredentialsProvider credentialsProvider = getCredentialsProvider(message, uri);
			ClientConfiguration config = buildClientConfiguration(message.getMessageTargetConfig());

			requestLength = getInputLength(message);
			URL preSignedUrl = upload(message, requestLength, uri, config, credentialsProvider, true);
			if (preSignedUrl == null)
				throw new IllegalStateException("Did not get presigned S3 uri from upload");

			config.setSignerOverride(null);
			sendToSqs(message, preSignedUrl.toString(), uri, config, credentialsProvider);
			statusCode = STATUS_OK;
			return SendResult.success();
		}
		catch (IOException e) {
			LOG.warn("S3 upload + SQS send failed", e);
			return SendResult.error("S3 upload + SQS send failed: %s", e.getMessage());
		}
		finally {
			messageLogService.logZippedMessage(
					start,
					message.getMessageType(),
					PROTOCOL_S3_SQS,
					uri.toString(),
					messageLogService.getApplicationName(),
					message.getTargetSystem(),
					Direction.OUTBOUND,
					(int) requestLength,
					message.getCompressedData(),
					Collections.emptyMap(),
					0,
					null,
					null,
					statusCode,
					null,
					mapMetadata(message.getMetadata()));
		}
	}


	private static AWSCredentialsProvider getCredentialsProvider(Message message, URI uri) throws CommunicationException {
		AWSCredentialsProvider baseCredentialsProvider = createBaseCredentialsProvider(message);

		Optional stsRoleArn = SendUtils.getTagReplacedTargetProperty(message, PROPERTY_STS_ASSUME_ROLE_ARN);
		if (stsRoleArn.isPresent()) {
			AWSSecurityTokenService stsClient = AWSSecurityTokenServiceClientBuilder.standard()
					.withCredentials(baseCredentialsProvider)
					.withRegion(parseHost(uri))
					.build();

			AssumeRoleRequest assumeRequest = new AssumeRoleRequest()
					.withRoleArn(stsRoleArn.get())
					.withExternalId(UUID.randomUUID().toString())
					.withDurationSeconds(message.getMessageTargetConfig().getProperty(PROPERTY_STS_ROLE_DURATION_SECONDS).map(Integer::parseInt).orElse(3600))
					.withRoleSessionName(message.getMessageTargetConfig().getProperty(PROPERTY_STS_SESSION_NAME).orElse("DefaultSessionName"));

			Credentials credentials = stsClient.assumeRole(assumeRequest).getCredentials();

			BasicSessionCredentials temporaryCredentials = new BasicSessionCredentials(
					credentials.getAccessKeyId(),
					credentials.getSecretAccessKey(),
					credentials.getSessionToken());

			return new AWSStaticCredentialsProvider(temporaryCredentials);
		}

		return baseCredentialsProvider;
	}


	private static AWSCredentialsProvider createBaseCredentialsProvider(Message message) {
		Optional accessKey = message.getMessageTargetConfig().getProperty(PROPERTY_ACCESS_KEY);
		Optional secretAccessKey = message.getMessageTargetConfig().getProperty(PROPERTY_SECRET_ACCESS_KEY);
		if (accessKey.isPresent() && secretAccessKey.isPresent())
			return new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey.get(), secretAccessKey.get()));
		return new InstanceProfileCredentialsProvider(false);
	}


	private static URL upload(Message message, long contentLength, URI uri, ClientConfiguration config, AWSCredentialsProvider credentialsProvider, boolean generatePreSignedUrl) throws CommunicationException {
		try {
			AmazonS3 s3Client = buildS3Client(uri, config, credentialsProvider);

			String[] pathParts = uri.getPath().split("/", 3);
			if (pathParts.length < 3)
				throw new IllegalArgumentException(String.format("Failed parsing S3 bucket name and filename from URI path: %s", uri.getPath()));

			String bucketName = pathParts[1];
			String fileName = pathParts[2];
			fileName = TagUtils.replaceTags(fileName, TagUtils.TIMESTAMP);

			ObjectMetadata metadata = new ObjectMetadata();
			metadata.setContentLength(contentLength);
			message.getMessageTargetConfig().getProperty(PROPERTY_ENCRYPTION)
					.filter(a -> !"false".equalsIgnoreCase(a))
					.map(a -> "true".equalsIgnoreCase(a) ? ObjectMetadata.AES_256_SERVER_SIDE_ENCRYPTION : a)
					.ifPresent(metadata::setSSEAlgorithm);

			PutObjectRequest request = new PutObjectRequest(bucketName, fileName, SendUtils.createDataStream(message), metadata);

			message.getMessageTargetConfig().getProperty(PROPERTY_KMS_KEY_ARN)
					.ifPresent(key -> request.withSSEAwsKeyManagementParams(new SSEAwsKeyManagementParams(key)));

			s3Client.putObject(request);

			if (generatePreSignedUrl)
				return generatePreSignedUrl(s3Client, fileName, bucketName, message);

			return null;
		}
		catch (CommunicationException e) {
			throw e;
		}
		catch (IOException e) {
			throw new CommunicationException(e, "Failed reading message to input stream");
		}
		catch (SdkClientException e) {
			throw new CommunicationException(e, "Failed uploading to AWS S3");
		}
	}


	private static URL generatePreSignedUrl(AmazonS3 s3Client, String key, String bucketName, Message message) throws CommunicationException {
		try {
			int ttl = message.getMessageTargetConfig().getIntProperty(PROPERTY_TIME_TO_LIVE_MS).orElse(DEFAULT_TTL_MS);
			GeneratePresignedUrlRequest generatePresignedUrlRequest =
					new GeneratePresignedUrlRequest(bucketName, key)
							.withExpiration(new Date(System.currentTimeMillis() + ttl));

			return s3Client.generatePresignedUrl(generatePresignedUrlRequest);
		}
		catch (SdkClientException e) {
			throw new CommunicationException(e, "Failed generating S3 pre signed URL for %s", key);
		}
	}


	private static void publishToSns(Message message, String text, URI uri, ClientConfiguration config, AWSCredentialsProvider credentialsProvider) throws CommunicationException {
		try {
			AmazonSNS snsClient = buildSnsClient(uri, config, credentialsProvider);
			String snsTopicArn = SendUtils.getTagReplacedTargetProperty(message, PROPERTY_SNS_TOPIC_ARN)
					.orElseThrow(() -> new CommunicationException("Missing required property: %s", PROPERTY_SNS_TOPIC_ARN));

			PublishRequest request = new PublishRequest()
					.withTopicArn(snsTopicArn)
					.withMessage(text);
			snsClient.publish(request);
		}
		catch (Exception e) {
			throw new CommunicationException(e, "Failed publishing message (%s) to SNS", text);
		}
	}

	private static void publishToKinesis(Message message, String text, URI uri, ClientConfiguration config, AWSCredentialsProvider credentialsProvider) throws CommunicationException {
		try {
			AmazonKinesis kinesisClient = buildKinesisClient(uri, config, credentialsProvider);
			String kinesisStreamName = SendUtils.getTagReplacedTargetProperty(message, PROPERTY_KINESIS_STREAM_NAME)
					.orElseThrow(() -> new CommunicationException("Missing required property %s", PROPERTY_KINESIS_STREAM_NAME));

			String partition = SendUtils.getTagReplacedTargetProperty(message, PROPERTY_KINESIS_PARTITION_KEY)
					.orElseThrow(() -> new CommunicationException("Missing required property %s", PROPERTY_KINESIS_PARTITION_KEY));

			PutRecordRequest request = new PutRecordRequest()
					.withStreamName(kinesisStreamName)
					.withPartitionKey(partition)
					.withData(
							StandardCharsets.UTF_8.newEncoder().encode(CharBuffer.wrap(text))
					);

			kinesisClient.putRecord(request);
		}
		catch (Exception e) {
			throw new CommunicationException(e, "Failed publishing message (%s) to Kinesis", text);
		}
	}


	private static void sendToSqs(Message message, String text, URI uri, ClientConfiguration config, AWSCredentialsProvider credentialsProvider) throws CommunicationException {
		try {
			AmazonSQS sqsClient = buildSqsClient(uri, config, credentialsProvider);
			String queue = SendUtils.getTagReplacedTargetProperty(message, PROPERTY_QUEUE)
					.orElseThrow(() -> new CommunicationException("Missing required property: %s", PROPERTY_QUEUE));

			SendMessageRequest request = new SendMessageRequest()
					.withQueueUrl(sqsClient.getQueueUrl(queue).getQueueUrl())
					.withMessageBody(text);

			if (queue.endsWith(".fifo")) {
				String groupId = SendUtils.getTagReplacedTargetProperty(message, PROPERTY_GROUP_ID).orElse("default");
				request = request.withMessageGroupId(groupId);
				request.setMessageDeduplicationId(String.format("%s-%s", message.getMessageChainId(), message.getId()));
			} else {
				int delaySeconds = message.getMessageTargetConfig().getIntProperty(PROPERTY_DELAY_SECONDS).orElse(5);
				request = request.withDelaySeconds(delaySeconds);
			}

			sqsClient.sendMessage(request);
		}
		catch (Exception e) {
			throw new CommunicationException(e, "Failed sending message (%s) to SQS", text);
		}
	}


	private static AmazonKinesis buildKinesisClient(URI uri, ClientConfiguration config, AWSCredentialsProvider credentialsProvider) throws CommunicationException {
		return AmazonKinesisClientBuilder.standard()
				.withRegion(parseHost(uri))
				.withClientConfiguration(config)
				.withCredentials(credentialsProvider)
				.build();
	}


	private static AmazonSNS buildSnsClient(URI uri, ClientConfiguration config, AWSCredentialsProvider credentialsProvider) throws CommunicationException {
		return AmazonSNSClientBuilder.standard()
				.withRegion(parseHost(uri))
				.withClientConfiguration(config)
				.withCredentials(credentialsProvider)
				.build();
	}


	private static AmazonSQS buildSqsClient(URI uri, ClientConfiguration config, AWSCredentialsProvider credentialsProvider) throws CommunicationException {
		return AmazonSQSClientBuilder.standard()
				.withRegion(parseHost(uri))
				.withClientConfiguration(config)
				.withCredentials(credentialsProvider)
				.build();
	}


	private static AmazonS3 buildS3Client(URI uri, ClientConfiguration config, AWSCredentialsProvider credentialsProvider) throws CommunicationException {
		return AmazonS3ClientBuilder.standard()
				.withRegion(parseHost(uri))
				.withClientConfiguration(config)
				.withCredentials(credentialsProvider)
				.build();
	}


	private static ClientConfiguration buildClientConfiguration(MessageTargetConfig messageTargetConfig) {
		ClientConfiguration config = new ClientConfiguration();
		messageTargetConfig.getIntProperty(PROPERTY_CONNECT_TIMEOUT_MS).ifPresent(config::setConnectionTimeout);
		messageTargetConfig.getIntProperty(PROPERTY_READ_TIMEOUT_MS).ifPresent(config::setSocketTimeout);
		return config;
	}


	private static String parseHost(URI uri) throws CommunicationException {
		String host = uri.getHost();
		if (host == null || host.isEmpty())
			throw new CommunicationException("URI (%s) missing required host information", uri);
		return host;
	}


	private static long getInputLength(Message message) throws IOException {
		CountingInputStream counter = new CountingInputStream(SendUtils.createDataStream(message));
		try (FinishingInputStream finisher = new FinishingInputStream(counter)) {
			/* Nothing to do */
		}
		return counter.getByteCount();
	}


	@Override
	public Set getSupportedProtocols() {
		return PROTOCOLS;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy