/**
* Copyright (C) 2011 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.web.analytics.push;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.opengamma.core.change.ChangeEvent;
import com.opengamma.core.change.ChangeListener;
import com.opengamma.id.ObjectId;
import com.opengamma.id.UniqueId;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.web.analytics.rest.MasterType;
/**
* Connection associated with one client (i.e. one browser window / tab / client app instance). Allows subscriptions
* to be set up so the client is notified if an entity or the contents of a master changes.
* The published notifications contain the REST URL of the thing that has changed.
* All subscriptions for a URL are automatically cancelled the first time a notification is published for the URL
* and must be re-established every time the client accesses the URL. This class is thread safe.
* TODO should this be package-private and everything moved into the same package?
*/
public class ClientConnection implements ChangeListener, MasterChangeListener, UpdateListener {
private static final Logger s_logger = LoggerFactory.getLogger(ClientConnection.class);
/** Login ID of the user that owns this connection TODO this isn't used yet */
private final String _userId;
/** Unique ID of this connection */
private final String _clientId;
/** Task that closes this connection if it is idle for too long */
private final ConnectionTimeoutTask _timeoutTask;
/** Listener that forwards changes over HTTP whenever any updates occur to which this connection subscribes */
private final UpdateListener _listener;
/** Listeners that are called when this connection closes. */
private final List<DisconnectionListener> _disconnectionListeners = new CopyOnWriteArrayList<DisconnectionListener>();
/** Lock which must be held when mutating any of the objects below */
private final Object _lock = new Object();
/** URLs which should be published when a master changes, keyed by the type of the master */
private final Multimap<MasterType, String> _masterUrls = HashMultimap.create();
/** URLs which should be published when an entity changes, keyed on the entity's ID */
private final Multimap<ObjectId, String> _entityUrls = HashMultimap.create();
/** Map of URLs for which changes should be published to their underlying objects. */
private final Map<String, UrlMapping> _urlMappings = new HashMap<String, UrlMapping>();
/** Connection flag. */
private boolean _connected = true;
/**
* @param userId Login ID of the user that owns this connection, null if not known
* @param clientId Unique ID of this connection
* @param listener Listener that forwards changes over HTTP whenever any updates occur to which this connection subscribes
* @param timeoutTask Task that closes this connection if it is idle for too long
*/
/* package */ ClientConnection(String userId,
String clientId,
UpdateListener listener,
ConnectionTimeoutTask timeoutTask) {
ArgumentChecker.notNull(listener, "listener");
ArgumentChecker.notNull(clientId, "clientId");
ArgumentChecker.notNull(timeoutTask, "timeoutTask");
s_logger.debug("Creating new client connection. userId: {}, clientId: {}", userId, clientId);
_userId = userId;
_listener = listener;
_clientId = clientId;
_timeoutTask = timeoutTask;
}
/**
* @return Login ID of the user that owns this connection
*/
/* package */ String getUserId() {
return _userId;
}
/**
* Disconnects this client.
*/
/* package */ void disconnect() {
s_logger.debug("Disconnecting client connection, userId: {}, clientId: {}", _userId, _clientId);
synchronized (_lock) {
_connected = false;
_timeoutTask.cancel();
for (DisconnectionListener listener : _disconnectionListeners) {
try {
listener.clientDisconnected();
} catch (Exception e) {
s_logger.warn("Problem calling disconnection listener", e);
}
}
}
}
/**
* Sets up a subscription that publishes an update to the client when an entity changes.
* The subscription is automatically cancelled after the first time the entity changes.
* @param uid The unique ID of an entity
* @param url The REST URL of the entity. This is published to the client when the entity is updated
*/
/* package */ void subscribe(UniqueId uid, String url) {
ArgumentChecker.notNull(uid, "uid");
ArgumentChecker.notNull(url, "url");
s_logger.debug("Client ID {} subscribing for changes to {}, URL: {}", new Object[]{_clientId, uid, url});
synchronized (_lock) {
_timeoutTask.reset();
ObjectId objectId = uid.getObjectId();
_entityUrls.put(objectId, url);
_urlMappings.put(url, UrlMapping.create(_urlMappings.get(url), objectId));
}
}
@Override
public void entityChanged(ChangeEvent event) {
s_logger.debug("Received ChangeEvent {}", event);
synchronized (_lock) {
ObjectId objectId = event.getObjectId();
Collection<String> urls = _entityUrls.removeAll(objectId);
removeSubscriptions(urls);
if (!urls.isEmpty()) {
_listener.itemsUpdated(urls);
}
}
}
/**
* Sets up a subscription that publishes an update to the client when any entity in a master changes.
* This tells a client that the results of a previously executed query <em>might</em> have changed.
* The subscription is automatically cancelled after the first time the master is updated.
* @param masterType The type of master
* @param url The REST URL whose results might be invalidated by changes in the master
*/
/* package */ void subscribe(MasterType masterType, String url) {
ArgumentChecker.notNull(masterType, "masterType");
ArgumentChecker.notNull(url, "url");
s_logger.debug("Subscribing to notifications for changes to {} master, notification URL: {}", masterType, url);
synchronized (_lock) {
_timeoutTask.reset();
_masterUrls.put(masterType, url);
_urlMappings.put(url, UrlMapping.create(_urlMappings.get(url), masterType));
}
}
@Override
public void masterChanged(MasterType masterType) {
s_logger.debug("Received notification {} master changed", masterType);
synchronized (_lock) {
Collection<String> urls = _masterUrls.removeAll(masterType);
removeSubscriptions(urls);
if (!urls.isEmpty()) {
_listener.itemsUpdated(urls);
}
}
}
/**
* Removes all subscriptions for the URLs. When an update is published for a URL all subscriptions for that
* URL for all {@link MasterType}s or entity {@link ObjectId}s are cancelled.
* @param urls The URLs for which updates have been published
*/
private void removeSubscriptions(Collection<String> urls) {
for (String url : urls) {
UrlMapping urlMapping = _urlMappings.get(url);
// remove mappings for this url for master type
for (MasterType type : urlMapping.getMasterTypes()) {
_masterUrls.remove(type, url);
}
// remove mappings for this url for all entities
for (ObjectId entityId : urlMapping.getEntityIds()) {
_entityUrls.remove(entityId, url);
}
}
}
@Override
public void itemUpdated(Object callbackId) {
_listener.itemUpdated(callbackId);
}
@Override
public void itemsUpdated(Collection<?> callbackIds) {
_listener.itemsUpdated(callbackIds);
}
/**
* Adds a listener that will be notified when the client disconnects. If this is called after the client has
* disconnected the listener will be called immediately.
* @param listener The listener
*/
public void addDisconnectionListener(DisconnectionListener listener) {
synchronized (_lock) {
if (_connected) {
_disconnectionListeners.add(listener);
} else {
listener.clientDisconnected();
}
}
}
/**
* <p>Container for sets of {@link MasterType}s or {@link ObjectId}s associated with a subscription for a REST URL.
* This is to allow all subscriptions for a URL to be cleared when its first update is published.</p>
* <p>This assumes there can be multiple subscriptions for a URL with different {@link MasterType}s or
* entity {@link ObjectId}s. TODO Need to check whether this is actually the case.
* If not this could probably be scrapped.</p>
*/
private static final class UrlMapping {
private final Set<MasterType> _masterTypes;
private final Set<ObjectId> _entityIds;
private UrlMapping(Set<MasterType> masterTypes, Set<ObjectId> entityIds) {
_masterTypes = masterTypes;
_entityIds = entityIds;
}
private Set<MasterType> getMasterTypes() {
return _masterTypes;
}
private Set<ObjectId> getEntityIds() {
return _entityIds;
}
private static UrlMapping create(UrlMapping urlMapping, MasterType masterType) {
if (urlMapping == null) {
return new UrlMapping(ImmutableSet.of(masterType), Collections.<ObjectId>emptySet());
} else {
ImmutableSet<MasterType> masterTypes =
ImmutableSet.<MasterType>builder().addAll(urlMapping.getMasterTypes()).add(masterType).build();
return new UrlMapping(masterTypes, urlMapping.getEntityIds());
}
}
private static UrlMapping create(UrlMapping urlMapping, ObjectId entityId) {
if (urlMapping == null) {
return new UrlMapping(Collections.<MasterType>emptySet(), ImmutableSet.of(entityId));
} else {
ImmutableSet<ObjectId> entityIds =
ImmutableSet.<ObjectId>builder().addAll(urlMapping.getEntityIds()).add(entityId).build();
return new UrlMapping(urlMapping.getMasterTypes(), entityIds);
}
}
}
/**
* Listeners are called when a connection disconnects.
*/
public interface DisconnectionListener {
/**
* Called when the {@link ClientConnection} disconnects.
*/
void clientDisconnected();
}
}