org.milyn.routing.jms.JMSRouter Maven / Gradle / Ivy
/*
* Milyn - Copyright (C) 2006 - 2010
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License (version 2.1) as published
* by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE.
*
* See the GNU Lesser General Public License for more details:
* http://www.gnu.org/licenses/lgpl.txt
*/
package org.milyn.routing.jms;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Map;
import java.util.Properties;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.QueueBrowser;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.milyn.SmooksException;
import org.milyn.assertion.AssertArgument;
import org.milyn.cdr.SmooksConfigurationException;
import org.milyn.cdr.annotation.ConfigParam;
import org.milyn.cdr.annotation.ConfigParam.Use;
import org.milyn.container.ExecutionContext;
import org.milyn.delivery.annotation.Initialize;
import org.milyn.delivery.annotation.Uninitialize;
import org.milyn.delivery.annotation.VisitAfterIf;
import org.milyn.delivery.annotation.VisitBeforeIf;
import org.milyn.delivery.dom.DOMElementVisitor;
import org.milyn.delivery.sax.*;
import org.milyn.delivery.ordering.Consumer;
import org.milyn.routing.SmooksRoutingException;
import org.milyn.routing.jms.message.creationstrategies.MessageCreationStrategy;
import org.milyn.routing.jms.message.creationstrategies.StrategyFactory;
import org.milyn.routing.jms.message.creationstrategies.TextMessageCreationStrategy;
import org.milyn.util.FreeMarkerUtils;
import org.milyn.util.FreeMarkerTemplate;
import org.w3c.dom.Element;
/**
*
* Router is a Visitor for DOM or SAX elements. It sends the content
* as a JMS Message object to the configured destination.
*
* The type of the JMS Message is determined by the "messageType" config param.
*
* Example configuration:
*
* <resource-config selector="orderItems">
* <resource>org.milyn.routing.jms.JMSRouter</resource>
* <param name="beanId">beanId</param>
* <param name="destinationName">/queue/smooksRouterQueue</param>
* </resource-config>
* ....
* Optional parameters:
* <param name="executeBefore">true</param>
* <param name="jndiContextFactory">ConnectionFactory</param>
* <param name="jndiProviderUrl">jnp://localhost:1099</param>
* <param name="jndiNamingFactory">org.jboss.naming:java.naming.factory.url.pkgs=org.jnp.interfaces</param>
* <param name="connectionFactory">ConnectionFactory</param>
* <param name="deliveryMode">persistent</param>
* <param name="priority">10</param>
* <param name="timeToLive">100000</param>
* <param name="securityPrincipal">username</param>
* <param name="securityCredential">password</param>
* <param name="acknowledgeMode">AUTO_ACKNOWLEDGE</param>
* <param name="transacted">false</param>
* <param name="correlationIdPattern">orderitem-${order.orderId}-${order.orderItem.itemId}</param>
* <param name="messageType">ObjectMessage</param>
* <param name="highWaterMark">50</param>
* <param name="highWaterMarkTimeout">5000</param>
* <param name="highWaterMarkPollFrequency">500</param>
*
* Description of configuration properties:
*
* - jndiContextFactory: the JNDI ContextFactory to use.
*
- jndiProviderUrl: the JNDI Provider URL to use.
*
- jndiNamingFactory: the JNDI NamingFactory to use.
*
- connectionFactory: the ConnectionFactory to look up.
*
- deliveryMode: the JMS DeliveryMode. 'persistent'(default) or 'non-persistent'.
*
- priority: the JMS Priority to be used.
*
- timeToLive: the JMS Time-To-Live to be used.
*
- securityPrincipal: security principal use when creating the JMS connection.
*
- securityCredential: the security credentials to use when creating the JMS connection.
*
- acknowledgeMode: the acknowledge mode to use. One of 'AUTO_ACKNOWLEDGE'(default), 'CLIENT_ACKNOWLEDGE', 'DUPS_OK_ACKNOWLEDGE'.
*
- transacted: determines if the session should be transacted. Defaults to 'false'.
*
- correlationIdPattern: JMS Correlation pattern that will be used for the outgoing message. Supports templating.
*
- messageType: type of JMS Message that should be sent. 'TextMessage'(default), 'ObjectMessage' or 'MapMessage'.
*
- highWaterMark: max number of messages that can be sitting in the JMS Destination at any any time. Default is 200.
*
- highWaterMarkTimeout: number of ms to wait for the system to process JMS Messages from the JMS destination
* so that the number of JMS Messages drops below the highWaterMark. Default is 60000 ms.
*
- highWaterMarkPollFrequency: number of ms to wait between checks on the High Water Mark, while
* waiting for it to drop. Default is 1000 ms.
*
*
* @author Daniel Bevenius
* @since 1.0
*
*/
@VisitBeforeIf( condition = "parameters.containsKey('executeBefore') && parameters.executeBefore.value == 'true'")
@VisitAfterIf( condition = "!parameters.containsKey('executeBefore') || parameters.executeBefore.value != 'true'")
public class JMSRouter implements DOMElementVisitor, SAXVisitBefore, SAXVisitAfter, Consumer
{
/*
* Log instance
*/
private final Log logger = LogFactory.getLog( JMSRouter.class );
/*
* JNDI Properties holder
*/
private final JNDIProperties jndiProperties = new JNDIProperties();
/*
* JMS Properties holder
*/
private final JMSProperties jmsProperties = new JMSProperties();
/*
* BeanId is a key that is used to look up a bean
* in the execution context
*/
@ConfigParam( use = ConfigParam.Use.REQUIRED )
private String beanId;
@ConfigParam( use = ConfigParam.Use.OPTIONAL )
private String correlationIdPattern;
private FreeMarkerTemplate correlationIdTemplate;
@ConfigParam(defaultVal = "200")
private int highWaterMark = 200;
@ConfigParam(defaultVal = "60000")
private long highWaterMarkTimeout = 60000;
@ConfigParam(defaultVal = "1000")
private long highWaterMarkPollFrequency = 1000;
/*
* Strategy for JMS Message object creation
*/
private MessageCreationStrategy msgCreationStrategy = new TextMessageCreationStrategy();
/*
* JMS Destination
*/
private Destination destination;
/*
* JMS Connection
*/
private Connection connection;
/*
* JMS Message producer
*/
private MessageProducer msgProducer;
/*
* JMS Session
*/
private Session session;
@Initialize
public void initialize() throws SmooksConfigurationException, JMSException {
Context context = null;
boolean initialized = false;
if(beanId == null) {
throw new SmooksConfigurationException("Mandatory 'beanId' property not defined.");
}
if(jmsProperties.getDestinationName() == null) {
throw new SmooksConfigurationException("Mandatory 'destinationName' property not defined.");
}
try
{
if(correlationIdPattern != null) {
correlationIdTemplate = new FreeMarkerTemplate(correlationIdPattern);
}
Properties jndiContextProperties = jndiProperties.toProperties();
if(jndiContextProperties.isEmpty()) {
context = new InitialContext();
} else {
context = new InitialContext(jndiContextProperties);
}
destination = (Destination) context.lookup( jmsProperties.getDestinationName() );
msgProducer = createMessageProducer( destination, context );
setMessageProducerProperties( );
initialized = true;
}
catch (NamingException e)
{
final String errorMsg = "NamingException while trying to lookup [" + jmsProperties.getDestinationName() + "]";
logger.error( errorMsg, e );
throw new SmooksConfigurationException( errorMsg, e );
} finally {
if ( context != null )
{
try { context.close(); } catch (NamingException e) { logger.warn( "NamingException while trying to close initial Context"); }
}
if(!initialized) {
releaseJMSResources();
}
}
}
@Uninitialize
public void uninitialize() throws JMSException {
releaseJMSResources();
}
public boolean consumes(Object object) {
if(object.toString().startsWith(beanId)) {
// We use startsWith (Vs equals) so as to catch bean populations e.g. "address.street".
return true;
}
return false;
}
public void setBeanId(String beanId) {
AssertArgument.isNotNullAndNotEmpty(beanId, "beanId");
this.beanId = beanId;
}
public void setCorrelationIdPattern(String correlationIdPattern) {
this.correlationIdPattern = correlationIdPattern;
}
public void setHighWaterMark(int highWaterMark) {
this.highWaterMark = highWaterMark;
}
public void setHighWaterMarkTimeout(long highWaterMarkTimeout) {
this.highWaterMarkTimeout = highWaterMarkTimeout;
}
public void setHighWaterMarkPollFrequency(long highWaterMarkPollFrequency) {
this.highWaterMarkPollFrequency = highWaterMarkPollFrequency;
}
@ConfigParam ( use = Use.OPTIONAL )
public void setJndiContextFactory( final String contextFactory )
{
jndiProperties.setContextFactory( contextFactory );
}
@ConfigParam ( use = Use.OPTIONAL )
public void setJndiProperties(final String propertiesFile )
{
jndiProperties.setPropertiesFile( propertiesFile );
}
public void setJndiProperties(final Properties properties )
{
jndiProperties.setProperties(properties);
}
@ConfigParam ( use = Use.OPTIONAL )
public void setJndiProviderUrl(final String providerUrl )
{
jndiProperties.setProviderUrl( providerUrl );
}
@ConfigParam ( use = Use.OPTIONAL )
public void setJndiNamingFactoryUrl(final String pkgUrl )
{
jndiProperties.setNamingFactoryUrlPkgs( pkgUrl );
}
@ConfigParam ( use = Use.REQUIRED )
public void setDestinationName( final String destinationName )
{
AssertArgument.isNotNullAndNotEmpty(destinationName, "destinationName");
jmsProperties.setDestinationName( destinationName );
}
@ConfigParam ( choice = { "persistent", "non-persistent" }, defaultVal = "persistent", use = Use.OPTIONAL )
public void setDeliveryMode( final String deliveryMode )
{
jmsProperties.setDeliveryMode( deliveryMode );
}
@ConfigParam ( use = Use.OPTIONAL )
public void setTimeToLive( final long timeToLive )
{
jmsProperties.setTimeToLive( timeToLive );
}
@ConfigParam ( use = Use.OPTIONAL )
public void setSecurityPrincipal( final String securityPrincipal )
{
jmsProperties.setSecurityPrincipal( securityPrincipal );
}
@ConfigParam ( use = Use.OPTIONAL )
public void setSecurityCredential( final String securityCredential )
{
jmsProperties.setSecurityCredential( securityCredential );
}
@ConfigParam ( use = Use.OPTIONAL, defaultVal = "false" )
public void setTransacted( final boolean transacted )
{
jmsProperties.setTransacted( transacted );
}
@ConfigParam( defaultVal = "ConnectionFactory" , use = Use.OPTIONAL )
public void setConnectionFactoryName( final String connectionFactoryName )
{
jmsProperties.setConnectionFactoryName( connectionFactoryName );
}
@ConfigParam ( use = Use.OPTIONAL )
public void setPriority( final int priority )
{
jmsProperties.setPriority( priority );
}
@ConfigParam (defaultVal = "AUTO_ACKNOWLEDGE",
choice = {"AUTO_ACKNOWLEDGE", "CLIENT_ACKNOWLEDGE", "DUPS_OK_ACKNOWLEDGE" } )
public void setAcknowledgeMode( final String jmsAcknowledgeMode )
{
jmsProperties.setAcknowledgeMode( jmsAcknowledgeMode );
}
@ConfigParam (
defaultVal = StrategyFactory.TEXT_MESSAGE,
choice = { StrategyFactory.TEXT_MESSAGE , StrategyFactory.OBJECT_MESSAGE } )
public void setMessageType( final String messageType )
{
msgCreationStrategy = StrategyFactory.getInstance().createStrategy( messageType );
jmsProperties.setMessageType( messageType );
}
// Vistor methods
public void visitAfter( final Element element, final ExecutionContext execContext ) throws SmooksException
{
visit( execContext );
}
public void visitBefore( final Element element, final ExecutionContext execContext ) throws SmooksException
{
visit( execContext );
}
public void visitAfter( final SAXElement element, final ExecutionContext execContext ) throws SmooksException, IOException
{
visit( execContext );
}
public void visitBefore( final SAXElement element, final ExecutionContext execContext ) throws SmooksException, IOException
{
visit( execContext );
}
private void visit( final ExecutionContext execContext ) throws SmooksException {
Message message = msgCreationStrategy.createJMSMessage(beanId, execContext, session);
if(correlationIdTemplate != null) {
setCorrelationID(execContext, message);
}
sendMessage(message);
}
// Lifecycle
protected MessageProducer createMessageProducer( final Destination destination, final Context context ) throws JMSException {
try
{
final ConnectionFactory connFactory = (ConnectionFactory) context.lookup( jmsProperties.getConnectionFactoryName() );
connection = (jmsProperties.getSecurityPrincipal() == null && jmsProperties.getSecurityCredential() == null ) ?
connFactory.createConnection():
connFactory.createConnection( jmsProperties.getSecurityPrincipal(), jmsProperties.getSecurityCredential() );
session = connection.createSession( jmsProperties.isTransacted(),
AcknowledgeModeEnum.getAckMode( jmsProperties.getAcknowledgeMode().toUpperCase() ).getAcknowledgeModeInt() );
msgProducer = session.createProducer( destination );
connection.start();
logger.info ("JMS Connection started");
}
catch( JMSException e)
{
final String errorMsg = "JMSException while trying to create MessageProducer for Queue [" + jmsProperties.getDestinationName() + "]";
releaseJMSResources();
throw new SmooksConfigurationException( errorMsg, e );
}
catch (NamingException e)
{
final String errorMsg = "NamingException while trying to lookup ConnectionFactory [" + jmsProperties.getConnectionFactoryName() + "]";
releaseJMSResources();
throw new SmooksConfigurationException( errorMsg, e );
}
return msgProducer;
}
/**
* Sets the following MessageProducer properties:
*
* TimeToLive
* Priority
* DeliveryMode
*
*
* Subclasses may override this behaviour.
*/
protected void setMessageProducerProperties() throws SmooksConfigurationException
{
try
{
msgProducer.setTimeToLive( jmsProperties.getTimeToLive() );
msgProducer.setPriority( jmsProperties.getPriority() );
final int deliveryModeInt = "non-persistent".equals( jmsProperties.getDeliveryMode() ) ?
DeliveryMode.PERSISTENT : DeliveryMode.NON_PERSISTENT;
msgProducer.setDeliveryMode( deliveryModeInt );
}
catch (JMSException e)
{
final String errorMsg = "JMSException while trying to set JMS Header Fields";
throw new SmooksConfigurationException( errorMsg, e );
}
}
protected void sendMessage( final Message message ) throws SmooksRoutingException
{
try {
waitWhileAboveHighWaterMark();
} catch (JMSException e) {
throw new SmooksRoutingException("Exception while attempting to check JMS Queue High Water Mark.", e );
}
try
{
msgProducer.send( message );
}
catch (JMSException e)
{
final String errorMsg = "JMSException while sending Message.";
throw new SmooksRoutingException( errorMsg, e );
}
}
private void waitWhileAboveHighWaterMark() throws JMSException, SmooksRoutingException {
if(highWaterMark == -1) {
return;
}
if(session instanceof QueueSession) {
QueueSession queueSession = (QueueSession) session;
QueueBrowser queueBrowser = queueSession.createBrowser((Queue) destination);
try {
int length = getQueueLength(queueBrowser);
long start = System.currentTimeMillis();
if(logger.isDebugEnabled() && length >= highWaterMark) {
logger.debug("Length of JMS destination Queue '" + jmsProperties.getDestinationName() + "' has reached " + length + ". High Water Mark is " + highWaterMark + ". Waiting for Queue length to drop.");
}
while(length >= highWaterMark && (System.currentTimeMillis() < start + highWaterMarkTimeout)) {
try {
Thread.sleep(highWaterMarkPollFrequency);
} catch (InterruptedException e) {
logger.error("Interrupted", e);
return;
}
length = getQueueLength(queueBrowser);
}
// Check did the queue length drop below the HWM...
if(length >= highWaterMark) {
throw new SmooksRoutingException("Failed to route JMS message to Queue destination '" + ((Queue) destination).getQueueName() + "'. Timed out (" + highWaterMarkTimeout + " ms) waiting for queue length to drop below High Water Mark (" + highWaterMark + "). Consider increasing 'highWaterMark' and/or 'highWaterMarkTimeout' param values.");
}
} finally {
queueBrowser.close();
}
}
}
private int getQueueLength(QueueBrowser queueBrowser) throws JMSException {
int length = 0;
Enumeration queueEnum = queueBrowser.getEnumeration();
while(queueEnum.hasMoreElements()) {
length++;
queueEnum.nextElement();
}
return length;
}
protected void close( final Connection connection )
{
if ( connection != null )
{
try
{
connection.close();
}
catch (JMSException e)
{
final String errorMsg = "JMSException while trying to close connection";
logger.error( errorMsg, e );
}
}
}
protected void close( final Session session )
{
if ( session != null )
{
try
{
session.close();
}
catch (JMSException e)
{
final String errorMsg = "JMSException while trying to close session";
logger.error( errorMsg, e );
}
}
}
public Destination getDestination()
{
return destination;
}
public String getJndiContextFactory()
{
return jndiProperties.getContextFactory();
}
public String getJndiProviderUrl()
{
return jndiProperties.getProviderUrl();
}
public String getJndiNamingFactoryUrl()
{
return jndiProperties.getNamingFactoryUrlPkgs();
}
public String getDestinationName()
{
return jmsProperties.getDestinationName();
}
private void setCorrelationID(ExecutionContext execContext, Message message) {
Map beanMap = FreeMarkerUtils.getMergedModel(execContext);
String correlationId = correlationIdTemplate.apply(beanMap);
try {
message.setJMSCorrelationID(correlationId);
} catch (JMSException e) {
throw new SmooksException("Failed to set CorrelationID '" + correlationId + "' on message.", e);
}
}
public String getDeliveryMode()
{
return jmsProperties.getDeliveryMode();
}
public long getTimeToLive()
{
return jmsProperties.getTimeToLive();
}
public String getSecurityPrincipal()
{
return jmsProperties.getSecurityPrincipal();
}
public String getSecurityCredential()
{
return jmsProperties.getSecurityCredential();
}
public boolean isTransacted()
{
return jmsProperties.isTransacted();
}
public String getConnectionFactoryName()
{
return jmsProperties.getConnectionFactoryName();
}
public int getPriority()
{
return jmsProperties.getPriority();
}
public String getAcknowledgeMode()
{
return jmsProperties.getAcknowledgeMode();
}
public void setMsgCreationStrategy( final MessageCreationStrategy msgCreationStrategy )
{
this.msgCreationStrategy = msgCreationStrategy;
}
private void releaseJMSResources() throws JMSException {
if (connection != null) {
try {
try {
connection.stop();
} finally {
try {
closeProducer();
} finally {
closeSession();
}
}
} catch (JMSException e) {
logger.error("JMSException while trying to stop JMS Connection.", e);
} finally {
connection.close();
connection = null;
}
}
}
private void closeProducer() {
if (msgProducer != null) {
try {
msgProducer.close();
} catch (JMSException e) {
logger.error("JMSException while trying to close JMS Message Producer.", e);
} finally {
msgProducer = null;
}
}
}
private void closeSession() {
if (session != null) {
try {
session.close();
} catch (JMSException e) {
logger.error("JMSException while trying to close JMS Session.", e);
} finally {
session = null;
}
}
}
}