/* The contents of this file are subject to the license and copyright terms
* detailed in the license directory at the root of the source tree (also
* available online at http://fedora-commons.org/license/).
*/
package fedora.client.messaging;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import javax.naming.Context;
import javax.jms.Destination;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.Topic;
import org.apache.log4j.Logger;
import fedora.server.errors.MessagingException;
import fedora.server.messaging.JMSManager;
import fedora.server.messaging.JMSManager.DestinationType;
/**
* A messaging client which listens for messages via JMS.
*
* @author Bill Branan
*/
public class JmsMessagingClient implements MessagingClient, MessageListener {
private static final int MAX_RETRIES = 5;
private static final int RETRY_INTERVAL = 20000;
private String m_clientId;
private MessagingListener m_listener;
private Properties m_connectionProperties;
private String m_messageSelector;
private boolean m_durable;
private JMSManager m_jmsManager = null;
private boolean m_connected = false;
private Logger LOG = Logger.getLogger(JmsMessagingClient.class.getName());
/**
* Creates a messaging client
* @see JmsMessagingClient#JmsMessagingClient(String, MessagingListener, Properties, String, boolean)
*/
public JmsMessagingClient(String clientId,
MessagingListener listener,
Properties connectionProperties)
throws MessagingException {
this(clientId, listener, connectionProperties, "", false);
}
/**
* Creates a messaging client
* @see JmsMessagingClient#JmsMessagingClient(String, MessagingListener, Properties, String, boolean)
*/
public JmsMessagingClient(String clientId,
MessagingListener listener,
Properties connectionProperties,
boolean durable)
throws MessagingException {
this(clientId, listener, connectionProperties, "", durable);
}
/**
* Creates a messaging client
*
* <h4>Client ID</h4>
* <p>
* The clientId provides applications with the opportunity to create
* multiple messaging clients and track them based on this identifier.
* The clientId is used within the MessagingClient when creating a
* connection for durable subscriptions.
* </p>
*
* <h4>Message Listener</h4>
* <p>
* A listener, the onMessage() method of which will be called when a
* message arrives from the messaging provider. See the documentation
* for javax.jms.MessageListener for more information.
* </p>
*
* <h4>Connection Properties</h4>
*
* <p>All of the following properties must be included:</p>
* <table border="1">
* <th>Property</th>
* <th>Description</th>
* <th>Example Value</th>
* <tr>
* <td>java.naming.factory.initial (javax.naming.Context.INITIAL_CONTEXT_FACTORY)</td>
* <td>The JNDI initial context factory which will allow lookup of the other attributes</td>
* <td>org.apache.activemq.jndi.ActiveMQInitialContextFactory</td>
* </tr>
* <tr>
* <td>java.naming.provider.url (javax.naming.Context.PROVIDER_URL)</td>
* <td>The JNDI provider URL
* <td>tcp://localhost:61616</td>
* </tr>
* <tr>
* <td>connection.factory.name (fedora.server.messaging.JMSManager.CONNECTION_FACTORY)</td>
* <td>The JNDI name of the connection factory needed to create a JMS Connection</td>
* <td>ConnectionFactory</td>
* </tr>
* </table>
* <p>One or more of the following properties must be specified:</p>
* <table border="1">
* <th>Property Name</th>
* <th>Description</th>
* <th>Example Value</th>
* <tr>
* <td>topic.X (where X = name of subscription topic, example - topic.fedoraManagement)</td>
* <td>A topic over which notification messages will be provided</td>
* <td>fedora.apim.*</td>
* </tr>
* <tr>
* <td>queue.X (where X = name of subscription queue, example - queue.fedoraManagement)</td>
* <td>A queue through which notification messages will be provided</td>
* <td>fedora.apim.update</td>
* </tr>
* </table>
*
* <h4>Durable</h4>
* <p>
* Specifies whether the topics included in the connection properties should
* have durable consumers. If set to true, all topics listeners will be
* constructed as durable subscribers. If set to false, all topic listeners
* will be constructed as non-durable subscribers. This does not affect
* queue listeners.
* </p>
* <p>
* If there is a need for multiple topics, some of which are durable and some
* of which are not, then two MessagingClients should be created.
* One client would include topics needing durable subscribers and the other
* client would include topics not needing durable subscribers.
* A single MessageListener can be registered as the listener for both clients.
* </p>
*
* <h4>Message Selector</h4>
* <p>
* A JMS message selector allows a client to specify, by header field references
* and property references, the messages it is interested in. Only messages
* whose header and property values match the selector are delivered.
* See the javadoc for javax.jms.Message for more information about message selectors.
* </p>
*
* @param clientId identification value for this messaging client
* @param listener the listener which will be called when messages arrive
* @param connectionProperties set of properties necessary to connect to JMS provider
* @param messageSelector a selection which determines the messages to deliver
* @param durable determines if the underlying JMS subscribers are durable
* @throws MessagingException if listener is null or required properties are not set
*/
public JmsMessagingClient(String clientId,
MessagingListener listener,
Properties connectionProperties,
String messageSelector,
boolean durable)
throws MessagingException {
// Check for a null listener
if (listener == null) {
throw new MessagingException("MessageListener may not be null");
}
// Check for null properties
if (connectionProperties == null) {
throw new MessagingException("Connection properties may not be null");
}
// Check for required property values
String initialContextFactory =
connectionProperties.getProperty(Context.INITIAL_CONTEXT_FACTORY);
String providerUrl =
connectionProperties.getProperty(Context.PROVIDER_URL);
String connectionFactoryName =
connectionProperties.getProperty(JMSManager.CONNECTION_FACTORY_NAME);
if (initialContextFactory == null
|| providerUrl == null
|| connectionFactoryName == null) {
throw new MessagingException("Propery values for "
+ "'java.naming.factory.initial', "
+ "'java.naming.provider.url', and"
+ "'connection.factory.name' must be provided "
+ "in order to initialize a messaging client");
}
// Check for valid client ID if durable subscriptions are required
if (durable) {
if (clientId == null || clientId.equals("")) {
throw new MessagingException("ClientId must be "
+ "specified for durable subscriptions");
}
}
m_clientId = clientId;
m_listener = listener;
m_connectionProperties = connectionProperties;
m_messageSelector = messageSelector;
m_durable = durable;
}
/**
* Starts the MessagingClient. This method must be called
* in order to receive messages. Waits for completed connection.
*/
public void start() throws MessagingException {
start(true);
}
/**
* Starts the MessagingClient. This method must be called
* in order to receive messages.
*
* @param wait Set to true to wait until the startup process
* is complete before returning. Set to false to
* allow for asynchronous startup.
*/
public void start(boolean wait) throws MessagingException {
Thread connector = new JMSBrokerConnector();
connector.start();
if(wait) {
int maxWait = RETRY_INTERVAL * MAX_RETRIES;
int waitTime = 0;
while(!isConnected()) {
if(waitTime < maxWait) {
try {
Thread.sleep(100);
waitTime += 100;
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
} else {
throw new MessagingException("Timeout reached waiting " +
"for messaging client to start.");
}
}
}
}
public boolean isConnected() {
return m_connected;
}
/**
* Creates topics and queues and starts listeners
*/
private void createDestinations() throws MessagingException {
try {
// Create Destinations based on properties
Enumeration<?> propertyNames = m_connectionProperties.keys();
while (propertyNames.hasMoreElements()) {
String propertyName = (String) propertyNames.nextElement();
if (propertyName.startsWith("topic.")) {
String destinationName =
m_connectionProperties.getProperty(propertyName);
m_jmsManager.createDestination(destinationName,
DestinationType.Topic);
} else if (propertyName.startsWith("queue.")) {
String destinationName =
m_connectionProperties.getProperty(propertyName);
m_jmsManager.createDestination(destinationName,
DestinationType.Queue);
}
}
// Get destination list
List<Destination> destinations = m_jmsManager.getDestinations();
// If there are no Destinations, throw an exception
if (destinations.size() == 0) {
throw new MessagingException("No destinations available for "
+ "subscription, make sure that there is at least one topic "
+ "or queue specified in the connection properties.");
}
// Subscribe
for (Destination destination : destinations) {
if (m_durable && (destination instanceof Topic)) {
m_jmsManager.listenDurable((Topic) destination,
m_messageSelector,
this,
null);
} else {
m_jmsManager.listen(destination, m_messageSelector, this);
}
}
} catch (MessagingException me) {
LOG.error("MessagingException encountered attempting to start "
+ "Messaging Client: " + m_clientId
+ ". Exception message: " + me.getMessage(), me);
throw me;
}
}
/**
* Stops the MessagingClient, shuts down connections. If the unsubscribe
* parameter is set to true, all durable subscriptions will be removed.
*
* @param unsubscribe
*/
public void stop(boolean unsubscribe) throws MessagingException {
try {
if (unsubscribe) {
m_jmsManager.unsubscribeAllDurable();
}
m_jmsManager.close();
m_jmsManager = null;
m_connected = false;
} catch (MessagingException me) {
LOG.error("Messaging Exception encountered attempting to stop "
+ "Messaging Client: " + m_clientId
+ ". Exception message: " + me.getMessage(), me);
throw me;
}
}
/**
* Receives messages and passes them to the MessagingListener
* along with the client id.
*
* {@inheritDoc}
*/
public void onMessage(Message message) {
m_listener.onMessage(m_clientId, message);
}
/**
* Starts the connection to the JMS Broker. Retries on failure.
*/
private class JMSBrokerConnector extends Thread {
public void run() {
try {
connect();
createDestinations();
m_connected = true;
} catch (MessagingException me) {
throw new RuntimeException(me);
}
}
private void connect() throws MessagingException {
int retries = 0;
while(m_jmsManager == null && retries < MAX_RETRIES) {
try {
m_jmsManager = new JMSManager(m_connectionProperties, m_clientId);
} catch(MessagingException me) {
Throwable rootCause = me.getCause();
while(rootCause.getCause() != null) {
rootCause = rootCause.getCause();
}
if(rootCause instanceof java.net.ConnectException) {
try {
sleep(RETRY_INTERVAL);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
retries++;
} else {
throw me;
}
}
}
if(m_jmsManager == null) {
String errorMessage =
"Unable to start JMS Messaging Client, " + MAX_RETRIES +
" attempts were made, each attempt resulted in a " +
"java.net.ConnectException. The messaging broker at " +
m_connectionProperties.getProperty(Context.PROVIDER_URL) +
" is not available";
throw new RuntimeException(errorMessage);
}
}
}
}