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

Java tutorial

Introduction

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

Source

/*******************************************************************************
 * Copyright (c) 2008 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
 *
 * Contributors:
 *     Cambridge Semantics Incorporated - initial API and implementation
 *******************************************************************************/

package org.openanzo.combus.bayeux;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.ExceptionListener;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TemporaryTopic;
import javax.jms.TextMessage;
import javax.jms.Topic;

import org.apache.activemq.broker.BrokerStoppedException;
import org.apache.activemq.transport.TransportDisposedIOException;
import org.apache.commons.lang.time.DateUtils;
import org.openanzo.analysis.Profiler;
import org.openanzo.cache.ICacheProvider;
import org.openanzo.datasource.IDatasource;
import org.openanzo.exceptions.AnzoException;
import org.openanzo.exceptions.ExceptionConstants;
import org.openanzo.exceptions.LogUtils;
import org.openanzo.rdf.Constants;
import org.openanzo.rdf.URI;
import org.openanzo.rdf.Constants.COMBUS;
import org.openanzo.rdf.Constants.NAMESPACES;
import org.openanzo.rdf.utils.Pair;
import org.openanzo.rdf.utils.SerializationConstants;
import org.openanzo.rdf.utils.UriGenerator;
import org.openanzo.services.AnzoPrincipal;
import org.openanzo.services.INotificationRegistrationService;
import org.openanzo.services.IOperationContext;
import org.openanzo.services.Privilege;
import org.openanzo.services.impl.ConfiguredCredentials;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

/**
 * This class contains the state kept by the BayeuxJMSBridge for the various connections made by clients. Its main purpose is to group the state essentially in
 * a monitor for concurrency synchronization. It exposes basic operations to manage connections such as adding connections, sending messages intended for
 * particular clients, etc.
 * 
 * The implementation is careful about how it locks while working with JMS to avoid deadlocks such as the one described at
 * http://www.openanzo.org/projects/openanzo/ticket/286
 * 
 * @author Jordi Albornoz Mulligan (<a href="mailto:jordi@cambridgesemantics.com">jordi@cambridgesemantics.com</a>)
 * @author Ben Szekely ( <a href="mailto:ben@cambridgesemantics.com">ben@cambridgesemantics.com </a>)
 * 
 */
class BridgeConnectionManager implements BayeuxJMSConstants {

    private static final Logger log = LoggerFactory.getLogger("org.openanzo.combus.bayeux.BayeuxJmsBridge");

    private static final Profiler profiler = new Profiler();

    private Connection conn = null;

    private Session session = null;

    private MessageProducer mp = null;

    private final Map<String, Destination> destinations = new HashMap<String, Destination>();

    /**
     * This lock protects read and write access to the client and graph subscription state. Specifically, do not access tempDestinationToClientState,
     * clientIdToClientState, graphSubscriptions, or topicsToDelete without having this lock. WARNING: Do not perform any JMS calls while holding this lock due
     * to possible deadlocks. This class's methods could be called from within JMS code. See http://www.openanzo.org/projects/openanzo/ticket/286 for a
     * description one such deadlock situation.
     */
    private final ReentrantLock mapLock = new ReentrantLock();

    /**
     * Map from JMS temporary topic for the ClientState for the client listening on that temporary topic.
     */
    private final Map<String, ClientState> tempDestinationToClientState = new HashMap<String, ClientState>();

    /**
     * Map from Bayeux clientId to the ClientState. Do not access without first acquiring mapLock.
     */
    private final Map<String, ClientState> clientIdToClientState = new HashMap<String, ClientState>();

    /**
     * Map from a topic to the information about the bayeux clients expecting messages on that topic. Do not access without first acquiring mapLock.
     */
    private final Map<String, TopicSubscription> topicSubscriptions = new HashMap<String, TopicSubscription>();

    /**
     * Map from correlationId to a temporary topic ready for deletion, its consumer, and the time (in milliseconds from the epoch) at which the topic was set
     * for deletion. This is used to keep track of the topics waiting for a response to an unregisterUser (NotificationRegistrationService) request. Once we
     * receive the response, we can finally delete the temporary topic. We also delete the topic after a given timeout if we haven't received the response after
     * a while.
     */
    private final Map<String, ClientStateToClose> topicsToDelete = new HashMap<String, ClientStateToClose>();

    private Timer topicDeletionTimeoutTimer = new Timer("BridgeConnectionManager Topic Deletion Timeout Thread",
            true);

    private static final long TOPIC_DELETE_RESPONSE_TIMEOUT = 4 * DateUtils.MILLIS_PER_MINUTE;

    //private final GraphUUIDCachedResolver        graphUuidResolver;

    private final IDatasource datasource;

    private final ConfiguredCredentials credentials;

    private boolean closed = false;

