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

io.github.microcks.minion.async.consumer.AMQPMessageConsumptionTask Maven / Gradle / Ivy

/*
 * 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.consumer;

import io.github.microcks.domain.Header;
import io.github.microcks.minion.async.AsyncTestSpecification;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import org.jboss.logging.Logger;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * An implementation of MessageConsumptionTask that consumes a queue on an RabbitMQ 3.x Server. Endpoint
 * URL should be specified using the following form:
 * amqp://{brokerhost[:port]}[/{virtualHost}]/{type}/{destination}[?option1=value1&option2=value2]
 * @author laurent
 */
public class AMQPMessageConsumptionTask implements MessageConsumptionTask {

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

   /** The string for Regular Expression that helps validating acceptable endpoints. */
   public static final String ENDPOINT_PATTERN_STRING = "amqp://(?[^:]+(:\\d+)?)/(?[a-zA-Z0-9-_]+/)?(?[q|d|f|t|h])/(?[^?]+)(\\?(?.+))?";
   /** The Pattern for matching groups within the endpoint regular expression. */
   public static final Pattern ENDPOINT_PATTERN = Pattern.compile(ENDPOINT_PATTERN_STRING);

   /** Constant representing a queue destination type in endpoint URL. */
   public static final String QUEUE_TYPE = "q";
   /** Constant representing a direct exchange destination type in endpoint URL. */
   public static final String DIRECT_TYPE = "d";
   /** Constant representing a fanout exchange destination type in endpoint URL. */
   public static final String FANOUT_TYPE = "f";
   /** Constant representing a topic exchange destination type in endpoint URL. */
   public static final String TOPIC_TYPE = "t";
   /** Constant representing a headers exchange destination type in endpoint URL. */
   public static final String HEADERS_TYPE = "h";

   /** The endpoint URL option representing routing key. */
   public static final String ROUTING_KEY_OPTION = "routingKey";
   /** The endpoint URL option representing durable property. */
   public static final String DURABLE_OPTION = "durable";

   private File trustStore;

   private final AsyncTestSpecification specification;

   protected Map optionsMap;

   private Connection connection;

   private String virtualHost;

   private String destinationType;

   private String destinationName;

   private String options;

   /**
    * Create a new consumption task from an Async test specification.
    * @param testSpecification The specification holding endpointURL and timeout.
    */
   public AMQPMessageConsumptionTask(AsyncTestSpecification testSpecification) {
      this.specification = testSpecification;
   }

   /**
    * Convenient static method for checking if this implementation will accept endpoint.
    * @param endpointUrl The endpoint URL to validate
    * @return True if endpointUrl can be used for connecting and consuming on endpoint
    */
   public static boolean acceptEndpoint(String endpointUrl) {
      return endpointUrl != null && endpointUrl.matches(ENDPOINT_PATTERN_STRING);
   }

   @Override
   public List call() throws Exception {
      if (connection == null) {
         initializeAMQPConnection();
      }
      List messages = new ArrayList<>();

      try (Channel channel = connection.createChannel()) {
         String queueName = destinationName;

         if (!destinationType.equals(QUEUE_TYPE)) {
            boolean durable = false;
            if (optionsMap != null && optionsMap.containsKey(DURABLE_OPTION)) {
               durable = Boolean.parseBoolean(optionsMap.get(DURABLE_OPTION));
            }
            String routingKey = "#";
            if (optionsMap != null && optionsMap.containsKey(ROUTING_KEY_OPTION)) {
               routingKey = optionsMap.get(ROUTING_KEY_OPTION);
            }

            switch (destinationType) {
               case DIRECT_TYPE:
                  channel.exchangeDeclare(destinationName, "direct", durable);
                  queueName = channel.queueDeclare().getQueue();
                  channel.queueBind(queueName, destinationName, routingKey);
                  break;
               case HEADERS_TYPE:
                  channel.exchangeDeclare(destinationName, "headers", durable);
                  queueName = channel.queueDeclare().getQueue();
                  // Specify any header if specified otherwise default to routing key.
                  Map bindingArgs = buildHeaderArgs();
                  if (bindingArgs != null && !bindingArgs.isEmpty()) {
                     bindingArgs.put("x-match", "any");
                     channel.queueBind(queueName, destinationName, "", bindingArgs);
                  } else {
                     channel.queueBind(queueName, destinationName, routingKey);
                  }
                  break;
               case FANOUT_TYPE:
                  channel.exchangeDeclare(destinationName, "fanout", durable);
                  queueName = channel.queueDeclare().getQueue();
                  channel.queueBind(queueName, destinationName, "");
                  break;
               case TOPIC_TYPE:
               default:
                  channel.exchangeDeclare(destinationName, "topic", durable);
                  queueName = channel.queueDeclare().getQueue();
                  channel.queueBind(queueName, destinationName, routingKey);
                  break;
            }
         }

         String consumerTag = channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                  byte[] body) throws IOException {
               logger.infof("Received a new AMQP Message: %s", new String(body));
               // Build a ConsumedMessage from AMQP message.
               ConsumedMessage message = new ConsumedMessage();
               message.setReceivedAt(System.currentTimeMillis());
               message.setHeaders(buildHeaders(properties.getHeaders()));
               message.setPayload(body);
               messages.add(message);

               channel.basicAck(envelope.getDeliveryTag(), false);
            }
         });

         Thread.sleep(specification.getTimeoutMS());

         channel.basicCancel(consumerTag);
      }

      return messages;
   }

   /**
    * Close the resources used by this task. Namely the AMQP connection and the optionally created truststore holding
    * server client SSL credentials.
    * @throws IOException should not happen.
    */
   @Override
   public void close() throws IOException {
      if (connection != null) {
         try {
            connection.close();
         } catch (IOException ioe) {
            logger.warn("Closing AMQP connection raised an exception", ioe);
         }
      }
      if (trustStore != null && trustStore.exists()) {
         Files.delete(trustStore.toPath());
      }
   }

   /** Initialize AMQP connection from test properties. */
   private void initializeAMQPConnection() throws Exception {
      Matcher matcher = ENDPOINT_PATTERN.matcher(specification.getEndpointUrl().trim());
      // Call matcher.find() to be able to use named expressions.
      matcher.find();
      String endpointBrokerUrl = matcher.group("brokerUrl");
      virtualHost = matcher.group("virtualHost");
      destinationType = matcher.group("type");
      destinationName = matcher.group("destination");
      options = matcher.group("options");

      // Parse options if specified.
      if (options != null && !options.isBlank()) {
         optionsMap = ConsumptionTaskCommons.initializeOptionsMap(options);
      }

      ConnectionFactory factory = new ConnectionFactory();
      if (endpointBrokerUrl.contains(":")) {
         String[] serverAndPort = endpointBrokerUrl.split(":");
         factory.setHost(serverAndPort[0]);
         factory.setPort(Integer.parseInt(serverAndPort[1]));
      } else {
         factory.setHost(endpointBrokerUrl);
      }
      if (virtualHost != null && !virtualHost.isEmpty()) {
         factory.setVirtualHost(virtualHost);
      }

      if (specification.getSecret() != null) {
         if (specification.getSecret().getUsername() != null && specification.getSecret().getPassword() != null) {
            logger.debug("Adding username/password authentication from secret " + specification.getSecret().getName());
            factory.setUsername(specification.getSecret().getUsername());
            factory.setPassword(specification.getSecret().getPassword());
         }

         if (specification.getSecret().getCaCertPem() != null) {
            logger.debug("Installing a broker certificate from secret " + specification.getSecret().getName());
            trustStore = ConsumptionTaskCommons.installBrokerCertificate(specification);

            // Load the truststore into a ssl context as explained here:
            // https://www.rabbitmq.com/ssl.html#java-client
            KeyStore tks = KeyStore.getInstance("JKS");
            tks.load(new FileInputStream(trustStore), ConsumptionTaskCommons.TRUSTSTORE_PASSWORD.toCharArray());
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
            tmf.init(tks);

            SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
            sslContext.init(null, tmf.getTrustManagers(), null);

            factory.useSslProtocol(sslContext);
            factory.enableHostnameVerification();
         }
      }
      connection = factory.newConnection();
   }

   /** Build the map of headers as binding arguments for headers type exchange. */
   private Map buildHeaderArgs() {
      if (optionsMap != null) {
         Map results = new HashMap<>();
         // ?h.header1=value1&h.header2=value2
         for (String option : optionsMap.keySet()) {
            if (option.startsWith(HEADERS_TYPE + ".")) {
               String headerName = option.substring(HEADERS_TYPE.length() + 1);
               String headerValue = optionsMap.get(option);
               results.put(headerName, headerValue);
            }
         }
         return results;
      }
      return null;
   }

   /** Build set of Microcks headers from RabbitMQ headers. */
   private Set
buildHeaders(Map headers) { if (headers == null || headers.isEmpty()) { return null; } Set
results = new HashSet<>(); for (String key : headers.keySet()) { Header result = new Header(); result.setName(key); result.setValues(Set.of(headers.get(key).toString())); results.add(result); } return results; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy