/*
* Copyright 2004,2005 The Apache Software Foundation.
*
* Licensed 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.synapse.transport.jms;
import org.apache.axiom.om.OMOutputFormat;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMText;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.util.UUIDGenerator;
import org.apache.axis2.AxisFault;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.description.TransportOutDescription;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.transport.TransportUtils;
import org.apache.axis2.transport.MessageFormatter;
import org.apache.axis2.transport.OutTransportInfo;
import org.apache.synapse.transport.base.AbstractTransportSender;
import org.apache.synapse.transport.base.BaseUtils;
import org.apache.synapse.transport.base.BaseConstants;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.commons.logging.LogFactory;
import javax.jms.*;
import javax.jms.Queue;
import javax.activation.DataHandler;
import javax.naming.Context;
import javax.naming.NamingException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
/**
* The TransportSender for JMS
*/
public class JMSSender extends AbstractTransportSender {
public static final String TRANSPORT_NAME = "jms";
/** A Map containing the JMS connection factories managed by this, keyed by name */
private Map connectionFactories = new HashMap();
public JMSSender() {
log = LogFactory.getLog(JMSSender.class);
}
/**
* Initialize the transport sender by reading pre-defined connection factories for
* outgoing messages. These will create sessions (one per each destination dealth with)
* to be used when messages are being sent.
* @param cfgCtx the configuration context
* @param transportOut the transport sender definition from axis2.xml
* @throws AxisFault on error
*/
public void init(ConfigurationContext cfgCtx, TransportOutDescription transportOut) throws AxisFault {
setTransportName(TRANSPORT_NAME);
super.init(cfgCtx, transportOut);
// read the connection factory definitions and create them
loadConnectionFactoryDefinitions(transportOut);
}
/**
* Get corresponding JMS connection factory defined within the transport sender for the
* transport-out information - usually constructed from a targetEPR
*
* @param trpInfo the transport-out information
* @return the corresponding JMS connection factory, if any
*/
private JMSConnectionFactory getJMSConnectionFactory(JMSOutTransportInfo trpInfo) {
if(trpInfo.getProperties() != null) {
String jmsConnectionFactoryName = (String) trpInfo.getProperties().get(JMSConstants.CONFAC_PARAM);
if(jmsConnectionFactoryName != null) {
return (JMSConnectionFactory) connectionFactories.get(jmsConnectionFactoryName);
}
}
Iterator cfNames = connectionFactories.keySet().iterator();
while (cfNames.hasNext()) {
String cfName = (String) cfNames.next();
JMSConnectionFactory cf = (JMSConnectionFactory) connectionFactories.get(cfName);
if (cf.equals(trpInfo)) {
return cf;
}
}
return null;
}
/**
* Performs the actual sending of the JMS message
*/
public void sendMessage(MessageContext msgCtx, String targetAddress,
OutTransportInfo outTransportInfo) throws AxisFault {
JMSConnectionFactory jmsConnectionFactory = null;
Connection connection = null; // holds a one time connection if used
JMSOutTransportInfo jmsOut = null;
Session session = null;
Destination destination = null;
Destination replyDestination = null;
try {
if (targetAddress != null) {
jmsOut = new JMSOutTransportInfo(targetAddress);
// do we have a definition for a connection factory to use for this address?
jmsConnectionFactory = getJMSConnectionFactory(jmsOut);
if (jmsConnectionFactory != null) {
// create new or get existing session to send to the destination from the CF
session = jmsConnectionFactory.getSessionForDestination(
JMSUtils.getDestination(targetAddress));
} else {
// digest the targetAddress and locate CF from the EPR
jmsOut.loadConnectionFactoryFromProperies();
try {
// create a one time connection and session to be used
Hashtable jndiProps = jmsOut.getProperties();
String user = (String) jndiProps.get(Context.SECURITY_PRINCIPAL);
String pass = (String) jndiProps.get(Context.SECURITY_CREDENTIALS);
QueueConnectionFactory qConFac = null;
TopicConnectionFactory tConFac = null;
ConnectionFactory conFac = null;
if (JMSConstants.DESTINATION_TYPE_QUEUE.equals(jmsOut.getDestinationType())) {
qConFac = (QueueConnectionFactory) jmsOut.getConnectionFactory();
} else if (JMSConstants.DESTINATION_TYPE_TOPIC.equals(jmsOut.getDestinationType())) {
tConFac = (TopicConnectionFactory) jmsOut.getConnectionFactory();
} else {
handleException("Unable to determine type of JMS " +
"Connection Factory - i.e Queue/Topic");
}
if (user != null && pass != null) {
if (qConFac != null) {
connection = qConFac.createQueueConnection(user, pass);
} else if (tConFac != null) {
connection = tConFac.createTopicConnection(user, pass);
}
} else {
if (qConFac != null) {
connection = qConFac.createQueueConnection();
} else if (tConFac != null) {
connection = tConFac.createTopicConnection();
}
}
if (JMSConstants.DESTINATION_TYPE_QUEUE.equals(jmsOut.getDestinationType())) {
session = ((QueueConnection)connection).
createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
} else if (JMSConstants.DESTINATION_TYPE_TOPIC.equals(jmsOut.getDestinationType())) {
session = ((TopicConnection)connection).
createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
}
} catch (JMSException e) {
handleException("Error creating a connection/session for : " + targetAddress);
}
}
destination = jmsOut.getDestination();
} else if (outTransportInfo != null && outTransportInfo instanceof JMSOutTransportInfo) {
jmsOut = (JMSOutTransportInfo) outTransportInfo;
jmsConnectionFactory = jmsOut.getJmsConnectionFactory();
session = jmsConnectionFactory.getSessionForDestination(
jmsOut.getDestination().toString());
destination = jmsOut.getDestination();
}
String replyDestName = (String) msgCtx.getProperty(JMSConstants.JMS_REPLY_TO);
if (replyDestName != null) {
if (jmsConnectionFactory != null) {
replyDestination = jmsConnectionFactory.getDestination(replyDestName);
} else {
replyDestination = jmsOut.getReplyDestination(replyDestName);
}
}
// now we are going to use the JMS session, but if this was a session from a
// defined JMS connection factory, we need to synchronize as sessions are not
// thread safe
synchronized(session) {
// convert the axis message context into a JMS Message that we can send over JMS
Message message = null;
String correlationId = null;
try {
message = createJMSMessage(msgCtx, session);
} catch (JMSException e) {
handleException("Error creating a JMS message from the axis message context", e);
}
String destinationType = jmsOut.getDestinationType();
// if the destination does not exist, see if we can create it
destination = JMSUtils.createDestinationIfRequired(
destination, destinationType, targetAddress, session);
// should we wait for a synchronous response on this same thread?
boolean waitForResponse = waitForSynchronousResponse(msgCtx);
// if this is a synchronous out-in, prepare to listen on the response destination
if (waitForResponse) {
replyDestination = JMSUtils.setReplyDestination(
replyDestination, session, message);
}
// send the outgoing message over JMS to the destination selected
JMSUtils.sendMessageToJMSDestination(session, destination, message);
// if we are expecting a synchronous response back for the message sent out
if (waitForResponse) {
try {
connection.start();
} catch (JMSException ignore) {}
try {
correlationId = message.getJMSMessageID();
} catch(JMSException ignore) {}
waitForResponseAndProcess(session, replyDestination, msgCtx, correlationId);
}
}
} finally {
if (connection != null) {
try {
connection.close();
} catch (JMSException ignore) {}
}
}
}
/**
* Create a Consumer for the reply destination and wait for the response JMS message
* synchronously. If a message arrives within the specified time interval, process it
* through Axis2
* @param session the session to use to listen for the response
* @param replyDestination the JMS reply Destination
* @param msgCtx the outgoing message for which we are expecting the response
* @throws AxisFault on error
*/
private void waitForResponseAndProcess(Session session, Destination replyDestination,
MessageContext msgCtx, String correlationId) throws AxisFault {
try {
MessageConsumer consumer = null;
if (replyDestination instanceof Queue) {
if (correlationId != null) {
consumer = ((QueueSession) session).createReceiver((Queue) replyDestination,
"JMSCorrelationID = '" + correlationId + "'");
} else {
consumer = ((QueueSession) session).createReceiver((Queue) replyDestination);
}
} else {
if (correlationId != null) {
consumer = ((TopicSession) session).createSubscriber((Topic) replyDestination,
correlationId, false);
} else {
consumer = ((TopicSession) session).createSubscriber((Topic) replyDestination);
}
}
// how long are we willing to wait for the sync response
long timeout = JMSConstants.DEFAULT_JMS_TIMEOUT;
String waitReply = (String) msgCtx.getProperty(JMSConstants.JMS_WAIT_REPLY);
if (waitReply != null) {
timeout = Long.valueOf(waitReply).longValue();
}
if (log.isDebugEnabled()) {
log.debug("Waiting for a maximum of " + timeout +
"ms for a response message to destination : " + replyDestination +
" with JMS correlation ID : " + correlationId);
}
Message reply = consumer.receive(timeout);
if (reply != null) {
processSyncResponse(msgCtx, reply);
} else {
log.warn("Did not receive a JMS response within " +
timeout + " ms to destination : " + replyDestination +
" with JMS correlation ID : " + correlationId);
}
} catch (JMSException e) {
handleException("Error creating consumer or receiving reply to : " +
replyDestination, e);
}
}
/**
* Create a JMS Message from the given MessageContext and using the given
* session
*
* @param msgContext the MessageContext
* @param session the JMS session
* @return a JMS message from the context and session
* @throws JMSException on exception
* @throws AxisFault on exception
*/
private Message createJMSMessage(MessageContext msgContext, Session session)
throws JMSException, AxisFault {
Message message = null;
String msgType = getProperty(msgContext, JMSConstants.JMS_MESSAGE_TYPE);
// check the first element of the SOAP body, do we have content wrapped using the
// default wrapper elements for binary (BaseConstants.DEFAULT_BINARY_WRAPPER) or
// text (BaseConstants.DEFAULT_TEXT_WRAPPER) ? If so, do not create SOAP messages
// for JMS but just get the payload in its native format
String jmsPayloadType = guessMessageType(msgContext);
if (jmsPayloadType == null) {
OMOutputFormat format = BaseUtils.getOMOutputFormat(msgContext);
MessageFormatter messageFormatter = null;
try {
messageFormatter = TransportUtils.getMessageFormatter(msgContext);
} catch (AxisFault axisFault) {
throw new JMSException("Unable to get the message formatter to use");
}
String contentType = messageFormatter.getContentType(
msgContext, format, msgContext.getSoapAction());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
messageFormatter.writeTo(msgContext, format, baos, true);
baos.flush();
} catch (IOException e) {
handleException("IO Error while creating BytesMessage", e);
}
if (msgType != null && JMSConstants.JMS_BYTE_MESSAGE.equals(msgType) ||
contentType.indexOf(HTTPConstants.HEADER_ACCEPT_MULTIPART_RELATED) > -1) {
message = session.createBytesMessage();
BytesMessage bytesMsg = (BytesMessage) message;
bytesMsg.writeBytes(baos.toByteArray());
} else {
message = session.createTextMessage(); // default
TextMessage txtMsg = (TextMessage) message;
txtMsg.setText(new String(baos.toByteArray()));
}
message.setStringProperty(BaseConstants.CONTENT_TYPE, contentType);
} else if (JMSConstants.JMS_BYTE_MESSAGE.equals(jmsPayloadType)) {
message = session.createBytesMessage();
BytesMessage bytesMsg = (BytesMessage) message;
OMElement wrapper = msgContext.getEnvelope().getBody().
getFirstChildWithName(BaseConstants.DEFAULT_BINARY_WRAPPER);
OMNode omNode = wrapper.getFirstOMChild();
if (omNode != null && omNode instanceof OMText) {
Object dh = ((OMText) omNode).getDataHandler();
if (dh != null && dh instanceof DataHandler) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
((DataHandler) dh).writeTo(baos);
} catch (IOException e) {
handleException("Error serializing binary content of element : " +
BaseConstants.DEFAULT_BINARY_WRAPPER, e);
}
bytesMsg.writeBytes(baos.toByteArray());
}
}
} else if (JMSConstants.JMS_TEXT_MESSAGE.equals(jmsPayloadType)) {
message = session.createTextMessage();
TextMessage txtMsg = (TextMessage) message;
txtMsg.setText(msgContext.getEnvelope().getBody().
getFirstChildWithName(BaseConstants.DEFAULT_TEXT_WRAPPER).getText());
}
// set the JMS correlation ID if specified
String correlationId = getProperty(msgContext, JMSConstants.JMS_COORELATION_ID);
if (correlationId == null && msgContext.getRelatesTo() != null) {
correlationId = msgContext.getRelatesTo().getValue();
}
if (correlationId != null) {
message.setJMSCorrelationID(correlationId);
}
if (msgContext.isServerSide()) {
// set SOAP Action as a property on the JMS message
setProperty(message, msgContext, BaseConstants.SOAPACTION);
} else {
String action = msgContext.getOptions().getAction();
if (action != null) {
message.setStringProperty(BaseConstants.SOAPACTION, action);
}
}
JMSUtils.setTransportHeaders(msgContext, message);
return message;
}
/**
* Guess the message type to use for JMS looking at the message contexts' envelope
* @param msgContext the message context
* @return JMSConstants.JMS_BYTE_MESSAGE or JMSConstants.JMS_TEXT_MESSAGE or null
*/
private String guessMessageType(MessageContext msgContext) {
OMElement firstChild = msgContext.getEnvelope().getBody().getFirstElement();
if (firstChild != null) {
if (BaseConstants.DEFAULT_BINARY_WRAPPER.equals(firstChild.getQName())) {
return JMSConstants.JMS_BYTE_MESSAGE;
} else if (BaseConstants.DEFAULT_TEXT_WRAPPER.equals(firstChild.getQName())) {
return JMSConstants.JMS_TEXT_MESSAGE;
}
}
return null;
}
/**
* Creates an Axis MessageContext for the received JMS message and
* sets up the transports and various properties
*
* @param outMsgCtx the outgoing message for which we are expecting the response
* @param message the JMS response message received
* @throws AxisFault on error
*/
private void processSyncResponse(MessageContext outMsgCtx, Message message) throws AxisFault {
MessageContext responseMsgCtx = createResponseMessageContext(outMsgCtx);
// load any transport headers from received message
JMSUtils.loadTransportHeaders(message, responseMsgCtx);
// workaround for Axis2 TransportUtils.createSOAPMessage() issue, where a response
// of content type "text/xml" is thought to be REST if !MC.isServerSide(). This
// question is still under debate and due to the timelines, I am commiting this
// workaround as Axis2 1.2 is about to be released and Synapse 1.0
responseMsgCtx.setServerSide(false);
String contentType = JMSUtils.getInstace().getProperty(message, BaseConstants.CONTENT_TYPE);
JMSUtils.getInstace().setSOAPEnvelope(message, responseMsgCtx, contentType);
responseMsgCtx.setServerSide(true);
handleIncomingMessage(
responseMsgCtx,
JMSUtils.getTransportHeaders(message),
JMSUtils.getInstace().getProperty(message, BaseConstants.SOAPACTION),
contentType
);
}
private void setProperty(Message message, MessageContext msgCtx, String key) {
String value = getProperty(msgCtx, key);
if (value != null) {
try {
message.setStringProperty(key, value);
} catch (JMSException e) {
log.warn("Couldn't set message property : " + key + " = " + value, e);
}
}
}
private String getProperty(MessageContext mc, String key) {
return (String) mc.getProperty(key);
}
/**
* Create JMSConnectionFactory instances for the definitions in the transport sender,
* and add these into our collection of connectionFactories map keyed by name
*
* @param transportOut the transport-in description for JMS
*/
private void loadConnectionFactoryDefinitions(TransportOutDescription transportOut) {
// iterate through all defined connection factories
Iterator conFacIter = transportOut.getParameters().iterator();
while (conFacIter.hasNext()) {
Parameter conFacParams = (Parameter) conFacIter.next();
JMSConnectionFactory jmsConFactory =
new JMSConnectionFactory(conFacParams.getName(), cfgCtx);
JMSUtils.setConnectionFactoryParameters(conFacParams, jmsConFactory);
try {
jmsConFactory.connectAndListen();
} catch (NamingException e) {
log.warn("Error looking up JMS connection factory : " + jmsConFactory.getName(), e);
} catch (JMSException e) {
log.warn("Error connecting to JMS connection factory : " + jmsConFactory.getName(), e);
}
connectionFactories.put(jmsConFactory.getName(), jmsConFactory);
}
}
}