    private class TopicDeletionTimerTask extends TimerTask {
        @Override
        public void run() {
            List<ClientStateToClose> timedoutTopics = null;
            mapLock.lock();
            try {
                if (topicsToDelete != null) {
                    long currentTime = System.currentTimeMillis();
                    List<String> timedoutKeys = null;
                    for (Map.Entry<String, ClientStateToClose> topicToDeleteEntry : topicsToDelete.entrySet()) {
                        ClientStateToClose topicToDeleteInfo = topicToDeleteEntry.getValue();
                        if (topicToDeleteInfo != null) {
                            if (topicToDeleteInfo.getDeletionRequestTime()
                                    + TOPIC_DELETE_RESPONSE_TIMEOUT <= currentTime) {
                                if (timedoutTopics == null) {
                                    timedoutTopics = new ArrayList<ClientStateToClose>();
                                }
                                timedoutTopics.add(topicToDeleteInfo); // add it to the list of topics to delete. We'll delete them all after we've released the mapLock to avoid deadlocks.

                                if (timedoutKeys == null) {
                                    timedoutKeys = new ArrayList<String>();
                                }
                                timedoutKeys.add(topicToDeleteEntry.getKey()); // We'll remove these from the map after iteration is done to avoid ConcurrentModificationExceptions
                            }
                        } else {
                            // If there is a null entry, it's useless, mark it for removal
                            if (timedoutKeys == null) {
                                timedoutKeys = new ArrayList<String>();
                            }
                            timedoutKeys.add(topicToDeleteEntry.getKey()); // We'll remove these from the map after iteration is done to avoid ConcurrentModificationExceptions
                        }
                    }
                    if (timedoutKeys != null) {
                        for (String keyToRemove : timedoutKeys) {
                            topicsToDelete.remove(keyToRemove);
                        }
                    }
                }
            } finally {
                mapLock.unlock();
            }
            if (timedoutTopics != null) {
                for (ClientStateToClose topicToRemove : timedoutTopics) {
                    log.debug(LogUtils.COMBUS_MARKER,
                            "Timed out waiting for unregister subscriber response. Removing topic for {}/{}",
                            topicToRemove.username, topicToRemove.clientId);
                    topicToRemove.close();
                }
            }
        }
    }

    protected BridgeConnectionManager(IDatasource datasource, ICacheProvider cacheProvider,
            ConfiguredCredentials credentials) {
        this.datasource = datasource;
        this.credentials = credentials;
        //  this.graphUuidResolver = new GraphUUIDCachedResolver(cacheProvider);

        topicDeletionTimeoutTimer.scheduleAtFixedRate(new TopicDeletionTimerTask(), 500,
                TOPIC_DELETE_RESPONSE_TIMEOUT / 2);
    }

    /**
     * Creates a single JMS connection and session for use by the BayeuxJMSBridge. It connects to the combus using a configured sysadmin account.
     * 
     * @param factory
     *            this will be used to create the JMS connection and session.
     * @param properties
     *            must contain the username and password
     * @throws JMSException
     */
    protected void initialize(ConnectionFactory factory, Properties properties) throws AnzoException {
        try {
            conn = factory.createConnection(credentials.getUserName(), credentials.getPassword());
            conn.setExceptionListener(new ExceptionListener() {
                public void onException(JMSException exception) {
                    if (!closed) { // if user has not requested disconnect
                        if (exception.getCause() instanceof BrokerStoppedException
                                || exception.getCause() instanceof TransportDisposedIOException) {
                            closed = true;
                            if (conn != null) {
                                try {
                                    conn.close();
                                } catch (JMSException e) {
                                    log.debug(LogUtils.COMBUS_MARKER, "Error closing JMS connection", e);
                                }
                            }
                        } else {
                            log.error(LogUtils.COMBUS_MARKER, "Exception over Bayeux JMS connection", exception);
                        }
                    }
                }
            });
            conn.start();
            session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
            mp = session.createProducer(null);
            mp.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
            // setup all the destination queues
            destinations.put(COMBUS.NOTIFICATION_SERVICE_QUEUE,
                    session.createQueue(COMBUS.NOTIFICATION_SERVICE_QUEUE));
            destinations.put(COMBUS.MODEL_SERVICE_QUEUE, session.createQueue(COMBUS.MODEL_SERVICE_QUEUE));
            destinations.put(COMBUS.UPDATE_SERVICE_QUEUE, session.createQueue(COMBUS.UPDATE_SERVICE_QUEUE));
            destinations.put(COMBUS.AUTHENTICATION_SERVICE_QUEUE,
                    session.createQueue(COMBUS.AUTHENTICATION_SERVICE_QUEUE));
            destinations.put(COMBUS.REPLICATION_SERVICE_QUEUE,
                    session.createQueue(COMBUS.REPLICATION_SERVICE_QUEUE));
            destinations.put(COMBUS.QUERY_SERVICE_QUEUE, session.createQueue(COMBUS.QUERY_SERVICE_QUEUE));
            destinations.put(COMBUS.RESET_SERVICE_QUEUE, session.createQueue(COMBUS.RESET_SERVICE_QUEUE));
            destinations.put(COMBUS.EXECUTION_SERVICE_QUEUE, session.createQueue(COMBUS.EXECUTION_SERVICE_QUEUE));
            destinations.put(COMBUS.AUTHORIZATION_SERVICE_QUEUE,
                    session.createQueue(COMBUS.AUTHORIZATION_SERVICE_QUEUE));
        } catch (JMSException jmsex) {
            throw new AnzoException(ExceptionConstants.COMBUS.JMS_CONNECT_FAILED, jmsex);
        }
    }

