org.openanzo.combus.bayeux.BayeuxJMSBridge.java Source code

Java tutorial

Introduction

Here is the source code for org.openanzo.combus.bayeux.BayeuxJMSBridge.java

Source

/*******************************************************************************
 * 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) {

    }

}