io.github.microcks.minion.async.producer.AmazonSNSProducerManager Maven / Gradle / Ivy
/*
* Licensed to Laurent Broudoux (the "Author") under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Author licenses this
* file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.github.microcks.minion.async.producer;
import io.github.microcks.domain.EventMessage;
import io.github.microcks.domain.Header;
import io.github.microcks.minion.async.AsyncMockDefinition;
import io.github.microcks.util.el.TemplateEngine;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sns.SnsClient;
import software.amazon.awssdk.services.sns.SnsClientBuilder;
import software.amazon.awssdk.services.sns.model.CreateTopicRequest;
import software.amazon.awssdk.services.sns.model.ListTopicsRequest;
import software.amazon.awssdk.services.sns.model.ListTopicsResponse;
import software.amazon.awssdk.services.sns.model.MessageAttributeValue;
import software.amazon.awssdk.services.sns.model.PublishResponse;
import software.amazon.awssdk.services.sns.model.Topic;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import java.net.URI;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Amazon WS SNS implementation of producer for async event messages.
* @author laurent
*/
@ApplicationScoped
public class AmazonSNSProducerManager {
/** Get a JBoss logging logger. */
private final Logger logger = Logger.getLogger(getClass());
private SnsClient snsClient;
private final ConcurrentHashMap topicArns = new ConcurrentHashMap<>();
AwsCredentialsProvider credentialsProvider;
@ConfigProperty(name = "amazonsns.region")
String region;
@ConfigProperty(name = "amazonsns.credentials-type")
AmazonCredentialsProviderType credentialsType;
@ConfigProperty(name = "amazonsns.credentials-profile-name")
String credentialsProfileName;
@ConfigProperty(name = "amazonsns.credentials-profile-location")
String credentialsProfileLocation;
@ConfigProperty(name = "amazonsns.endpoint-override")
Optional endpointOverride;
/**
* Initialize the AWS SNS connection post construction.
* @throws Exception If connection to SQS cannot be done.
*/
@PostConstruct
public void create() throws Exception {
try {
switch (credentialsType) {
case ENV_VARIABLE:
credentialsProvider = EnvironmentVariableCredentialsProvider.create();
break;
case PROFILE:
if (credentialsProfileLocation != null && credentialsProfileLocation.length() > 0) {
ProfileFile profileFile = ProfileFile.builder()
.type(ProfileFile.Type.CREDENTIALS)
.content(Paths.get(credentialsProfileLocation))
.build();
credentialsProvider = ProfileCredentialsProvider.builder()
.profileFile(profileFile)
.profileName(credentialsProfileName)
.build();
}
break;
}
// Default if not set.
if (credentialsProvider == null) {
credentialsProvider = DefaultCredentialsProvider.create();
}
// Now create the SNS client with credential provider.
SnsClientBuilder snsClientBuilder = SnsClient.builder()
.region(Region.of(region))
.credentialsProvider(credentialsProvider);
if (endpointOverride.filter(URI::isAbsolute).isPresent()) {
snsClientBuilder.endpointOverride(endpointOverride.get());
}
snsClient = snsClientBuilder.build();
} catch (Exception e) {
logger.errorf("Cannot connect to AWS SNS region %s", region);
logger.errorf("Connection exception: %s", e.getMessage());
throw e;
}
}
/**
* Publish a message on specified AWS SNS topic.
* @param topic The short name of topic within the configured region
* @param value The message payload
* @param headers The headers if any (as rendered by renderEventMessageHeaders() method)
*/
public void publishMessage(String topic, String value, Map headers) {
logger.infof("Publishing on topic {%s}, message: %s ", topic, value);
try {
if (topicArns.get(topic) == null) {
// Ensure topic exists on AWS by trying to get it in the list.
ListTopicsRequest listRequest = ListTopicsRequest.builder().build();
ListTopicsResponse listResponse = snsClient.listTopics(listRequest);
if (listResponse.hasTopics() && listResponse.topics().size() > 0) {
logger.infof("listResponse.hasTopics(): %d", listResponse.topics().size());
for (Topic topicTopic : listResponse.topics()) {
logger.infof("Found AWS SNS topic: %s", topicTopic.toString());
if (topicTopic.topicArn().endsWith(":" + topic)) {
topicArns.put(topic, topicTopic.topicArn());
break;
}
}
} else {
topicArns.put(topic, createTopicAndGetArn(topic));
}
}
// Retrieve topic ARN from local defs and publish message.
String topicArn = topicArns.get(topic);
PublishResponse response = snsClient.publish(pr -> pr.topicArn(topicArn)
.message(value)
.messageAttributes(headers)
.build());
} catch (Throwable t) {
logger.warnf("Message sending has thrown an exception", t);
// As it may be relative to queue being deleted and recreated so having different id
// than previous and thus url, we should clean our cache.
topicArns.remove(topic);
t.printStackTrace();
}
}
/**
* Render Microcks headers using the template engine.
* @param engine The template engine to reuse (because we do not want to initialize and manage a context at the AmazonSQSProducerManager level.)
* @param headers The Microcks event message headers definition.
* @return A map of rendered headers of Amazon SQS message sender.
*/
public Map renderEventMessageHeaders(TemplateEngine engine, Set headers) {
if (headers != null && !headers.isEmpty()) {
return headers.stream().collect(Collectors.toMap(
header -> header.getName(),
header -> {
String firstValue = header.getValues().stream().findFirst().get();
String finaleValue = firstValue;
if (firstValue.contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) {
try {
finaleValue = engine.getValue(firstValue);
} catch (Throwable t) {
logger.error("Failing at evaluating template " + firstValue, t);
}
}
return MessageAttributeValue.builder()
.stringValue(finaleValue)
.dataType("String")
.build();
}));
}
return null;
}
/**
* Compute a topic name from async mock definition.
* @param definition The mock definition
* @param eventMessage The event message to get dynamic part from
* @return The short name of a SNS topic
*/
public String getTopicName(AsyncMockDefinition definition, EventMessage eventMessage) {
logger.debugf("AsyncAPI Operation {%s}", definition.getOperation().getName());
// Produce service name part of topic name.
String serviceName = definition.getOwnerService().getName().replace(" ", "");
serviceName = serviceName.replace("-", "");
// Produce version name part of topic name.
String versionName = definition.getOwnerService().getVersion().replace(" ", "");
versionName = versionName.replace(".", "");
// Produce operation name part of topic name.
String operationName = definition.getOperation().getName();
if (operationName.startsWith("SUBSCRIBE ") || operationName.startsWith("PUBLISH ")) {
operationName = operationName.substring(operationName.indexOf(" ") + 1);
}
operationName = operationName.replace('/', '-');
operationName = ProducerManager.replacePartPlaceholders(eventMessage, operationName);
// Aggregate the 3 parts using '-' as delimiter.
return serviceName + "-" + versionName + "-" + operationName;
}
private String createTopicAndGetArn(String topicName) {
CreateTopicRequest createTopicRequest = CreateTopicRequest.builder()
.name(topicName)
.build();
return snsClient.createTopic(createTopicRequest).topicArn();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy