/* * Copyright 2015-2016 the original author or 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 org.springframework.xd.dirt.integration.rabbit; import java.net.URI; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.amqp.AmqpException; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionListener; import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; import org.springframework.amqp.rabbit.connection.RoutingConnectionFactory; import org.springframework.core.io.Resource; import org.springframework.util.Assert; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.xd.dirt.integration.bus.RabbitManagementUtils; /** * A {@link RoutingConnectionFactory} that determines the node on which a queue is located and * returns a factory that connects directly to that node. * The RabbitMQ management plugin is called over REST to determine the node and the corresponding * address for that node is injected into the connection factory. * A single instance of each connection factory is retained in a cache. * If the location cannot be determined, the default connection factory is returned. This connection * factory is typically configured to connect to all the servers in a fail-over mode. * <p>{@link #getTargetConnectionFactory(Object)} is invoked by the * {@code SimpleMessageListenerContainer}, when establishing a connection, with the lookup key having * the format {@code '[queueName]'}. * <p>All {@link ConnectionFactory} methods delegate to the default * * @author Gary Russell * @author David Turanski * @since 1.2 */ public class LocalizedQueueConnectionFactory implements ConnectionFactory, RoutingConnectionFactory { private final Log logger = LogFactory.getLog(getClass()); private final Map<String, ConnectionFactory> nodeFactories = new HashMap<>(); private final ConnectionFactory defaultConnectionFactory; private final String[] addresses; private final String[] adminAdresses; private final String[] nodes; private final String vhost; private final String username; private final String password; private final boolean useSSL; private final Resource sslPropertiesLocation; private final String keyStore; private final String keyStorePassphrase; private final String trustStore; private final String trustStorePassphrase; /** * * @param defaultConnectionFactory the fallback connection factory to use if the queue can't be located. * @param addresses the rabbitmq server addresses (host:port, ...). * @param adminAddresses the rabbitmq admin addresses (http://host:port, ...) must * be the same length as addresses. * @param nodes the rabbitmq nodes corresponding to addresses (rabbit@server1, ...). * @param vhost the virtual host. * @param username the user name. * @param password the password. * @param useSSL flag to enable SSL. * @param sslPropertiesLocation location of SSL properties file. If this is set, the * following parameters are not used. * @param keyStore the key store location if not using properties file. * @param keyStorePassphrase the key store passphrase if not using properties file. * @param trustStore the trust store location if not using properties file. * @param truststorePassphrase the trust store passphrase if not using properties * file. */ public LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFactory, String[] addresses, String[] adminAddresses, String[] nodes, String vhost, String username, String password, boolean useSSL, Resource sslPropertiesLocation, String keyStore, String keyStorePassphrase, String trustStore, String truststorePassphrase) { Assert.isTrue(addresses.length == adminAddresses.length && addresses.length == nodes.length, "'addresses', 'adminAddresses', and 'nodes' properties must have equal length"); this.defaultConnectionFactory = defaultConnectionFactory; this.addresses = Arrays.copyOf(addresses, addresses.length); this.adminAdresses = Arrays.copyOf(adminAddresses, adminAddresses.length); this.nodes = Arrays.copyOf(nodes, nodes.length); this.vhost = vhost; this.username = username; this.password = password; this.useSSL = useSSL; this.sslPropertiesLocation = sslPropertiesLocation; this.keyStore = keyStore; this.keyStorePassphrase = keyStorePassphrase; this.trustStore = trustStore; this.trustStorePassphrase = truststorePassphrase; } @Override public Connection createConnection() throws AmqpException { return this.defaultConnectionFactory.createConnection(); } @Override public String getHost() { return this.defaultConnectionFactory.getHost(); } @Override public int getPort() { return this.defaultConnectionFactory.getPort(); } @Override public String getVirtualHost() { return this.vhost; } @Override public void addConnectionListener(ConnectionListener listener) { this.defaultConnectionFactory.addConnectionListener(listener); } @Override public boolean removeConnectionListener(ConnectionListener listener) { return this.defaultConnectionFactory.removeConnectionListener(listener); } @Override public void clearConnectionListeners() { this.defaultConnectionFactory.clearConnectionListeners(); } @Override public ConnectionFactory getTargetConnectionFactory(Object key) { String queue = ((String) key); queue = queue.substring(1, queue.length() - 1); ConnectionFactory connectionFactory = determineConnectionFactory(queue); if (connectionFactory == null) { return this.defaultConnectionFactory; } else { return connectionFactory; } } private ConnectionFactory determineConnectionFactory(String queue) { for (int i = 0; i < this.adminAdresses.length; i++) { String adminUri = this.adminAdresses[i]; RestTemplate template = createRestTemplate(adminUri); URI uri = UriComponentsBuilder.fromUriString(adminUri + "/api") .pathSegment("queues", "{vhost}", "{queue}") .buildAndExpand(this.vhost, queue).encode().toUri(); try { @SuppressWarnings("unchecked") Map<String, Object> queueInfo = template.getForObject(uri, Map.class); if (queueInfo != null) { String node = (String) queueInfo.get("node"); if (node != null) { for (int j = 0; j < this.nodes.length; j++) { if (this.nodes[j].equals(node)) { return nodeConnectionFactory(queue, j); } } } } } catch (Exception e) { logger.error("Failed to determine queue location for: " + queue + " at: " + uri.toString(), e); } } logger.warn("Failed to determine queue location for: " + queue); return null; } private synchronized ConnectionFactory nodeConnectionFactory(String queue, int index) throws Exception { String address = this.addresses[index]; String node = this.nodes[index]; if (logger.isDebugEnabled()) { logger.debug("Queue: " + queue + " is on node: " + node + " at: " + address); } ConnectionFactory cf = this.nodeFactories.get(node); if (cf == null) { if (logger.isDebugEnabled()) { logger.debug("Creating new connection factory for: " + address); } cf = createConnectionFactory(address); this.nodeFactories.put(node, cf); } return cf; } /** * Create a RestTemplate for the supplied URI. * @param adminUri the URI. * @return the template. */ protected RestTemplate createRestTemplate(String adminUri) { return RabbitManagementUtils.buildRestTemplate(adminUri, this.username, this.password); } /** * Create a dedicated connection factory for the address. * @param address the address to which the factory should connect. * @return the connection factory. * @throws Exception if errors occur during creation. */ protected ConnectionFactory createConnectionFactory(String address) throws Exception { RabbitConnectionFactoryBean rcfb = new RabbitConnectionFactoryBean(); rcfb.setUseSSL(this.useSSL); rcfb.setSslPropertiesLocation(this.sslPropertiesLocation); rcfb.setKeyStore(this.keyStore); rcfb.setKeyStorePassphrase(this.keyStorePassphrase); rcfb.setTrustStore(this.trustStore); rcfb.setKeyStorePassphrase(this.trustStorePassphrase); rcfb.afterPropertiesSet(); CachingConnectionFactory ccf = new CachingConnectionFactory(rcfb.getObject()); ccf.setAddresses(address); ccf.setUsername(this.username); ccf.setPassword(this.password); ccf.setVirtualHost(this.vhost); return ccf; } }