Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.atmosphere.gwt.client.AtmosphereClient Maven / Gradle / Ivy
/*
* Copyright 2013 Jeanfrancois Arcand
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
/*
* Copyright 2009 Richard Zschech.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.atmosphere.gwt.client;
import com.google.gwt.core.client.Duration;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.ClosingEvent;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.StatusCodeException;
import org.atmosphere.gwt.client.impl.CometTransport;
import org.atmosphere.gwt.client.impl.WebSocketCometTransport;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.atmosphere.gwt.client.impl.CometTransport;
/**
* This class is the client interface. It will connect to the given url and notify the given
* {@link AtmosphereListener} of events. To receive GWT serialized objects supply a {@link AtmosphereGWTSerializer}
* class with the appropriate annotations.
*
* The sequence of events are as follows: The application calls {@link AtmosphereClient#start()}.
* {@link AtmosphereListener#onConnected(int,String)} gets called when the connection is established.
* {@link AtmosphereListener#onMessage(List)} gets called when messages are received from the server.
* {@link AtmosphereListener#onDisconnected()} gets called when the connection is disconnected
* {@link AtmosphereListener#onError(Throwable, boolean)} gets called if there is an error with the connection.
* For more details about the possible events see {@link AtmosphereListener}.
*
* The client will attempt to maintain a connection when disconnections occur until the application calls
* {@link AtmosphereClient#stop()}.
*
* The server sends heart beat messages to ensure the connection is maintained and that disconnections can be detected
* in all cases.
*
* @author Richard Zschech
* @author Pierre Havelaar
*/
public class AtmosphereClient implements UserInterface {
private enum RefreshState {
CONNECTING, PRIMARY_DISCONNECTED, REFRESH_CONNECTED, PRIMARY_RECONNECT
}
private String url;
private final AtmosphereGWTSerializer serializer;
private final AtmosphereListener listener;
private CometClientTransportWrapper primaryTransport;
private CometClientTransportWrapper refreshTransport;
private HandlerRegistration unloadHandlerReg;
private boolean running;
private RefreshState refreshState;
private List refreshQueue;
private static final Object REFRESH = new Object();
private static final Object DISCONNECT = new Object();
private int connectionCount;
private int connectionTimeout = 10000;
private int reconnectionTimeout = 1000;
private int reconnectionCount = -1;
private boolean webSocketsEnabled = false;
private Logger logger = Logger.getLogger(getClass().getName());
private AsyncCallback postCallback = new AsyncCallback() {
@Override
public void onFailure(Throwable caught) {
logger.log(Level.SEVERE, "Failed to post message", caught);
}
@Override
public void onSuccess(Void result) {
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "post succeeded");
}
}
};
public AtmosphereClient(String url, AtmosphereListener listener) {
this(url, null, listener);
}
public AtmosphereClient(String url, AtmosphereGWTSerializer serializer, AtmosphereListener listener) {
this(url, serializer, listener, true);
}
public AtmosphereClient(String url, AtmosphereGWTSerializer serializer, AtmosphereListener listener, boolean webSocketsEnabled) {
this.url = url;
this.serializer = serializer;
this.listener = listener;
this.webSocketsEnabled = webSocketsEnabled;
primaryTransport = new CometClientTransportWrapper();
}
public boolean isWebSocketsEnabled() {
return webSocketsEnabled;
}
public void setWebSocketsEnabled(boolean webSocketsEnabled) {
this.webSocketsEnabled = webSocketsEnabled;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public AtmosphereGWTSerializer getSerializer() {
return serializer;
}
public AtmosphereListener getListener() {
return listener;
}
/**
* This is the amount of time the client waits until a connection is established. If the connection is
* not established within this time the connection is assumed to be dead.
*
* @param connectionTimeout the timeout in milliseconds defaults to 10000
*/
public void setConnectionTimeout(int connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}
public int getConnectionTimeout() {
return connectionTimeout;
}
/**
* This establishes the wait time before a new connection is started.
* When a connection has dropped this timeout is used to setup a wait time before a new connection is
* started. The algorithm will start with the given reconnectionTimeout and will multiply the timeout
* with the number of failed attempts to reconnect. This will prevent for instance a server that is
* rebooted from being flooded with connections.
*
* @see #setReconnectionCount(int)
* @param reconnectionTimeout
*/
public void setReconnectionTimeout(int reconnectionTimeout) {
this.reconnectionTimeout = reconnectionTimeout;
}
public int getReconnectionTimeout() {
return reconnectionTimeout;
}
public int getReconnectionCount() {
return reconnectionCount;
}
/**
* The amount of times to try to reconnect if a connection has failed.
* If the count has been reached then client is stopped {@link #stop() } and {@link #isRunning() }
* will return false.
*
* @param reconnectionCount Set to -1 to keep trying (default -1)
*/
public void setReconnectionCount(int reconnectionCount) {
this.reconnectionCount = reconnectionCount;
}
/**
* This is true between {@link #start() } and {@link #stop() }
* If the connection is failing and the {@link #reconnectionCount} has been reached then stop is also called.
* @return
*/
public boolean isRunning() {
return running;
}
/**
* The unique connection UUID
*
* @return
*/
public String getConnectionUUID() {
return primaryTransport.connectionUUID;
}
/**
* push message back to the server on this connection. On the serverside the message can be received
* by the {@link AtmosphereGwtHandler#doPost}
*
*/
public void post(Object message) {
primaryTransport.post(message, postCallback);
}
/**
* push message back to the server on this connection. On the serverside the message can be received
* by the {@link AtmosphereGwtHandler#doPost}
*
*/
public void post(Object message, AsyncCallback callback) {
primaryTransport.post(message, callback);
}
/**
* push messages back to the server on this connection. On the serverside the message can be received
* by the {@link AtmosphereGwtHandler#doPost}
*
*/
public void post(List> messages) {
primaryTransport.post(messages, postCallback);
}
/**
* push messages back to the server on this connection. On the serverside the message can be received
* by the {@link AtmosphereGwtHandler#doPost}
*
*/
public void post(List> messages, AsyncCallback callback) {
primaryTransport.post(messages, callback);
}
/**
* Send a message back to the server on this connection and use our broadcaster to send this message to
* other clients connected to the same broadcaster.
*/
public void broadcast(Object message) {
primaryTransport.broadcast(message);
}
/**
* Send a message back to the server on this connection and use our broadcaster to send this message to
* other clients connected to the same broadcaster.
*/
public void broadcast(List> messages) {
primaryTransport.broadcast(messages);
}
/**
* This will startup the connection to the server and the client will try to maintain the connection
* until {@ling #stop()} is called.
*/
public void start() {
// avoid spinning mouse cursor in chrome by starting as a deferred command
Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
if (!running) {
running = true;
if (unloadHandlerReg != null) {
unloadHandlerReg.removeHandler();
}
UnloadHandler handler = new UnloadHandler();
final HandlerRegistration reg1 = Window.addCloseHandler(handler);
final HandlerRegistration reg2 = Window.addWindowClosingHandler(handler);
unloadHandlerReg = new HandlerRegistration() {
@Override
public void removeHandler() {
reg1.removeHandler();
reg2.removeHandler();
}
};
doConnect();
}
}
});
}
private class UnloadHandler implements CloseHandler, Window.ClosingHandler {
@Override
public void onClose(CloseEvent event) {
doUnload();
}
@Override
public void onWindowClosing(ClosingEvent event) {
doUnload();
}
private void doUnload() {
if (!done) {
done = true;
logger.log(Level.FINE, "Stopping comet client because of page unload");
stop();
}
}
private boolean done = false;
}
/**
* End the connection.
*/
public void stop() {
if (running) {
running = false;
if (unloadHandlerReg != null) {
unloadHandlerReg.removeHandler();
unloadHandlerReg = null;
}
doDisconnect();
}
}
private void doConnect() {
primaryTransport.connect();
}
private void scheduleConnect(CometClientTransportWrapper transport) {
if (running && transport == primaryTransport) {
primaryTransport.reconnectionTimer.schedule(reconnectionTimeout);
} else {
logger.warning("Schedule for reconnection is called without the primary transport");
}
}
private void doDisconnect() {
refreshState = null;
primaryTransport.disconnect();
if (refreshTransport != null) {
refreshTransport.disconnect();
}
}
private void doOnConnected(int heartbeat, String connectionUUID, CometClientTransportWrapper transport) {
if (refreshState != null) {
if (transport == refreshTransport) {
if (refreshState == RefreshState.PRIMARY_DISCONNECTED) {
doneRefresh(connectionUUID);
} else if (refreshState == RefreshState.CONNECTING) {
primaryTransport.disconnect();
doneRefresh(connectionUUID);
} else {
throw new IllegalStateException("Unexpected refresh state");
}
} else {
// refreshed connection after connection failed
if (refreshState != RefreshState.CONNECTING
&& refreshState != RefreshState.PRIMARY_RECONNECT) {
throw new IllegalStateException("Unexpected refresh state");
}
refreshState = null;
listener.onAfterRefresh(connectionUUID);
}
} else {
listener.onConnected(heartbeat, connectionUUID);
}
}
private void doOnBeforeDisconnected(CometClientTransportWrapper transport) {
if (refreshState == null && transport == primaryTransport) {
listener.onBeforeDisconnected();
}
}
private void doOnDisconnected(CometClientTransportWrapper transport) {
if (refreshState != null) {
if (transport == primaryTransport) {
if (refreshState != RefreshState.CONNECTING
&& refreshState != RefreshState.PRIMARY_RECONNECT) {
throw new IllegalStateException("Unexpected refreshState");
}
if (refreshState == RefreshState.PRIMARY_RECONNECT) {
scheduleConnect(transport);
} else {
refreshState = RefreshState.PRIMARY_DISCONNECTED;
logger.warning("primary disconnected before refresh transport was connected");
}
} else {
// the refresh transport has disconnected
failedRefresh();
}
} else {
listener.onDisconnected();
if (running) {
scheduleConnect(transport);
}
}
}
private void failedRefresh() {
refreshState = null;
logger.severe("Failed refesh");
// dispatch remaining messages;
if (refreshQueue != null) {
for (Object object : refreshQueue) {
if (object == REFRESH || object == DISCONNECT) {
} else {
doOnMessage((List>) object, primaryTransport);
}
}
refreshQueue.clear();
}
listener.onError(new RuntimeException("Failed to refresh connection. Will try to establish a new connection"), false);
scheduleConnect(primaryTransport);
}
@SuppressWarnings("unchecked")
private void doneRefresh(String connectionUUID) {
refreshState = null;
CometClientTransportWrapper temp = primaryTransport;
primaryTransport = refreshTransport;
refreshTransport = temp;
listener.onAfterRefresh(connectionUUID);
if (refreshQueue != null) {
if (refreshQueue.size() > 0) {
logger.fine("pushing queued messages");
}
for (Object object : refreshQueue) {
if (object == REFRESH) {
doOnRefresh(primaryTransport);
} else if (object == DISCONNECT) {
doOnDisconnected(primaryTransport);
} else {
doOnMessage((List>) object, primaryTransport);
}
}
refreshQueue.clear();
}
}
private void doOnHeartbeat(CometClientTransportWrapper transport) {
if (transport == primaryTransport) {
listener.onHeartbeat();
}
}
private void doOnRefresh(CometClientTransportWrapper transport) {
if (refreshState == null && transport == primaryTransport) {
refreshState = RefreshState.CONNECTING;
if (refreshTransport == null) {
refreshTransport = new CometClientTransportWrapper();
}
refreshTransport.connect();
listener.onRefresh();
} else if (transport == refreshTransport) {
refreshEnqueue(REFRESH);
} else {
throw new IllegalStateException("Unexpected refresh from primaryTransport");
}
}
private void refreshEnqueue(Object message) {
if (refreshQueue == null) {
refreshQueue = new ArrayList();
}
refreshQueue.add(message);
}
private void doOnError(Throwable exception, boolean connected, CometClientTransportWrapper transport) {
if (connected) {
transport.disconnect();
} else if (exception instanceof StatusCodeException) {
running = false;
}
listener.onError(exception, connected);
}
private void doOnMessage(List> messages, CometClientTransportWrapper transport) {
if (transport == primaryTransport) {
listener.onMessage(messages);
} else if (RefreshState.PRIMARY_DISCONNECTED.equals(refreshState)) {
refreshEnqueue(messages);
}
}
private class CometClientTransportWrapper implements AtmosphereListener {
private CometTransport transport;
private final Timer connectionTimer = createConnectionTimer();
private final Timer reconnectionTimer = createReconnectionTimer();
private final Timer heartbeatTimer = createHeartbeatTimer();
private boolean webSocketSuccessful = false;
private int heartbeatTimeout;
private double lastReceivedTime;
private String connectionUUID;
private int reconnectionCounter = 1;
public CometClientTransportWrapper() {
// Websocket support not enabled yet
if (webSocketsEnabled && WebSocketCometTransport.hasWebSocketSupport()) {
transport = new WebSocketCometTransport();
} else {
transport = GWT.create(CometTransport.class);
}
logger.info("Created transport: " + transport.getClass().getName());
transport.initiate(AtmosphereClient.this, this);
}
public void post(Object message, AsyncCallback callback) {
transport.post(message, callback);
}
public void post(List> messages, AsyncCallback callback) {
transport.post(messages, callback);
}
public void broadcast(Object message) {
transport.broadcast(message);
}
public void broadcast(List> messages) {
transport.broadcast(messages);
}
public String getConnectionUUID() {
return connectionUUID;
}
public void connect() {
connectionTimer.schedule(connectionTimeout);
transport.connect(++connectionCount);
}
public void disconnect() {
transport.disconnect();
}
@Override
public void onConnected(int heartbeat, String connectionUUID) {
heartbeatTimeout = heartbeat + connectionTimeout;
lastReceivedTime = Duration.currentTimeMillis();
this.connectionUUID = connectionUUID;
if (transport instanceof WebSocketCometTransport) {
webSocketSuccessful = true;
}
reconnectionTimer.cancel();
reconnectionCounter = 1;
connectionTimer.cancel();
heartbeatTimer.schedule(heartbeatTimeout);
doOnConnected(heartbeat, connectionUUID, this);
}
@Override
public void onBeforeDisconnected() {
doOnBeforeDisconnected(this);
}
@Override
public void onDisconnected() {
heartbeatTimer.cancel();
connectionTimer.cancel();
if (transport instanceof WebSocketCometTransport && webSocketSuccessful == false) {
// server doesn't support WebSocket's degrade the connection ...
logger.info("Server does not support WebSockets");
webSocketsEnabled = false;
transport = GWT.create(CometTransport.class);
transport.initiate(AtmosphereClient.this, this);
transport.connect(++connectionCount);
} else {
doOnDisconnected(this);
}
}
@Override
public void onError(Throwable exception, boolean connected) {
heartbeatTimer.cancel();
connectionTimer.cancel();
if (transport instanceof WebSocketCometTransport && webSocketSuccessful == false) {
if (connected) {
transport.disconnect();
}
} else {
doOnError(exception, connected, this);
}
}
@Override
public void onHeartbeat() {
lastReceivedTime = Duration.currentTimeMillis();
doOnHeartbeat(this);
}
@Override
public void onRefresh() {
lastReceivedTime = Duration.currentTimeMillis();
doOnRefresh(this);
}
@Override
public void onAfterRefresh(String connectionUUID) {
// this is for the client only and does not happen on transports
lastReceivedTime = Duration.currentTimeMillis();
}
@Override
public void onMessage(List> messages) {
lastReceivedTime = Duration.currentTimeMillis();
doOnMessage(messages, this);
}
private Timer createConnectionTimer() {
return new Timer() {
@Override
public void run() {
doDisconnect();
doOnError(new TimeoutException(url, connectionTimeout), false, CometClientTransportWrapper.this);
}
};
}
private Timer createHeartbeatTimer() {
return new Timer() {
@Override
public void run() {
double currentTimeMillis = Duration.currentTimeMillis();
double difference = currentTimeMillis - lastReceivedTime;
if (difference >= heartbeatTimeout) {
doDisconnect();
doOnError(new AtmosphereClientException("Heartbeat failed"), false, CometClientTransportWrapper.this);
} else {
// we have received a message since the timer was
// schedule so reschedule it.
schedule(heartbeatTimeout - (int) difference);
}
}
};
}
private Timer createReconnectionTimer() {
return new Timer() {
@Override
public void run() {
reconnectionCounter++;
logger.finest("Running reconnect: count="+ (reconnectionCounter - 1));
if (reconnectionCount != -1 && reconnectionCounter - 2 > reconnectionCount) {
logger.info("Reconnection attempts exceeded " + reconnectionCount + ". Giving up...");
stop();
} else if (running) {
logger.fine("Starting reconnect");
refreshState = RefreshState.PRIMARY_RECONNECT;
doConnect();
}
}
@Override
public void schedule(int delayMillis) {
int delay = delayMillis * (reconnectionCounter < 30 ? reconnectionCounter : 30);
logger.finest("Scheduling for reconnect, waiting " + (delay / 1000) + "s");
super.cancel();
super.schedule(delay);
}
};
}
}
// TODO precompile all regexps
public native static JsArrayString split(String string, String separator) /*-{
return string.split(separator);
}-*/;
/*
* @Override public void start() { if (!closing) { Window.addWindowCloseListener(windowListener); super.start(); } }
*
* @Override public void stop() { super.stop(); Window.removeWindowCloseListener(windowListener); }
*
* private WindowCloseListener windowListener = new WindowCloseListener() {
*
* @Override public void onWindowClosed() { closing = true; }
*
* @Override public String onWindowClosing() { return null; } };
*/
}