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

io.github.microcks.minion.async.producer.ProducerManager Maven / Gradle / Ivy

The newest version!
/*
 * Copyright The Microcks Authors.
 *
 * Licensed 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.Binding;
import io.github.microcks.domain.BindingType;
import io.github.microcks.domain.EventMessage;
import io.github.microcks.domain.Operation;
import io.github.microcks.domain.ResourceType;
import io.github.microcks.minion.async.AsyncMockDefinition;
import io.github.microcks.minion.async.AsyncMockRepository;
import io.github.microcks.minion.async.Constants;
import io.github.microcks.minion.async.SchemaRegistry;
import io.github.microcks.util.AvroUtil;
import io.github.microcks.util.SchemaMap;
import io.github.microcks.util.asyncapi.AsyncAPISchemaUtil;
import io.github.microcks.util.asyncapi.AsyncAPISchemaValidator;
import io.github.microcks.util.el.TemplateEngine;
import io.github.microcks.util.el.TemplateEngineFactory;

import com.fasterxml.jackson.databind.JsonNode;
import io.quarkus.arc.Unremovable;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericRecord;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

/**
 * ProducerManager is the responsible for emitting mock event messages when specific frequency triggered is reached.
 * Need to specify it as @Unremovable to avoid Quarkus ARC optimization removing beans that are not injected elsewhere
 * (this one is resolved using Arc.container().instance() method from ProducerScheduler).
 * @author laurent
 */
@Unremovable
@ApplicationScoped
public class ProducerManager {

   /** Get a JBoss logging logger. */
   private final Logger logger = Logger.getLogger(getClass());

   final AsyncMockRepository mockRepository;

   final SchemaRegistry schemaRegistry;

   final KafkaProducerManager kafkaProducerManager;
   final MQTTProducerManager mqttProducerManager;
   final NATSProducerManager natsProducerManager;
   final AMQPProducerManager amqpProducerManager;
   final GooglePubSubProducerManager googlePubSubProducerManager;
   final AmazonSQSProducerManager amazonSQSProducerManager;
   final AmazonSNSProducerManager amazonSNSProducerManager;

   @Inject
   @RootWebSocketProducerManager
   WebSocketProducerManager wsProducerManager;

   @ConfigProperty(name = "minion.supported-bindings")
   String[] supportedBindings;

   @ConfigProperty(name = "minion.default-avro-encoding", defaultValue = "RAW")
   String defaultAvroEncoding;

   public ProducerManager(AsyncMockRepository mockRepository, SchemaRegistry schemaRegistry,
         KafkaProducerManager kafkaProducerManager, MQTTProducerManager mqttProducerManager,
         NATSProducerManager natsProducerManager, AMQPProducerManager amqpProducerManager,
         GooglePubSubProducerManager googlePubSubProducerManager, AmazonSQSProducerManager amazonSQSProducerManager,
         AmazonSNSProducerManager amazonSNSProducerManager) {
      this.mockRepository = mockRepository;
      this.schemaRegistry = schemaRegistry;
      this.kafkaProducerManager = kafkaProducerManager;
      this.mqttProducerManager = mqttProducerManager;
      this.natsProducerManager = natsProducerManager;
      this.amqpProducerManager = amqpProducerManager;
      this.googlePubSubProducerManager = googlePubSubProducerManager;
      this.amazonSQSProducerManager = amazonSQSProducerManager;
      this.amazonSNSProducerManager = amazonSNSProducerManager;
   }

   /**
    * Produce all the async mock messages corresponding to specified frequency.
    * @param frequency The frequency to emit messages for
    */
   public void produceAsyncMockMessagesAt(Long frequency) {
      logger.info("Producing async mock messages for frequency: " + frequency);

      Set mockDefinitions = mockRepository.getMockDefinitionsByFrequency(frequency);
      for (AsyncMockDefinition definition : mockDefinitions) {
         logger.debugf("Processing definition of service {%s}",
               definition.getOwnerService().getName() + ':' + definition.getOwnerService().getVersion());

         for (String binding : definition.getOperation().getBindings().keySet()) {
            // Ensure this minion supports this binding.
            if (Arrays.asList(supportedBindings).contains(binding)) {
               Binding bindingDef = definition.getOperation().getBindings().get(binding);

               switch (BindingType.valueOf(binding)) {
                  case KAFKA:
                     produceKafkaMockMessages(definition);
                     break;
                  case NATS:
                     produceNatsMockMessages(definition);
                     break;
                  case MQTT:
                     produceMQTTMocksMessages(definition);
                     break;
                  case WS:
                     produceWSMockMessages(definition);
                     break;
                  case AMQP:
                     produceAMQPMockMessages(definition, bindingDef);
                     break;
                  case GOOGLEPUBSUB:
                     produceGooglePubSubMockMessages(definition);
                     break;
                  case SQS:
                     produceSQSMockMessages(definition);
                     break;
                  case SNS:
                     produceSNSMockMessages(definition);
                     break;
                  default:
                     break;
               }
            }
         }
      }
   }

