package org.swellrt.beta.client; import java.util.HashMap; import java.util.Map; import org.swellrt.beta.client.ServiceBasis.ConnectionHandler; import org.swellrt.beta.client.wave.RemoteViewServiceMultiplexer; import org.swellrt.beta.client.wave.WaveWebSocketClient; import org.swellrt.beta.client.wave.WaveWebSocketClient.ConnectState; import org.swellrt.beta.client.wave.WaveWebSocketClient.StartCallback; import org.swellrt.beta.common.SException; import org.swellrt.beta.model.remote.SObjectRemote; import org.waveprotocol.wave.client.account.ServerAccountData; import org.waveprotocol.wave.concurrencycontrol.common.ChannelException; import org.waveprotocol.wave.concurrencycontrol.common.Recoverable; import org.waveprotocol.wave.concurrencycontrol.common.ResponseCode; import org.waveprotocol.wave.model.id.IdGenerator; import org.waveprotocol.wave.model.id.IdGeneratorImpl; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.util.CopyOnWriteSet; import org.waveprotocol.wave.model.wave.ParticipantId; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.SettableFuture; import com.google.gwt.user.client.Random; /** * This class is the stateful part of a SwellRT client. It supports the * {@link ServiceFronted} and {@link Operation} instances. * <p> * A Service context has the following responsibilities: * <li>Connect/Reconnect the Websocket</li> * <li>Keep a registry/cache of Waves/Objects</li> * <li>Provide a sanity check method for Wave contexts and the API</li> * <p> * This context handles exceptions coming from websocket. * * @author pablojan@gmail.com (Pablo Ojanguren) * */ public class ServiceContext implements WaveWebSocketClient.StatusListener, ServiceStatus { public static final String SWELL_DATAMODEL_VERSION = "1.0"; public static final String WAVEID_NAMESPACE_PREFIX = "s"; // TODO move to utility class static final char[] WEB64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" .toCharArray(); static final int WAVE_ID_SEED_LENGTH = 10; private static String getRandomBase64(int length) { StringBuilder result = new StringBuilder(length); int bits = 0; int bitCount = 0; while (result.length() < length) { if (bitCount < 6) { bits = Random.nextInt(); bitCount = 32; } result.append(WEB64_ALPHABET[bits & 0x3F]); bits >>= 6; bitCount -= 6; } return result.toString(); } /** * @return Websocket server address, e.g. ws://swellrt.acme.com:8080 */ private static String getWebsocketAddress(String httpAddress) { String websocketAddress = httpAddress; if (!websocketAddress.endsWith("/")) websocketAddress += "/"; if (websocketAddress.startsWith("http://")) websocketAddress = websocketAddress.replace("http://", "ws://"); else if (websocketAddress.startsWith("https://")) websocketAddress = websocketAddress.replace("https://", "wss://"); return websocketAddress; } private static String WINDOW_ID = getRandomBase64(4); static { String now = String.valueOf(System.currentTimeMillis()); WINDOW_ID += now.substring(now.length()-4, now.length()); } private Map<WaveId, WaveContext> waveRegistry = new HashMap<WaveId, WaveContext>(); private IdGenerator legacyIdGenerator; private SessionManager sessionManager; private boolean sessionCookieAvailable = false; private final String httpAddress; private final String websocketAddress; private WaveWebSocketClient websocketClient; private SettableFuture<RemoteViewServiceMultiplexer> serviceMultiplexerFuture = SettableFuture .<RemoteViewServiceMultiplexer> create(); private final CopyOnWriteSet<ServiceFrontend.ConnectionHandler> connectionHandlers = CopyOnWriteSet .createListSet(); private ConnectState connectState = null; private SException exception = null; public ServiceContext(SessionManager sessionManager, String httpAddress) { this.sessionManager = sessionManager; this.httpAddress = httpAddress; this.websocketAddress = getWebsocketAddress(httpAddress); } public void addConnectionHandler(ConnectionHandler h) { connectionHandlers.add(h); } public void removeConnectionHandler(ConnectionHandler h) { connectionHandlers.remove(h); } protected void setupIdGenerator() { final String seed = getRandomBase64(WAVE_ID_SEED_LENGTH); this.legacyIdGenerator = new IdGeneratorImpl(sessionManager.getWaveDomain(), new IdGeneratorImpl.Seed() { @Override public String get() { return seed; } }); } /** * @return HTTP server address, e.g. http://swellrt.acme.com:8080 */ public String getHTTPAddress() { return httpAddress; } /** * @return true iff the HTTP client sends the session cookie to the server. */ public boolean isSessionCookieAvailable() { return sessionCookieAvailable; } public void setSessionCookieAvailability(boolean isEnabled) { sessionCookieAvailable = isEnabled; } /** * Initializes the service context, resetting first if it is necessary and * setting a new session. * * @param accountData */ public void init(ServerAccountData accountData) { reset(); sessionManager.setSession(accountData); setupIdGenerator(); } public String getSessionId() { return sessionManager.getSessionId(); } public String getTransientSessionId() { return sessionManager.getTransientSessionId(); } public String getWindowId() { return WINDOW_ID; } public String getParticipantId() { return sessionManager.getUserId(); } /** * Clean up the internal state of this context. This will normally happen on a * session close */ public void reset() { // TODO clean text editor for (WaveContext wc : waveRegistry.values()) wc.close(); waveRegistry.clear(); if (websocketClient != null) { websocketClient.stop(false); websocketClient = null; } serviceMultiplexerFuture = SettableFuture.<RemoteViewServiceMultiplexer> create(); sessionManager.removeSession(); } public boolean isSession() { return sessionManager.isSession(); } public WaveId generateWaveId() { return WaveId.of(sessionManager.getWaveDomain(), legacyIdGenerator.newId(WAVEID_NAMESPACE_PREFIX)); } /** * Returns a SObject instance supported by a Wave. * * @param waveId * @param callback */ public void getObject(WaveId waveId, FutureCallback<SObjectRemote> callback) { if (sessionManager == null || !sessionManager.isSession()) { callback.onFailure(new SException(ResponseCode.NOT_LOGGED_IN)); return; } if (!waveRegistry.containsKey(waveId)) { waveRegistry.put(waveId, new WaveContext(waveId, sessionManager.getWaveDomain(), ParticipantId.ofUnsafe(sessionManager.getUserId()), this)); } WaveContext waveContext = waveRegistry.get(waveId); lazyWebsocketStart(); Futures.addCallback(serviceMultiplexerFuture, new FutureCallback<RemoteViewServiceMultiplexer>() { @Override public void onSuccess(RemoteViewServiceMultiplexer multiplexer) { if (!waveContext.isActive()) waveContext.init(multiplexer, ServiceContext.this.legacyIdGenerator); waveContext.getSObject(callback); } @Override public void onFailure(Throwable t) { callback.onFailure(t); } }); } /** * Try to connect the Websocket. * * @return true if this call actually starts a new connection */ private boolean lazyWebsocketStart() { if (websocketClient == null) { String sessionToken = sessionManager.getSessionId()+":"+getTransientSessionId()+":"+getWindowId(); websocketClient = new WaveWebSocketClient(sessionToken, websocketAddress); websocketClient.attachStatusListener(ServiceContext.this); RemoteViewServiceMultiplexer serviceMultiplexer = new RemoteViewServiceMultiplexer( websocketClient, sessionManager.getUserId()); websocketClient.start(new StartCallback() { @Override public void onStart() { serviceMultiplexerFuture.set(serviceMultiplexer); } @Override public void onFailure(String e) { serviceMultiplexerFuture .setException(new SException(SException.WEBSOCKET_ERROR, null, e)); onStateChange(ConnectState.ERROR, e); } }); return true; } return false; } /** * Handle WebSocket status */ @Override public void onStateChange(ConnectState state, String e) { if (connectState == ConnectState.ERROR) { // ignore further error messages return; } // At this moment we cannot get recovered from // a Websocket fatal error, so in that case, let's shutdown // all Wave contexts gracefully SException sexception = null; if (state.equals(ConnectState.ERROR)) { sexception = new SException(SException.WEBSOCKET_ERROR, null, e); if (!serviceMultiplexerFuture.isDone()) serviceMultiplexerFuture.setException(new SException(SException.WEBSOCKET_ERROR)); for (WaveContext ctx : waveRegistry.values()) { ctx.onFailure(new ChannelException(ResponseCode.WEBSOCKET_ERROR, e, null, Recoverable.NOT_RECOVERABLE, null, null)); } } connectState = state; exception = sexception; for (ServiceFrontend.ConnectionHandler ch : connectionHandlers) ch.exec(state.toString(), sexception); } @Override public void check() throws SException { if (connectState == ConnectState.ERROR) { if (exception != null) throw exception; else throw new SException(ResponseCode.UNKNOWN); } else if (!websocketClient.isConnected()) { throw new SException(ResponseCode.WEBSOCKET_ERROR); } } @Override public void raise(String waveId, SException ex) { if (connectState == ConnectState.ERROR) return; connectState = ConnectState.ERROR; for (ServiceFrontend.ConnectionHandler ch : connectionHandlers) ch.exec(connectState.toString(), ex); } public String getWaveDomain() { return sessionManager.getWaveDomain(); } }