    /**
     * Sets up state for connecting a single Bayeux client to the BayeuxJMSBridge.
     * 
     * @param clientId
     *            the Bayeux client id.
     * @return true if client was connected. false if there was already a connection for the client.
     */
    protected boolean connectClient(String clientId, MessageListener listener, AnzoPrincipal principal)
            throws JMSException {
        // checks if connection already exists, create topic and client state, add to maps.

        boolean ret = false;

        boolean clientAlreadyConnected = false;
        mapLock.lock();
        try {
            clientAlreadyConnected = clientIdToClientState.containsKey(clientId);
        } finally {
            mapLock.unlock();
        }

        if (!clientAlreadyConnected) {
            // We don't have a temporary topic for this client yet so we'll create one.
            // We make sure to do this while NOT holding the mapLock to avoid deadlocks
            // (like http://www.openanzo.org/projects/openanzo/ticket/286). This
            // means that it's possible, in rare cases, that we could create a temp topic which we'll
            // have to just throw out immediately but there's not much harm in that.
            TemporaryTopic topic = session.createTemporaryTopic();
            String tempDestinationId = topic.getTopicName();
            MessageConsumer consumer = session.createConsumer(topic);
            consumer.setMessageListener(listener);

            boolean destroyNewJMSState = false;
            mapLock.lock();
            try {
                if (clientIdToClientState.containsKey(clientId)) {
                    // Some other thread seems to have connected this client while we were busy creating
                    // JMS topics, etc. That's okay, we'll just close the topics we created since they aren't needed anymore.
                    // But we don't want to destroy them while holding the mapLock, so we'll just mark a boolean so that they
                    // are deleted after releasing the lock.
                    destroyNewJMSState = true;
                } else {
                    ClientState state = new ClientState(principal, topic, clientId, consumer);
                    tempDestinationToClientState.put(tempDestinationId, state);
                    clientIdToClientState.put(clientId, state);
                    ret = true;
                }
            } finally {
                mapLock.unlock();
                if (destroyNewJMSState) {
                    consumer.close();
                    topic.delete();
                }
            }
        }

        return ret;
    }

    protected boolean isTopicSubscribed(String topic) {
        return this.topicSubscriptions.containsKey(topic);
    }

    /**
     * Sets up a subscription of the given Bayeux client to the JMS topic.
     * 
     * @param topic
     * @param clientId
     * @return true if a subscription was added. false if there was already a subscription.
     * @throws AnzoException
     *             if the user does not have access to the topic
     * @throws JMSException
     */
    protected boolean topicSubscribe(String topic, String clientId, AnzoPrincipal principal,
            MessageListener listener, IOperationContext opContext) throws JMSException, AnzoException {
        // check if subscription already exists, update maps and client state
        boolean ret = false;

        boolean subscriptionAlreadyExists = false;
        boolean consumerAlreadyExists = false;
        mapLock.lock();
        try {
            TopicSubscription topicSubscription = topicSubscriptions.get(topic);
            consumerAlreadyExists = topicSubscription != null;
            subscriptionAlreadyExists = topicSubscription != null
                    && topicSubscription.subscribedClients.containsKey(clientId);

            if (!subscriptionAlreadyExists) {

                // If we're going to be adding a subscription, check the access control first.
                if (!userHasTopicAccess(topic, principal, opContext)) {
                    throw new AnzoException(ExceptionConstants.DATASOURCE.NO_READ_ERROR, topic,
                            opContext.getOperationPrincipal().getUserURI().toString());
                }

                if (consumerAlreadyExists) {
                    // If there is already a JMS consumer for the topic, then we can finish things here with some
                    // simple manipulation of the relevant maps.
                    addTopicSubscription(topic, clientId, topicSubscription);
                    ret = true;
                    subscriptionAlreadyExists = true;
                }
            }
        } finally {
            mapLock.unlock();
        }

        if (!subscriptionAlreadyExists) {
            // Handle adding the subscription when the JMS topic consumer doesn't exist.
            // We make sure to create the consumer while NOT holding mapLock to avoid deadlocks
            // (like http://www.openanzo.org/projects/openanzo/ticket/286). This
            // means that it's possible, in rare cases, that we could create a duplicate consumer
            // which we'll have to just throw out immediately but there's not much harm in that.
            assert !consumerAlreadyExists;
            Destination destination = session.createTopic(topic);
            MessageConsumer consumer = session.createConsumer(destination);
            consumer.setMessageListener(listener);

            boolean destroyNewJMSState = false;
            mapLock.lock();
            try {
                TopicSubscription topicSubscription = topicSubscriptions.get(topic);
                if (topicSubscription == null) {
                    topicSubscription = new TopicSubscription(consumer);
                    topicSubscriptions.put(topic, topicSubscription);
                } else {
                    // Some other thread seems to have created a consumer for this graph topic while we were busy creating
                    // JMS topics, etc. That's okay, we'll just close the consumer we created since they aren't needed now.
                    // But we don't want to destroy them while holding the mapLock, so we'll just mark a boolean so that they
                    // are deleted after releasing the lock.
                    destroyNewJMSState = true;
                }

                if (!topicSubscription.subscribedClients.containsKey(clientId)) {
                    // NOTE: Access control was already verified earlier in the method.
                    addTopicSubscription(topic, clientId, topicSubscription);
                    ret = true;
                }
            } finally {
                mapLock.unlock();
                if (destroyNewJMSState) {
                    consumer.close();
                }
            }
        }

        return ret;
    }

    /**
     * Unsubscribe the given client from the given topic.
     * 
     * @param topic
     * @param clientId
     */
    protected void topicUnsubscribe(String topic, String clientId) {
        MessageConsumer consumer = null;
        mapLock.lock();
        try {
            consumer = unsubscribeTopic(topic, clientId);
            ClientState state = clientIdToClientState.get(clientId);
            if (state != null) {
                state.topicSubscriptions.remove(topic);
            } else {
                log.warn(LogUtils.COMBUS_MARKER, "topicUnsubscribe - ClientState is null");
            }
        } finally {
            mapLock.unlock();
        }
        if (consumer != null) {
            closeMessageConsumer(consumer); // Close the consumer while not holding mapLock
        }
        log.debug(LogUtils.COMBUS_MARKER, "Unsubscribed client {} from topic {}", clientId, topic);
    }

