com.rabbitmq.jms.admin.RMQObjectFactory Maven / Gradle / Ivy
/* Copyright (c) 2013-2023 Broadcom. All Rights Reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. */
package com.rabbitmq.jms.admin;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.rabbitmq.jms.client.AuthenticationMechanism;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.JMSException;
import jakarta.jms.Queue;
import jakarta.jms.Topic;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.spi.ObjectFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* JNDI Factory to create resources in containers such as Tomcat.
*
* An example Tomcat configuration for a {@link ConnectionFactory} would look like:
*
*
* <Resource name="jms/ConnectionFactory" type="jakarta.jms.ConnectionFactory"
* factory="com.rabbitmq.jms.admin.RMQObjectFactory"
* username="guest"
* password="guest"
* virtualHost="/"
* host="localhost"/>
*
* Alternatively, a AMQP uri can be used:
*
*
* <Resource name="jms/ConnectionFactory" type="jakarta.jms.ConnectionFactory"
* factory="com.rabbitmq.jms.admin.RMQObjectFactory"
* uri="amqp://guest:[email protected]"
*
*
* the type attribute can be {@link jakarta.jms.ConnectionFactory}, {@link jakarta.jms.QueueConnectionFactory},
* {@link jakarta.jms.TopicConnectionFactory} or the actual classname of the implementation,
* {@link com.rabbitmq.jms.admin.RMQConnectionFactory}.
*
*
* A destination, {@link Queue} or {@link Topic}, can be created using the following configuration ({@link Queue} first):
*
*
* <Resource name="jms/Queue" type="jakarta.jms.Queue"
* factory="com.rabbitmq.jms.admin.RMQObjectFactory"
* destinationName="queueName"/>
*
*
* and a {@link Topic} would be created thus:
*
*
* <Resource name="jms/Topic" type="jakarta.jms.Topic"
* factory="com.rabbitmq.jms.admin.RMQObjectFactory"
* destinationName="topicName"/>
*
*
* An example Wildfly configuration for a {@link ConnectionFactory} would look like:
*
*
* <object-factory name="java:global/jms/ConnectionFactory" module="org.jboss.genericjms.provider" class="com.rabbitmq.jms.admin.RMQObjectFactory">
* <environment>
* <property name="className" value="jakarta.jms.ConnectionFactory"/>
* <property name="username" value="guest"/>
* <property name="password" value="guest"/>
* <property name="virtualHost" value="/"/>
* <property name="host" value="localhost"/>
* </environment>
* </object-factory>
*
*
* Valid types are:
*
*
* jakarta.jms.ConnectionFactory
* jakarta.jms.QueueConnectionFactory
* jakarta.jms.TopicConnectionFactory
* jakarta.jms.Topic
* jakarta.jms.Queue
*
*
* Valid properties for a {@link ConnectionFactory} are:
*
*
* - uri
* - host
* - password
* - port
* - queueBrowserReadMax
* - onMessageTimeoutMs
* - channelsQos
* - ssl
* - terminationTimeout
* - username
* - virtualHost
* - className - only applies when properties are provided via environment HashTable
*
* and are applied in this order, if they are present. If a property is not present, or is not set by means of the
* uri
attribute, the default value is the same as that obtained by instantiating a
* {@link RMQConnectionFactory} object with the default constructor.
*
* Properties for a {@link Topic} or a {@link Queue} are:
*
*
* - destinationName
*
* TODO Implement socket options.
*/
public class RMQObjectFactory implements ObjectFactory {
private static final String ENV_CLASS_NAME = "className";
private static final Logger LOGGER = LoggerFactory.getLogger(RMQObjectFactory.class);
private final NamingStrategy namingStrategy = NamingStrategy.DEFAULT;
/**
* {@inheritDoc}
*/
@Override
public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable, ?> environment) throws Exception {
if ((obj == null) ) {
return null;
}
Reference ref = obj instanceof Reference ? (Reference) obj : null;
if (ref == null && (environment == null || environment.isEmpty())) {
throw new NamingException("Unable to instantiate object: obj is not a Reference instance and environment table is empty");
}
String className = ref!= null ? ref.getClassName(): (String) environment.get(ENV_CLASS_NAME);
if (className == null || className.trim().length() == 0) {
throw new NamingException("Unable to instantiate object: type has not been specified");
}
/*
* Valid class names are:
* jakarta.jms.ConnectionFactory
* jakarta.jms.QueueConnectionFactory
* jakarta.jms.TopicConnectionFactory
* com.rabbitmq.jms.admin.RMQConnectionFactory
* jakarta.jms.Topic
* jakarta.jms.Queue
* com.rabbitmq.jms.admin.RMQDestination
*
*/
boolean topic = false;
boolean connectionFactory = false;
if (isClassNameMatching(className)) {
connectionFactory = true;
} else if (jakarta.jms.Topic.class.getName().equals(className)) {
topic = true;
} else if (jakarta.jms.Queue.class.getName().equals(className)) {
} else if (RMQDestination.class.getName().equals(className)) {
topic = !getBooleanProperty(ref, environment, "isQueue", true, false);
} else {
throw new NamingException(String.format("Unknown class [%s]", className));
}
if (connectionFactory) {
return createConnectionFactory(ref, environment, name);
} else {
return createDestination(ref, environment, name, topic);
}
}
/**
* Creates a RMQConnectionFactory from a Reference or environment Hashtable
* @param ref the reference containing properties
* @param environment the environment containing properties
* @param name the name of the object
* @return a {@link RMQConnectionFactory} object configured
* @throws NamingException if a required property is missing or invalid
*/
public Object createConnectionFactory(Reference ref, Hashtable, ?> environment, Name name) throws NamingException {
LOGGER.trace("Creating connection factory ref '{}', name '{}'.", ref, name);
RMQConnectionFactory f = new RMQConnectionFactory();
try { // set uri first, which may fail if it doesn't parse
f.setUri(getStringProperty(ref, environment, "uri", true, f.getUri()));
} catch (JMSException e) {
LOGGER.warn("Failed to set RMQConnectionFactory properties by URI--defaults taken initially.", e);
}
String urisString = getStringProperty(ref, environment, "uris", true, null);
if (urisString != null) {
try {
f.setUris(Arrays.stream(urisString.split(",")).map(String::trim).collect(Collectors.toList()));
} catch (JMSException e) {
LOGGER.warn("Failed to set RMQConnectionFactory properties by URIs.", e);
}
}
// explicit properties (these override the uri, if set)
f.setHost (getStringProperty (ref, environment, "host", true, f.getHost() ));
f.setPassword (getStringProperty (ref, environment, "password", true, f.getPassword() ));
f.setPort (getIntProperty (ref, environment, "port", true, f.getPort() ));
f.setQueueBrowserReadMax(getIntProperty (ref, environment, "queueBrowserReadMax", true, f.getQueueBrowserReadMax()));
f.setOnMessageTimeoutMs (getIntProperty (ref, environment, "onMessageTimeoutMs", true, f.getOnMessageTimeoutMs() ));
f.setChannelsQos (getIntProperty (ref, environment, "channelsQos", true, f.getChannelsQos() ));
if (getBooleanProperty(ref, environment, "ssl", true, f.isSsl())) {
try {
f.useSslProtocol();
} catch (NoSuchAlgorithmException e) {
throw new NamingException("Error while enabling TLS: " + e.getMessage());
}
}
f.setTerminationTimeout (getLongProperty (ref, environment, "terminationTimeout", true, f.getTerminationTimeout() ));
f.setUsername (getStringProperty (ref, environment, "username", true, f.getUsername() ));
f.setVirtualHost (getStringProperty (ref, environment, "virtualHost", true, f.getVirtualHost() ));
f.setCleanUpServerNamedQueuesForNonDurableTopicsOnSessionClose(getBooleanProperty(ref, environment, "cleanUpServerNamedQueuesForNonDurableTopicsOnSessionClose", true, f.isCleanUpServerNamedQueuesForNonDurableTopicsOnSessionClose() ));
f.setDeclareReplyToDestination(getBooleanProperty(ref, environment, "declareReplyToDestination", true, true));
f.setKeepTextMessageType(getBooleanProperty(ref, environment, "keepTextMessageType", true, false));
f.setNackOnRollback(getBooleanProperty(ref, environment, "nackOnRollback", true, false));
String authenticationMechanismString = getStringProperty(ref, environment, "authenticationMechanism", true, null);
if (authenticationMechanismString != null) {
try {
f.setAuthenticationMechanism(AuthenticationMechanism.valueOf(authenticationMechanismString));
} catch (IllegalArgumentException e) {
LOGGER.warn("Failed to set AuthenticationMechanism on RMQConnectionFactory.", e);
}
}
return f;
}
/**
* Create a {@link RMQDestination} from a Reference of environment Hashtable
* @param ref the reference containing the properties
* @param environment the environment containing the properties
* @param name the name
* @param topic true if this is a topic, false if it is a queue (ignored if this is amqp-mapped)
* @return a {@link RMQDestination} object with the destinationName configured
* @throws NamingException if the destinationName
property is missing
*/
public Object createDestination(Reference ref, Hashtable, ?> environment, Name name, boolean topic) throws NamingException {
LOGGER.trace("Creating destination ref '{}', name '{}' (topic={}).", ref, name, topic);
String dname = getStringProperty(ref, environment, "destinationName", false, null);
Map queueDeclareArguments = convertQueueDeclareArguments(getMapProperty(
ref, environment, "queueDeclareArguments", true, null
));
boolean amqp = getBooleanProperty(ref, environment, "amqp", true, false);
if (amqp) {
if (queueDeclareArguments != null) {
LOGGER.warn("Queue declare arguments are ignored for AMQP destinations");
}
String amqpExchangeName = getStringProperty(ref, environment, "amqpExchangeName", true, null);
String amqpRoutingKey = getStringProperty(ref, environment,"amqpRoutingKey", true, null);
String amqpQueueName = getStringProperty(ref, environment, "amqpQueueName", true, null);
return new RMQDestination(dname, amqpExchangeName, amqpRoutingKey, amqpQueueName);
} else {
return new RMQDestination(dname, !topic, false, queueDeclareArguments, namingStrategy);
}
}
/**
* Returns the value of a set property in a reference
* @param ref the Reference containing the value
* @param environment the environment Hashtable containing the value
* @param propertyName the name of the property
* @param mayBeNull true if the property may be missing or contain a null value, in this case defaultValue
will be returned
* @param defaultValue the defaultValue to return if the property is null and mayBeNull==true
* @return the String value for the property
* @throws NamingException if the property is missing and mayBeNull==false
*/
private static String getStringProperty(Reference ref,
Hashtable, ?> environment,
String propertyName,
boolean mayBeNull,
String defaultValue) throws NamingException {
String content = propertyContent(ref, environment, propertyName, mayBeNull);
if (content == null) return defaultValue;
return content;
}
/**
* Reads a property from the reference and returns the boolean value it represents
* @param ref the Reference containing the value
* @param environment the environment Hashtable containing the value
* @param propertyName the name of the property
* @param mayBeNull true if the property may be missing or contain a null value, in this case defaultValue
will be returned
* @param defaultValue the defaultValue to return if the property is null and mayBeNull==true
* @return the boolean value of the property
* @throws NamingException if the property is missing and mayBeNull==false
*/
private static boolean getBooleanProperty(Reference ref,
Hashtable, ?> environment,
String propertyName,
boolean mayBeNull,
boolean defaultValue) throws NamingException {
String content = propertyContent(ref, environment, propertyName, mayBeNull);
if (content == null) return defaultValue;
return Boolean.valueOf(content);
}
/**
* Reads a property from the reference and returns the int value it represents
* @param ref the Reference containing the value
* @param environment the environment Hashtable containing the value
* @param propertyName the name of the property
* @param mayBeNull true if the property may be missing, in which case defaultValue
will be returned
* @param defaultValue the default value to return if mayBeNull
is set to true
* @return the integer value representing the property value
* @throws NamingException if the property is missing while mayBeNull is set to false, or a number format exception happened
*/
private static int getIntProperty(Reference ref,
Hashtable, ?> environment,
String propertyName,
boolean mayBeNull,
int defaultValue) throws NamingException {
String content = propertyContent(ref, environment, propertyName, mayBeNull);
if (content == null) return defaultValue;
try {
return Integer.parseInt(content);
} catch (Exception x) {
NamingException nx = new NamingException(String.format("Property [%s] is present but is not an integer value [%s]", propertyName, content));
nx.setRootCause(x);
throw nx;
}
}
/**
* Reads a property from the reference and returns the long integer value it represents
* @param ref the Reference containing the value
* @param environment the environment Hashtable containing the value
* @param propertyName the name of the property
* @param mayBeNull true if the property may be missing, in which case defaultValue
will be returned
* @param defaultValue the default value to return if mayBeNull
is set to true
* @return the long integer value representing the property value
* @throws NamingException if the property is missing while mayBeNull is set to false, or a number format exception happened
*/
private static long getLongProperty(Reference ref,
Hashtable, ?> environment,
String propertyName,
boolean mayBeNull,
long defaultValue) throws NamingException {
String content = propertyContent(ref, environment, propertyName, mayBeNull);
if (content == null) return defaultValue;
try {
return Long.parseLong(content);
} catch (Exception x) {
NamingException nx = new NamingException(String.format("Property [%s] is present but is not a long integer value [%s]", propertyName, content));
nx.setRootCause(x);
throw nx;
}
}
/**
* Reads a property from the reference and returns the map value it represents
* @param ref the Reference containing the value
* @param environment the environment Hashtable containing the value
* @param propertyName the name of the property
* @param mayBeNull true if the property may be missing, in which case defaultValue
will be returned
* @param defaultValue the default value to return if mayBeNull
is set to true
* @return the map value representing the property value
* @throws NamingException if the property is missing while mayBeNull is set to false, or a conversion exception happened
*/
private static Map getMapProperty(Reference ref,
Hashtable, ?> environment,
String propertyName,
boolean mayBeNull,
Map defaultValue) throws NamingException {
String content = propertyContent(ref, environment, propertyName, mayBeNull);
if (content == null) return defaultValue;
try {
Map result = new LinkedHashMap<>();
String [] entries = content.split(",");
for (String entry : entries) {
String [] keyValue = entry.split("=");
result.put(keyValue[0], keyValue[1]);
}
return result;
} catch (Exception x) {
NamingException nx = new NamingException(String.format("Property [%s] is present but is not a map value [%s]", propertyName, content));
nx.setRootCause(x);
throw nx;
}
}
private static String propertyStringContent(RefAddr ra) {
return (ra == null ? null : ra.getContent() == null ? null : ra.getContent().toString());
}
private static String environmentPropertyStringContent(Object propValue) {
return (propValue == null ? null : propValue.toString());
}
private static String propertyContent(Reference ref, Hashtable,?> environment, String propertyName, boolean mayBeNull) throws NamingException {
if (!mayBeNull && (ref == null || ref.get(propertyName) == null) && (environment == null || environment.get(propertyName) == null)) {
throw new NamingException(String.format("Property [%s] may not be null.", propertyName));
}
String content = ref != null ? propertyStringContent(ref.get(propertyName)) : environmentPropertyStringContent(environment.get(propertyName));
if (content == null && !mayBeNull) {
throw new NamingException(String.format("Property [%s] is present but is lacking a value.", propertyName));
}
return content;
}
// from rabbit_amqqueue:declare_args/0
private static final Map> QUEUE_DECLARE_ARGUMENTS_RULES =
new ConcurrentHashMap>(){{
put("x-expires", Integer::parseInt);
put("x-message-ttl", Integer::parseInt);
put("x-dead-letter-exchange", String::valueOf);
put("x-dead-letter-routing-key", String::valueOf);
put("x-dead-letter-strategy", String::valueOf);
put("x-max-length", Integer::parseInt);
put("x-max-length-bytes", Integer::parseInt);
put("x-max-in-memory-length", Integer::parseInt);
put("x-max-in-memory-bytes", Integer::parseInt);
put("x-max-priority", Integer::parseInt);
put("x-overflow", String::valueOf);
put("x-queue-mode", String::valueOf);
put("x-queue-version", Integer::valueOf);
put("x-single-active-consumer", Boolean::valueOf);
put("x-queue-type", String::valueOf);
put("x-quorum-initial-group-size", Integer::parseInt);
put("x-max-age", String::valueOf);
put("x-stream-max-segment-size-bytes", Integer::parseInt);
put("x-initial-cluster-size", Integer::parseInt);
put("x-queue-leader-locator", String::valueOf);
}};
private static Map convertQueueDeclareArguments(
Map queueDeclareArguments) {
if (queueDeclareArguments == null) {
return null;
}
LinkedHashMap result = new LinkedHashMap<>(queueDeclareArguments.size());
for (Entry entry : queueDeclareArguments.entrySet()) {
String key = entry.getKey();
String rawValue = entry.getValue();
Function conversionRule = QUEUE_DECLARE_ARGUMENTS_RULES.get(key);
Object convertedValue = rawValue;
try {
convertedValue = conversionRule == null ? rawValue : conversionRule.apply(rawValue);
} catch (Exception e) {
LOGGER.info("Could not convert queue declare argument {} = {} to appropriate type, "
+ "using string value. Error message: {}", key, rawValue, e.getMessage());
}
result.put(key, convertedValue);
}
return result;
}
/**
* Returns if the class name matches any of the connection factory names
* @param className
* @return the boolean value representing the result of conditional statement
*/
private static boolean isClassNameMatching(String className) {
return jakarta.jms.QueueConnectionFactory.class.getName().equals(className)
|| jakarta.jms.TopicConnectionFactory.class.getName().equals(className)
|| jakarta.jms.ConnectionFactory.class.getName().equals(className)
|| RMQConnectionFactory.class.getName().equals(className);
}
}