/*******************************************************************************
* 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>());
}
}
}