com.zipwhip.api.ClientZipwhipNetworkSupport Maven / Gradle / Ivy
package com.zipwhip.api;
import com.zipwhip.api.exception.NotAuthenticatedException;
import com.zipwhip.api.response.ServerResponse;
import com.zipwhip.api.settings.PreferencesSettingsStore;
import com.zipwhip.api.settings.SettingsStore;
import com.zipwhip.api.settings.SettingsVersionStore;
import com.zipwhip.api.settings.VersionStore;
import com.zipwhip.api.signals.SignalProvider;
import com.zipwhip.api.signals.TearDownConnectionObserver;
import com.zipwhip.api.signals.VersionMapEntry;
import com.zipwhip.api.signals.commands.SubscriptionCompleteCommand;
import com.zipwhip.api.signals.sockets.ConnectionHandle;
import com.zipwhip.api.signals.sockets.ConnectionHandleAware;
import com.zipwhip.api.signals.sockets.ConnectionState;
import com.zipwhip.concurrent.*;
import com.zipwhip.events.Observer;
import com.zipwhip.important.ImportantTaskExecutor;
import com.zipwhip.lifecycle.DestroyableBase;
import com.zipwhip.signals.presence.Presence;
import com.zipwhip.util.Asserts;
import com.zipwhip.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import static com.zipwhip.concurrent.ThreadUtil.ensureLock;
/**
* A base class for future implementation to extend.
*
* It takes all the non-API specific stuff out of ZipwhipClient implementations.
*
* If some class wants to communicate with Zipwhip, then it needs to extend this
* class. This class gives functionality that can be used to parse Zipwhip API.
* This naming convention was copied from Spring (JmsSupport) base class.
*/
public abstract class ClientZipwhipNetworkSupport extends ZipwhipNetworkSupport {
protected static final Logger LOGGER = LoggerFactory.getLogger(ClientZipwhipNetworkSupport.class);
protected final ImportantTaskExecutor importantTaskExecutor;
protected long signalsConnectTimeoutInSeconds = 10;
protected SignalProvider signalProvider;
protected SettingsStore settingsStore;
protected VersionStore versionsStore;
// this is so we can block until SubscriptionCompleteCommand comes in.
// do we have a current SubscriptionCompleteCommand to use.
protected ObservableFuture connectFuture;
/**
*
*
* @param executor The Executor that's used for processing callbacks, futures, and SignalProvider events.
* @param importantTaskExecutor This class gives us the ability to expire and cancel futures (SubscriptionCompleteCommand never comes back).
* @param connection For talking with Zipwhip (message/send)
* @param signalProvider For signal i/o
*/
public ClientZipwhipNetworkSupport(SettingsStore store, Executor executor, ImportantTaskExecutor importantTaskExecutor, ApiConnection connection, SignalProvider signalProvider) {
super(executor, connection);
if (signalProvider != null) {
setSignalProvider(signalProvider);
link(signalProvider);
}
if (importantTaskExecutor == null){
importantTaskExecutor = new ImportantTaskExecutor();
this.link(importantTaskExecutor);
}
this.importantTaskExecutor = importantTaskExecutor;
if (store == null) {
store = new PreferencesSettingsStore();
}
this.setSettingsStore(store);
if (signalProvider != null){
// Start listening to provider events that interest us
initSignalProviderEvents();
}
}
public ObservableFuture connect() throws Exception {
return connect(null);
}
public synchronized ObservableFuture connect(final Presence presence) throws Exception {
synchronized (signalProvider) {
final ObservableFuture finalConnectFuture = getUnchangingConnectFuture();
if (finalConnectFuture != null) {
LOGGER.debug(String.format("Returning %s since it's still active", connectFuture));
return finalConnectFuture;
}
// throw any necessary sanity checks.
validateConnectState();
synchronized (settingsStore) {
// put the state into the local store
accessSettings();
setupSettingsStoreForConnection();
// pull the state out.
boolean expectingSubscriptionCompleteCommand = Boolean.parseBoolean(settingsStore.get(SettingsStore.Keys.EXPECTS_SUBSCRIPTION_COMPLETE));
String clientId = settingsStore.get(SettingsStore.Keys.CLIENT_ID);
String sessionKey = settingsStore.get(SettingsStore.Keys.SESSION_KEY);
Map versions = versionsStore.get();
if (isConnected()) {
return new FakeObservableFuture(this, signalProvider.getConnectionHandle());
}
// this future updates itself (clearing out this.connectFuture)
final NestedObservableFuture future = new NestedObservableFuture(this);
synchronized (future) {
// setting this will cause the other threads to notice that we're connecting.
// set it FIRST in case the below line finishes too early! (aka: synchronously!)
setConnectFuture(future);
ObservableFuture requestFuture = executeConnectWithFailureDetection(
clientId,
sessionKey,
presence,
versions,
expectingSubscriptionCompleteCommand);
requestFuture.addObserver(
new OnlyRunIfSuccessfulObserverAdapter(
new ThreadSafeObserver>(
// make sure this is still the active connectionHandle.
new ConnectionHandleStillActiveObserverAdapter>(
new UpdateLocalStoreWithLastKnownSubscribedClientIdOnSuccessObserver(this)))));
requestFuture.addObserver(
new OnlyRunIfNotSuccessfulObserverAdapter(
new ThreadSafeObserver>(
new TearDownConnectionObserver(false))));
requestFuture.addObserver(
new ThreadSafeObserver>(
new ClearConnectFutureOnCompleteObserver(future)));
// need to only alert success from the executor thread.
// requestFuture.addObserver(new CopyFutureStatusToNestedFuture(future));
// let the cancellation cascade down.
future.setNestedFuture(requestFuture);
return future;
}
}
}
}
private ObservableFuture executeConnectWithFailureDetection(String clientId, String sessionKey, Presence presence, Map versions, boolean expectingSubscriptionCompleteCommand) {
ObservableFuture requestFuture = importantTaskExecutor.enqueue(
callbackExecutor,
new ConnectViaSignalProviderTask(this, signalProvider, clientId, sessionKey, presence, versions, expectingSubscriptionCompleteCommand),
getSignalsConnectTimeoutInSeconds() * 2);
return requestFuture;
}
public synchronized ObservableFuture disconnect() {
return disconnect(false);
}
public synchronized ObservableFuture disconnect(final boolean causedByNetwork) {
// validateConnectState();
return signalProvider.disconnect(causedByNetwork);
}
/**
* Tells you if this connection is 100% ready to go.
*
* Within the context of ZipwhipClient, we have defined the "connected" state
* to mean both having a TCP connection AND having a SubscriptionComplete. (ie: connected & authenticated)
*
* Internally we shouldn't use this method because we don't control
*
* @return
*/
public boolean isConnected() {
ConnectionState connectionState = signalProvider.getConnectionState();
switch (connectionState) {
case CONNECTING:
case CONNECTED:
case DISCONNECTING:
case DISCONNECTED:
return false;
case AUTHENTICATED:
// the SignalProvider says that they are AUTHENTICATED. That just means that they have
// received the {action:CONNECT} command back. We on the other hand need to receive
// a SubscriptionCompleteCommand. The best way to do that is to check that the current
// clientId is in our local database as "subscribed"
synchronized (settingsStore) {
String clientId = signalProvider.getClientId();
String savedClientId = this.settingsStore.get(SettingsStore.Keys.CLIENT_ID);
String lastSubscribedClientId = this.settingsStore.get(SettingsStore.Keys.LAST_SUBSCRIBED_CLIENT_ID);
if (StringUtil.equals(clientId, savedClientId)) {
// it's current!
if (StringUtil.equals(clientId, lastSubscribedClientId)) {
// we're up to date!
return true;
}
}
return false;
}
}
return false;
}
protected void initSignalProviderEvents() {
signalProvider.getVersionChangedEvent().addObserver(
new DifferentExecutorObserverAdapter(callbackExecutor,
new ThreadSafeObserver(
new ConnectionHandleStillActiveObserverAdapter(
updateVersionsStoreOnVersionChanged))));
// We don't need to do this because we do our own checking in the ConnectionChangedEvent.
// Don't trust the SignalProvider to know what the word "new" is within the context of a clientId.
// signalProvider.getNewClientIdReceivedEvent().addObserver(
// new DifferentExecutorObserverAdapter(executor,
// new ThreadSafeObserver(
// new ActiveConnectionHandleFilter(
// onNewClientIdReceivedObserver))));
// signalProvider.getSubscriptionCompleteReceivedEvent().addObserver(
// new DifferentExecutorObserverAdapter(executor,
// new ThreadSafeObserver(
// new ActiveConnectionHandleFilter(
// new Observer() {
// @Override
// public void notify(Object sender, SubscriptionCompleteCommand command) {
// LOGGER.warn(sender);
// }
// }))));
signalProvider.getConnectionChangedEvent().addObserver(
new DifferentExecutorObserverAdapter(callbackExecutor,
new ThreadSafeObserver(
new Observer() {
@Override
public void notify(Object sender, Boolean connected) {
if (connected) {
ObservableFuture connectFuture = getUnchangingConnectFuture();
if (connectFuture != null && !connectFuture.isDone()) {
LOGGER.debug("SignalProvider.onConnectionChanged: Connect future is processing, we're going to ignore this one.");
return;
}
final ConnectionHandle connectionHandle = (ConnectionHandle) sender;
String sessionKey = settingsStore.get(SettingsStore.Keys.SESSION_KEY);
String lastSuccessfulClientId = settingsStore.get(SettingsStore.Keys.LAST_SUBSCRIBED_CLIENT_ID);
final String currentClientId = signalProvider.getClientId();
LOGGER.warn(String.format("SignalProvider.onConnectionChanged: lastSuccessfulClientId: %s; currentClientId: %s", lastSuccessfulClientId, currentClientId));
if (!StringUtil.equals(lastSuccessfulClientId, currentClientId)) {
LOGGER.warn("We should ask for a SubscriptionComplete now!");
final ObservableFuture future = executeSignalsConnect(connectionHandle, currentClientId, sessionKey);
future.addObserver(
new ThreadSafeObserver>(
new Observer>() {
@Override
public void notify(Object sender, ObservableFuture future) {
synchronized (future) {
if (!future.isSuccess()) {
LOGGER.error("UpdateLocalStoreObserver: The future was cancelled or errored. Quitting: " + connectionHandle);
return; // this covers isCancelled.
}
}
synchronized (settingsStore) {
synchronized (connectionHandle) {
if (connectionHandle.isDestroyed()) {
LOGGER.error("UpdateLocalStoreObserver: The connectionHandle wasn't active anymore. This must have been for a previous connection. Quitting: " + connectionHandle);
return;
}
LOGGER.debug("UpdateLocalStoreObserver: saving data " + currentClientId);
onSubscriptionComplete(currentClientId);
}
}
}
}));
future.addObserver(
new ThreadSafeObserver>(
new TearDownConnectionObserver(true)));
}
}
}
}
)));
}
private void setupSettingsStoreForConnection() {
synchronized (settingsStore) {
/**
* Validate SESSION KEYS
*/
boolean cleared = false;
boolean expectingSubscriptionCompleteCommand = false;
String existingSessionKey = connection.getSessionKey();
String existingClientId = signalProvider == null ? null : signalProvider.getClientId();
String storedSessionKey = settingsStore.get(SettingsStore.Keys.SESSION_KEY);
String storedClientId = settingsStore.get(SettingsStore.Keys.CLIENT_ID);
String correctSessionKey = storedSessionKey;
String correctClientId = existingClientId;
/**
* If the sessionKey has changed we need to invalidate the settings data
*/
if (StringUtil.exists(existingSessionKey) && !StringUtil.equals(existingSessionKey, storedSessionKey)) {
expectingSubscriptionCompleteCommand = true;
LOGGER.debug("New or changed sessionKey, resetting session key in settings store");
cleared = true;
settingsStore.clear();
correctSessionKey = existingSessionKey;
}
/**
* If the clientId has changed we need to invalidate the settings data
*/
if (StringUtil.isNullOrEmpty(storedClientId) || (StringUtil.exists(existingClientId) && !StringUtil.equals(storedClientId, existingClientId))) {
expectingSubscriptionCompleteCommand = true;
LOGGER.debug("ClientId has changed, resetting client id in settings store");
cleared = true;
settingsStore.clear();
correctClientId = existingClientId;
}
if (cleared) {
settingsStore.put(SettingsStore.Keys.CLIENT_ID, correctClientId);
settingsStore.put(SettingsStore.Keys.SESSION_KEY, correctSessionKey);
}
// always put this one in
settingsStore.put(SettingsStore.Keys.EXPECTS_SUBSCRIPTION_COMPLETE, String.valueOf(expectingSubscriptionCompleteCommand));
}
}
private void validateConnectState() throws Exception {
// if we are already connecting, don't do another connect.
// we need to determine if we're authenticated enough
if (!connection.isConnected() || !connection.isAuthenticated()) {
throw new NotAuthenticatedException("The connection cannot operate at this time");
}
}
private final Observer updateVersionsStoreOnVersionChanged = new Observer() {
@Override
public void notify(Object sender, VersionMapEntry item) {
versionsStore.set(item.getKey(), item.getValue());
}
};
private void accessConnectingFuture() {
ensureLock(signalProvider);
}
private void modifyConnectingFuture(ObservableFuture future) {
accessConnectingFuture();
ensureLock(connectFuture);
ensureLock(future);
Asserts.assertTrue(connectFuture == null || future == connectFuture, "");
}
private void clearConnectingFuture(ObservableFuture future) {
modifyConnectingFuture(future);
connectFuture = null;
}
private void setConnectFuture(ObservableFuture future) {
accessConnectingFuture();
modifyConnectingFuture(future);
if (future == null) {
throw new RuntimeException("Use clearConnectingFuture() instead");
}
if (connectFuture != null) {
throw new RuntimeException("The connectFuture was not null. You have to clear it first");
}
connectFuture = future;
}
private ObservableFuture getUnchangingConnectFuture() {
accessConnectingFuture();
return connectFuture;
}
public SettingsStore getSettingsStore() {
return settingsStore;
}
public void setSettingsStore(SettingsStore store) {
this.settingsStore = store;
this.versionsStore = new SettingsVersionStore(store);
}
public SignalProvider getSignalProvider() {
return signalProvider;
}
public void setSignalProvider(SignalProvider signalProvider) {
this.signalProvider = signalProvider;
}
public long getSignalsConnectTimeoutInSeconds() {
return signalsConnectTimeoutInSeconds;
}
public void setSignalsConnectTimeoutInSeconds(long signalsConnectTimeoutInSeconds) {
this.signalsConnectTimeoutInSeconds = signalsConnectTimeoutInSeconds;
}
/**
* This class lets us 100% guarantee that requests won't overlap/criss-cross. We are able to capture the
* incoming clientId/sessionKey/connectionHandle and ensure that it doesn't change while we're processing
* this multi-staged process. We'll do a number of "ConnectionHandle" verification steps to ensure
* that we're reacting to events that we need.
*
* This Task will execute a signalProvider.connect() and IF NEEDED queue up a /signals/connect request. If
* we don't expect to receive a SubscriptionCompleteCommand then we'll just quit early.
*
* The caller must decide what to do if this request fails. This class does not do any reconnect or error
* handling if timeouts occur.
*
* This task _IS_ allowed to change the global state. Most tasks were designed to not change the parent state.
*/
private static class ConnectViaSignalProviderTask extends DestroyableBase implements Callable>, ConnectionHandleAware {
final ClientZipwhipNetworkSupport client;
final SignalProvider signalProvider;
final String clientId;
final String sessionKey;
final boolean expectingSubscriptionCompleteCommand;
final Presence presence;
final Map versions;
ConnectionHandle signalsConnectionHandle;
// this is the resultingFuture that we send back to the caller.
// the caller will "nest" this with the "connectFuture"
final ObservableFuture resultFuture;
private boolean started = false;
private ConnectViaSignalProviderTask(ClientZipwhipNetworkSupport client, SignalProvider signalProvider, String clientId, String sessionKey, Presence presence, Map versions, boolean expectingSubscriptionCompleteCommand) {
this.client = client;
this.signalProvider = signalProvider;
this.clientId = clientId;
this.sessionKey = sessionKey;
this.expectingSubscriptionCompleteCommand = expectingSubscriptionCompleteCommand;
this.presence = presence;
this.versions = versions;
this.resultFuture = new DefaultObservableFuture(this) {
@Override
public String toString() {
return "[ConnectViaSignalProviderTask: " + super.toString();
}
};
}
@Override
public synchronized ObservableFuture call() throws Exception {
try {
// do some basic assertions to ensure sanity checks. (did it change while we waited to execute?)
validateState();
} catch (Exception e) {
return new FakeFailingObservableFuture(this, e);
}
// When resultFuture completes, we need to tear down the global observers that we added.
unbindGlobalEventsOnComplete(resultFuture);
bindGlobalEvents();
ObservableFuture requestFuture;
try {
started = true;
requestFuture = signalProvider.connect(clientId, versions, presence);
} catch (Exception e) {
resultFuture.setFailure(e);
// this future will be linked to the "connectFuture" correctly.
return resultFuture;
}
/**
* The finish conditions are 2 fold.
*
* == Are we expecting a subscriptionCompleteCommand?
*
* 1. If so, we finish when we receive it, or time out.
* 2. If not, we finish when the {action:CONNECT} comes back.
*/
attachFinishingEvents(requestFuture);
selfDestructOnComplete();
return resultFuture;
}
private void selfDestructOnComplete() {
resultFuture.addObserver(new DestroyOnComplete>(ConnectViaSignalProviderTask.this));
}
/**
* The finish conditions are 2 fold.
*
* == Are we expecting a subscriptionCompleteCommand?
*
* 1. If so, we finish when we receive it, or time out.
* 2. If not, we finish when the {action:CONNECT} comes back.
*/
private void attachFinishingEvents(ObservableFuture requestFuture) {
LOGGER.debug("Expecting subscriptionComplete: " + expectingSubscriptionCompleteCommand);
requestFuture.addObserver(onSignalProviderConnectCompleteObserver);
if (expectingSubscriptionCompleteCommand) {
// only run the "reset connectFuture" observable if this initial internet request fails
// (timeout, socket exception, etc).
// If requestFuture is successful, we still need to wait for the SubscriptionCompleteCommand
// to come back.
requestFuture.addObserver(
new OnlyRunIfNotSuccessfulObserverAdapter(
new CopyFutureStatusToNestedFuture(resultFuture)));
// if this "requestFuture" succeeds, we need to let the onSubscriptionCompleteCommand finish
// the "connectFuture". That's because we need to wait for the SubscriptionCompleteCommand.
} else {
requestFuture.addObserver(new CopyFutureStatusToNestedFuture(resultFuture));
}
}
private synchronized void unbindGlobalEventsOnComplete(ObservableFuture future) {
future.addObserver(new Observer>() {
@Override
public void notify(Object sender, ObservableFuture item) {
if (!started) {
LOGGER.error("It seems that our future completed without us starting. Is this possible? Did someone cancel something?");
return;
}
unbindGlobalEvents();
}
});
}
private synchronized void bindGlobalEvents() {
// NOTE: We have to protect against it calling too soon.
// we need to add our observers early (in case connect() is synchronous or too fast!)
// what if we addObserver here, and then between this line and the next it gets called.
signalProvider.getConnectionChangedEvent().addObserver(failConnectingFutureIfDisconnectedObserver);
}
private synchronized void unbindGlobalEvents() {
signalProvider.getConnectionChangedEvent().removeObserver(failConnectingFutureIfDisconnectedObserver);
}
private final Observer> onSignalProviderConnectCompleteObserver = new Observer>() {
@Override
public void notify(Object sender, ObservableFuture requestFuture) {
LOGGER.debug("onSignalProviderConnectCompleteObserver.notify()");
synchronized (resultFuture) {
if (resultFuture.isCancelled()) {
LOGGER.error("onSignalProviderConnectCompleteObserver: The resultFuture was cancelled. So we're going to quit.");
return;
}
Asserts.assertTrue(!resultFuture.isDone(), "The resultFuture should not be done!");
synchronized (requestFuture) {
if (!requestFuture.isSuccess()) { // this also covers isCancelled()
LOGGER.error("onSignalProviderConnectCompleteObserver: The requestFuture was cancelled or failed. Cascading the value to our resultFuture: " + resultFuture);
NestedObservableFuture.syncState(requestFuture, resultFuture, null);
return;
}
LOGGER.debug("onSignalProviderConnectCompleteObserver: requestFuture was successful. We now have a connectionHandle.");
final ConnectionHandle signalProviderConnectionHandle = ConnectViaSignalProviderTask.this.signalsConnectionHandle = requestFuture.getResult();
Asserts.assertTrue(signalProviderConnectionHandle != null, "This can never be null");
if (!expectingSubscriptionCompleteCommand) {
NestedObservableFuture.syncState(requestFuture, resultFuture, signalProviderConnectionHandle);
return;
}
synchronized (signalsConnectionHandle) {
if (signalsConnectionHandle.isDestroyed()) {
LOGGER.warn("The connectionHandle was destroyed, so it must not be active.");
// The other listeners should have already failed this, right?
resultFuture.setFailure(new IllegalStateException("The connectionHandle was destroyed"));
return;
}
// NOTE: We guarantee that this clientId is the exact same we started with because our connectionHandle is still active!!
String clientId = signalProvider.getClientId();
ObservableFuture signalsConnectFuture = client.executeSignalsConnect(signalProviderConnectionHandle, clientId, sessionKey);
// when done, copy it over.
signalsConnectFuture.addObserver(
new CopyFutureStatusToNestedFutureWithCustomResult(
resultFuture, signalProviderConnectionHandle));
}
}
}
}
@Override
public String toString() {
return "[ConnectViaSignalProvider/ProcessSignalProvider.connect()]";
}
};
private final Observer failConnectingFutureIfDisconnectedObserver = new Observer() {
boolean fired = false;
@Override
public synchronized void notify(Object sender, Boolean connected) {
if (!started || isDestroyed()) {
// this fired too fast. It's not for our request! Ignore it.
LOGGER.warn(String.format("failConnectingFutureIfDisconnectedObserver skipping call. requestFuture:%s/isDestroyed:%s", started, isDestroyed()));
return;
}
// if not connected, null out the parent future.
// NOTE: We need to do this the hard way because we need to ensure the right connection got killed.
if (!connected) {
signalProvider.getConnectionChangedEvent().removeObserver(this);
Asserts.assertTrue(!fired, "failConnectingFutureIfDisconnectedObserver fired twice. That's not allowed.");
fired = true;
resultFuture.setFailure(new Exception("Disconnected"));
}
}
@Override
public String toString() {
return "failConnectingFutureIfDisconnectedObserver";
}
};
private void validateState() {
// we are executing either synchronously (we already have the lock) or async in the core executor.
// either way it's proper to sync on "this"
// this is the proper order of sync [client->provider]
synchronized (signalProvider) {
Asserts.assertTrue(signalProvider.getConnectionState() != ConnectionState.CONNECTED, "Order of operations failure! Already connected!");
Asserts.assertTrue(signalProvider.getConnectionState() != ConnectionState.AUTHENTICATED, "Order of operations failure! Already authenticated!");
}
}
@Override
public String toString() {
return "ConnectViaSignalProviderTask";
}
@Override
protected void onDestroy() {
LOGGER.debug(this.getClass().toString() + " destroyed.");
}
public ConnectionHandle getConnectionHandle() {
return signalsConnectionHandle;
}
}
// private final Observer onNewClientIdReceivedObserver = new Observer() {
//
// @Override
// public synchronized void notify(Object sender, String newClientId) {
// final ObservableFuture finalConnectFuture = getUnchangingConnectFuture();
// if (finalConnectFuture == null || !finalConnectFuture.isDone()) {
// LOGGER.debug("Got a newClientId while connecting. Ignoring this one.");
// return;
// }
//
// ConnectionHandle connectionHandle = (ConnectionHandle) sender;
//
// String clientId = settingsStore.get(SettingsStore.Keys.CLIENT_ID);
// String sessionKey = settingsStore.get(SettingsStore.Keys.SESSION_KEY);
//
// processNewClientId(connectionHandle, sessionKey, clientId, newClientId);
// }
// };
// private ObservableFuture processNewClientId(ConnectionHandle connectionHandle, String sessionKey, String oldClientId, String newClientId) {
// // NOTE: when we do a reset, we're going to get a new clientId that is null
//// if (StringUtil.isNullOrEmpty(newClientId)) {
//// settingsStore.put(SettingsStore.Keys.CLIENT_ID, newClientId);
//// return;
//// }
//
// final ObservableFuture signalsConnectFuture;
//
// if (StringUtil.exists(oldClientId)) {
// // clientId changed, unsubscribe the old one, and sub the new one
// if (!oldClientId.equals(newClientId)) {
// synchronized (settingsStore) {
// accessSettings();
// settingsStore.clear();
//
// settingsStore.put(SettingsStore.Keys.SESSION_KEY, sessionKey);
// settingsStore.put(SettingsStore.Keys.CLIENT_ID, newClientId);
// settingsStore.put(SettingsStore.Keys.EXPECTS_SUBSCRIPTION_COMPLETE, "true");
//
// executeSignalsDisconnect(sessionKey, oldClientId);
//
// signalsConnectFuture = executeSignalsConnect(connectionHandle, newClientId, sessionKey);
// }
// } else {
// signalsConnectFuture = null;
// // just the same clientId
// }
// } else {
// synchronized (settingsStore) {
// accessSettings();
//
// settingsStore.put(SettingsStore.Keys.CLIENT_ID, newClientId);
// settingsStore.put(SettingsStore.Keys.EXPECTS_SUBSCRIPTION_COMPLETE, "true");
//
// signalsConnectFuture = executeSignalsConnect(connectionHandle, newClientId, sessionKey);
//
// signalsConnectFuture.addObserver(new Observer>() {
// @Override
// public void notify(Object sender, ObservableFuture item) {
// new UpdateLocalStoreWithLastKnownSubscribedClientIdOnSuccessObserver(this).no;
// }
// });
// }
// }
//
// return signalsConnectFuture;
// }
private static class ConnectionHandleStillActiveObserverAdapter extends ObserverAdapter {
private ConnectionHandleStillActiveObserverAdapter(Observer observer) {
super(observer);
}
@Override
public void notify(Object sender, T item) {
ConnectionHandle connectionHandle = getConnectionHandle(sender, item);
if (connectionHandle == null) {
LOGGER.error("The connectionHandle passed in was null. Was this a bad disconnect? Quitting " + getObserver());
return;
}
synchronized (connectionHandle) {
if (connectionHandle.isDestroyed() || connectionHandle.getDisconnectFuture().isDone()) {
LOGGER.error("The connectionHandle was destroyed. Was this connection torn down?");
return;
}
super.notify(sender, item);
}
}
protected ConnectionHandle getConnectionHandle(Object sender, T item) {
if (sender instanceof ConnectionHandle) {
return (ConnectionHandle) sender;
} else if (sender instanceof ConnectionHandleAware) {
return ((ConnectionHandleAware) sender).getConnectionHandle();
} else if (item instanceof ConnectionHandle) {
return (ConnectionHandle) item;
} else {
throw new IllegalArgumentException("Cannot find connectionHandle on sender");
}
}
}
/**
* Execute a /signals/connect webcall s
*
* @param clientId
* @param sessionKey
* @return
*/
private ObservableFuture executeSignalsConnect(ConnectionHandle connectionHandle, final String clientId, final String sessionKey) {
// asdfasdf // An "ImportantTask" is never allowed to modify the state of "this". Just return the future and let the caller decide what to do on success/failure.
/**
*
* This is a call that will time out. We had to do the "importantTaskExecutor" in order to allow Android AlarmManager
* to run the scheduling.
*/
final ObservableFuture signalsConnectFuture =
importantTaskExecutor.enqueue(callbackExecutor,
new SignalsConnectTask(connectionHandle, sessionKey, clientId), signalsConnectTimeoutInSeconds);
signalsConnectFuture.addObserver(new DebugObserver());
signalsConnectFuture.addObserver(new Observer>() {
@Override
public void notify(Object sender, ObservableFuture item) {
LOGGER.debug("/signals/connect >> Success:" + item.isSuccess());
}
});
return signalsConnectFuture;
}
private void executeSignalsDisconnect(String sessionKey, String clientId) {
// Do a disconnect then connect
Map params = new HashMap();
params.put("clientId", clientId);
params.put("sessions", sessionKey);
try {
executeAsync(SIGNALS_DISCONNECT, params);
} catch (Exception e) {
LOGGER.warn("Couldn't execute SIGNALS_DISCONNECT. We're going to ignore this problem.", e);
}
}
/**
* Will update the settingsStore with the right information when this future completes successfully.
*/
private static class UpdateLocalStoreWithLastKnownSubscribedClientIdOnSuccessObserver implements Observer> {
private static final Logger LOGGER = LoggerFactory.getLogger(UpdateLocalStoreWithLastKnownSubscribedClientIdOnSuccessObserver.class);
private final ClientZipwhipNetworkSupport client;
private UpdateLocalStoreWithLastKnownSubscribedClientIdOnSuccessObserver(ClientZipwhipNetworkSupport client) {
this.client = client;
}
@Override
public void notify(Object sender, ObservableFuture future) {
synchronized (future) {
if (future.isCancelled()) {
LOGGER.error("Future was cancelled. Quitting!");
return;
}
if (future.isSuccess()) {
LOGGER.debug("Successfully updating the settings store with the new information");
synchronized (client) {
synchronized (client.getSignalProvider()) {
synchronized (client.getSettingsStore()) {
// the subscriptionId is the sessionKey because we request it as so.
ConnectionHandle connectionHandle = future.getResult();
synchronized (connectionHandle) {
if (connectionHandle.isDestroyed()) {
LOGGER.warn("The connectionHandle was destroyed, so we're not updating the database.");
return;
}
// do some quick assertions to ensure that we're doing things right.
// Asserts.assertTrue(StringUtil.equals(command.getSubscriptionId(), sessionKey), "");
// Just logging for now - we should have gotten the subscriptionId in the SubscriptionCompleteCommand: http://angela.zipwhip.com/issues/7678
String clientId = client.getSignalProvider().getClientId();
client.onSubscriptionComplete(clientId);
}
}
}
}
}
}
}
}
private synchronized void onSubscriptionComplete(String clientId) {
accessSettings();
settingsStore.put(SettingsStore.Keys.CLIENT_ID, clientId);
settingsStore.put(SettingsStore.Keys.EXPECTS_SUBSCRIPTION_COMPLETE, "false");
settingsStore.put(SettingsStore.Keys.LAST_SUBSCRIBED_CLIENT_ID, clientId);
}
private void accessSettings() {
ensureLock(ClientZipwhipNetworkSupport.this);
ensureLock(signalProvider);
ensureLock(settingsStore);
}
/**
* This class's job is to do a /signals/connect and wrap the return from the server in one mega Future.
*
* It is NOT allowed to change the state of any parent property.
*/
private class SignalsConnectTask implements Callable>, ConnectionHandleAware {
private final String sessionKey;
private final String clientId;
/**
* This is the connectionHandle that we are operating on.
*/
private final ConnectionHandle connectionHandle;
private SignalsConnectTask(ConnectionHandle connectionHandle, String sessionKey, String clientId) {
this.sessionKey = sessionKey;
this.connectionHandle = connectionHandle;
this.clientId = clientId;
}
@Override
public synchronized ObservableFuture call() throws Exception {
// it's important that this future is synchronous (no executor)
final ObservableFuture resultFuture = new DefaultObservableFuture(this);
final Observer[] onConnectionChangedObserver = new Observer[1];
final Observer onSubscriptionCompleteObserver = new Observer() {
@Override
public void notify(Object sender, SubscriptionCompleteCommand item) {
synchronized (SignalsConnectTask.this) {
signalProvider.getSubscriptionCompleteReceivedEvent().removeObserver(this);
signalProvider.getConnectionChangedEvent().removeObserver(onConnectionChangedObserver[0]);
LOGGER.debug("Successing");
resultFuture.setSuccess(item);
}
}
@Override
public String toString() {
return "SignalsConnectTask/onSubscriptionCompleteObserver";
}
};
onConnectionChangedObserver[0] = new Observer() {
@Override
public void notify(Object sender, Boolean connected) {
synchronized (SignalsConnectTask.this) {
if (connected) {
// we connected?
return;
}
// on any kind of connection change, we need to just abort
signalProvider.getConnectionChangedEvent().removeObserver(onConnectionChangedObserver[0]);
signalProvider.getSubscriptionCompleteReceivedEvent().removeObserver(onSubscriptionCompleteObserver);
LOGGER.debug("Failing the resultFuture since (connected == false)");
resultFuture.setFailure(new Exception("Disconnected while waiting for SubscriptionCompleteCommand to come in! " + connected));
}
}
@Override
public String toString() {
return "SignalsConnectTask/onConnectionChangedObserver";
}
};
signalProvider.getConnectionChangedEvent().addObserver(onConnectionChangedObserver[0]);
signalProvider.getSubscriptionCompleteReceivedEvent().addObserver(onSubscriptionCompleteObserver);
if (resultFuture.isDone()) {
// wow it finished already?
return resultFuture;
}
ServerResponse response;
try {
response = executeSync(ZipwhipNetworkSupport.SIGNALS_CONNECT, getSignalsConnectParams(sessionKey, clientId));
} catch (Exception e) {
LOGGER.error("Failed to execute request: ", e);
resultFuture.setFailure(e);
return resultFuture;
}
if (response == null) {
LOGGER.error("The response from zipwhip was null!?");
resultFuture.setFailure(new NullPointerException("The executeSync() response was null"));
return resultFuture;
}
if (!response.isSuccess()) {
resultFuture.setFailure(new Exception(response.getRaw()));
return resultFuture;
}
resultFuture.addObserver(new Observer>() {
@Override
public void notify(Object sender, ObservableFuture item) {
synchronized (SignalsConnectTask.this) {
signalProvider.getSubscriptionCompleteReceivedEvent().removeObserver(onSubscriptionCompleteObserver);
signalProvider.getConnectionChangedEvent().removeObserver(onConnectionChangedObserver[0]);
}
}
@Override
public String toString() {
return "SignalsConnectTask/CleanUpObserver";
}
});
LOGGER.debug("/signals/connect executed successfully. You should get back a SubscriptionCompleteCommand any time now. (Maybe already?)");
return resultFuture;
}
private Map getSignalsConnectParams(String sessionKey, String clientId) {
Map params = new HashMap();
params.put("sessions", sessionKey);
params.put("clientId", clientId);
params.put("subscriptionId", sessionKey);
if (signalProvider.getPresence() != null) {
params.put("category", signalProvider.getPresence().getCategory());
}
return params;
}
public ConnectionHandle getConnectionHandle() {
return connectionHandle;
}
@Override
public String toString() {
return "SignalsConnectTask(Waiting for SubscriptionCompleteCommand)";
}
}
/**
* This observer fires in the thread that the future completes in. This future can either complete in the
* Timer thread (HashWheelTimer or pubsub via intent/AlarmManager) OR it can
*/
private class ClearConnectFutureOnCompleteObserver implements Observer> {
private final ObservableFuture myConnectFuture;
private ClearConnectFutureOnCompleteObserver(ObservableFuture connectFuture) {
this.myConnectFuture = connectFuture;
}
@Override
public void notify(Object sender, ObservableFuture item) {
// we are not allowed to "synchronize" on the ZipwhipClient because we are in a bad thread. (causes
// a deadlock). We can rely on the property that only 1 connectFuture is allowed to exist at any
// given time. It only is allowed to be filled when not null.
synchronized (signalProvider) {
final ObservableFuture finalConnectingFuture = getUnchangingConnectFuture();
if (finalConnectingFuture == myConnectFuture) {
synchronized (myConnectFuture) {
clearConnectingFuture(myConnectFuture);
}
}
}
}
}
private class ThreadSafeObserver extends ObserverAdapter {
private ThreadSafeObserver(Observer observer) {
super(observer);
}
@Override
public void notify(Object sender, T item) {
synchronized (ClientZipwhipNetworkSupport.this) {
synchronized (signalProvider) {
super.notify(sender, item);
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy