/******************************************************************************* * Copyright (C) 2015, MOHAMED-ALI SAID and International Business Machines * All Rights Reserved *******************************************************************************/ package com.ibm.streamsx.messaging.rabbitmq; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; import com.ibm.streams.operator.AbstractOperator; import com.ibm.streams.operator.OperatorContext; import com.ibm.streams.operator.StreamSchema; import com.ibm.streams.operator.OperatorContext.ContextCheck; import com.ibm.streams.operator.Type.MetaType; import com.ibm.streams.operator.compile.OperatorContextChecker; import com.ibm.streams.operator.logging.LogLevel; import com.ibm.streams.operator.logging.TraceLevel; import com.ibm.streams.operator.metrics.Metric; import com.ibm.streams.operator.model.CustomMetric; import com.ibm.streams.operator.model.Libraries; import com.ibm.streams.operator.model.Parameter; import com.ibm.streamsx.messaging.common.PropertyProvider; import com.rabbitmq.client.Address; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.Recoverable; @Libraries({ "opt/downloaded/*"/*, "@RABBITMQ_HOME@" */}) public class RabbitMQBaseOper extends AbstractOperator { protected Channel channel; protected Connection connection; protected String username = "", //$NON-NLS-1$ password = "", //$NON-NLS-1$ exchangeName = "", exchangeType = "direct"; //$NON-NLS-1$ //$NON-NLS-2$ protected List<String> hostAndPortList = new ArrayList<String>(); protected Address[] addressArr; private String vHost; private Boolean autoRecovery = true; private AtomicBoolean shuttingDown = new AtomicBoolean(false); protected boolean readyForShutdown = true; protected AttributeHelper messageHeaderAH = new AttributeHelper("message_header"), //$NON-NLS-1$ routingKeyAH = new AttributeHelper("routing_key"), //$NON-NLS-1$ messageAH = new AttributeHelper("message"); //$NON-NLS-1$ private final static Logger trace = Logger.getLogger(RabbitMQBaseOper.class .getCanonicalName()); protected Boolean usingDefaultExchange = false; private String URI = ""; //$NON-NLS-1$ private long networkRecoveryInterval = 5000; protected SynchronizedConnectionMetric isConnected; private Metric reconnectionAttempts; private String appConfigName = ""; //$NON-NLS-1$ private String userPropName; private String passwordPropName; /* * The method checkParametersRuntime validates that the reconnection policy * parameters are appropriate */ @ContextCheck(compile = false) public static void checkParametersRuntime(OperatorContextChecker checker) { if((checker.getOperatorContext().getParameterNames().contains("appConfigName"))) { //$NON-NLS-1$ String appConfigName = checker.getOperatorContext().getParameterValues("appConfigName").get(0); //$NON-NLS-1$ String userPropName = checker.getOperatorContext().getParameterValues("userPropName").get(0); //$NON-NLS-1$ String passwordPropName = checker.getOperatorContext().getParameterValues("passwordPropName").get(0); //$NON-NLS-1$ PropertyProvider provider = new PropertyProvider(checker.getOperatorContext().getPE(), appConfigName); String userName = provider.getProperty(userPropName); String password = provider.getProperty(passwordPropName); if(userName == null || userName.trim().length() == 0) { trace.log(LogLevel.ERROR, Messages.getString("PROPERTY_NOT_FOUND_IN_APP_CONFIG", userPropName, appConfigName)); //$NON-NLS-1$ checker.setInvalidContext( Messages.getString("PROPERTY_NOT_FOUND_IN_APP_CONFIG"), //$NON-NLS-1$ new Object[] {userPropName, appConfigName}); } if(password == null || password.trim().length() == 0) { trace.log(LogLevel.ERROR, Messages.getString("PROPERTY_NOT_FOUND_IN_APP_CONFIG", passwordPropName, appConfigName)); //$NON-NLS-1$ checker.setInvalidContext( Messages.getString("PROPERTY_NOT_FOUND_IN_APP_CONFIG"), //$NON-NLS-1$ new Object[] {passwordPropName, appConfigName}); } } } // add check for appConfig userPropName and passwordPropName @ContextCheck(compile = true) public static void checkParameters(OperatorContextChecker checker) { // Make sure if appConfigName is specified then both userPropName and passwordPropName are needed checker.checkDependentParameters("appConfigName", "userPropName", "passwordPropName"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ checker.checkDependentParameters("userPropName", "appConfigName", "passwordPropName"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ checker.checkDependentParameters("passwordPropName", "appConfigName", "userPropName"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } public synchronized void initialize(OperatorContext context) throws Exception { isConnected.setReconnectionAttempts(reconnectionAttempts); // Must call super.initialize(context) to correctly setup an operator. super.initialize(context); } protected boolean newCredentialsExist() { PropertyProvider propertyProvider = null; boolean newProperties = false; if (!getAppConfigName().isEmpty()) { OperatorContext context = getOperatorContext(); propertyProvider = new PropertyProvider(context.getPE(), getAppConfigName()); if (propertyProvider.contains(userPropName) && !username.equals(propertyProvider.getProperty(userPropName))) { newProperties = true; } if (propertyProvider.contains(passwordPropName) && !password.equals(propertyProvider.getProperty(passwordPropName))) { newProperties = true; } } trace.log(TraceLevel.INFO, "newPropertiesExist() is returning a value of: " + newProperties); //$NON-NLS-1$ return newProperties; } public void resetRabbitClient() throws KeyManagementException, MalformedURLException, NoSuchAlgorithmException, URISyntaxException, IOException, TimeoutException, InterruptedException, Exception { if (autoRecovery){ trace.log(TraceLevel.WARN, "Resetting Rabbit Client."); //$NON-NLS-1$ closeRabbitConnections(); initializeRabbitChannelAndConnection(); } else { trace.log(TraceLevel.INFO, "AutoRecovery was not enabled, so we are not resetting client."); //$NON-NLS-1$ } } /* * Setup connection and channel. If automatic recovery is enabled, we will reattempt * to connect every networkRecoveryInterval */ public void initializeRabbitChannelAndConnection() throws MalformedURLException, URISyntaxException, NoSuchAlgorithmException, KeyManagementException, IOException, TimeoutException, InterruptedException, OperatorShutdownException, FailedToConnectToRabbitMQException { do { try { ConnectionFactory connectionFactory = setupConnectionFactory(); // If we return from this without throwing an exception, then we // have successfully connected connection = setupNewConnection(connectionFactory, URI, addressArr, isConnected); channel = initializeExchange(); isConnected.setValue(1); trace.log(TraceLevel.INFO, "Initializing channel connection to exchange: " + exchangeName //$NON-NLS-1$ + " of type: " + exchangeType + " as user: " + connectionFactory.getUsername()); //$NON-NLS-1$ //$NON-NLS-2$ trace.log(TraceLevel.INFO, "Connection to host: " + connection.getAddress()); //$NON-NLS-1$ } catch (IOException | TimeoutException e) { e.printStackTrace(); trace.log(LogLevel.ERROR, Messages.getString("FAILED_TO_SETUP_CONNECTION", e.getMessage())); //$NON-NLS-1$ if (autoRecovery == true){ Thread.sleep(networkRecoveryInterval); } } } while (autoRecovery == true && (connection == null || channel == null) && !shuttingDown.get()); if (connection == null || channel == null){ throw new FailedToConnectToRabbitMQException(Messages.getString("FAILED_TO_INIT_CONNECTION_OR_CHANNEL_TO_SERVER")); //$NON-NLS-1$ } } private ConnectionFactory setupConnectionFactory() throws MalformedURLException, URISyntaxException, NoSuchAlgorithmException, KeyManagementException { ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setExceptionHandler(new RabbitMQConnectionExceptionHandler(isConnected)); connectionFactory.setAutomaticRecoveryEnabled(autoRecovery); if (autoRecovery){ connectionFactory.setNetworkRecoveryInterval(networkRecoveryInterval); } if (URI.isEmpty()){ configureUsernameAndPassword(connectionFactory); if (vHost != null) connectionFactory.setVirtualHost(vHost); addressArr = buildAddressArray(hostAndPortList); } else{ //use specified URI rather than username, password, vHost, hostname, etc if (!username.isEmpty() | !password.isEmpty() | vHost != null | !hostAndPortList.isEmpty()){ trace.log(TraceLevel.WARNING, "You specified a URI, therefore username, password" //$NON-NLS-1$ + ", vHost, and hostname parameters will be ignored."); //$NON-NLS-1$ } connectionFactory.setUri(URI); } return connectionFactory; } /* * Attempts to make a connection and throws an exception if it fails */ private Connection setupNewConnection(ConnectionFactory connectionFactory, String URI, Address[] addressArr, SynchronizedConnectionMetric isConnected2) throws IOException, TimeoutException, InterruptedException, OperatorShutdownException { Connection connection = null; connection = getConnection(connectionFactory, URI, addressArr); if (connectionFactory.isAutomaticRecoveryEnabled()) { ((Recoverable) connection).addRecoveryListener(new AutoRecoveryListener(isConnected2)); } return connection; } private Connection getConnection(ConnectionFactory connectionFactory, String URI, Address[] addressArr) throws IOException, TimeoutException { Connection connection; if (URI.isEmpty()){ connection = connectionFactory.newConnection(addressArr); trace.log(TraceLevel.INFO, "Creating a new connection based on an address list."); //$NON-NLS-1$ } else { connection = connectionFactory.newConnection(); trace.log(TraceLevel.INFO, "Creating a new connection based on a provided URI."); //$NON-NLS-1$ } return connection; } /* * Set the username and password either from the parameters provided or from * the appConfig. appConfig credentials have higher precedence than parameter credentials. */ private void configureUsernameAndPassword(ConnectionFactory connectionFactory) { // Lowest priority parameters first // We overwrite those values if we find them in the appConfig PropertyProvider propertyProvider = null; // Overwrite with appConfig values if present. if (!getAppConfigName().isEmpty()) { OperatorContext context = getOperatorContext(); propertyProvider = new PropertyProvider(context.getPE(), getAppConfigName()); if (propertyProvider.contains(userPropName)) { username = propertyProvider.getProperty(userPropName); } if (propertyProvider.contains(passwordPropName)) { password = propertyProvider.getProperty(passwordPropName); } } if (!username.isEmpty()) { connectionFactory.setUsername(username); trace.log(TraceLevel.INFO, "Set username."); //$NON-NLS-1$ } else { trace.log(TraceLevel.INFO, "Default username: " + connectionFactory.getUsername() ); //$NON-NLS-1$ } if (!password.isEmpty()) { connectionFactory.setPassword(password); trace.log(TraceLevel.INFO, "Set password."); //$NON-NLS-1$ } else { trace.log(TraceLevel.INFO, "Default password: " + connectionFactory.getPassword()); //$NON-NLS-1$ } } private Channel initializeExchange() throws IOException { Channel channel = connection.createChannel(); try{ //check to see if the exchange exists if not then it is the default exchange if ( !exchangeName.isEmpty()){ channel.exchangeDeclarePassive(exchangeName); trace.log(TraceLevel.INFO, "Exchange was found, therefore no exchange will be declared."); //$NON-NLS-1$ } else { usingDefaultExchange = true; trace.log(TraceLevel.INFO, "Using the default exchange. Name \"\""); //$NON-NLS-1$ } } catch (IOException e){ // if exchange doesn't exist, we will create it // we must also create a new channel since last one erred channel = connection.createChannel(); // declare non-durable, auto-delete exchange channel.exchangeDeclare(exchangeName, exchangeType, false, true, null); trace.log(TraceLevel.INFO, "Exchange was not found, therefore non-durable exchange will be declared."); //$NON-NLS-1$ } return channel; } private Address[] buildAddressArray(List<String> hostsAndPorts) throws MalformedURLException { Address[] addrArr = new Address[hostsAndPorts.size()]; int i = 0; for (String hostAndPort : hostsAndPorts){ URL tmpURL = new URL("http://" + hostAndPort); //$NON-NLS-1$ addrArr[i++] = new Address(tmpURL.getHost(), tmpURL.getPort()); trace.log(TraceLevel.INFO, "Adding: " + tmpURL.getHost() + ":"+ tmpURL.getPort()); //$NON-NLS-1$ //$NON-NLS-2$ } trace.log(TraceLevel.INFO, "Built address array: \n" + addrArr.toString()); //$NON-NLS-1$ return addrArr; } public void shutdown() throws Exception { shuttingDown.set(true); closeRabbitConnections(); // Need this to make sure that we return from the process method // before exiting shutdown while(!readyForShutdown){ Thread.sleep(100); } super.shutdown(); } private void closeRabbitConnections() { if (channel != null){ try { channel.close(); } catch (Exception e){ e.printStackTrace(); trace.log(LogLevel.ALL, Messages.getString("EXCEPTION_AT_CHANNEL_CLOSE", e.toString())); //$NON-NLS-1$ } finally { channel = null; } } if (connection != null){ try { connection.close(); } catch (Exception e) { e.printStackTrace(); trace.log(LogLevel.ALL, Messages.getString("EXCEPTION_AT_CONNECTION_CLOSE", e.toString())); //$NON-NLS-1$ } finally { connection = null; } } isConnected.setValue(0); } public void initSchema(StreamSchema ss) throws Exception { Set<MetaType> supportedTypes = new HashSet<MetaType>(); supportedTypes.add(MetaType.MAP); messageHeaderAH.initialize(ss, false, supportedTypes); supportedTypes.remove(MetaType.MAP); supportedTypes.add(MetaType.RSTRING); supportedTypes.add(MetaType.USTRING); routingKeyAH.initialize(ss, false, supportedTypes); supportedTypes.add(MetaType.BLOB); messageAH.initialize(ss, true, supportedTypes); } @Parameter(optional = true, description = "List of host and port in form: \\\"myhost1:3456\\\",\\\"myhost2:3456\\\".") public void setHostAndPort(List<String> value) { hostAndPortList.addAll(value); } @Parameter(optional = true, description = "Username for RabbitMQ authentication.") public void setUsername(String value) { username = value; } @Parameter(optional = true, description = "Password for RabbitMQ authentication.") public void setPassword(String value) { password = value; } @Parameter(optional = true, description = "This parameter specifies the name of application configuration that stores client credentials, " + "the property specified via application configuration is overridden by the application parameters. " + "The hierarchy of credentials goes: credentials from the appConfig beat out parameters (username and password). " + "The valid key-value pairs in the appConfig are <userPropName>=<username> and <passwordPropName>=<password>, where " + "<userPropName> and <passwordPropName> are specified by the corresponding parameters. " + "If the operator loses connection to the RabbitMQ server, or it fails authentication, it will " + "check for new credentials in the appConfig and attempt to reconnect if they exist. " + "The attempted reconnection will only take place if automaticRecovery is set to true (which it is by default).") public void setAppConfigName(String appConfigName) { this.appConfigName = appConfigName; } public String getAppConfigName() { return appConfigName; } @Parameter(optional = true, description = "This parameter specifies the property name of user name in the application configuration. If the appConfigName parameter is specified and the userPropName parameter is not set, a compile time error occurs.") public void setUserPropName(String userPropName) { this.userPropName = userPropName; } @Parameter(optional = true, description = "This parameter specifies the property name of password in the application configuration. If the appConfigName parameter is specified and the passwordPropName parameter is not set, a compile time error occurs.") public void setPasswordPropName(String passwordPropName) { this.passwordPropName = passwordPropName; } @Parameter(optional = true, description = "Optional attribute. Name of the RabbitMQ exchange type. Default direct.") public void setExchangeType(String value) { exchangeType = value; } @Parameter(optional = true, description = "Convenience URI of form: amqp://userName:password@hostName:portNumber/virtualHost. If URI is specified, you cannot specify username, password, and host.") public void setURI(String value) { URI = value; } @Parameter(optional = true, description = "Name of the attribute for the message. Default is \\\"message\\\".") public void setMessageAttribute(String value) { messageAH.setName(value); } @Parameter(optional = true, description = "Name of the attribute for the routing_key. Default is \\\"routing_key\\\".") public void setRoutingKeyAttribute(String value) { routingKeyAH.setName(value); } @Parameter(optional = true, description = "Name of the attribute for the message_header. Schema of type must be Map<ustring,ustring>. Default is \\\"message_header\\\".") public void setMsgHeaderAttribute(String value) { messageHeaderAH.setName(value); } @Parameter(optional = true, description = "Set Virtual Host. Default is null.") public void setVirtualHost(String value) { vHost = value; } @Parameter(optional = true, description = "Have connections to RabbitMQ automatically recovered. Default is true.") public void setAutomaticRecovery(Boolean value) { autoRecovery = value; } public Boolean getAutoRecovery() { return autoRecovery; } @Parameter(optional = true, description = "If automaticRecovery is set to true, this is the interval (in ms) that will be used between reconnection attempts. The default is 5000 ms.") public void setSetNetworkRecoveryInterval(long value) { networkRecoveryInterval = value; } @CustomMetric(name = "isConnected", kind = Metric.Kind.GAUGE, description = "Describes whether we are currently connected to the RabbitMQ server.") public void setIsConnectedMetric(Metric isConnected) { this.isConnected = new SynchronizedConnectionMetric(isConnected); } @CustomMetric(name = "reconnectionAttempts", kind = Metric.Kind.COUNTER, description = "The number of times we have attempted to reconnect since the last successful connection.") public void setReconnectionAttempts(Metric reconnectionAttempts) { this.reconnectionAttempts = reconnectionAttempts; } public static final String BASE_DESC = "\\n\\n**AppConfig**: " //$NON-NLS-1$ + "The hierarchy of credentials goes: credentials from the appConfig beat out parameters (username and password). " //$NON-NLS-1$ + "The valid key-value pairs in the appConfig are <userPropName>=<username> and <passwordPropName>=<password>, where " //$NON-NLS-1$ + "<userPropName> and <passwordPropName> are specified by the corresponding parameters. " //$NON-NLS-1$ + "This operator will only automatically recover with new credentials from the appConfig if automaticRecovery " //$NON-NLS-1$ + "is set to true. "; //$NON-NLS-1$ }