/**
* 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.Map;
import java.util.Timer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import com.google.common.base.Objects;
import com.opengamma.DataNotFoundException;
import com.opengamma.core.change.ChangeManager;
import com.opengamma.id.UniqueId;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.web.analytics.rest.MasterType;
/**
* {@link ConnectionManager} implementation that creates an instance of {@link ClientConnection} for each
* client. It creates {@link Timer} tasks for each connection that closes them and cleans up if they are idle
* for too long. This class is thread safe.
*/
public class ConnectionManagerImpl implements ConnectionManager {
/** Period for the tasks that check whether the client connections have been idle for too long */
private static final long DEFAULT_TIMEOUT_CHECK_PERIOD = 20000;
/** By default a client is disconnected if it hasn't been heard from for 60 seconds */
private static final long DEFAULT_TIMEOUT = 60000;
// TODO a better way to generate client IDs
/** Client ID of the next connection */
private final AtomicLong _clientConnectionId = new AtomicLong();
/** Provides a connection to the long-polling HTTP connections */
private final LongPollingConnectionManager _longPollingConnectionManager;
/** Maximum time a client is allow to be idle before it's disconnected */
private final long _timeout;
/** Period for the tasks that check for idle clients */
private final long _timeoutCheckPeriod;
/** Connections keyed on client ID */
private final Map<String, ClientConnection> _connectionsByClientId = new ConcurrentHashMap<String, ClientConnection>();
/** Timer for tasks that check for idle clients */
private final Timer _timer = new Timer();
/** For listening for changes in entity data */
private final ChangeManager _changeManager;
/** For listening for changes to any data in a master */
private final MasterChangeManager _masterChangeManager;
public ConnectionManagerImpl(ChangeManager changeManager,
MasterChangeManager masterChangeManager,
LongPollingConnectionManager longPollingConnectionManager) {
this(changeManager,
masterChangeManager,
longPollingConnectionManager,
DEFAULT_TIMEOUT,
DEFAULT_TIMEOUT_CHECK_PERIOD);
}
public ConnectionManagerImpl(ChangeManager changeManager,
MasterChangeManager masterChangeManager,
LongPollingConnectionManager longPollingConnectionManager,
long timeout,
long timeoutCheckPeriod) {
_changeManager = changeManager;
_longPollingConnectionManager = longPollingConnectionManager;
_timeout = timeout;
_timeoutCheckPeriod = timeoutCheckPeriod;
_masterChangeManager = masterChangeManager;
}
/**
* Creates a new connection for a client and returns its client ID. The client ID should be used by the client
* when subscribing for asynchronous updates. A connection typically corresponds to a single browser tab or
* window. A user can have multiple simultaneous connections.
* @param userId The ID of the user creating the connection, null if not known
* @return The client ID of the new connection, must be supplied by the client when subscribing for updates
*/
@Override
public String clientConnected(String userId) {
String clientId = Long.toString(_clientConnectionId.getAndIncrement());
ConnectionTimeoutTask timeoutTask = new ConnectionTimeoutTask(this, userId, clientId, _timeout);
LongPollingUpdateListener updateListener = _longPollingConnectionManager.handshake(userId, clientId, timeoutTask);
ClientConnection connection = new ClientConnection(userId, clientId, updateListener, timeoutTask);
_changeManager.addChangeListener(connection);
_masterChangeManager.addChangeListener(connection);
_connectionsByClientId.put(clientId, connection);
_timer.scheduleAtFixedRate(timeoutTask, _timeoutCheckPeriod, _timeoutCheckPeriod);
return clientId;
}
@Override
public void clientDisconnected(String userId, String clientId) {
ClientConnection connection = getConnectionByClientId(userId, clientId);
_connectionsByClientId.remove(clientId);
_changeManager.removeChangeListener(connection);
_masterChangeManager.removeChangeListener(connection);
_longPollingConnectionManager.disconnect(clientId);
connection.disconnect();
}
@Override
public void subscribe(String userId, String clientId, UniqueId uid, String url) {
getConnectionByClientId(userId, clientId).subscribe(uid, url);
}
@Override
public void subscribe(String userId, String clientId, MasterType masterType, String url) {
getConnectionByClientId(userId, clientId).subscribe(masterType, url);
}
/**
* Returns the {@link ClientConnection} corresponding to a client ID.
* @param userId The ID of the user who owns the connection, null if not known
* @param clientId The client ID
* @return The connection
* @throws DataNotFoundException If there is no connection for the specified ID, the user ID is invalid or if
* the client and user IDs don't correspond
* TODO not sure this should be public
* TODO or should it be specified in ClientConnection?
*/
@Override
public ClientConnection getConnectionByClientId(String userId, String clientId) {
ArgumentChecker.notEmpty(clientId, "clientId");
ClientConnection connection = _connectionsByClientId.get(clientId);
if (connection == null) {
throw new DataNotFoundException("Unknown client ID: " + clientId);
}
userId = ("permissive".equals(userId) ? null : userId);
if (!Objects.equal(userId, connection.getUserId())) {
throw new DataNotFoundException("User ID " + userId + " is not associated with client ID " + clientId);
}
return connection;
}
}