   /** Take care publishing Kafka mock messages for definition. */
   protected void produceKafkaMockMessages(AsyncMockDefinition definition) {
      for (EventMessage eventMessage : definition.getEventMessages()) {
         String topic = kafkaProducerManager.getTopicName(definition, eventMessage);
         String key = String.valueOf(System.currentTimeMillis());
         String message = renderEventMessageContent(eventMessage);

         // Check it Avro binary is expected, we should convert to bytes.
         if (Constants.AVRO_BINARY_CONTENT_TYPES.contains(eventMessage.getMediaType())) {
            // Retrieve an Avro schema for this operation.
            Schema schema = null;

            // First browse schema entries for this operation.
            List entries = schemaRegistry.getSchemaEntries(definition.getOwnerService())
                  .stream().filter(entry -> entry.getOperations() != null
                        && entry.getOperations().contains(definition.getOperation().getName()))
                  .toList();

            if (entries.isEmpty()) {
               // If no entry found for the operation, we have to extract Avro schema from the AsyncAPI spec.
               entries = schemaRegistry.getSchemaEntries(definition.getOwnerService()).stream()
                     .filter(entry -> ResourceType.ASYNC_API_SPEC.equals(entry.getType())).toList();

               SchemaMap schemaMap = new SchemaMap();
               schemaRegistry.getSchemaEntries(definition.getOwnerService())
                     .forEach(schemaEntry -> schemaMap.putSchemaEntry(schemaEntry.getPath(), schemaEntry.getContent()));

               try {
                  // Extract embedded Avro schema from AsyncAPI spec.
                  JsonNode specificationNode = AsyncAPISchemaValidator
                        .getJsonNodeForSchema(entries.getFirst().getContent());
                  schema = AsyncAPISchemaUtil.retrieveMessageAvroSchema(specificationNode, AsyncAPISchemaUtil
                        .findMessagePathPointer(specificationNode, definition.getOperation().getName()), schemaMap);

                  if (schema.isUnion() && schema.getTypes().size() == 1) {
                     schema = schema.getTypes().getFirst();
                  }
               } catch (Exception e) {
                  logger.errorf("Exception while extracting Avro schema from AsyncAPI spec", e);
               }
            } else {
               // Directly get the Avro schema from one schema entry (.asvc file as external ref).
               schema = AvroUtil.getSchema(entries.getFirst().getContent());
            }

            if (schema != null) {
               logger.debugf("Found an Avro schema '%s' for operation '%s'", schema,
                     definition.getOperation().getName());

               try {
                  if (Constants.REGISTRY_AVRO_ENCODING.equals(defaultAvroEncoding)
                        && kafkaProducerManager.isRegistryEnabled()) {
                     logger.debug("Using a registry and converting message to Avro record");
                     GenericRecord avroRecord = AvroUtil.jsonToAvroRecord(message, schema);
                     kafkaProducerManager.publishMessage(topic, key, avroRecord,
                           kafkaProducerManager.renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(),
                                 eventMessage.getHeaders()));
                  } else {
                     logger.debug("Converting message to Avro bytes array");
                     byte[] avroBinary = AvroUtil.jsonToAvro(message, schema);
                     kafkaProducerManager.publishMessage(topic, key, avroBinary,
                           kafkaProducerManager.renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(),
                                 eventMessage.getHeaders()));
                  }
               } catch (Exception e) {
                  logger.errorf("Exception while converting {%s} to Avro using schema {%s}", message, schema.toString(),
                        e);
               }
            } else {
               logger.warnf("Failed finding a suitable Avro schema for the '%s' operation. No publication done.",
                     definition.getOperation().getName());
            }
         } else {
            kafkaProducerManager.publishMessage(topic, key, message, kafkaProducerManager
                  .renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), eventMessage.getHeaders()));
         }
      }
   }

   /** Take care publishing Nats mock messages for definition. */
   protected void produceNatsMockMessages(AsyncMockDefinition definition) {
      for (EventMessage eventMessage : definition.getEventMessages()) {
         String topic = natsProducerManager.getTopicName(definition, eventMessage);
         String message = renderEventMessageContent(eventMessage);
         natsProducerManager.publishMessage(topic, message, natsProducerManager
               .renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), eventMessage.getHeaders()));
      }
   }

   /** Take care publishing MQTT mock messages for definition. */
   protected void produceMQTTMocksMessages(AsyncMockDefinition definition) {
      for (EventMessage eventMessage : definition.getEventMessages()) {
         String topic = mqttProducerManager.getTopicName(definition, eventMessage);
         String message = renderEventMessageContent(eventMessage);
         mqttProducerManager.publishMessage(topic, message);
      }
   }

   /** Take care publishing WebSocket mock messages for definition. */
   protected void produceWSMockMessages(AsyncMockDefinition definition) {
      for (EventMessage eventMessage : definition.getEventMessages()) {
         String channel = wsProducerManager.getRequestURI(definition, eventMessage);
         String message = renderEventMessageContent(eventMessage);
         wsProducerManager.publishMessage(channel, message, eventMessage.getHeaders());
      }
   }

   /** Take care publishing AMQP mock messages for definition. */
   protected void produceAMQPMockMessages(AsyncMockDefinition definition, Binding bindingDef) {
      for (EventMessage eventMessage : definition.getEventMessages()) {
         String destinationName = amqpProducerManager.getDestinationName(definition, eventMessage);
         String message = renderEventMessageContent(eventMessage);
         amqpProducerManager.publishMessage(bindingDef.getDestinationType(), destinationName, message,
               amqpProducerManager.renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(),
                     eventMessage.getHeaders()));
      }
   }

   /** Take care publishing Google PubSub mock messages for definition. */
   protected void produceGooglePubSubMockMessages(AsyncMockDefinition definition) {
      for (EventMessage eventMessage : definition.getEventMessages()) {
         String topicName = googlePubSubProducerManager.getTopicName(definition, eventMessage);
         String message = renderEventMessageContent(eventMessage);
         googlePubSubProducerManager.publishMessage(topicName, message, googlePubSubProducerManager
               .renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), eventMessage.getHeaders()));
      }
   }

   /** Take care publishing SQS mock messages for definition. */
   protected void produceSQSMockMessages(AsyncMockDefinition definition) {
      for (EventMessage eventMessage : definition.getEventMessages()) {
         String queueName = amazonSQSProducerManager.getQueueName(definition, eventMessage);
         String message = renderEventMessageContent(eventMessage);
         amazonSQSProducerManager.publishMessage(queueName, message, amazonSQSProducerManager
               .renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), eventMessage.getHeaders()));
      }
   }

   /** Take care publishing SNS mock messages for definition. */
   protected void produceSNSMockMessages(AsyncMockDefinition definition) {
      for (EventMessage eventMessage : definition.getEventMessages()) {
         String topicName = amazonSNSProducerManager.getTopicName(definition, eventMessage);
         String message = renderEventMessageContent(eventMessage);
         amazonSNSProducerManager.publishMessage(topicName, message, amazonSNSProducerManager
               .renderEventMessageHeaders(TemplateEngineFactory.getTemplateEngine(), eventMessage.getHeaders()));
      }
   }

   /**
    * Format the destination operation part by computing an address with optional dynamic parts. This doesn't include
    * protocol specific sanitization (like replacing `/` with `-`, etc.)
    * @param operation    The operation to format a destination part for
    * @param eventMessage The message to format a destination part for
    * @return The operation part in a full destination name (usually service + version + operation)
    */
   public static String getDestinationOperationPart(Operation operation, EventMessage eventMessage) {
      // In AsyncAPI v2, channel address is directly the operation name.
      String operationPart = removeActionInOperationName(operation.getName());

      // Take care of templatized address for URI_PART dispatcher style.
      if ("URI_PARTS".equals(operation.getDispatcher())) {
         // In AsyncAPI v3, operation is different from channel and channel templatized address may be in resourcePaths.
         for (String resourcePath : operation.getResourcePaths()) {
            if (resourcePath.contains("{")) {
               operationPart = resourcePath;
               break;
            }
         }
         operationPart = replacePartPlaceholders(operationPart, eventMessage);
      }
      return operationPart;
   }

   /** Render event message content from definition applying template rendering if required. */
   private String renderEventMessageContent(EventMessage eventMessage) {
      String content = eventMessage.getContent();
      if (content.contains(TemplateEngine.DEFAULT_EXPRESSION_PREFIX)) {
         logger.debug("EventMessage contains dynamic EL expression, rendering it...");
         TemplateEngine engine = TemplateEngineFactory.getTemplateEngine();

         try {
            content = engine.getValue(content);
         } catch (Throwable t) {
            logger.errorf("Failing at evaluating template '%s'", content, t);
         }
      }
      return content;
   }

   /** Remove the AsyncAPI action (or verb) at the beginning of operation name if present. */
   private static String removeActionInOperationName(String operationName) {
      if (operationName.startsWith("SUBSCRIBE ") || operationName.startsWith("PUBLISH ")
            || operationName.startsWith("SEND ") || operationName.startsWith("RECEIVE ")) {
         return operationName.substring(operationName.indexOf(" ") + 1);
      }
      return operationName;
   }

   /** Replace address placeholders ('{}') with their values coming from message dispatch criteria. */
   private static String replacePartPlaceholders(String address, EventMessage eventMessage) {
      String partsCriteria = eventMessage.getDispatchCriteria();
      if (partsCriteria != null && !partsCriteria.isBlank()) {
         String[] criterion = partsCriteria.split("/");
         for (String criteria : criterion) {
            if (criteria != null && !criteria.isBlank()) {
               String[] element = criteria.split("=");
               String key = String.format("\\{%s\\}", element[0]);
               address = address.replaceAll(key, URLEncoder.encode(element[1], Charset.defaultCharset()));
            }
         }
      }
      return address;
   }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy