/* * 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 net.zschech.gwt.comet.client; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import net.zschech.gwt.comet.client.impl.CometTransport; import net.zschech.gwt.comet.client.impl.EventSourceCometTransport; import net.zschech.gwt.eventsource.client.EventSource; import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.Timer; /** * This class is the Comet client. It will connect to the given url and notify the given {@link CometListener} of comet * events. To receive GWT serialized objects supply a {@link CometSerializer} method to parse the messages. * * The sequence of events are as follows: The application calls {@link CometClient#start()}. * {@link CometListener#onConnected(int)} gets called when the connection is established. * {@link CometListener#onMessage(List)} gets called when messages are received from the server. * {@link CometListener#onDisconnected()} gets called when the connection is disconnected this includes connection * refreshes. {@link CometListener#onError(Throwable, boolean)} gets called if there is an error with the connection. * * The Comet client will attempt to maintain to connection when disconnections occur until the application calls * {@link CometClient#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 */ public class CometClient { private enum RefreshState { CONNECTING, PRIMARY_DISCONNECTED, REFRESH_CONNECTED } private final String url; private final CometSerializer serializer; private final CometListener listener; private CometClientTransportWrapper primaryTransport; private CometClientTransportWrapper refreshTransport; private boolean running; private RefreshState refreshState; private List<Object> refreshQueue; private static final Object REFRESH = new Object(); private static final Object DISCONNECT = new Object(); private int connectionCount; private int connectionTimeout = 10000; private int reconnectionTimout = 1000; public CometClient(String url, CometListener listener) { this(url, null, listener); } public CometClient(String url, CometSerializer serializer, CometListener listener) { this.url = url; this.serializer = serializer; this.listener = listener; primaryTransport = new CometClientTransportWrapper(); } public String getUrl() { return url; } public CometSerializer getSerializer() { return serializer; } public CometListener getListener() { return listener; } public void setConnectionTimeout(int connectionTimeout) { this.connectionTimeout = connectionTimeout; } public int getConnectionTimeout() { return connectionTimeout; } public void setReconnectionTimout(int reconnectionTimout) { this.reconnectionTimout = reconnectionTimout; } public int getReconnectionTimout() { return reconnectionTimout; } public boolean isRunning() { return running; } public void start() { if (!running) { running = true; doConnect(); } } public void stop() { if (running) { running = false; doDisconnect(); } } private void doConnect() { primaryTransport.connect(); } private void doDisconnect() { refreshState = null; primaryTransport.disconnect(); if (refreshTransport != null) { refreshTransport.disconnect(); } } private void doOnConnected(int heartbeat, CometClientTransportWrapper transport) { if (refreshState != null) { if (transport == refreshTransport) { if (refreshState == RefreshState.PRIMARY_DISCONNECTED) { doneRefresh(); } else if (refreshState == RefreshState.CONNECTING) { refreshState = RefreshState.REFRESH_CONNECTED; } else { assert false; } } else { assert false; } } else { listener.onConnected(heartbeat); } } private void doOnDisconnected(CometClientTransportWrapper transport) { if (refreshState != null) { if (transport == primaryTransport) { if (refreshState == RefreshState.REFRESH_CONNECTED) { doneRefresh(); } else if (refreshState == RefreshState.CONNECTING) { refreshState = RefreshState.PRIMARY_DISCONNECTED; } else { assert false; } } else { // the refresh transport has disconnected before the primary disconnected refreshEnqueue(DISCONNECT); } } else { listener.onDisconnected(); if (running) { doConnect(); } } } @SuppressWarnings("unchecked") private void doneRefresh() { refreshState = null; CometClientTransportWrapper temp = primaryTransport; primaryTransport = refreshTransport; refreshTransport = temp; if (refreshQueue != null) { for (Object object : refreshQueue) { if (object == REFRESH) { doOnRefresh(primaryTransport); } else if (object == DISCONNECT) { doOnDisconnected(primaryTransport); } else { doOnMessage((List<? extends Serializable>) 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 { assert false; } } private void refreshEnqueue(Object message) { if (refreshQueue == null) { refreshQueue = new ArrayList<Object>(); } refreshQueue.add(message); } private void doOnError(Throwable exception, boolean connected, CometClientTransportWrapper transport) { if (connected) { doDisconnect(); } listener.onError(exception, connected); if (running) { primaryTransport.reconnectionTimer.schedule(reconnectionTimout); } } private void doOnMessage(List<? extends Serializable> messages, CometClientTransportWrapper transport) { if (transport == primaryTransport) { listener.onMessage(messages); } else { refreshEnqueue(messages); } } private class CometClientTransportWrapper implements CometListener { private final CometTransport transport; private final Timer connectionTimer = createConnectionTimer(); private final Timer reconnectionTimer = createReconnectionTimer(); private final Timer heartbeatTimer = createHeartbeatTimer(); private int heartbeatTimeout; private double lastReceivedTime; public CometClientTransportWrapper() { if (EventSource.isSupported()) { transport = new EventSourceCometTransport(); } else { transport = GWT.create(CometTransport.class); } transport.initiate(CometClient.this, this); } public void connect() { connectionTimer.schedule(connectionTimeout); transport.connect(++connectionCount); } public void disconnect() { cancelTimers(); transport.disconnect(); } @Override public void onConnected(int heartbeat) { heartbeatTimeout = heartbeat + connectionTimeout; lastReceivedTime = Duration.currentTimeMillis(); cancelTimers(); heartbeatTimer.schedule(heartbeatTimeout); doOnConnected(heartbeat, this); } @Override public void onDisconnected() { cancelTimers(); doOnDisconnected(this); } @Override public void onError(Throwable exception, boolean connected) { cancelTimers(); 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 onMessage(List<? extends Serializable> messages) { lastReceivedTime = Duration.currentTimeMillis(); doOnMessage(messages, this); } private void cancelTimers() { connectionTimer.cancel(); reconnectionTimer.cancel(); heartbeatTimer.cancel(); } private Timer createConnectionTimer() { return new Timer() { @Override public void run() { doDisconnect(); doOnError(new CometTimeoutException(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 CometException("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() { if (running) { doConnect(); } } }; } } }