/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.nifi.jms.cf;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import javax.jms.ConnectionFactory;
import javax.net.ssl.SSLContext;
import org.apache.nifi.annotation.behavior.DynamicProperty;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.SeeAlso;
import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.annotation.lifecycle.OnDisabled;
import org.apache.nifi.annotation.lifecycle.OnEnabled;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.controller.AbstractControllerService;
import org.apache.nifi.controller.ConfigurationContext;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.reporting.InitializationException;
import org.apache.nifi.ssl.SSLContextService;
import org.apache.nifi.ssl.SSLContextService.ClientAuth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides a factory service that creates and initializes
* {@link ConnectionFactory} specific to the third party JMS system.
* <p>
* It accomplishes it by adjusting current classpath by adding to it the
* additional resources (i.e., JMS client libraries) provided by the user via
* {@link JMSConnectionFactoryProviderDefinition#CLIENT_LIB_DIR_PATH}, allowing
* it then to create an instance of the target {@link ConnectionFactory} based
* on the provided {@link JMSConnectionFactoryProviderDefinition#CONNECTION_FACTORY_IMPL}
* which can be than access via {@link #getConnectionFactory()} method.
* </p>
*/
@Tags({ "jms", "messaging", "integration", "queue", "topic", "publish", "subscribe" })
@CapabilityDescription("Provides a generic service to create vendor specific javax.jms.ConnectionFactory implementations. "
+ "ConnectionFactory can be served once this service is configured successfully")
@DynamicProperty(name = "The name of a Connection Factory configuration property.", value = "The value of a given Connection Factory configuration property.",
description = "The properties that are set following Java Beans convention where a property name is derived from the 'set*' method of the vendor "
+ "specific ConnectionFactory's implementation. For example, 'com.ibm.mq.jms.MQConnectionFactory.setChannel(String)' would imply 'channel' "
+ "property and 'com.ibm.mq.jms.MQConnectionFactory.setTransportType(int)' would imply 'transportType' property.")
@SeeAlso(classNames = { "org.apache.nifi.jms.processors.ConsumeJMS", "org.apache.nifi.jms.processors.PublishJMS" })
public class JMSConnectionFactoryProvider extends AbstractControllerService implements JMSConnectionFactoryProviderDefinition {
private final Logger logger = LoggerFactory.getLogger(JMSConnectionFactoryProvider.class);
private static final List<PropertyDescriptor> propertyDescriptors;
static {
propertyDescriptors = Collections.unmodifiableList(Arrays.asList(CONNECTION_FACTORY_IMPL, CLIENT_LIB_DIR_PATH, BROKER_URI, SSL_CONTEXT_SERVICE));
}
private volatile boolean configured;
private volatile ConnectionFactory connectionFactory;
/**
*
*/
@Override
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
return propertyDescriptors;
}
/**
*
*/
@Override
protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
return new PropertyDescriptor.Builder()
.description("Specifies the value for '" + propertyDescriptorName
+ "' property to be set on the provided ConnectionFactory implementation.")
.name(propertyDescriptorName).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).dynamic(true)
.build();
}
/**
*
* @return new instance of {@link ConnectionFactory}
*/
@Override
public ConnectionFactory getConnectionFactory() {
if (this.configured) {
return this.connectionFactory;
}
throw new IllegalStateException("ConnectionFactory can not be obtained unless "
+ "this ControllerService is configured. See onConfigure(ConfigurationContext) method.");
}
/**
*
*/
@OnEnabled
public void enable(ConfigurationContext context) throws InitializationException {
try {
if (!this.configured) {
if (logger.isInfoEnabled()) {
logger.info("Configuring " + this.getClass().getSimpleName() + " for '"
+ context.getProperty(CONNECTION_FACTORY_IMPL).evaluateAttributeExpressions().getValue() + "' to be connected to '"
+ BROKER_URI + "'");
}
// will load user provided libraries/resources on the classpath
Utils.addResourcesToClasspath(context.getProperty(CLIENT_LIB_DIR_PATH).evaluateAttributeExpressions().getValue());
this.createConnectionFactoryInstance(context);
this.setConnectionFactoryProperties(context);
}
this.configured = true;
} catch (Exception e) {
logger.error("Failed to configure " + this.getClass().getSimpleName(), e);
this.configured = false;
throw new IllegalStateException(e);
}
}
/**
*
*/
@OnDisabled
public void disable() {
this.connectionFactory = null;
this.configured = false;
}
/**
* This operation follows standard bean convention by matching property name
* to its corresponding 'setter' method. Once the method was located it is
* invoked to set the corresponding property to a value provided by during
* service configuration. For example, 'channel' property will correspond to
* 'setChannel(..) method and 'queueManager' property will correspond to
* setQueueManager(..) method with a single argument.
*
* There are also few adjustments to accommodate well known brokers. For
* example ActiveMQ ConnectionFactory accepts address of the Message Broker
* in a form of URL while IBMs in the form of host/port pair (more common).
* So this method will use value retrieved from the 'BROKER_URI' static
* property 'as is' if ConnectionFactory implementation is coming from
* ActiveMQ and for all others (for now) the 'BROKER_URI' value will be
* split on ':' and the resulting pair will be used to execute
* setHostName(..) and setPort(..) methods on the provided
* ConnectionFactory. This may need to be maintained and adjusted to
* accommodate other implementation of ConnectionFactory, but only for
* URL/Host/Port issue. All other properties are set as dynamic properties
* where user essentially provides both property name and value, The bean
* convention is also explained in user manual for this component with links
* pointing to documentation of various ConnectionFactories.
*
* @see #setProperty(String, String) method
*/
private void setConnectionFactoryProperties(ConfigurationContext context) {
for (final Entry<PropertyDescriptor, String> entry : context.getProperties().entrySet()) {
PropertyDescriptor descriptor = entry.getKey();
String propertyName = descriptor.getName();
if (descriptor.isDynamic()) {
this.setProperty(propertyName, entry.getValue());
} else {
if (propertyName.equals(BROKER)) {
String brokerValue = context.getProperty(descriptor).evaluateAttributeExpressions().getValue();
if (context.getProperty(CONNECTION_FACTORY_IMPL).evaluateAttributeExpressions().getValue().startsWith("org.apache.activemq")) {
this.setProperty("brokerURL", brokerValue);
} else {
String[] hostPort = brokerValue.split(":");
if (hostPort.length == 2) {
this.setProperty("hostName", hostPort[0]);
this.setProperty("port", hostPort[1]);
} else if (hostPort.length != 2) {
this.setProperty("serverUrl", brokerValue); // for tibco
} else {
throw new IllegalArgumentException("Failed to parse broker url: " + brokerValue);
}
}
SSLContextService sc = context.getProperty(SSL_CONTEXT_SERVICE).asControllerService(SSLContextService.class);
if (sc != null) {
SSLContext ssl = sc.createSSLContext(ClientAuth.NONE);
this.setProperty("sSLSocketFactory", ssl.getSocketFactory());
}
} // ignore 'else', since it's the only non-dynamic property that is relevant to CF configuration
}
}
}
/**
* Sets corresponding {@link ConnectionFactory}'s property to a
* 'propertyValue' by invoking a 'setter' method that corresponds to
* 'propertyName'. For example, 'channel' property will correspond to
* 'setChannel(..) method and 'queueManager' property will correspond to
* setQueueManager(..) method with a single argument.
*
* NOTE: There is a limited type conversion to accommodate property value
* types since all NiFi configuration properties comes as String. It is
* accomplished by checking the argument type of the method and executing
* its corresponding conversion to target primitive (e.g., value 'true' will
* go thru Boolean.parseBoolean(propertyValue) if method argument is of type
* boolean). None-primitive values are not supported at the moment and will
* result in {@link IllegalArgumentException}. It is OK though since based
* on analysis of several ConnectionFactory implementation the all seem to
* follow bean convention and all their properties using Java primitives as
* arguments.
*/
private void setProperty(String propertyName, Object propertyValue) {
String methodName = this.toMethodName(propertyName);
Method method = Utils.findMethod(methodName, this.connectionFactory.getClass());
if (method != null) {
try {
Class<?> returnType = method.getParameterTypes()[0];
if (String.class.isAssignableFrom(returnType)) {
method.invoke(this.connectionFactory, propertyValue);
} else if (int.class.isAssignableFrom(returnType)) {
method.invoke(this.connectionFactory, Integer.parseInt((String) propertyValue));
} else if (long.class.isAssignableFrom(returnType)) {
method.invoke(this.connectionFactory, Long.parseLong((String) propertyValue));
} else if (boolean.class.isAssignableFrom(returnType)) {
method.invoke(this.connectionFactory, Boolean.parseBoolean((String) propertyValue));
} else {
method.invoke(this.connectionFactory, propertyValue);
}
} catch (Exception e) {
throw new IllegalStateException("Failed to set property " + propertyName, e);
}
} else if (propertyName.equals("hostName")) {
this.setProperty("host", propertyValue); // try 'host' as another common convention.
}
}
/**
* Creates an instance of the {@link ConnectionFactory} from the provided
* 'CONNECTION_FACTORY_IMPL'.
*/
private void createConnectionFactoryInstance(ConfigurationContext context) {
String connectionFactoryImplName = context.getProperty(CONNECTION_FACTORY_IMPL).evaluateAttributeExpressions().getValue();
this.connectionFactory = Utils.newDefaultInstance(connectionFactoryImplName);
}
/**
* Will convert propertyName to a method name following bean convention. For
* example, 'channel' property will correspond to 'setChannel method and
* 'queueManager' property will correspond to setQueueManager method name
*/
private String toMethodName(String propertyName) {
char c[] = propertyName.toCharArray();
c[0] = Character.toUpperCase(c[0]);
return "set" + new String(c);
}
}