/**
* 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.cxf.transport.jms;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jms.Connection;
import javax.jms.Destination;
import javax.jms.ExceptionListener;
import javax.jms.JMSException;
import javax.jms.MessageListener;
import javax.jms.Session;
import org.apache.cxf.Bus;
import org.apache.cxf.buslifecycle.BusLifeCycleListener;
import org.apache.cxf.buslifecycle.BusLifeCycleManager;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.configuration.ConfigurationException;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageUtils;
import org.apache.cxf.security.SecurityContext;
import org.apache.cxf.transport.AbstractConduit;
import org.apache.cxf.transport.jms.util.JMSListenerContainer;
import org.apache.cxf.transport.jms.util.JMSSender;
import org.apache.cxf.transport.jms.util.JMSUtil;
import org.apache.cxf.transport.jms.util.MessageListenerContainer;
import org.apache.cxf.transport.jms.util.ResourceCloser;
import org.apache.cxf.ws.addressing.EndpointReferenceType;
/**
* JMSConduit is instantiated by the JMSTransportFactory which is selected by a client if the transport
* protocol starts with "jms:". JMSConduit converts CXF Messages to JMS Messages and sends the request
* over a queue or a topic.
* If the Exchange is not one way it then receives the response and converts it to
* a CXF Message. This is then provided in the Exchange and also sent to the IncomingObserver.
*/
public class JMSConduit extends AbstractConduit implements JMSExchangeSender, MessageListener {
static final Logger LOG = LogUtils.getL7dLogger(JMSConduit.class);
private static final String CORRELATED = JMSConduit.class.getName() + ".correlated";
private JMSConfiguration jmsConfig;
private Map<String, Exchange> correlationMap = new ConcurrentHashMap<String, Exchange>();
private JMSListenerContainer jmsListener;
private String conduitId;
private final AtomicLong messageCount = new AtomicLong(0);
private JMSBusLifeCycleListener listener;
private Bus bus;
private volatile Connection connection;
private volatile Destination staticReplyDestination;
public JMSConduit(EndpointReferenceType target,
JMSConfiguration jmsConfig,
Bus b) {
super(target);
bus = b;
this.jmsConfig = jmsConfig;
conduitId = UUID.randomUUID().toString().replaceAll("-", "");
}
/**
* Prepare the message to be sent. The message will be sent after the caller has written the payload to
* the OutputStream of the message and called the stream's close method. In the JMS case the
* JMSOutputStream will then call back the sendExchange method of this class. {@inheritDoc}
*/
public void prepare(final Message message) throws IOException {
boolean isTextPayload = JMSConstants.TEXT_MESSAGE_TYPE.equals(jmsConfig.getMessageType());
MessageStreamUtil.prepareStream(message, isTextPayload, this);
}
@Override
public void close(Message msg) throws IOException {
MessageStreamUtil.closeStreams(msg);
super.close(msg);
}
private Connection getConnection() throws JMSException {
Connection result = connection;
if (result == null) {
synchronized (this) {
result = connection;
if (result == null) {
result = JMSFactory.createConnection(jmsConfig);
trySetExListener(result);
result.start();
connection = result;
}
}
}
return result;
}
/**
* Register exception listener to react faster when a connection is reset.
*
* @param conn
*/
private void trySetExListener(Connection conn) {
try {
conn.setExceptionListener(new ExceptionListener() {
@Override
public void onException(JMSException exception) {
jmsConfig.resetCachedReplyDestination();
staticReplyDestination = null;
}
});
} catch (JMSException e) {
// setException is not supported on all providers
}
}
/**
* Send the JMS message and if the MEP is not oneway receive the response.
*
* @param exchange the Exchange containing the outgoing message
* @param request the payload of the outgoing JMS message
*/
public void sendExchange(final Exchange exchange, final Object request) {
LOG.log(Level.FINE, "JMSConduit send message");
final Message outMessage = exchange.getOutMessage() == null
? exchange.getOutFaultMessage()
: exchange.getOutMessage();
if (outMessage == null) {
throw new RuntimeException("Exchange to be sent has no outMessage");
}
jmsConfig.ensureProperlyConfigured();
assertIsNotTextMessageAndMtom(outMessage);
try (ResourceCloser closer = new ResourceCloser()) {
Connection c = getConnection();
Session session = closer.register(c.createSession(false,
Session.AUTO_ACKNOWLEDGE));
if (exchange.isOneWay()) {
sendMessage(request, outMessage, null, null, closer, session);
} else {
sendAndReceiveMessage(exchange, request, outMessage, closer, session);
}
} catch (JMSException e) {
// Close connection so it will be refreshed on next try
ResourceCloser.close(connection);
this.connection = null;
jmsConfig.resetCachedReplyDestination();
this.staticReplyDestination = null;
if (this.jmsListener != null) {
this.jmsListener.shutdown();
}
this.jmsListener = null;
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
// Ignore
}
throw JMSUtil.convertJmsException(e);
}
}
private void setupReplyDestination(Session session) throws JMSException {
if (staticReplyDestination == null) {
synchronized (this) {
if (staticReplyDestination == null) {
staticReplyDestination = jmsConfig.getReplyDestination(session);
String messageSelector = JMSFactory.getMessageSelector(jmsConfig, conduitId);
if (messageSelector == null && !jmsConfig.isPubSubDomain()) {
// Do not open listener without selector on a queue as we then can not share the queue.
// An option for this might be a good idea for people who do not plan to share queues.
return;
}
MessageListenerContainer container = new MessageListenerContainer(getConnection(),
staticReplyDestination,
this);
container.setMessageSelector(messageSelector);
Object executor = bus.getProperty(JMSFactory.JMS_CONDUIT_EXECUTOR);
if (executor instanceof Executor) {
container.setExecutor((Executor) executor);
}
container.start();
jmsListener = container;
addBusListener();
}
}
}
}
private void sendAndReceiveMessage(final Exchange exchange, final Object request, final Message outMessage,
ResourceCloser closer,
Session session) throws JMSException {
setupReplyDestination(session);
JMSMessageHeadersType headers = getOrCreateJmsHeaders(outMessage);
String userCID = headers.getJMSCorrelationID();
assertIsNotAsyncAndUserCID(exchange, userCID);
String correlationId = createCorrelationId(exchange, userCID);
if (correlationId != null) {
correlationMap.put(correlationId, exchange);
}
// Synchronize on exchange early to make sure we do not miss the notify
synchronized (exchange) {
String replyTo = headers.getJMSReplyTo();
String jmsMessageID = sendMessage(request, outMessage,
jmsConfig.getReplyToDestination(session, replyTo),
correlationId, closer, session);
Destination replyDestination = jmsConfig.getReplyDestination(session, replyTo);
boolean useSyncReceive = ((correlationId == null || userCID != null) && !jmsConfig.isPubSubDomain())
|| !replyDestination.equals(staticReplyDestination);
if (correlationId == null) {
correlationId = jmsMessageID;
correlationMap.put(correlationId, exchange);
}
if (!exchange.isSynchronous()) {
return;
}
try {
if (useSyncReceive) {
javax.jms.Message replyMessage = JMSUtil.receive(session, replyDestination,
correlationId,
jmsConfig.getReceiveTimeout(),
jmsConfig.isPubSubNoLocal());
processReplyMessage(exchange, replyMessage);
} else {
try {
exchange.wait(jmsConfig.getReceiveTimeout());
} catch (InterruptedException e) {
throw new JMSException("Interrupted while correlating " + e.getMessage());
}
if (!Boolean.TRUE.equals(exchange.get(CORRELATED))) {
throw new JMSException("Timeout receiving message with correlationId "
+ correlationId);
}
}
} finally {
correlationMap.remove(correlationId);
}
}
}
private String sendMessage(final Object request, final Message outMessage,
Destination replyToDestination, String correlationId,
ResourceCloser closer, Session session) throws JMSException {
JMSMessageHeadersType headers = getOrCreateJmsHeaders(outMessage);
javax.jms.Message message = JMSMessageUtils.asJMSMessage(jmsConfig,
outMessage,
request,
jmsConfig.getMessageType(),
session,
correlationId,
JMSConstants.JMS_CLIENT_REQUEST_HEADERS);
if (replyToDestination != null) {
message.setJMSReplyTo(replyToDestination);
}
JMSSender sender = JMSFactory.createJmsSender(jmsConfig, headers);
Destination targetDest = jmsConfig.getTargetDestination(session);
sender.sendMessage(session, targetDest, message);
String jmsMessageID = message.getJMSMessageID();
LOG.log(Level.FINE, "client sending request message "
+ jmsMessageID + " to " + targetDest);
headers.setJMSMessageID(jmsMessageID);
return jmsMessageID;
}
private void assertIsNotAsyncAndUserCID(Exchange exchange, String userCID) {
if (!exchange.isSynchronous() && userCID != null) {
throw new IllegalArgumentException("User CID can not be used for asynchronous exchanges");
}
}
private void assertIsNotTextMessageAndMtom(final Message outMessage) {
boolean isTextPayload = JMSConstants.TEXT_MESSAGE_TYPE.equals(jmsConfig.getMessageType());
if (isTextPayload && MessageUtils.isTrue(outMessage.getContextualProperty(
org.apache.cxf.message.Message.MTOM_ENABLED))
&& outMessage.getAttachments() != null && outMessage.getAttachments().size() > 0) {
org.apache.cxf.common.i18n.Message msg =
new org.apache.cxf.common.i18n.Message("INVALID_MESSAGE_TYPE", LOG);
throw new ConfigurationException(msg);
}
}
private String createCorrelationId(final Exchange exchange, String userCID) {
if (userCID != null) {
return userCID;
} else if (!jmsConfig.isSetConduitSelectorPrefix() && !jmsConfig.isReplyPubSubDomain()
&& exchange.isSynchronous()
&& (!jmsConfig.isUseConduitIdSelector())) {
// in this case the correlation id will be set to
// the message id later
return null;
} else {
String prefix = (jmsConfig.isUseConduitIdSelector()) ? jmsConfig.getConduitSelectorPrefix()
+ conduitId : jmsConfig.getConduitSelectorPrefix();
return JMSUtil.createCorrelationId(prefix, messageCount.incrementAndGet());
}
}
private JMSMessageHeadersType getOrCreateJmsHeaders(final Message outMessage) {
JMSMessageHeadersType headers = (JMSMessageHeadersType)outMessage
.get(JMSConstants.JMS_CLIENT_REQUEST_HEADERS);
if (headers == null) {
headers = new JMSMessageHeadersType();
outMessage.put(JMSConstants.JMS_CLIENT_REQUEST_HEADERS, headers);
}
return headers;
}
static class JMSBusLifeCycleListener implements BusLifeCycleListener {
final WeakReference<JMSConduit> ref;
BusLifeCycleManager blcm;
JMSBusLifeCycleListener(JMSConduit c, BusLifeCycleManager b) {
ref = new WeakReference<JMSConduit>(c);
blcm = b;
blcm.registerLifeCycleListener(this);
}
public void initComplete() {
}
public void postShutdown() {
}
public void preShutdown() {
unreg();
blcm = null;
JMSConduit c = ref.get();
if (c != null) {
c.listener = null;
c.close();
}
}
public void unreg() {
if (blcm != null) {
blcm.unregisterLifeCycleListener(this);
}
}
}
private synchronized void addBusListener() {
if (listener == null && bus != null) {
BusLifeCycleManager blcm = bus.getExtension(BusLifeCycleManager.class);
if (blcm != null) {
listener = new JMSBusLifeCycleListener(this,
blcm);
}
}
}
/**
* When a message is received on the reply destination the correlation map is searched for the
* correlationId. If it is found the message is converted to a CXF message and the thread sending the
* request is notified {@inheritDoc}
*/
public void onMessage(javax.jms.Message jmsMessage) {
try {
String correlationId = jmsMessage.getJMSCorrelationID();
LOG.log(Level.FINE, "Received reply message with correlation id " + correlationId);
Exchange exchange = getExchange(correlationId);
if (exchange == null) {
LOG.log(Level.WARNING, "Could not correlate message with correlationId " + correlationId);
} else {
processReplyMessage(exchange, jmsMessage);
}
} catch (JMSException e) {
throw JMSUtil.convertJmsException(e);
}
}
/**
* Try to correlate the incoming message with some timeout as it may have been
* added to the map after the message was sent
*
* @param correlationId
* @return exchange for correlationId or null if none was found
*/
private Exchange getExchange(String correlationId) {
int count = 0;
Exchange exchange = null;
while (exchange == null && count < 100) {
exchange = correlationMap.remove(correlationId);
if (exchange == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while correlating", e);
}
}
count++;
}
return exchange;
}
/**
* Process the reply message
* @throws JMSException
*/
protected void processReplyMessage(Exchange exchange, javax.jms.Message jmsMessage) throws JMSException {
LOG.log(Level.FINE, "client received reply: ", jmsMessage);
try {
Message inMessage = JMSMessageUtils.asCXFMessage(jmsMessage,
JMSConstants.JMS_CLIENT_RESPONSE_HEADERS);
if (jmsConfig.isCreateSecurityContext()) {
SecurityContext securityContext = SecurityContextFactory.buildSecurityContext(jmsMessage, jmsConfig);
inMessage.put(SecurityContext.class, securityContext);
}
exchange.setInMessage(inMessage);
Object responseCode = inMessage.get(org.apache.cxf.message.Message.RESPONSE_CODE);
exchange.put(org.apache.cxf.message.Message.RESPONSE_CODE, responseCode);
if (exchange.isSynchronous()) {
synchronized (exchange) {
exchange.put(CORRELATED, Boolean.TRUE);
exchange.notifyAll();
}
}
if (incomingObserver != null) {
incomingObserver.onMessage(exchange.getInMessage());
}
} catch (UnsupportedEncodingException ex) {
getLogger().log(Level.WARNING, "can't get the right encoding information " + ex);
}
}
private synchronized void shutdownListeners() {
if (listener != null) {
listener.unreg();
listener = null;
}
if (jmsListener != null) {
jmsListener.stop();
jmsListener.shutdown();
jmsListener = null;
staticReplyDestination = null;
}
}
public synchronized void close() {
shutdownListeners();
ResourceCloser.close(connection);
connection = null;
LOG.log(Level.FINE, "JMSConduit closed ");
}
protected Logger getLogger() {
return LOG;
}
public JMSConfiguration getJmsConfig() {
return jmsConfig;
}
public void setJmsConfig(JMSConfiguration jmsConfig) {
this.jmsConfig = jmsConfig;
}
protected static boolean isSetReplyTo(Message message) {
Boolean ret = (Boolean)message.get(JMSConstants.JMS_SET_REPLY_TO);
return ret == null || ret.booleanValue();
}
@Override
protected void finalize() throws Throwable {
close();
super.finalize();
}
}