/*
* 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.base.CommandMessage;
import org.jboss.errai.bus.client.api.messaging.Message;
import org.jboss.errai.bus.client.framework.BuiltInServices;
import org.jboss.errai.bus.client.framework.BusState;
import org.jboss.errai.bus.client.framework.ClientMessageBusImpl;
import org.jboss.errai.bus.client.protocols.BusCommand;
import org.jboss.errai.bus.client.util.BusToolsCli;
import org.jboss.errai.common.client.api.Assert;
import org.jboss.errai.common.client.protocols.MessageParts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gwt.user.client.Timer;
/**
* @author Mike Brock
*/
public class WebsocketHandler implements TransportHandler, TransportStatistics {
private final ClientMessageBusImpl messageBus;
private String webSocketUrl;
private String webSocketToken;
private Object webSocketChannel;
private HttpPollingHandler longPollingTransport;
private long connectedTime = -1;
private long lastTransmission;
private int txCount;
private int rxCount;
private boolean configured;
private boolean hosed;
private boolean stopped;
private int retries;
private String unsupportedReason = UNSUPPORTED_MESSAGE_NO_SERVER_SUPPORT;
private static Logger logger = LoggerFactory.getLogger(WebsocketHandler.class);
public WebsocketHandler(final ClientMessageBusImpl messageBus) {
this.longPollingTransport = HttpPollingHandler.newLongPollingInstance(messageBus);
this.messageBus = Assert.notNull(messageBus);
}
@Override
public void configure(final Message capabilitiesMessage) {
configured = true;
if (!isWebSocketSupported()) {
unsupportedReason = UNSUPPORTED_MESSAGE_NO_CLIENT_SUPPORT;
logger.warn("websockets not supported by this browser");
hosed = true;
return;
}
webSocketUrl = capabilitiesMessage.get(String.class, MessageParts.WebSocketURL);
webSocketToken = capabilitiesMessage.get(String.class, MessageParts.WebSocketToken);
hosed = (webSocketUrl == null || webSocketToken == null);
if (hosed) {
logger.warn("server reported it supports websockets but did not send configuration information.");
}
}
@Override
public void start() {
if (webSocketChannel != null && isConnected(webSocketChannel)) {
return;
}
longPollingTransport.start();
logger.info("attempting web sockets connection at URL: " + webSocketUrl);
attemptWebSocketConnect(webSocketUrl);
}
@Override
public void transmit(final List<Message> txMessages) {
// The HTTP long polling handler is cancelled when the websocket channel becomes available
if (longPollingTransport.isCancelled()) {
boolean success = transmitToSocket(webSocketChannel, BusToolsCli.encodeMessages(txMessages));
if (!success) {
logger.error("failed to deliver " + txMessages.size() + " message(s) using websocket");
}
}
else {
longPollingTransport.transmit(txMessages);
}
}
@Override
public void handleProtocolExtension(final Message message) {
switch (BusCommand.valueOf(message.getCommandType())) {
case WebsocketChannelVerify:
logger.info("received verification token for websocket connection");
longPollingTransport
.transmit(Collections.singletonList(CommandMessage.create()
.toSubject(BuiltInServices.ServerBus.name()).command(BusCommand.WebsocketChannelVerify)
.copy(MessageParts.WebSocketToken, message)));
break;
case WebsocketChannelOpen:
if (messageBus.getState() == BusState.CONNECTION_INTERRUPTED)
messageBus.setState(BusState.CONNECTED);
// send final message to open the channel
transmitToSocket(webSocketChannel, getWebSocketNegotiationString());
longPollingTransport.stop(false);
webSocketToken = message.get(String.class, MessageParts.WebSocketToken);
logger.info("web socket channel successfully negotiated. comet channel deactivated. (reconnect token: "
+ webSocketToken + ")");
retries = 0;
break;
case WebsocketNegotiationFailed:
hosed = true;
unsupportedReason = "Negotiation Failed (Bad Key)";
disconnectSocket(webSocketChannel);
webSocketChannel = null;
messageBus.reconsiderTransport();
break;
}
}
@Override
public Collection<Message> stop(final boolean stopAllCurrentRequests) {
longPollingTransport.stop(stopAllCurrentRequests);
if (webSocketChannel != null) {
disconnectSocket(webSocketChannel);
webSocketChannel = null;
}
stopped = true;
return Collections.emptyList();
}
@Override
public boolean isUsable() {
return configured && !hosed;
}
public void attachWebSocketChannel(final Object o) {
logger.info("web socket opened. sending negotiation message.");
transmitToSocket(o, getWebSocketNegotiationString());
webSocketChannel = o;
connectedTime = System.currentTimeMillis();
}
private String getWebSocketNegotiationString() {
return "{\"" + MessageParts.CommandType.name() + "\":\"" + BusCommand.Associate.name() + "\", \""
+ MessageParts.ConnectionSessionKey + "\":\"" + messageBus.getSessionId() + "\"" + ",\""
+ MessageParts.WebSocketToken + "\":\"" + webSocketToken + "\"}";
}
private void handleReceived(String json) {
BusToolsCli.decodeToCallback(json, messageBus);
rxCount++;
lastTransmission = System.currentTimeMillis();
}
@Override
public String toString() {
return "WebSockets";
}
public native void disconnectSocket(final Object channel) /*-{
channel.close();
}-*/;
public native static boolean isWebSocketSupported() /*-{
return !!window.WebSocket;
}-*/;
public native Object attemptWebSocketConnect(final String websocketAddr) /*-{
var thisRef = this;
var socket;
if (window.WebSocket) {
socket = new WebSocket(websocketAddr);
socket.onmessage = function (event) {
thisRef.@org.jboss.errai.bus.client.framework.transports.WebsocketHandler::handleReceived(Ljava/lang/String;)(event.data);
};
socket.onopen = function (event) {
thisRef.@org.jboss.errai.bus.client.framework.transports.WebsocketHandler::attachWebSocketChannel(Ljava/lang/Object;)(socket);
};
socket.onclose = function (event) {
thisRef.@org.jboss.errai.bus.client.framework.transports.WebsocketHandler::log(Ljava/lang/String;)("websocket closed with code: " + event.code);
thisRef.@org.jboss.errai.bus.client.framework.transports.WebsocketHandler::notifyDisconnected()();
};
socket.onerror = function (event) {
if (event.srcElement.readyState != WebSocket.OPEN) {
thisRef.@org.jboss.errai.bus.client.framework.transports.WebsocketHandler::notifyDisconnected()();
}
}
return socket;
}
else {
return "NotSupportedByBrowser";
}
}-*/;
public native boolean transmitToSocket(final Object socket, final String text) /*-{
if (socket.readyState == WebSocket.OPEN) {
socket.send(text);
return true;
}
else {
return false;
}
}-*/;
public native static boolean isConnected(final Object socket) /*-{
return socket.readyState == WebSocket.OPEN;
}-*/;
private void log(final String message) {
logger.info(message);
}
private void notifyDisconnected() {
logger.info("websocket disconnected");
messageBus.setState(BusState.CONNECTION_INTERRUPTED);
disconnectSocket(webSocketChannel);
webSocketChannel = null;
connectedTime = -1;
if (!stopped) {
retries++;
new Timer() {
@Override
public void run() {
logger.info("attempting reconnection ... ");
longPollingTransport.stop(false);
start();
}
}.schedule(retries * 1000);
}
}
@Override
public TransportStatistics getStatistics() {
return this;
}
@Override
public String getTransportDescription() {
return "WebSockets";
}
@Override
public String getUnsupportedDescription() {
return unsupportedReason;
}
@Override
public int getMessagesSent() {
return txCount;
}
@Override
public int getMessagesReceived() {
return rxCount;
}
@Override
public long getConnectedTime() {
return connectedTime;
}
@Override
public long getLastTransmissionTime() {
return lastTransmission;
}
@Override
public int getMeasuredLatency() {
return -1;
}
@Override
public boolean isFullDuplex() {
return true;
}
@Override
public String getRxEndpoint() {
return webSocketUrl;
}
@Override
public String getTxEndpoint() {
return null;
}
@Override
public int getPendingMessages() {
return 0;
}
@Override
public void close() {
if (!stopped) {
stop(true);
}
configured = false;
}
}