/* * Copyright (C) 2012 Red Hat, Inc. and/or its affiliates. * * 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 java.util.Collection; import java.util.Collections; import java.util.List; import org.jboss.errai.bus.client.api.Subscription; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gwt.http.client.URL; import com.google.gwt.user.client.Timer; /** * An ErraiBus transport handler using server-sent events. It relies on * {@link HttpPollingHandler} for transmitting messages. * * @author Mike Brock * @author Christian Sadilek <csadilek@redhat.com> */ public class SSEHandler implements TransportHandler, TransportStatistics { private static final String SSE_AGENT_SERVICE = "SSEAgent"; private final ClientMessageBusImpl clientMessageBus; 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 pingTimeout = new Timer() { @Override public void run() { if (!connected) { logger.warn(this + ": initial timeout expired"); notifyDisconnected(); } } }; private Object sseChannel; /** * Bus subscription that receives ping responses from the server bus. This is * used for verifying that the SSE channel is actually working. */ private final Subscription sseAgentSubscription; private static final Logger logger = LoggerFactory.getLogger(SSEHandler.class); public SSEHandler(final ClientMessageBusImpl clientMessageBus) { this.clientMessageBus = clientMessageBus; this.pollingHandler = HttpPollingHandler.newNoPollingInstance(clientMessageBus); sseAgentSubscription = 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; logger.warn("this browser does not support SSE"); return; } this.sseEntryPoint = URL.encode(clientMessageBus.getApplicationLocation(clientMessageBus.getInServiceEntryPoint())) + "?&sse=1&clientId=" + URL.encodePathSegment(clientMessageBus.getClientId()); } @Override public void start() { stopped = false; if (connected) { logger.info("did not start SSE handler: already started."); return; } sseChannel = attemptSSEChannel(clientMessageBus, sseEntryPoint + "&z=" + retries); } @Override public Collection<Message> stop(final boolean stopAllCurrentRequests) { stopped = true; if (sseChannel != null) { 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, clientMessageBus); } 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) { $wnd.console.log("SSE channel error (according to the browser)"); $wnd.console.log(e); thisRef.@org.jboss.errai.bus.client.framework.transports.SSEHandler::notifyDisconnected()(); }; var openHandler = function () { $wnd.console.log("SSE channel opened (according to the browser)"); 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; }-*/; /** * Sends a ping request to the server. If the ping response is not received * within a reasonable time limit, notifyDisconnected() will be called. */ private void verifyConnected() { // in case we were in the middle of something already pingTimeout.cancel(); transmit(Collections.singletonList(MessageBuilder.createMessage() .toSubject("ServerEchoService") .signalling().done().repliesToSubject(SSE_AGENT_SERVICE).getMessage())); pingTimeout.schedule(2500); } private void notifyConnected() { pingTimeout.cancel(); retries = 0; if (!connected) { connected = true; connectedTime = System.currentTimeMillis(); logger.info(this + ": SSE channel is active."); } if (clientMessageBus.getState() == BusState.CONNECTION_INTERRUPTED) { clientMessageBus.setState(BusState.CONNECTED); } } private void notifyDisconnected() { connected = false; pingTimeout.cancel(); logger.info(this + " channel disconnected."); connectedTime = -1; clientMessageBus.setState(BusState.CONNECTION_INTERRUPTED); disconnect(sseChannel); if (!stopped) { if (retries == 0) { transmit(Collections.singletonList(MessageBuilder.createMessage() .toSubject("ServerEchoService") .signalling().done().repliesToSubject(SSE_AGENT_SERVICE).getMessage())); } final int retryDelay = Math.min((retries * 1000) + 1, 10000); logger.info("attempting SSE reconnection in " + retryDelay + "ms -- attempt: " + (++retries)); new Timer() { @Override public void run() { if (!stopped) { start(); } } }.schedule(retryDelay); } } @Override public String toString() { return "SSE[" + System.identityHashCode(this) + "]"; } @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(); } @Override public void close() { if (!stopped) { stop(true); } sseAgentSubscription.remove(); configured = false; } }