/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.waveprotocol.box.webclient.client.atmosphere; import com.google.gwt.core.client.Callback; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.ScriptInjector; /** * The wrapper implementation of the atmosphere javascript client. * * https://github.com/Atmosphere/atmosphere/wiki/jQuery.atmosphere.js-atmosphere * .js-API * * More info about transports * https://github.com/Atmosphere/atmosphere/wiki/Supported * -WebServers-and-Browsers * * It tries to use Server-Sent Events first and fallback transport to * long-polling. We ignore Websockets by now because they are the default * transport on WiAB and atmosphere is the fallback. * * http://stackoverflow.com/questions/9397528/server-sent-events-vs-polling * * * @author pablojan@gmail.com (Pablo Ojanguren) * */ public class AtmosphereConnectionImpl implements AtmosphereConnection { private static final class AtmosphereSocket extends JavaScriptObject { public static native AtmosphereSocket create(AtmosphereConnectionImpl impl, String urlBase) /*-{ var client = $wnd.atmosphere; var atsocket = { request: null, socket: null }; var connectionUrl = window.location.protocol + "//" + $wnd.__websocket_address + "/"; connectionUrl += 'atmosphere'; //console.log("Connection URL is "+urlBase); atsocket.request = new client.AtmosphereRequest(); atsocket.request.url = connectionUrl; atsocket.request.contenType = 'text/plain;charset=UTF-8'; atsocket.request.transport = 'sse'; atsocket.request.fallbackTransport = 'long-polling'; atsocket.request.onOpen = $entry(function() { impl.@org.waveprotocol.box.webclient.client.atmosphere.AtmosphereConnectionImpl::onConnect()(); }); atsocket.request.onMessage = $entry(function(response) { var r = response.responseBody; if (r.indexOf('|') == 0) { while (r.indexOf('|') == 0 && r.length > 1) { r = r.substring(1); var marker = r.indexOf('}|'); impl.@org.waveprotocol.box.webclient.client.atmosphere.AtmosphereConnectionImpl::onMessage(Ljava/lang/String;)(r.substring(0, marker+1)); r = r.substring(marker+1); } } else { impl.@org.waveprotocol.box.webclient.client.atmosphere.AtmosphereConnectionImpl::onMessage(Ljava/lang/String;)(r); } }); atsocket.request.onClose = $entry(function(response) { client.util.info("Connection closed"); impl.@org.waveprotocol.box.webclient.client.atmosphere.AtmosphereConnectionImpl::onDisconnect(Ljava/lang/String;)(response); }); atsocket.request.onTransportFailure = function(errorMsg, request) { client.util.info(errorMsg); }; atsocket.request.onReconnect = function(request, response) { client.util.info("Reconnected to the server"); }; atsocket.request.onError = function(response) { client.util.info("Unexpected Error"); }; return atsocket; }-*/; protected AtmosphereSocket() { } public native void close() /*-{ this.socket.unsubscribe(); }-*/; public native AtmosphereSocket connect() /*-{ this.socket = $wnd.atmosphere.subscribe(this.request); }-*/; public native void send(String data) /*-{ this.socket.push(data); }-*/; } private final AtmosphereConnectionListener listener; private String urlBase; private AtmosphereConnectionState state; private AtmosphereSocket socket = null; public AtmosphereConnectionImpl(AtmosphereConnectionListener listener, String urlBase) { this.listener = listener; this.urlBase = urlBase; } @Override public void connect() { if (socket == null) { ScriptInjector.fromUrl("/atmosphere/atmosphere.js").setCallback( new Callback<Void, Exception>() { public void onFailure(Exception reason) { throw new IllegalStateException("atmosphere.js load failed!"); } public void onSuccess(Void result) { socket = AtmosphereSocket.create(AtmosphereConnectionImpl.this, urlBase); socket.connect(); } }).setWindow(ScriptInjector.TOP_WINDOW).inject(); } else { if (AtmosphereConnectionState.CLOSED.equals(this.state)) socket.connect(); } } @Override public void close() { if (!AtmosphereConnectionState.CLOSED.equals(this.state)) socket.close(); } @Override public void sendMessage(String message) { this.state = AtmosphereConnectionState.MESSAGE_PUBLISHED; socket.send(message); } @SuppressWarnings("unused") private void onConnect() { this.state = AtmosphereConnectionState.OPENED; listener.onConnect(); } /** * This method is called when an Atmosphere onClose event happens: * * when an error occurs. when the server or a proxy closes the connection. * when an expected exception occurs. when the specified transport is not * supported or fail to connect. * * @param response * */ @SuppressWarnings("unused") private void onDisconnect(String response) { this.state = AtmosphereConnectionState.CLOSED; listener.onDisconnect(); } @SuppressWarnings("unused") private void onMessage(String message) { this.state = AtmosphereConnectionState.MESSAGE_RECEIVED; listener.onMessage(message); } }