/** * Copyright 2014-2017 Linagora, Université Joseph Fourier, Floralis * * The present code is developed in the scope of the joint LINAGORA - * Université Joseph Fourier - Floralis research program and is designated * as a "Result" pursuant to the terms and conditions of the LINAGORA * - Université Joseph Fourier - Floralis research program. Each copyright * holder of Results enumerated here above fully & independently holds complete * ownership of the complete Intellectual Property rights applicable to the whole * of said Results, and may freely exploit it in any manner which does not infringe * the moral rights of the other copyright holders. * * 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 net.roboconf.messaging.rabbitmq.internal; import static net.roboconf.messaging.rabbitmq.RabbitMqConstants.RABBITMQ_SSL_AS_USER_DATA; import static net.roboconf.messaging.rabbitmq.RabbitMqConstants.RABBITMQ_SSL_KEY_MNGR_FACTORY; import static net.roboconf.messaging.rabbitmq.RabbitMqConstants.RABBITMQ_SSL_KEY_STORE_PASSPHRASE; import static net.roboconf.messaging.rabbitmq.RabbitMqConstants.RABBITMQ_SSL_KEY_STORE_PATH; import static net.roboconf.messaging.rabbitmq.RabbitMqConstants.RABBITMQ_SSL_KEY_STORE_TYPE; import static net.roboconf.messaging.rabbitmq.RabbitMqConstants.RABBITMQ_SSL_TRUST_MNGR_FACTORY; import static net.roboconf.messaging.rabbitmq.RabbitMqConstants.RABBITMQ_SSL_TRUST_STORE_PASSPHRASE; import static net.roboconf.messaging.rabbitmq.RabbitMqConstants.RABBITMQ_SSL_TRUST_STORE_PATH; import static net.roboconf.messaging.rabbitmq.RabbitMqConstants.RABBITMQ_SSL_TRUST_STORE_TYPE; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.logging.Logger; import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Channel; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.Recoverable; import net.roboconf.core.model.beans.Application; import net.roboconf.messaging.api.MessagingConstants; import net.roboconf.messaging.api.extensions.IMessagingClient; import net.roboconf.messaging.api.extensions.MessagingContext; import net.roboconf.messaging.api.extensions.MessagingContext.RecipientKind; import net.roboconf.messaging.api.jmx.RoboconfMessageQueue; import net.roboconf.messaging.api.messages.Message; import net.roboconf.messaging.api.reconfigurables.ReconfigurableClient; import net.roboconf.messaging.api.utils.MessagingUtils; import net.roboconf.messaging.api.utils.SerializationUtils; import net.roboconf.messaging.rabbitmq.RabbitMqConstants; import net.roboconf.messaging.rabbitmq.internal.impl.RoboconfConsumer; import net.roboconf.messaging.rabbitmq.internal.impl.RoboconfRecoveryListener; import net.roboconf.messaging.rabbitmq.internal.impl.RoboconfReturnListener; import net.roboconf.messaging.rabbitmq.internal.utils.RabbitMqUtils; /** * Common RabbitMQ client-related stuffs. * @author Pierre Bourret - Université Joseph Fourier * @author Vincent Zurczak - Linagora */ public class RabbitMqClient implements IMessagingClient { private final Logger logger = Logger.getLogger( getClass().getName()); private final Map<String,String> configuration; private final WeakReference<ReconfigurableClient<?>> reconfigurable; private RoboconfMessageQueue messageQueue; private RecipientKind ownerKind; private String applicationName, scopedInstancePath, domain; String consumerTag; Channel channel; /** * Constructor. * @param reconfigurable * @param messagingProperties */ protected RabbitMqClient( ReconfigurableClient<?> reconfigurable, Map<String,String> messagingProperties ) { this( reconfigurable, messagingProperties, reconfigurable.getOwnerKind()); } /** * Constructor. * @param reconfigurable * @param messagingProperties * @param ownerKind */ protected RabbitMqClient( ReconfigurableClient<?> reconfigurable, Map<String,String> messagingProperties, RecipientKind ownerKind ) { this.reconfigurable = new WeakReference<ReconfigurableClient<?>>( reconfigurable ); this.ownerKind = ownerKind; Map<String,String> copy = new LinkedHashMap<>( messagingProperties ); copy.put( MessagingConstants.MESSAGING_TYPE_PROPERTY, RabbitMqConstants.FACTORY_RABBITMQ ); this.configuration = Collections.unmodifiableMap( copy ); } /** * @return the wrapping reconfigurable client (may be {@code null}). */ public final ReconfigurableClient<?> getReconfigurableClient() { return this.reconfigurable.get(); } @Override public final void setMessageQueue( RoboconfMessageQueue messageQueue ) { this.messageQueue = messageQueue; } @Override public final synchronized boolean isConnected() { return this.channel != null; } @Override public final String getMessagingType() { return RabbitMqConstants.FACTORY_RABBITMQ; } @Override public final Map<String,String> getConfiguration() { // Filter SSL parameters so that there are not // written in user data if the configuration says so. Map<String,String> result = new HashMap<>( this.configuration ); if( "false".equalsIgnoreCase( this.configuration.get( RABBITMQ_SSL_AS_USER_DATA ))) { result.remove( RABBITMQ_SSL_KEY_STORE_PASSPHRASE ); result.remove( RABBITMQ_SSL_KEY_STORE_PATH ); result.remove( RABBITMQ_SSL_KEY_STORE_TYPE ); result.remove( RABBITMQ_SSL_KEY_MNGR_FACTORY ); result.remove( RABBITMQ_SSL_TRUST_MNGR_FACTORY ); result.remove( RABBITMQ_SSL_TRUST_STORE_PASSPHRASE ); result.remove( RABBITMQ_SSL_TRUST_STORE_PATH ); result.remove( RABBITMQ_SSL_TRUST_STORE_TYPE ); } else { result.put( RABBITMQ_SSL_AS_USER_DATA, "true" ); } return result; } @Override public void setOwnerProperties( RecipientKind ownerKind, String domain, String applicationName, String scopedInstancePath ) { this.ownerKind = ownerKind; this.applicationName = applicationName; this.scopedInstancePath = scopedInstancePath; this.domain = domain; this.logger.fine( "Owner properties changed to " + getId()); } @Override public void openConnection() throws IOException { // Already connected? Do nothing this.logger.info( getId() + " is opening a connection to RabbitMQ." ); if( isConnected()) { this.logger.info( getId() + " has already a connection to RabbitMQ." ); return; } // Initialize the connection ConnectionFactory factory = new ConnectionFactory(); RabbitMqUtils.configureFactory( factory, this.configuration ); this.channel = factory.newConnection().createChannel(); this.logger.info( getId() + " established a new connection with RabbitMQ. Channel # " + this.channel.getChannelNumber()); // Be notified when a message does not arrive in a queue (i.e. nobody is listening) this.channel.addReturnListener( new RoboconfReturnListener()); // Add a recoverable listener (when broken connections are recovered). // Given the way the RabbitMQ factory is configured, the channel should be "recoverable". ((Recoverable) this.channel).addRecoveryListener( new RoboconfRecoveryListener()); // Declare the exchanges. RabbitMqUtils.declareGlobalExchanges( this.domain, this.channel ); RabbitMqUtils.declareApplicationExchanges( this.domain, this.applicationName, this.channel ); // Declare the dedicated queue. String queueName = getQueueName(); this.channel.queueDeclare( queueName, true, false, true, null ); // Start listening to messages. RoboconfConsumer consumer = new RoboconfConsumer( getId(), this.channel, this.messageQueue ); consumer.handleConsumeOk( queueName ); this.consumerTag = this.channel.basicConsume( queueName, true, consumer ); this.logger.finer( "A new consumer tag was created: " + this.consumerTag ); } @Override public void closeConnection() throws IOException { StringBuilder sb = new StringBuilder( getId() + " is closing its connection to RabbitMQ." ); if( this.channel != null ) sb.append(" Channel # ").append(this.channel.getChannelNumber()); this.logger.info( sb.toString()); // Stop listening messages if( this.channel != null && this.channel.isOpen() && this.consumerTag != null ) { this.channel.basicCancel( this.consumerTag ); this.logger.finer( "A consumer tag was cancelled: " + this.consumerTag ); } // Close the connection this.consumerTag = null; if( isConnected()) { this.logger.finer( "Closing the connection and the channel # " + this.channel.getChannelNumber()); RabbitMqUtils.closeConnection( this.channel ); } this.channel = null; } @Override public void deleteMessagingServerArtifacts( Application application ) throws IOException { // We delete the application exchanges. There is only one now. // The DM and inter-applications stuff have each one a single exchange, shared by all the applications. this.channel.exchangeDelete( RabbitMqUtils.buildExchangeNameForAgent( this.domain, application.getName())); this.logger.fine( "Messaging artifacts were deleted for application " + application ); // Queues are deleted automatically by RabbitMQ. } @Override public void publish( MessagingContext ctx, Message msg ) throws IOException { // To which exchange? String exchangeName = RabbitMqUtils.buildExchangeName( ctx ); // With which routing key? String routingKey = ctx.getTopicName(); // Log a trace. this.logger.fine( "A message is about to be published to " + exchangeName + " with routing key = " + routingKey ); // Special case for the DM sending to the DM. // To prevent spamming and residual messages, messages sent by the DM // (to itself or its siblings) have a life span of 500 ms. If there is no // client connected during this period, the message will be dropped. BasicProperties props = null; if( ctx.getKind() == RecipientKind.DM ) { props = new BasicProperties.Builder().expiration( "500" ).build(); } // Do we want to be notified when a message is not delivered to anyone? // Yes, when the DM sends a message to an agent or when an agent sends a // message to the DM. If the message is not delivered to any queue, // the client will be notified (RoboconfReturnListener). boolean mandatory = false; if( this.ownerKind == RecipientKind.DM && this.ownerKind != ctx.getKind() || this.ownerKind == RecipientKind.AGENTS && ctx.getKind() == RecipientKind.DM ) mandatory = true; // Send the message. this.channel.basicPublish( exchangeName, // The exchange name routingKey, // The routing key mandatory, // Mandatory => we want it to be delivered false, // Useless, RabbitMQ does not support it for now. props, // The publish properties SerializationUtils.serializeObject( msg )); } @Override public void subscribe( MessagingContext ctx ) throws IOException { // Subscribing means creating a routing key between an exchange and a queue. String exchangeName = RabbitMqUtils.buildExchangeName( ctx ); String queueName = getQueueName(); this.logger.fine( "Binding queue " + queueName + " and exchange " + exchangeName + " with routing key = " + ctx.getTopicName()); this.channel.queueBind( queueName, exchangeName, ctx.getTopicName()); } @Override public void unsubscribe( MessagingContext ctx ) throws IOException { // Un-subscribing means deleting a routing key between an exchange and a queue. String exchangeName = RabbitMqUtils.buildExchangeName( ctx ); String queueName = getQueueName(); this.logger.fine( "Unbinding queue " + queueName + " and exchange " + exchangeName + " with routing key = " + ctx.getTopicName()); this.channel.queueUnbind( queueName, exchangeName, ctx.getTopicName()); } String getQueueName() { StringBuilder queueName = new StringBuilder(); queueName.append( this.domain ); queueName.append( "." ); if( this.ownerKind == RecipientKind.DM ) { queueName.append( "roboconf-dm" ); } else { queueName.append( this.applicationName ); queueName.append( "." ); queueName.append( MessagingUtils.escapeInstancePath( this.scopedInstancePath )); } return queueName.toString(); } String getId() { return MessagingUtils.buildId( this.ownerKind, this.domain, this.applicationName, this.scopedInstancePath ); } }