    /**
     * Send a JMS message on behalf of the given client to a specific destination. The destination is a string that names an abstract queue such as that in
     * Constants.NOTIFICATION_SERVICE_QUEUE, etc.
     * 
     * @param clientId
     * @param destination
     * @param messageProperties
     * @param msgBody
     * @return returns whether or not this message was published to a topic
     * @throws JMSException
     * @throws AnzoException
     */
    protected boolean sendClientMessage(String clientId, AnzoPrincipal principal, String destination,
            Map<?, ?> messageProperties, String msgBody, IOperationContext opContext)
            throws JMSException, AnzoException {
        //long destinationProfiler = profiler.start("Resolving destination.");
        Destination dest = destinations.get(destination);
        //profiler.stop(destinationProfiler);
        if (dest == null && destination.startsWith("services/")) {
            dest = session.createQueue(destination);
            destinations.put(destination, dest);
        }
        if (dest == null) { // we probably have a statement channel
            //long nullDestProfiler = profiler.start("Sending client message with null destination.");
            if (destination == null || !destination.startsWith(NAMESPACES.STREAM_TOPIC_PREFIX)) {
                //profiler.stop(nullDestProfiler);
                throw new AnzoException(ExceptionConstants.COMBUS.INVALID_TOPIC, destination);
            }
            // first we have to get the named graph uri out of the statement channel topic.
            String uri = UriGenerator.stripEncapsulatedString(NAMESPACES.STREAM_TOPIC_PREFIX, destination);
            URI graphUri = Constants.valueFactory.createURI(uri);
            if (!userHasGraphAddAccess(graphUri, principal, opContext)) {
                //profiler.stop(nullDestProfiler);
                throw new AnzoException(ExceptionConstants.COMBUS.NOT_AUTHORIZED_FOR_TOPIC,
                        opContext.getOperationPrincipal().getUserURI().toString(), destination);
            }
            Topic topic = session.createTopic(destination);

            TextMessage tmsg = session.createTextMessage();
            for (Map.Entry<?, ?> prop : messageProperties.entrySet()) {
                tmsg.setStringProperty(prop.getKey().toString(), prop.getValue().toString());
            }
            tmsg.setText(msgBody);
            mp.send(topic, tmsg);
            //profiler.stop(nullDestProfiler);
            return true;
        } else {
            TemporaryTopic tempTopicForReply;
            //long = clientStateProfiler = profiler.start("Obtaining Bayeux client state.");
            mapLock.lock();
            try {
                ClientState state = clientIdToClientState.get(clientId);
                if (state == null) {
                    throw new AnzoException(ExceptionConstants.CLIENT.CLIENT_NOT_CONNECTED);
                }
                tempTopicForReply = state.topic;
            } finally {
                mapLock.unlock();
                //profiler.stop(clientStateProfiler);
            }

            //long prepareJmsProfiler = profiler.start("Preparing JMS Message.");
            TextMessage tmsg = session.createTextMessage();
            int priority = 4;
            for (Map.Entry<?, ?> prop : messageProperties.entrySet()) {
                if (JMS_MSG_PROPERTY_CORRELATION_ID.equals(prop.getKey())) {
                    tmsg.setJMSCorrelationID(prop.getValue().toString());
                }
                if (JMS_MSG_PROPERTY_PRIORITY.equals(prop.getKey())) {
                    priority = Integer.parseInt(prop.getValue().toString());
                } else {
                    tmsg.setStringProperty(prop.getKey().toString(), prop.getValue().toString());
                }
            }
            tmsg.setJMSPriority(priority);
            tmsg.setJMSReplyTo(tempTopicForReply);
            tmsg.setText(msgBody);
            String username = principal.getName();
            tmsg.setStringProperty("runAsUser", username);
            //profiler.stop(prepareJmsProfiler);
            long sendJmsProfiler = profiler.start("Sending JMS Message");
            mp.setPriority(priority);
            mp.send(dest, tmsg);
            profiler.stop(sendJmsProfiler);
            return false;
        }

    }

    /**
     * Handle a single Bayeux client disconnecting from the server. This closes the client's state like its JSM temporary topic and consumer and removes the
     * client's subscriptions to any graph update topics.
     * 
     * @param clientId
     */
    protected void disconnectClient(String clientId) {
        if (log.isDebugEnabled()) {
            log.debug(LogUtils.COMBUS_MARKER, "Disconnecting:{} ", clientId);
        }

        // Avoid a deadlock by not doing JMS operations while holding mapLock.
        // We instead gather up the references we need and close them after releasing mapLock.
        List<MessageConsumer> consumersToClose = Collections.emptyList();
        TemporaryTopic topicToClose = null;
        MessageConsumer tempTopicConsumerToClose = null;
        String username = null;
        String errorMsg = "Error while disconnecting:" + clientId;

        mapLock.lock();
        try {
            ClientState state = clientIdToClientState.get(clientId);
            if (state != null) {
                username = state.principal.getName();
                errorMsg += username + "/" + clientId;

                topicToClose = state.topic;
                tempTopicConsumerToClose = state.consumer;

                try {
                    tempDestinationToClientState.remove(state.topic.getTopicName());
                } catch (JMSException e) {
                    log.warn(LogUtils.COMBUS_MARKER, errorMsg, e);
                }

                if (state.topicSubscriptions.size() > 0) {
                    consumersToClose = new ArrayList<MessageConsumer>(state.topicSubscriptions.size());
                    for (String topic : state.topicSubscriptions) {
                        MessageConsumer consumer = unsubscribeTopic(topic, state.clientId);
                        consumersToClose.add(consumer);
                    }
                }
                state.topicSubscriptions.clear();

                clientIdToClientState.remove(clientId);
            }
        } finally {
            mapLock.unlock();
        }

        // Now actually destroy the JMS state since we're no longer holding mapLock.
        for (MessageConsumer consumer : consumersToClose) {
            if (consumer != null) {
                try {
                    consumer.close();
                } catch (NullPointerException npe) {
                    //Catch exception due to defect within activemq's ActiveMQMessageConsumer.dispose() method
                } catch (JMSException e) {
                    log.warn(LogUtils.COMBUS_MARKER, errorMsg, e);
                }
            }
        }
        cleanupTemporaryTopic(topicToClose, tempTopicConsumerToClose, username, clientId);
    }

