/*
* Copyright 2012 JBoss, by Red Hat, Inc
*
* 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.jboss.errai.bus.client.framework.transports;
import com.google.gwt.http.client.URL;
import com.google.gwt.user.client.Timer;
import org.jboss.errai.bus.client.api.base.MessageBuilder;
import org.jboss.errai.bus.client.api.messaging.Message;
import org.jboss.errai.bus.client.api.messaging.MessageCallback;
import org.jboss.errai.bus.client.framework.BusState;
import org.jboss.errai.bus.client.framework.ClientMessageBusImpl;
import org.jboss.errai.bus.client.util.BusToolsCli;
import org.jboss.errai.common.client.util.LogUtil;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* @author Mike Brock
*/
public class SSEHandler implements TransportHandler, TransportStatistics {
private static final String SSE_AGENT_SERVICE = "SSEAgent";
private final ClientMessageBusImpl clientMessageBus;
private final MessageCallback messageCallback;
private final HttpPollingHandler pollingHandler;
private String sseEntryPoint;
private int rxCount;
private long connectedTime = -1;
private boolean stopped;
private boolean connected;
private int retries;
private boolean configured;
private boolean hosed;
private String unsupportedReason = UNSUPPORTED_MESSAGE_NO_SERVER_SUPPORT;
private final Timer initialTimeoutTimer = new Timer() {
@Override
public void run() {
if (!connected) {
notifyDisconnected();
}
}
};
private Object sseChannel;
public SSEHandler(final MessageCallback messageCallback, final ClientMessageBusImpl clientMessageBus) {
this.clientMessageBus = clientMessageBus;
this.messageCallback = messageCallback;
this.pollingHandler = HttpPollingHandler.newNoPollingInstance(messageCallback, clientMessageBus);
clientMessageBus.subscribe(SSE_AGENT_SERVICE, new MessageCallback() {
@Override
public void callback(final Message message) {
notifyConnected();
}
});
}
@Override
public void configure(final Message capabilitiesMessage) {
configured = true;
if (!isSSESupported()) {
hosed = true;
unsupportedReason = UNSUPPORTED_MESSAGE_NO_SERVER_SUPPORT;
LogUtil.log("this browser does not support SSE");
return;
}
this.sseEntryPoint = URL.encode(clientMessageBus.getApplicationLocation(clientMessageBus.getInServiceEntryPoint()))
+ "?z=0000&sse=1&clientId=" + URL.encodePathSegment(clientMessageBus.getClientId());
}
@Override
public void start() {
stopped = false;
if (connected) {
LogUtil.log("did not start SSE handler: already started.");
return;
}
sseChannel = attemptSSEChannel(clientMessageBus, sseEntryPoint);
// time out after 2 seconds and attempt reconnect. (note: this is really to deal with a bug a firefox).
initialTimeoutTimer.cancel();
initialTimeoutTimer.schedule(2500);
}
@Override
public Collection<Message> stop(final boolean stopAllCurrentRequests) {
stopped = true;
disconnect(sseChannel);
sseChannel = null;
return pollingHandler.stop(stopAllCurrentRequests);
}
@Override
public void transmit(final List<Message> txMessages) {
this.pollingHandler.transmit(txMessages);
}
@Override
public void handleProtocolExtension(final Message message) {
}
@Override
public boolean isUsable() {
return !hosed && configured;
}
private void handleReceived(final String json) {
rxCount++;
BusToolsCli.decodeToCallback(json, messageCallback);
}
private static native void disconnect(Object channel) /*-{
channel.close();
}-*/;
private native boolean isSSESupported() /*-{
return !!window.EventSource;
}-*/;
private native Object attemptSSEChannel(final ClientMessageBusImpl bus, final String sseAddress) /*-{
var thisRef = this;
var errorHandler = function (e) {
if (e.srcElement.readyState === EventSource.CLOSED) {
thisRef.@org.jboss.errai.bus.client.framework.transports.SSEHandler::notifyDisconnected()();
}
};
var openHandler = function () {
thisRef.@org.jboss.errai.bus.client.framework.transports.SSEHandler::verifyConnected()();
};
var sseSource = new EventSource(sseAddress);
sseSource.addEventListener('message', function (e) {
thisRef.@org.jboss.errai.bus.client.framework.transports.SSEHandler::handleReceived(Ljava/lang/String;)(e.data);
}, false);
sseSource.onerror = errorHandler;
sseSource.onopen = openHandler;
return sseSource;
}-*/;
private void verifyConnected() {
transmit(Collections.singletonList(MessageBuilder.createMessage()
.toSubject("ServerEchoService")
.signalling().done().repliesToSubject(SSE_AGENT_SERVICE).getMessage()));
}
private void notifyConnected() {
initialTimeoutTimer.cancel();
if (connected) {
return;
}
connected = true;
connectedTime = System.currentTimeMillis();
LogUtil.log("SSE channel opened.");
retries = 0;
if (clientMessageBus.getState() == BusState.CONNECTION_INTERRUPTED)
clientMessageBus.setState(BusState.CONNECTED);
}
private void notifyDisconnected() {
connected = false;
initialTimeoutTimer.cancel();
LogUtil.log("SSE channel disconnected.");
connectedTime = -1;
clientMessageBus.setState(BusState.CONNECTION_INTERRUPTED);
disconnect(sseChannel);
if (!stopped) {
retries++;
new Timer() {
@Override
public void run() {
LogUtil.log("attempting reconnection ... ");
transmit(Collections.singletonList(MessageBuilder.createMessage()
.toSubject("SSEAgent")
.signalling().done().repliesToSubject("ClientBus").getMessage()));
pollingHandler.performPoll();
start();
}
}.schedule(retries * 1000);
}
}
@Override
public String toString() {
return "SSE";
}
@Override
public TransportStatistics getStatistics() {
return this;
}
@Override
public String getTransportDescription() {
return "HTTP + Server-Sent Events";
}
@Override
public String getUnsupportedDescription() {
return unsupportedReason;
}
@Override
public int getMessagesSent() {
return pollingHandler.getMessagesSent();
}
@Override
public int getMessagesReceived() {
return rxCount;
}
@Override
public long getConnectedTime() {
return connectedTime;
}
@Override
public int getMeasuredLatency() {
return pollingHandler.getMeasuredLatency();
}
@Override
public long getLastTransmissionTime() {
return pollingHandler.getLastTransmissionTime();
}
@Override
public boolean isFullDuplex() {
return false;
}
@Override
public String getRxEndpoint() {
return clientMessageBus.getInServiceEntryPoint();
}
@Override
public String getTxEndpoint() {
return clientMessageBus.getOutServiceEntryPoint();
}
@Override
public int getPendingMessages() {
return pollingHandler.getStatistics().getPendingMessages();
}
}