/**
* 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.Set;
import org.eclipse.jetty.continuation.Continuation;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Sets;
import com.opengamma.util.ArgumentChecker;
/**
* {@link UpdateListener} that pushes updates over a long-polling HTTP connection using Jetty's continuations.
* If any updates arrive while there is no connection they are queued and sent as soon as the connection
* is re-established. If multiple updates for the same object are queued only one is sent. All updates
* only contain the REST URL of the updated object so they are identical.
*/
/* package */ class LongPollingUpdateListener implements UpdateListener {
private static final Logger s_logger = LoggerFactory.getLogger(LongPollingUpdateListener.class);
/** Key for the array of updated URLs in the JSON */
static final String UPDATES = "updates";
private final Object _lock = new Object();
private final Set<Object> _updates = Sets.newHashSet();
private final String _userId;
private final ConnectionTimeoutTask _timeoutTask;
private final String _clientId;
private Continuation _continuation;
/**
* Creates a new listener for a user.
* @param clientId Client ID of the connection
* @param userId Login ID of the user
* @param timeoutTask Connection timeout task that the listener must reset every time the connection is set up
*/
/* package */ LongPollingUpdateListener(String clientId, String userId, ConnectionTimeoutTask timeoutTask) {
_clientId = clientId;
ArgumentChecker.notEmpty(clientId, "clientId");
//ArgumentChecker.notEmpty(userId, "userId");
ArgumentChecker.notNull(timeoutTask, "timeoutTask");
_userId = userId;
_timeoutTask = timeoutTask;
}
/**
* Publishes {@code url} to the client as JSON. If the client is connected (i.e. this listener has a
* continuation) the URL is sent immediately. If the client isn't connected it is queued until the
* connection is re-established.
* @param callbackId REST URL of the item that has been updated
*/
@Override
public void itemUpdated(Object callbackId) {
ArgumentChecker.notNull(callbackId, "url");
synchronized (_lock) {
if (_continuation != null) {
try {
sendUpdate(formatUpdate(callbackId));
} catch (JSONException e) {
// this shouldn't ever happen
s_logger.warn("Unable to format callback ID as JSON: " + callbackId, e);
}
} else {
_updates.add(callbackId);
}
}
}
/**
* Publishes {@code urls} to the client as JSON. If the client is connected (i.e. this listener has a
* continuation) the URLs are sent immediately. If the client isn't connected they are queued until the
* connection is re-established.
* @param callbackIds REST URLs of the items that have been updated
*/
@Override
public void itemsUpdated(Collection<?> callbackIds) {
ArgumentChecker.notNull(callbackIds, "callbackIds");
if (callbackIds.isEmpty()) {
return;
}
synchronized (_lock) {
if (_continuation != null) {
try {
sendUpdate(formatUpdate(callbackIds));
} catch (JSONException e) {
// this shouldn't ever happen, the updates are all URLs
s_logger.warn("Unable to format URLs as JSON. URLs: " + callbackIds, e);
}
} else {
_updates.addAll(callbackIds);
}
}
}
/**
* Invoked when a client establishes a long-polling HTTP connection.
* @param continuation The connection's continuation
*/
/* package */ void connect(Continuation continuation) {
synchronized (_lock) {
s_logger.debug("Long polling connection established, resetting timeout task {}", _timeoutTask);
_timeoutTask.reset();
_continuation = continuation;
_continuation.setTimeout(10000);
// if there are updates queued sent them immediately otherwise save the continuation until an update
if (!_updates.isEmpty()) {
try {
sendUpdate(formatUpdate(_updates));
} catch (JSONException e) {
// this shouldn't ever happen, the updates are all URLs
s_logger.warn("Unable to format updates as JSON. updates: " + _updates, e);
}
_updates.clear();
}
}
}
/**
* Adds {@code urls} to the connection's continuation and resumes it so the response is sent to the client.
* @param update URLs of the changed items
*/
private void sendUpdate(String update) {
_continuation.setAttribute(LongPollingServlet.RESULTS, update);
_continuation.resume();
_continuation = null;
s_logger.debug("Sent update to client {}: {}", _clientId, update);
}
// for testing
/* package */ boolean isConnected() {
synchronized (_lock) {
return _continuation != null;
}
}
/**
* Formats a URL as JSON.
* @param url A URL
* @return {@code {updates: [url]}}
* @throws JSONException Never
*/
private String formatUpdate(Object url) throws JSONException {
return new JSONObject().put(UPDATES, new Object[]{url}).toString();
}
/**
* Formats URLs as JSON.
* @param urls URLs
* @return {@code {updates: [url1, url2, ...]}}
* @throws JSONException Never
*/
private String formatUpdate(Collection<?> urls) throws JSONException {
return new JSONObject().put(UPDATES, urls).toString();
}
/**
* Closes this listener's HTTP connection.
*/
/* package */ void disconnect() {
synchronized (_lock) {
if (_continuation != null && _continuation.isSuspended()) {
_continuation.complete();
}
_continuation = null;
}
}
/**
* @return Login ID of the user who owns this listener's connection
*/
/* package */ String getUserId() {
return _userId;
}
/**
* Invoked when this listener's continuation times out before any data is sent.
* @param continuation The continuation that timed out - should be this listener's continuation.
*/
/* package */ void timeout(Continuation continuation) {
synchronized (_lock) {
if (continuation == _continuation) {
_continuation = null;
}
}
}
}