    private static class ClientStateToClose {
        private TemporaryTopic temporaryTopic;

        private MessageConsumer consumer;

        private long deletionRequestTime;

        private String username;

        private String clientId;

        ClientStateToClose(MessageConsumer consumer, TemporaryTopic temporaryTopic, long deletionRequestTime,
                String username, String clientId) {
            this.consumer = consumer;
            this.temporaryTopic = temporaryTopic;
            this.deletionRequestTime = deletionRequestTime;
            this.username = username;
            this.clientId = clientId;
        }

        long getDeletionRequestTime() {
            return deletionRequestTime;
        }

        void close() {
            if (consumer != null) {
                try {
                    consumer.close();
                    consumer = null;
                } catch (JMSException e) {
                    log.debug(LogUtils.COMBUS_MARKER, "Error while removing timed-out consumer.", e);
                }
            }
            if (temporaryTopic != null) {
                try {
                    temporaryTopic.delete();
                    temporaryTopic = null;
                } catch (JMSException e) {
                    log.debug(LogUtils.COMBUS_MARKER, "Error while removing timed-out temporary topic.", e);
                }
            }
        }
    }

    /**
     * Marks a temporary topic for deletion. Before deleting the topic, it will be unregistered via the notification registration service. The topic will be
     * deleted once that operation completes.
     * 
     * @param topicToClose
     *            the temporary topic to close.
     * @param username
     *            the username of the user to which the temporary topic belongs
     * @param clientId
     *            the clientId of the specific Bayeux connection to which the temporary topic applies.
     */
    private void cleanupTemporaryTopic(TemporaryTopic topicToClose, MessageConsumer tempTopicConsumerToClose,
            String username, String clientId) {

        String correlationId = UUID.randomUUID().toString();
        mapLock.lock();
        try {
            topicsToDelete.put(correlationId, new ClientStateToClose(tempTopicConsumerToClose, topicToClose,
                    System.currentTimeMillis(), username, clientId));
        } finally {
            mapLock.unlock();
        }

        log.debug(LogUtils.COMBUS_MARKER, "Sending unregister subscriber message for {}/{}", username, clientId);
        try {
            TextMessage tmsg = session.createTextMessage();
            tmsg.setJMSCorrelationID(correlationId);
            tmsg.setJMSReplyTo(topicToClose);
            tmsg.setStringProperty(SerializationConstants.operation,
                    INotificationRegistrationService.UNREGISTER_SUBSCRIBER);
            tmsg.setStringProperty("runAsUser", username);
            mp.send(destinations.get(COMBUS.NOTIFICATION_SERVICE_QUEUE), tmsg);
        } catch (JMSException e) {
            MDC.put(LogUtils.USER, username);
            log.warn(LogUtils.COMBUS_MARKER, "Error while sending real-time update subscription remove request for "
                    + username + "/" + clientId, e);
            MDC.clear();
        }

    }

    /**
     * Checks if this is a message that the BridgeConnectionManager expects to handle and, if so, handles the message. These are messages that typically are
     * responses to requests sent by the BridgeConnectionManager code for its own purposes rather than messages explicitly sent or intended for a client.
     * 
     * @param msg
     *            the message the handle
     * @return true if the message was handled, false otherwise.
     * @throws JMSException
     */
    public boolean handleInternalMessage(Message msg) throws JMSException {
        boolean handled = false;
        if (msg != null) {
            String correlationId = msg.getJMSCorrelationID();
            if (correlationId != null) {
                ClientStateToClose topicToDelete = null;
                mapLock.lock();
                try {
                    topicToDelete = topicsToDelete.get(correlationId);
                    if (topicToDelete != null) {
                        topicsToDelete.remove(correlationId);
                        handled = true;
                    }
                } finally {
                    mapLock.unlock();
                }

                if (topicToDelete != null) {
                    log.debug(LogUtils.COMBUS_MARKER, "Finished deleting client JMS state for {}/{}",
                            topicToDelete.username, topicToDelete.clientId);
                    topicToDelete.close();
                    topicToDelete = null;
                }
            }
        }
        return handled;
    }

    /**
     * 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.
     * @throws AnzoException
     */
    protected void removeUnauthorizedSubscriptions(URI namedGraphUri, URI role, IOperationContext opContext)
            throws AnzoException {

        List<String> possiblyAffectedTopics = new ArrayList<String>();

        //        String uuidStr = graphUuidResolver.getUuid(namedGraphUri, opContext, datasource.getModelService());
        URI uuid = datasource.getModelService().getUUIDforUri(opContext, namedGraphUri);
        if (uuid != null) {
            possiblyAffectedTopics.add(
                    UriGenerator.generateEncapsulatedString(NAMESPACES.NAMEDGRAPH_TOPIC_PREFIX, uuid.toString()));
        }
        possiblyAffectedTopics.add(
                UriGenerator.generateEncapsulatedString(NAMESPACES.STREAM_TOPIC_PREFIX, namedGraphUri.toString()));

        for (String topic : possiblyAffectedTopics) {

            Map<String, MessageConsumer> consumersToClose = null;

            mapLock.lock();
            try {
                TopicSubscription subscriptions = topicSubscriptions.get(topic);
                if (subscriptions != null && !subscriptions.subscribedClients.isEmpty()) {
                    // There are users subscribed to this graph's updates. We need to go through each
                    // such user, retrieve their latest roles, and compare it to the roles which are allowed to
                    // read the graph.

                    // First we retrieve the read access information for the graph.
                    Set<URI> graphRoles = datasource.getAuthorizationService().getRolesForGraph(opContext,
                            namedGraphUri, Privilege.READ);

                    // Now we check each user for access to this graph.
                    Iterator<Map.Entry<String, ClientState>> subscribedClientsIterator = subscriptions.subscribedClients
                            .entrySet().iterator();
                    while (subscribedClientsIterator.hasNext()) {
                        ClientState state = subscribedClientsIterator.next().getValue();
                        if (state != null) {
                            if (state.principal != null) {
                                Set<URI> userRoles = state.principal.getRoles();
                                if (!state.principal.isSysadmin()
                                        && !org.openanzo.rdf.utils.Collections.memberOf(graphRoles, userRoles)) {
                                    // This user no longer has access to this graph so unsubscribe them.
                                    state.topicSubscriptions.remove(topic);
                                    subscribedClientsIterator.remove();
                                    if (subscriptions.subscribedClients.size() <= 0) {
                                        // This was the last subscriber so we can close this graph's consumer.
                                        topicSubscriptions.remove(topic);
                                        if (consumersToClose == null) {
                                            consumersToClose = new HashMap<String, MessageConsumer>();
                                        }
                                        consumersToClose.put(topic, subscriptions.consumer);
                                    }
                                }
                            }
                        }
                    }

                }
            } finally {
                mapLock.unlock();
            }

            // Now that we've released mapLock, we can close any consumers that were marked for closure.
            if (consumersToClose != null) {
                for (Map.Entry<String, MessageConsumer> entry : consumersToClose.entrySet()) {
                    closeMessageConsumer(entry.getValue());
                }
            }
        }
    }

    /**
     * Find the Bayeux username and clientId to which messages sent to the given topic should be relayed. Returns null if no such mapping could be found.
     * 
     * @param topic
     * @return a username and clientId pair.
     * @throws JMSException
     */
    protected Pair<String, String> findBayeuxReplyChannelForTopic(TemporaryTopic topic) throws JMSException {
        String destination = topic.getTopicName();
        Pair<String, String> ret = null;
        mapLock.lock();
        try {
            ClientState state = tempDestinationToClientState.get(destination);
            if (state != null) {
                ret = new Pair<String, String>(state.principal.getName(), state.clientId);
            }
        } finally {
            mapLock.unlock();
        }
        return ret;
    }

    /**
     * Find the collection of username and clientId pairs that are subscribed to the given topic.
     * 
     * @param graphUuid
     * @return a username and clientId pair.
     */
    protected Collection<Pair<String, String>> findChannelsSubscribedToTopic(String graphUuid) {
        Collection<Pair<String, String>> ret = Collections.emptyList();
        mapLock.lock();
        try {
            TopicSubscription subscriptions = topicSubscriptions.get(graphUuid);
            if (subscriptions != null && !subscriptions.subscribedClients.isEmpty()) {
                ret = new ArrayList<Pair<String, String>>(subscriptions.subscribedClients.size());
                for (Map.Entry<String, ClientState> subscribedClient : subscriptions.subscribedClients.entrySet()) {
                    ClientState state = subscribedClient.getValue();
                    if (state != null) {
                        Pair<String, String> pair = new Pair<String, String>(state.principal.getName(),
                                state.clientId);
                        ret.add(pair);
                    }
                }
            }
        } finally {
            mapLock.unlock();
        }
        return ret;
    }

    /**
     * Called when the service container is reset. This mainly clears any state that depends on the match between a graph's UUID and its URI. That is because
     * upon a server reset, the UUID may change. So all graph update subscriptions will be cleared, for example. But the temporary topics for the particular
     * Bayeux clients remain.
     */
    protected void reset() {
        //graphUuidResolver.clear();

        // Clear all of the graph subscriptions since they are essentially meaningless after a reset
        // since graph UUIDs will have changed.
        Collection<MessageConsumer> consumersToClose = removeAllTopicSubscriptions();
        for (MessageConsumer consumer : consumersToClose) {
            if (consumer != null) {
                closeMessageConsumer(consumer);
            }
        }
    }

