io.github.microcks.minion.async.producer.GooglePubSubProducerManager 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 com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures;
import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.core.ExecutorProvider;
import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.api.gax.core.InstantiatingExecutorProvider;
import com.google.api.gax.core.NoCredentialsProvider;
import com.google.api.gax.rpc.NotFoundException;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.pubsub.v1.Publisher;
import com.google.cloud.pubsub.v1.TopicAdminClient;
import com.google.cloud.pubsub.v1.TopicAdminSettings;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.ByteString;
import com.google.pubsub.v1.PubsubMessage;
import com.google.pubsub.v1.TopicName;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Google Cloud PubSub implementation of producer for async event messages.
* @author laurent
*/
@ApplicationScoped
public class GooglePubSubProducerManager {
/** Get a JBoss logging logger. */
private final Logger logger = Logger.getLogger(getClass());
private static final String CLOUD_OAUTH_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
/**
* As {@link Publisher} is bound to topic, default would be to create it at each invocation.
* We'll use this cache, that enforces only one {@link Publisher} per PubSub topic exists.
*/
private final ConcurrentHashMap publishers = new ConcurrentHashMap<>();
/**
* As {@link Publisher} is by default associated to its own executor, we have to override
* this to avoid wasting resources. As the push of mock messages is sequential, 1 thread is enough.
*/
private final ExecutorProvider executorProvider = InstantiatingExecutorProvider
.newBuilder().setExecutorThreadCount(1).build();
CredentialsProvider credentialsProvider;
@ConfigProperty(name = "googlepubsub.project")
String project;
@ConfigProperty(name = "googlepubsub.service-account-location")
String serviceAccountLocation;
/**
* Initialize the PubSub connection post construction.
* @throws Exception If connection to PubSub cannot be done.
*/
@PostConstruct
public void create() throws Exception {
try {
if (serviceAccountLocation != null && serviceAccountLocation.length() > 0) {
FileInputStream is = new FileInputStream(serviceAccountLocation);
credentialsProvider = FixedCredentialsProvider.create(GoogleCredentials.fromStream(is).createScoped(CLOUD_OAUTH_SCOPE));
} else {
credentialsProvider = NoCredentialsProvider.create();
}
} catch (Exception e) {
logger.errorf("Cannot read Google Cloud credentials %s", serviceAccountLocation);
throw e;
}
}
/**
* Publish a message on specified PubSub topic.
* @param topic The short name of topic within the configured project
* @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 (publishers.get(topic) == null) {
// Ensure topic exists on PubSub.
TopicName topicName = TopicName.of(project, topic);
TopicAdminSettings topicAdminSettings = TopicAdminSettings.newBuilder()
.setCredentialsProvider(credentialsProvider).build();
TopicAdminClient topicAdminClient = TopicAdminClient.create(topicAdminSettings);
try {
topicAdminClient.getTopic(topicName);
} catch (NotFoundException nfe) {
logger.infof("Topic {%s} does not exist yet, creating it", topic);
topicAdminClient.createTopic(topicName);
}
}
// Build a message for corresponding publisher.
Publisher publisher = getPublisher(topic);
ByteString data = ByteString.copyFromUtf8(value);
PubsubMessage pubsubMessage = PubsubMessage.newBuilder()
.setData(data)
.putAllAttributes(headers)
.build();
// Publish the message.
ApiFuture messageIdFuture = publisher.publish(pubsubMessage);
ApiFutures.addCallback(messageIdFuture, new ApiFutureCallback<>() {
// Wait for message submission and log the result
public void onSuccess(String messageId) {
logger.debugv("Published with message id {0}", messageId);
}
public void onFailure(Throwable t) {
logger.debugv("Failed to publish: {0}", t);
}
}, MoreExecutors.directExecutor());
} catch (IOException ioe) {
logger.warnf("Message sending has thrown an exception", ioe);
ioe.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 GooglePubSubProducerManager level.)
* @param headers The Microcks event message headers definition.
* @return A map of rendered headers for GCP Publisher.
*/
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();
if (firstValue.contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) {
try {
return engine.getValue(firstValue);
} catch (Throwable t) {
logger.error("Failing at evaluating template " + firstValue, t);
return firstValue;
}
}
return firstValue;
}));
}
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 PubSub 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(" ", "");
// 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 Publisher getPublisher(String topic) throws IOException {
try {
return publishers.computeIfAbsent(topic, this::createPublisher);
} catch (RuntimeException re) {
// Just unwrap the underlying IOException...
throw (IOException) re.getCause();
}
}
private Publisher createPublisher(String topic) {
try {
return Publisher.newBuilder(TopicName.of(project, topic))
.setExecutorProvider(executorProvider)
.setCredentialsProvider(credentialsProvider)
.build();
} catch (IOException ioe) {
logger.errorf("IOException while creating a Publisher for topic %s", topic);
throw new RuntimeException("IOException while creating a Publisher for topic ", ioe);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy