/*******************************************************************************
* Copyright (c) 2007-2009 Cambridge Semantics Incorporated.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Created by: Ben Szekely ( <a href="mailto:ben@cambridgesemantics.com">ben@cambridgesemantics.com </a>)
* Created on: Oct 10, 2007
*
* Contributors:
* Cambridge Semantics Incorporated - initial API and implementation
*******************************************************************************/
package org.openanzo.combus.bayeux;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TemporaryTopic;
import javax.jms.TextMessage;
import javax.jms.Topic;
import javax.servlet.ServletContextAttributeEvent;
import javax.servlet.ServletContextAttributeListener;
import org.apache.commons.lang.StringUtils;
import org.cometd.Bayeux;
import org.cometd.Channel;
import org.cometd.Client;
import org.cometd.Listener;
import org.cometd.SecurityPolicy;
import org.eclipse.jetty.util.ajax.JSON;
import org.openanzo.analysis.Profiler;
import org.openanzo.cache.ICacheProvider;
import org.openanzo.combus.IJmsProvider;
import org.openanzo.combus.MessageUtils;
import org.openanzo.datasource.IDatasource;
import org.openanzo.exceptions.AnzoException;
import org.openanzo.exceptions.LogUtils;
import org.openanzo.rdf.URI;
import org.openanzo.rdf.utils.Pair;
import org.openanzo.services.AnzoPrincipal;
import org.openanzo.services.IAuthenticationService;
import org.openanzo.services.IOperationContext;
import org.openanzo.services.impl.BaseOperationContext;
import org.openanzo.services.impl.ConfiguredCredentials;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
/**
* The BayeuxJMSBridge is essentially a translator between HTTP and JMS built on top of the Bayeux Comet protocol. Bayeux is a protocol and techniques that add
* bidirectional communication to HTTP. Anzo.JS clients connect to the BayeuxJMSBridgeServlet and the Bayeux implementation will send messages to this
* BayeuxJMSBridge.
*
* The BayeuxJMSBridge will keep one JMS connection that all clients will share. Authentication is handled by blocking access to the BayeuxJMSBridgeServlet via
* any authentication mechanism such as HTTP Basic Authentication, Form Authentication, etc. Authorization of the JMS messages is done by communicating with the
* Anzo authorization service and via the runAsUser functionality of services.
*
* For more information, see http://www.openanzo.org/projects/openanzo/wiki/CommunicationBusComet
*
* @author Ben Szekely (<a href="mailto:ben@cambridgesemantics.com">ben@cambridgesemantics.com</a>)
* @author Jordi Albornoz Mulligan (<a href="mailto:jordi@cambridgesemantics.com">jordi@cambridgesemantics.com</a>)
*
*/
class BayeuxJMSBridge implements ServletContextAttributeListener, BayeuxJMSConstants {
private static final Logger log = LoggerFactory.getLogger("org.openanzo.combus.bayeux.BayeuxJmsBridge");
private static final Profiler profiler = new Profiler();
private static final String SEND_BAYEUX_MESSAGE_TO_JMS = "sendBayeuxMessageToJMS";
private static final String CHECK_ACCESS_FOR_SUBSCRIPTION = "checkAccessForSubscription";
private static final String ACL_UPDATE_IN_BAYEUX_JMS_BRIDGE = "ACLUpdateInBayeuxJMSBridge";
private static final String GET_ROLES_FOR_USER = "GetRolesForUser";
private static final String AUTHENTICATE_SERVICE = "AuthenticateServiceUser";
private static final String PROTOCOL_VERSION = "1.1";
private static final int THREAD_POOL_SIZE_DEFAULT = 10;
private BridgeMessageListener bridgeListener = null;
private final DisconnectListener bayeuxDisconnectListener = new DisconnectListener();
private final BridgeConnectionManager bridgeConnectionManager;
private final AnzoPrincipal servicePrincipal;
private ThreadPoolExecutor workerPool;
protected BayeuxJMSBridge(IAuthenticationService authenticationService, IJmsProvider jmsProvider, ICacheProvider cacheProvider, Properties properties, ConfiguredCredentials credentials, IDatasource datasource) throws AnzoException {
this.bridgeConnectionManager = new BridgeConnectionManager(datasource, cacheProvider, credentials);
servicePrincipal = authenticationService.authenticateUser(new BaseOperationContext(AUTHENTICATE_SERVICE, BaseOperationContext.generateOperationId(), null), credentials.getUserName(), credentials.getPassword());
ConnectionFactory factory = jmsProvider.createConnectionFactory(properties);
bridgeConnectionManager.initialize(factory, properties);
log.info(LogUtils.COMBUS_MARKER, "JMS-Bayeux Bridge initialized.");
}
private synchronized void initialize(Bayeux bayeux, Properties properties) {
int threadPoolSize = THREAD_POOL_SIZE_DEFAULT;
Integer size = BayeuxBridgeDictionary.getThreadPoolSize(properties);
if (size != null) {
threadPoolSize = size;
}
log.info(LogUtils.LIFECYCLE_MARKER, "BayeuxBridge using thread pool size '{}'.", threadPoolSize);
workerPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadPoolSize);
if (!bayeux.hasChannel(CHANNEL_CONTROL)) {
Client bayeuxControlChannelClient = bayeux.newClient("control");
bayeuxControlChannelClient.addListener(new ControlListener(bayeux));
bayeux.getChannel(CHANNEL_CONTROL, true).subscribe(bayeuxControlChannelClient);
Client bayeuxBridgeChannelClient = bayeux.newClient("bridge");
bayeuxBridgeChannelClient.addListener(new BridgeListener(bayeux));
bayeux.getChannel(CHANNEL_BRIDGE, true).subscribe(bayeuxBridgeChannelClient);
if (log.isTraceEnabled()) {
log.trace(LogUtils.LIFECYCLE_MARKER, "BayeuxJMSBridge initialized with TRACE logging enabled. Adding MonitorListener to log all Bayeux messages.");
Client bayeuxAllChannelClient = bayeux.newClient("monitor");
bayeuxAllChannelClient.addListener(new MonitorListener());
bayeux.getChannel("/**", true).subscribe(bayeuxAllChannelClient);
}
bayeux.setSecurityPolicy(new BayeuxJMSSecurityPolicy());
bridgeListener = new BridgeMessageListener(bayeux);
}
}
protected void refreshThreadPoolSize(Dictionary<? extends Object, ? extends Object> configProperties) {
int threadPoolSize = 0;
Integer size = BayeuxBridgeDictionary.getThreadPoolSize(configProperties);
if (size != null) {
threadPoolSize = Integer.parseInt(size.toString());
}
workerPool.setCorePoolSize(threadPoolSize);
workerPool.setMaximumPoolSize(threadPoolSize);
}
/**
* Called when the service container component's (CometdServletEndpoint) stop method is called. This destroys all of the JMS connection state and Bayeux
* connection state.
*
* @param bundleStopping
* bundle stopping
* @throws AnzoException
*/
protected void stop(boolean bundleStopping) throws AnzoException {
bridgeConnectionManager.destroy(bundleStopping);
}
/**
* Called when the service container component's (CometdServletEndpoint) reset method is called.
*/
protected void resetStarting() {
}
protected void resetFinished() {
}
protected void reset() {
bridgeConnectionManager.reset();
}
public void attributeAdded(ServletContextAttributeEvent scab) {
Properties properties = (Properties) scab.getServletContext().getAttribute("initParams");
if (scab.getName().equals(Bayeux.ATTRIBUTE)) {
Bayeux bayeux = (Bayeux) scab.getValue();
initialize(bayeux, properties);
}
}
/**
* Returns the id of the Bayeux channel used to send responses to the current user's particular client. The authenticated username is pulled from the given
* principal.
*/
private String getReplyChannelId(AnzoPrincipal principal, Client fromClient) {
String username = principal.getName();
return getReplyChannelId(username, fromClient.getId());
}
/**
* Returns the id of the Bayeux channel used to send responses to given username and client id. The Channel ID is essentially a path which concatenates the
* username and client id along with a prefix.
*/
private static String getReplyChannelId(String username, String clientId) {
String replyChannelId = CHANNEL_USER_PREFIX + username + "/" + clientId;
return replyChannelId;
}
private static void publishIfChannelExists(Bayeux bayeux, String channelId, Client fromClient, Object data, String msgId) {
Channel replyChannel = bayeux.getChannel(channelId, false);
if (replyChannel == null) {
log.warn(LogUtils.COMBUS_MARKER, "sendError - Bayeux reply channel {} missing.", channelId);
} else {
replyChannel.publish(fromClient, data, "R" + msgId);
}
}
private void sendError(Bayeux bayeux, Client fromClient, Object data, String msgId, AnzoPrincipal principal, String status, String errorMsg) {
Map<?, ?> obj = (Map<?, ?>) data;
String replyChannelId = getReplyChannelId(principal, fromClient);
Map<String, Object> replyData = new HashMap<String, Object>();
replyData.put(CONTROL_MSG_STATUS, status);
if (errorMsg != null) {
replyData.put(CONTROL_MSG_ERROR_MESSSAGE, errorMsg);
}
if (obj.get(CONTROL_MSG_CORRELATION_ID) != null) {
replyData.put(CONTROL_MSG_CORRELATION_ID, obj.get(CONTROL_MSG_CORRELATION_ID).toString());
} else {
Map<?, ?> props = (Map<?, ?>) obj.get(JMS_MSG_PROPERTIES);
if (props != null) {
if (props.get(JMS_MSG_PROPERTY_CORRELATION_ID) != null) {
Map<String, String> p = new HashMap<String, String>();
replyData.put(JMS_MSG_PROPERTIES, p);
p.put(JMS_MSG_PROPERTY_CORRELATION_ID, (String) props.get(JMS_MSG_PROPERTY_CORRELATION_ID));
}
}
}
publishIfChannelExists(bayeux, replyChannelId, fromClient, replyData, "R" + msgId);
}
/**
* Called whenever any graph's read privilege is removed for any role. We need to go through our graph update subscriptions when this happens to kick out
* any user that no longer has access to get those messages.
*
* @param namedGraphUri
* the URI of the graph whose read access changed.
* @param role
* the role who's read access to the graph was removed.
*/
protected void namedGraphReadPrivilegeRemoved(URI namedGraphUri, URI role) {
IOperationContext opContext = null;
try {
opContext = new BaseOperationContext(ACL_UPDATE_IN_BAYEUX_JMS_BRIDGE, ACL_UPDATE_IN_BAYEUX_JMS_BRIDGE + namedGraphUri, servicePrincipal);
opContext.setMDC();
bridgeConnectionManager.removeUnauthorizedSubscriptions(namedGraphUri, role, opContext);
} catch (AnzoException e) {
log.error(LogUtils.COMBUS_MARKER, "Error removing unauthorized subscriptions for graph " + namedGraphUri, e);
} finally {
if (opContext != null) {
opContext.clearMDC();
}
}
}
/**
* The Bayeux listener received messages sent to the 'control' channel. Typically those are things like connect, topic subscription, etc.
*/
private class ControlListener implements Listener {
private static final String ERROR_MSG_GRAPH_SUBSCRIBE_PREFIX = "Could not subscribe to graph update events for graph:";
private static final String ERROR_MSG_GRAPH_UNSUBSCRIBE_PREFIX = "Could not unsubscribe from graph update events for graph:";
final Bayeux bayeux;
protected ControlListener(Bayeux bayeux) {
this.bayeux = bayeux;
}
public void removed(String id, boolean timeout) {
log.debug(LogUtils.COMBUS_MARKER, "User removed (control).");
}
public void deliver(final Client fromClient, final Client toClient, org.cometd.Message msg) {
if (log.isTraceEnabled()) {
log.trace(LogUtils.COMBUS_MARKER, "Control message received:{}", msg.toString());
}
final AnzoPrincipal principal = PrincipalFilter.getPrincipal(); // This is dependent on the current request thread so we get this now rather than in the worker thread
// Grab the parts of the message that we need now since the message object may be recycled by CometD for another request
// by the time the worker task executes.
final HashMap<?, ?> data = (HashMap<?, ?>) msg.getData();
final String msgId = msg.getId();
final String type = (String) data.get(CONTROL_MSG_CONTROL_TYPE);
final String clientId = fromClient.getId();
final String replyChannelId = getReplyChannelId(principal.getName(), clientId);
final String correlationId = data.get(CONTROL_MSG_CORRELATION_ID).toString();
workerPool.execute(new Runnable() {
public void run() {
if (type.equals(CONTROL_TYPE_CONNECT)) {
handleBridgeConnect(fromClient, data, msgId, principal, clientId, replyChannelId, correlationId);
} else if (type.equals(CONTROL_TYPE_TOPIC_SUBSCRIBE)) {
handleTopicSubscriptionOperation(true, fromClient, data, msgId, principal, clientId, replyChannelId, correlationId);
} else if (type.equals(CONTROL_TYPE_TOPIC_UNSUBSCRIBE)) {
handleTopicSubscriptionOperation(false, fromClient, data, msgId, principal, clientId, replyChannelId, correlationId);
} else {
String errorMsg = "Unknown control message: " + type;
log.error(LogUtils.COMBUS_MARKER, errorMsg);
sendError(bayeux, fromClient, data, msgId, principal, STATUS_BAD_REQUEST, errorMsg);
}
}
});
}
/**
* Handle a control channel message to connect create a JMS session for the client. The response to the request will contain the user's user and role
* information.
*
* @param fromClient
* @param data
* @param msgId
* @param username
* @param clientId
* @param replyChannelId
* @param correlationId
*/
private void handleBridgeConnect(Client fromClient, HashMap<?, ?> data, String msgId, AnzoPrincipal principal, String clientId, String replyChannelId, String correlationId) {
String protocolVersion = (String) data.get(CONTROL_MSG_PROTOCOL_VERSION);
if (protocolVersion == null || !protocolVersion.equals(PROTOCOL_VERSION)) {
if (principal != null)
MDC.put(LogUtils.USER, principal.getName());
String errorString = "Protocol version mismatch, received " + protocolVersion + " expecting " + PROTOCOL_VERSION;
log.error(LogUtils.COMBUS_MARKER, errorString);
MDC.clear();
sendError(bayeux, fromClient, data, msgId, principal, CONNECTION_STATUS_FAILED, errorString);
return;
}
Map<String, Object> replyData = new HashMap<String, Object>();
replyData.put(CONTROL_MSG_CORRELATION_ID, correlationId);
try {
bridgeConnectionManager.connectClient(clientId, bridgeListener, principal);
IOperationContext opContext = new BaseOperationContext("GetUserPrincipal", GET_ROLES_FOR_USER, servicePrincipal);
opContext.setMDC();
Set<URI> roles = principal.getRoles();
StringBuilder rolesBuffer = new StringBuilder();
int count = 0;
for (URI uri : roles) {
if (count > 0) {
rolesBuffer.append("\n");
}
count++;
rolesBuffer.append(uri.toString());
}
replyData.put(CONTROL_MSG_USER_ROLES, rolesBuffer.toString());
replyData.put(CONTROL_MSG_USER_IS_SYSADMIN, principal.isSysadmin());
URI userURI = principal.getUserURI();
if (userURI != null) {
replyData.put(CONTROL_MSG_USER_URI, userURI.toString());
}
replyData.put(CONTROL_MSG_STATUS, CONNECTION_STATUS_CONNECTED);
} catch (JMSException e) {
if (principal != null)
MDC.put(LogUtils.USER, principal.getName());
String errorMsg = "Error setting up temporary topic";
log.error(LogUtils.COMBUS_MARKER, errorMsg, e);
sendError(bayeux, fromClient, data, msgId, principal, CONNECTION_STATUS_FAILED, errorMsg);
return;
}
fromClient.addListener(bayeuxDisconnectListener);
publishIfChannelExists(bayeux, replyChannelId, fromClient, replyData, "R" + msgId);
}
/**
* Handles a control channel message to subscribe or unsubscribe from a JMS topic.
*
* @param subscribeOrUnsubscribe
* @param fromClient
* @param data
* @param msgId
* @param username
* @param clientId
* @param replyChannelId
* @param correlationId
*/
private void handleTopicSubscriptionOperation(boolean subscribeOrUnsubscribe, Client fromClient, HashMap<?, ?> data, String msgId, AnzoPrincipal principal, String clientId, String replyChannelId, String correlationId) {
Object topicInput = data.get(CONTROL_MSG_TOPICS);
if (!(topicInput instanceof Object[])) {
if (principal != null)
MDC.put(LogUtils.USER, principal.getName());
String errorMsg = "Missing or malformed " + CONTROL_MSG_TOPICS + " in " + (subscribeOrUnsubscribe ? CONTROL_TYPE_TOPIC_SUBSCRIBE : CONTROL_TYPE_TOPIC_UNSUBSCRIBE) + " message. Must be an Array of Strings.";
log.error(LogUtils.COMBUS_MARKER, errorMsg);
MDC.clear();
sendError(bayeux, fromClient, data, msgId, principal, TOPIC_SUBSCRIBE_STATUS_FAILED, errorMsg);
return;
}
long pid = profiler.start("Subscribing to topics {}", msgId);
Object[] topics = (Object[]) topicInput;
List<String> failedTopics = new ArrayList<String>();
List<String> errors = new ArrayList<String>();
for (Object topicObject : topics) {
if (topicObject instanceof String) {
String topic = (String) topicObject;
if (StringUtils.isNotEmpty(topic)) {
if (subscribeOrUnsubscribe) {
IOperationContext opContext = null;
try {
opContext = new BaseOperationContext(CHECK_ACCESS_FOR_SUBSCRIPTION, correlationId, principal);
opContext.setMDC();
long ipid = profiler.start("Subscribing to topic {}", topic);
bridgeConnectionManager.topicSubscribe(topic, clientId, principal, bridgeListener, opContext);
profiler.stop(ipid);
} catch (Exception e) {
failedTopics.add(topic);
String errorMsg = ERROR_MSG_GRAPH_SUBSCRIBE_PREFIX + topic + "." + e.getMessage();
errors.add(errorMsg);
if (principal != null)
MDC.put(LogUtils.USER, principal.getName());
log.error(LogUtils.COMBUS_MARKER, ERROR_MSG_GRAPH_SUBSCRIBE_PREFIX + topic, e);
MDC.clear();
} finally {
if (opContext != null) {
opContext.clearMDC();
}
}
} else {
try {
bridgeConnectionManager.topicUnsubscribe(topic, clientId);
} catch (Exception e) {
failedTopics.add(topic);
String errorMsg = ERROR_MSG_GRAPH_UNSUBSCRIBE_PREFIX + topic + "." + e.getMessage();
errors.add(errorMsg);
if (principal != null)
MDC.put(LogUtils.USER, principal.getName());
log.error(LogUtils.COMBUS_MARKER, ERROR_MSG_GRAPH_UNSUBSCRIBE_PREFIX + topic, e);
MDC.clear();
}
}
}
} else {
String errorMsg = "Missing or malformed topic string inside array of " + CONTROL_MSG_TOPICS + " argument in " + (subscribeOrUnsubscribe ? CONTROL_TYPE_TOPIC_SUBSCRIBE : CONTROL_TYPE_TOPIC_UNSUBSCRIBE) + " message. Elements of the array must be strings.";
errors.add(errorMsg);
if (principal != null)
MDC.put(LogUtils.USER, principal.getName());
log.error(LogUtils.COMBUS_MARKER, errorMsg);
MDC.clear();
}
}
// Send back the response
Map<String, Object> replyData = new HashMap<String, Object>();
replyData.put(CONTROL_MSG_CORRELATION_ID, correlationId);
replyData.put(CONTROL_MSG_STATUS, (failedTopics.size() == 0 && errors.size() == 0) ? TOPIC_SUBSCRIBE_STATUS_SUCCESS : TOPIC_SUBSCRIBE_STATUS_FAILED);
replyData.put(TOPIC_SUBSCRIBE_FAILED_TOPICS, failedTopics.toArray());
replyData.put(TOPIC_SUBSCRIBE_TOPIC_ERRORS, errors.toArray());
profiler.stop(pid);
publishIfChannelExists(bayeux, replyChannelId, fromClient, replyData, "R" + msgId);
}
}
private class BridgeListener implements Listener {
final Bayeux bayeux;
protected BridgeListener(Bayeux bayeux) {
this.bayeux = bayeux;
}
public void removed(String id, boolean timeout) {
log.debug(LogUtils.COMBUS_MARKER, "User removed (bridge).");
}
public void deliver(final Client fromClient, final Client toClient, org.cometd.Message msg) {
// This is dependent on the current request thread so we get this now rather than in the worker thread
final AnzoPrincipal principal = PrincipalFilter.getPrincipal();
// Grab the parts of the message that we need now since the message object may be recycled by CometD for another request
// by the time the worker task executes.
final String msgId = msg.getId();
final HashMap<?, ?> data = (HashMap<?, ?>) msg.getData();
workerPool.execute(new Runnable() {
public void run() {
long outerProfiler = profiler.start("Delivering Bayeux message over JMS. Message ID:{}", msgId);
long readProfiler = profiler.start("Reading Bayeux Message");
HashMap<?, ?> obj = data;
Map<?, ?> props = (Map<?, ?>) obj.get(JMS_MSG_PROPERTIES);
String destination = (String) obj.get(JMS_MSG_DESTINATION);
String body = null;
// The Bayeux system will have parsed the JSON into a Map but we need it as a string
// to re-transmit it to the anzo system.
Object msgBody = obj.get(JMS_MSG_BODY);
profiler.stop(readProfiler);
if (msgBody instanceof Map<?, ?> || msgBody instanceof Object[]) {
long doubleJsonProfiler = profiler.start("Converting Bayeux message from Map to String.");
body = JSON.toString(msgBody);
profiler.stop(doubleJsonProfiler);
} else {
body = (String) msgBody;
}
long corrIdProfiler = profiler.start("Finding correlation id.");
String correlationId = null;
if (obj.get(CONTROL_MSG_CORRELATION_ID) != null) {
correlationId = obj.get(CONTROL_MSG_CORRELATION_ID).toString();
} else {
if (props != null) {
if (props.get(JMS_MSG_PROPERTY_CORRELATION_ID) != null) {
correlationId = (String) props.get(JMS_MSG_PROPERTY_CORRELATION_ID);
}
}
}
profiler.stop(corrIdProfiler);
IOperationContext opContext = null;
try {
long opContextProfiler = profiler.start("Creating operation context for sending client message.");
opContext = new BaseOperationContext(SEND_BAYEUX_MESSAGE_TO_JMS, correlationId, principal);
opContext.setMDC();
profiler.stop(opContextProfiler);
long sendJmsProfiler = profiler.start("Bridge Sending JMS Message");
boolean publishedToTopic = bridgeConnectionManager.sendClientMessage(fromClient.getId(), principal, destination, props, body, opContext);
profiler.stop(sendJmsProfiler);
if (publishedToTopic) {
// if we published the message to a topic, (as opposed to a service destination),
// then we send back the response immediately, saying that the message has been
// successfully published.
long topicSuccessProfiler = profiler.start("Replying success on topic send.");
String replyChannelId = getReplyChannelId(principal, fromClient);
Map<String, Object> replyData = new HashMap<String, Object>();
replyData.put(CONTROL_MSG_STATUS, PUBLISH_STATUS_SUCCESS);
if (obj.get(CONTROL_MSG_CORRELATION_ID) != null) {
replyData.put(CONTROL_MSG_CORRELATION_ID, obj.get(CONTROL_MSG_CORRELATION_ID).toString());
} else {
if (props != null) {
if (props.get(JMS_MSG_PROPERTY_CORRELATION_ID) != null) {
Map<String, String> p = new HashMap<String, String>();
replyData.put(JMS_MSG_PROPERTIES, p);
p.put(JMS_MSG_PROPERTY_CORRELATION_ID, (String) props.get(JMS_MSG_PROPERTY_CORRELATION_ID));
}
}
}
publishIfChannelExists(bayeux, replyChannelId, fromClient, replyData, "R" + msgId);
profiler.stop(topicSuccessProfiler);
}
} catch (JMSException e) {
String errorMsg = "Error publishing message over JMS";
sendError(bayeux, fromClient, data, msgId, principal, PUBLISH_STATUS_FAILED, errorMsg);
log.error(LogUtils.COMBUS_MARKER, errorMsg, e);
} catch (AnzoException e) {
String errorMsg = "Error publishing message over JMS";
sendError(bayeux, fromClient, data, msgId, principal, PUBLISH_STATUS_FAILED, errorMsg);
log.error(LogUtils.COMBUS_MARKER, errorMsg, e);
} finally {
if (opContext != null) {
long clearMdcProfiler = profiler.start("Clearing operation context.");
opContext.clearMDC();
profiler.stop(clearMdcProfiler);
}
profiler.stop(outerProfiler);
}
}
});
}
}
/**
* Listens to disconnections from the Bayeux side of the bridge. It will disconnect the corresponding JMS state. This includes disconnections due to
* client-side timeouts. See http://cometd.org/documentation/cometd-java/server/listeners
*/
private class DisconnectListener implements Listener {
public DisconnectListener() {
}
public void removed(String id, boolean timeout) {
bridgeConnectionManager.disconnectClient(id);
}
public void deliver(Client fromClient, Client toClient, org.cometd.Message msg) {
}
}
/**
* A Bayeux listener that listens on all channels. It simply logs messages received for debugging purposes.
*/
static private class MonitorListener implements Listener {
public MonitorListener() {
}
public void removed(String id, boolean timeout) {
log.debug(LogUtils.COMBUS_MARKER, "User removed (monitor) client id:{}", id);
}
public void deliver(Client fromClient, Client toClient, org.cometd.Message msg) {
String toChannel = msg.getChannel();
Object data = msg.getData();
if (log.isTraceEnabled()) {
log.trace(LogUtils.COMBUS_MARKER, "MonitorListener: {} - {} -> {} {}", new Object[] { fromClient, toClient, toChannel, data });
}
}
}
/**
* The JMS listener. All JMS messages sent to the bridge are handled by this class. It is responsible for forwarding the messages to the appropriate Bayeux
* channel.
*/
private class BridgeMessageListener implements MessageListener {
private final Bayeux _bayeux;
protected BridgeMessageListener(Bayeux bayeux) {
_bayeux = bayeux;
}
public void onMessage(Message msg) {
try {
if (log.isTraceEnabled()) {
log.trace(LogUtils.COMBUS_MARKER, MessageUtils.prettyPrint(msg, "BayeuxJMSBridge Received Message"));
}
if (!(msg instanceof TextMessage)) {
log.error(LogUtils.COMBUS_MARKER, "Received non-text message, cannot deliver to web client.");
return;
}
if (bridgeConnectionManager.handleInternalMessage(msg)) {
// The it was handled as a cleanup message, then we have nothing left to do.
return;
}
// Build the corresponding Bayeux message with the info from the JMS message
Map<String, Object> replyData = new HashMap<String, Object>();
Map<String, String> props = new HashMap<String, String>();
replyData.put(JMS_MSG_PROPERTIES, props);
Enumeration<?> propNames = msg.getPropertyNames();
while (propNames.hasMoreElements()) {
String name = (String) propNames.nextElement();
props.put(name, msg.getStringProperty(name));
}
if (msg.getJMSCorrelationID() != null) {
props.put(JMS_MSG_PROPERTY_CORRELATION_ID, msg.getJMSCorrelationID());
}
replyData.put(JMS_MSG_BODY, ((TextMessage) msg).getText());
Destination jmsDestination = msg.getJMSDestination();
if (jmsDestination instanceof TemporaryTopic) {
// This is a message bound for a client's temporary topic, so send it
// via the particular client's Bayeux channel.
Pair<String, String> userInfo = bridgeConnectionManager.findBayeuxReplyChannelForTopic((TemporaryTopic) jmsDestination);
if (userInfo != null) {
String replyChannelId = getReplyChannelId(userInfo.first, userInfo.second);
publishIfChannelExists(_bayeux, replyChannelId, _bayeux.getClient(userInfo.second), replyData, "jms-" + msg.getJMSMessageID());
}
} else if (jmsDestination instanceof Topic) {
// If this isn't for a client's temporary topic then this is likely a topic message.
// We need to find out which particular topic then send the
// message to each Bayeux client that is subscribed to that topic.
Topic topicDest = (Topic) msg.getJMSDestination();
String topic = topicDest.getTopicName();
if (bridgeConnectionManager.isTopicSubscribed(topic)) {
props.put(JMS_MSG_PROPERTY_TYPE, MSG_TYPE_TOPIC_MESSAGE);
props.put(JMS_MSG_PROPERTY_TOPIC, topic);
log.debug(LogUtils.COMBUS_MARKER, "Topic message for {} received by BayeuxJMSBridge.", topic);
Collection<Pair<String, String>> userInfoCollection = bridgeConnectionManager.findChannelsSubscribedToTopic(topic);
for (Pair<String, String> subscriberUserInfo : userInfoCollection) {
String replyChannelId = getReplyChannelId(subscriberUserInfo.first, subscriberUserInfo.second);
publishIfChannelExists(_bayeux, replyChannelId, _bayeux.getClient(subscriberUserInfo.second), replyData, "jms-" + msg.getJMSMessageID());
}
} else {
log.debug(LogUtils.COMBUS_MARKER, "Message received by BayeuxJMSBridge that is neither intended directly for a user nor a topic message. Ignoring message.");
}
} else {
log.debug(LogUtils.COMBUS_MARKER, "Message received by BayeuxJMSBridge that is neither intended directly for a user nor a topic message. Ignoring message.");
}
} catch (JMSException e) {
log.error(LogUtils.COMBUS_MARKER, "Error relaying JMS message over bayeux", e);
}
}
}
/**
* This implements the security policy for the Bayeux connections. It prevents any user from creating a Bayeux channel that would receive messages intended
* for another user. The channels with the format /anzo/user/USER/CLIENTID are used to communicate with the user named by USER in the channel name. So the
* policy checks the currently logged in user and the channel name's USER match before allowing the channel to be opened.
*/
static private class BayeuxJMSSecurityPolicy implements SecurityPolicy {
public boolean canCreate(Client client, String channel, org.cometd.Message message) {
String username = getUsername();
if (channel.startsWith("/anzo/user/")) {
String[] parts = channel.split("/");
if (parts.length < 4)
return false;
return username.equals(parts[3]);
} else {
return false;
}
}
public boolean canSubscribe(Client client, String channel, org.cometd.Message message) {
return canCreate(client, channel, message);
}
public boolean canPublish(Client client, String channel, org.cometd.Message message) {
return true;
}
public boolean canHandshake(org.cometd.Message message) {
return true;
}
private String getUsername() {
Principal principal = PrincipalFilter.getPrincipal();
if (principal != null) {
return principal.getName();
} else {
return null;
}
}
}
// Ignored events
public void attributeRemoved(ServletContextAttributeEvent scab) {
}
public void attributeReplaced(ServletContextAttributeEvent scab) {
}
}