    /**
     * Destroy all state including the JMS connection and Bayeux client state.
     * 
     * @param bundleStopping
     *            bundle stopping
     * @throws AnzoException
     */
    protected void destroy(boolean bundleStopping) throws AnzoException {
        closed = true;

        topicDeletionTimeoutTimer.cancel();
        topicDeletionTimeoutTimer = null;

        if (conn != null) {
            try {
                Collection<MessageConsumer> consumersToClose = null;
                Collection<TemporaryTopic> topicsToDelete = null;
                mapLock.lock();
                try {
                    if (bundleStopping) {
                        Collection<MessageConsumer> graphTopicConsumersToClose = removeAllTopicSubscriptions();
                        consumersToClose = new ArrayList<MessageConsumer>(
                                graphTopicConsumersToClose.size() + clientIdToClientState.size());
                        consumersToClose.addAll(graphTopicConsumersToClose);
                        topicsToDelete = new ArrayList<TemporaryTopic>(clientIdToClientState.size());
                        for (Map.Entry<String, ClientState> entry : clientIdToClientState.entrySet()) {
                            ClientState state = entry.getValue();
                            if (state != null) {
                                consumersToClose.add(state.consumer);
                                topicsToDelete.add(state.topic);
                            }
                        }
                    }
                    clientIdToClientState.clear();
                } finally {
                    mapLock.unlock();
                }
                if (bundleStopping) {
                    if (consumersToClose != null) {
                        for (MessageConsumer consumer : consumersToClose) {
                            if (consumer != null) {
                                closeMessageConsumer(consumer);
                            }
                        }
                    }
                    if (topicsToDelete != null) {
                        for (TemporaryTopic topic : topicsToDelete) {
                            if (topic != null) {
                                try {
                                    topic.delete();
                                } catch (JMSException jmsex) {
                                    log.warn(LogUtils.COMBUS_MARKER,
                                            "Error deleting bayuex connection manager temporary topic:"
                                                    + topic.toString(),
                                            jmsex);
                                }
                            }
                        }
                    }
                    if (session != null) {
                        try {
                            session.close();
                        } catch (JMSException jmsex) {
                            log.warn(LogUtils.COMBUS_MARKER, "Error closing bayuex connection manager session",
                                    jmsex);
                        } finally {
                            session = null;
                        }
                    }
                }
            } finally {
                try {
                    conn.close();
                } catch (JMSException jmsex) {
                    log.warn(LogUtils.COMBUS_MARKER, "Error closing bayuex connection manager  connection", jmsex);
                } finally {
                    conn = null;
                }
            }
        }
    }

    /**
     * Unsubscribe all clients from all topics. This is useful for resetting or stopping the bridge. This method will not close any of the JMS consumers for the
     * topics. Instead it will return them in a collection so that the caller can close them.
     */
    private Collection<MessageConsumer> removeAllTopicSubscriptions() {
        Collection<MessageConsumer> consumersToClose = null;
        mapLock.lock();
        try {
            int topicSubscriptionsSize = topicSubscriptions.size();
            if (topicSubscriptionsSize > 0) {
                consumersToClose = new ArrayList<MessageConsumer>(topicSubscriptionsSize);
                for (Map.Entry<String, TopicSubscription> subscriptionEntry : topicSubscriptions.entrySet()) {
                    TopicSubscription topicSubscription = subscriptionEntry.getValue();
                    consumersToClose.add(topicSubscription.consumer);
                    for (Map.Entry<String, ClientState> clientEntry : topicSubscription.subscribedClients
                            .entrySet()) {
                        ClientState state = clientEntry.getValue();
                        if (state != null) {
                            state.topicSubscriptions.clear();
                        }
                    }
                }
            }
            topicSubscriptions.clear();
        } finally {
            mapLock.unlock();
        }
        if (consumersToClose == null) {
            consumersToClose = Collections.emptyList();
        }
        return consumersToClose;
    }

    /**
     * Edits the state in the connection manager to reflect the addition of a subscription to given topic for the given client id. This method mainly does the
     * bookkeeping. It doesn't handle creating any JMS consumers or checking ACLs. That is expected to be done by the calling method.
     * 
     * @param topic
     * @param clientId
     * @param topicSubscription
     */
    private void addTopicSubscription(String topic, String clientId, TopicSubscription topicSubscription) {
        mapLock.lock();
        try {
            ClientState state = clientIdToClientState.get(clientId);
            if (state != null) {
                topicSubscription.subscribedClients.put(clientId, state);
                state.topicSubscriptions.add(topic);
            }
        } finally {
            mapLock.unlock();
        }
        log.debug(LogUtils.COMBUS_MARKER, "Subscribed client {} to topic {}", clientId, topic);
    }

    /**
     * Removes the client's subscription to the topic. If this is the last subscriber, then the topic's JMS consumer is returned so that it can be closed by the
     * caller.
     * 
     * @param topic
     *            the topic from which to unsubscribe.
     * @param clientId
     *            the id of the client to unsubscribe.
     */
    private MessageConsumer unsubscribeTopic(String topic, String clientId) {
        MessageConsumer consumerToClose = null;
        mapLock.lock();
        try {
            TopicSubscription topicSubscription = topicSubscriptions.get(topic);
            if (topicSubscription != null) {
                topicSubscription.subscribedClients.remove(clientId);
                if (topicSubscription.subscribedClients.size() <= 0) {
                    // This was the last subscriber so we can close this topic's consumer.
                    // But we can't do it while holding mapLock to avoid a deadlock, so we'll let the caller
                    // take care of destroying it.
                    consumerToClose = topicSubscription.consumer;
                    topicSubscriptions.remove(topic);
                }
            }
        } finally {
            mapLock.unlock();
        }
        return consumerToClose;
    }

    /**
     * Closes the given consumer with some exception handling to handle an ActiveMQ bug. WARNING: This method make JMS calls so don't call this method while
     * holding mapLock due to possible deadlock.
     * 
     * @param consumer
     */
    private void closeMessageConsumer(MessageConsumer consumer) {
        if (consumer != null) {
            try {
                consumer.close();
            } catch (NullPointerException npe) {

            } catch (JMSException jmsex) {
                log.warn(LogUtils.COMBUS_MARKER, "Error unsubscribing from graph updates.", jmsex);
            }
        }
    }

    /**
     * determine whether or not the current user can subscribe to the given topic.
     * 
     * @param topic
     * @param authorizationService
     * @param opContext
     * @return
     * @throws AnzoException
     */
    private boolean userHasTopicAccess(String topic, AnzoPrincipal principal, IOperationContext opContext)
            throws AnzoException {
        if (topic.startsWith(NAMESPACES.NAMEDGRAPH_TOPIC_PREFIX)) {
            String graphUuid = UriGenerator.stripEncapsulatedString(NAMESPACES.NAMEDGRAPH_TOPIC_PREFIX, topic);
            URI graphUri = datasource.getModelService().getUriForUUID(opContext,
                    Constants.valueFactory.createURI(graphUuid));
            // URI graphUri = graphUuidResolver.getGraphUri(graphUuid, opContext, datasource.getModelService());
            return userHasGraphReadAccess(graphUri, principal, opContext);
        } else if (topic.equals(COMBUS.TRANSACTIONS_TOPIC)) {
            return true;
        } else if (topic.startsWith(NAMESPACES.STREAM_TOPIC_PREFIX)) {
            String graphUuid = UriGenerator.stripEncapsulatedString(NAMESPACES.STREAM_TOPIC_PREFIX, topic);
            URI graphUri = datasource.getModelService().getUriForUUID(opContext,
                    Constants.valueFactory.createURI(graphUuid));
            //URI graphUri = graphUuidResolver.getGraphUri(graphUuid, opContext, datasource.getModelService());
            return userHasGraphReadAccess(graphUri, principal, opContext);
        } else {
            throw new AnzoException(ExceptionConstants.COMBUS.INVALID_TOPIC, topic);
        }
    }

    /**
     * Checks if the current user has read access to the given graph.
     * 
     * @param graphUri
     *            The graph URI to check access.
     * @param opContext
     *            The operation context to use when communicating with the Anzo authorization service.
     * @return true if read access is granted, false otherwise.
     * @throws AnzoException
     */
    private boolean userHasGraphReadAccess(URI graphUri, AnzoPrincipal principal, IOperationContext opContext)
            throws AnzoException {
        boolean ret = false;
        if (principal == null) {
            throw new SecurityException("No currrently logged in principal.");
        }
        Set<URI> principalRoles = principal.getRoles();

        if (principal.isSysadmin()) {
            ret = true;
        } else {
            Set<URI> roles = datasource.getAuthorizationService().getRolesForGraph(opContext, graphUri,
                    Privilege.READ);
            ret = org.openanzo.rdf.utils.Collections.memberOf(roles, principalRoles);
        }
        return ret;
    }

    /**
     * Checks if the current user has read access to the given graph.
     * 
     * @param graphUri
     *            The graph URI to check access.
     * @param opContext
     *            The operation context to use when communicating with the Anzo authorization service.
     * @return true if read access is granted, false otherwise.
     * @throws AnzoException
     */
    private boolean userHasGraphAddAccess(URI graphUri, AnzoPrincipal principal, IOperationContext opContext)
            throws AnzoException {
        boolean ret = false;
        if (principal == null) {
            throw new SecurityException("No currrently logged in principal.");
        }
        Set<URI> principalRoles = principal.getRoles();
        if (principal.isSysadmin()) {
            ret = true;
        } else {
            Set<URI> roles = datasource.getAuthorizationService().getRolesForGraph(opContext, graphUri,
                    Privilege.ADD);
            ret = org.openanzo.rdf.utils.Collections.memberOf(roles, principalRoles);
        }
        return ret;
    }

    /**
     * Represents state held for one connection to the BayeuxJMSBridge. A particular user can connect many times under a different client id. This represents
     * one of those connections. The two main pieces of state held here are the temporary topic and the consumer. The temporary topic is the topic which
     * receives messages intended for the client. The consumer is listening on that topic.
     * 
     * The rest of the state kept for basic bookkeeping.
     */
    static class ClientState {

        protected ClientState(AnzoPrincipal principal, TemporaryTopic topic, String clientId,
                MessageConsumer consumer) {
            this.topic = topic;
            this.principal = principal;
            this.clientId = clientId;
            this.consumer = consumer;
            this.topicSubscriptions = Collections.synchronizedSet(new HashSet<String>());
        }

        final AnzoPrincipal principal;

        final String clientId;

        final TemporaryTopic topic;

        final MessageConsumer consumer;

        /**
         * The set of topics to which this client is subscribed. We keep this mainly so that we can unsubscribe for them all during disconnection. Do not access
         * without first acquiring mapLock.
         */
        final Set<String> topicSubscriptions;
    }

    /**
     * A TopicSubscription represents the state related to a subscription to topic. It keeps the JMS Consumer listening to the updates and it keeps a set of
     * Bayeux clientIds who are subscribed to messages for that topic.
     */
    static class TopicSubscription {
        final MessageConsumer consumer;

        /**
         * Map from clientId to ClientState. This is essentially just a set of all of the Bayeux clients subscribed to a particular topic. The only reason this
         * is a Map rather than a Set is because it's more efficient to already have the ClientState (which we'll need to send the topic messages via Bayeux)
         * rather than having to lookup each ClientState as we iterate through the subscribedClients.
         */
        final Map<String, ClientState> subscribedClients;

        protected TopicSubscription(MessageConsumer consumer) {
            this.consumer = consumer;
            this.subscribedClients = Collections.synchronizedMap(new HashMap<String, ClientState>());
        }
    }

}