/** * Copyright 2009 Google 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.waveprotocol.box.consoleclient; import static org.waveprotocol.box.common.CommonConstants.INDEX_WAVE_ID; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; import com.google.protobuf.RpcCallback; import com.google.protobuf.RpcController; import org.waveprotocol.box.common.DocumentConstants; import org.waveprotocol.box.common.IndexEntry; import org.waveprotocol.box.common.IndexWave; import org.waveprotocol.box.common.comms.WaveClientRpc.ProtocolAuthenticate; import org.waveprotocol.box.common.comms.WaveClientRpc.ProtocolAuthenticationResult; import org.waveprotocol.box.common.comms.WaveClientRpc.ProtocolOpenRequest; import org.waveprotocol.box.common.comms.WaveClientRpc.ProtocolSubmitRequest; import org.waveprotocol.box.common.comms.WaveClientRpc.ProtocolSubmitResponse; import org.waveprotocol.box.common.comms.WaveClientRpc.ProtocolWaveClientRpc; import org.waveprotocol.box.common.comms.WaveClientRpc.ProtocolWaveletUpdate; import org.waveprotocol.box.common.comms.WaveClientRpc.WaveletSnapshot; import org.waveprotocol.box.server.authentication.SessionManager; import org.waveprotocol.box.server.common.CoreWaveletOperationSerializer; import org.waveprotocol.box.server.rpc.ClientRpcChannel; import org.waveprotocol.box.server.rpc.WebSocketClientRpcChannel; import org.waveprotocol.box.server.util.BlockingSuccessFailCallback; import org.waveprotocol.box.server.util.NetUtils; import org.waveprotocol.box.server.util.SuccessFailCallback; import org.waveprotocol.box.server.util.WaveletDataUtil; import org.waveprotocol.wave.federation.Proto.ProtocolWaveletDelta; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.id.IdGenerator; import org.waveprotocol.wave.model.id.IdGeneratorImpl; import org.waveprotocol.wave.model.id.IdURIEncoderDecoder; import org.waveprotocol.wave.model.id.InvalidIdException; import org.waveprotocol.wave.model.id.ModernIdSerialiser; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.id.WaveletName; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.wave.AddParticipant; import org.waveprotocol.wave.model.operation.wave.BasicWaveletOperationContextFactory; import org.waveprotocol.wave.model.operation.wave.BlipContentOperation; import org.waveprotocol.wave.model.operation.wave.NoOp; import org.waveprotocol.wave.model.operation.wave.RemoveParticipant; import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletBlipOperation; import org.waveprotocol.wave.model.operation.wave.WaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletOperation; import org.waveprotocol.wave.model.operation.wave.WaveletOperationContext; import org.waveprotocol.wave.model.util.Pair; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.version.HashedVersionFactory; import org.waveprotocol.wave.model.version.HashedVersionZeroFactoryImpl; import org.waveprotocol.wave.model.wave.Constants; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.data.BlipData; import org.waveprotocol.wave.model.wave.data.ObservableWaveletData; import org.waveprotocol.wave.model.wave.data.WaveletData; import org.waveprotocol.wave.util.escapers.jvm.JavaUrlCodec; import org.waveprotocol.wave.util.logging.Log; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.HttpCookie; import java.net.InetSocketAddress; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.logging.Handler; import java.util.logging.SimpleFormatter; import java.util.logging.StreamHandler; /** * The "backend" of a basic wave client, designed for user interfaces to * interact with. */ public class ClientBackend { /** * Factory to create a client backend. */ public interface Factory { /** * Creates a ClientBacked tied permanently and connected to a given server * and user. * * @param userAtDomain the user and their domain (for example, foo@bar.org). * @param server to connect to (for example, acmewave.com:9898). * @throws IOException if the client backend can't connect to the server. * @return the new ClientBackend. */ ClientBackend create(String userAtDomain, String server) throws IOException; } /** * Factory for the various RPC objects used by the client backend. */ public interface RpcObjectFactory { /** * @return a {@link ClientRpcChannel} connected to the given server and * port. */ ClientRpcChannel createClientChannel(InetSocketAddress serverAddress) throws IOException; /** * @return an RPC server interface backed by the given * {@link ClientRpcChannel}. */ ProtocolWaveClientRpc.Interface createServerInterface(ClientRpcChannel channel); } /** * The default ClientBackend factory. */ public static class DefaultFactory implements Factory { @Override public ClientBackend create(String userAtDomain, String server) throws IOException { return new ClientBackend(userAtDomain, server); } } /** * Container for data to {@link WaveletOperationListener}s on events. */ private static class WaveletEventData { private final String author; private final HashedVersion hashedVersion; private final WaveletData waveletData; private final WaveletOperation waveletOperation; // Hack to mark whether this is actually the "end of a delta sequence", in which case the // above data is irrelevant. private final boolean isDeltaSequenceEnd; // Another hack to mark whether this is a commit notice event. // TODO(Michael): Get rid of these hacks! private final boolean isCommitNotice; /** * Standard constructor. */ WaveletEventData(String author, WaveletData waveletData, WaveletOperation waveletOperation) { this.author = author; this.hashedVersion = null; this.waveletData = waveletData; this.waveletOperation = waveletOperation; this.isDeltaSequenceEnd = false; this.isCommitNotice = false; } /** * Constructor to set this to a delta sequence end marker. */ WaveletEventData(WaveletData waveletData) { this.author = null; this.hashedVersion = null; this.waveletData = waveletData; this.waveletOperation = null; this.isDeltaSequenceEnd = true; this.isCommitNotice = false; } /** * Constructor for commit notice events. */ WaveletEventData(WaveletData waveletData, HashedVersion hashedVersion) { this.author = null; this.hashedVersion = hashedVersion; this.waveletData = waveletData; this.waveletOperation = null; this.isDeltaSequenceEnd = false; this.isCommitNotice = true; } String getAuthor() { return author; } HashedVersion getHashedVersion() { return hashedVersion; } WaveletData getWaveletData() { return waveletData; } WaveletOperation getWaveletOperation() { return waveletOperation; } boolean isDeltaSequenceEnd() { return isDeltaSequenceEnd; } boolean isCommitNotice() { return isCommitNotice; } } private static final Log LOG = Log.get(ClientBackend.class); private static final ByteArrayOutputStream logOutput = new ByteArrayOutputStream(); static { LOG.getLogger().addHandler(new StreamHandler(logOutput, new SimpleFormatter())); LOG.getLogger().setUseParentHandlers(false); } /** Id URI encoder and decoder. */ private static final IdURIEncoderDecoder URI_CODEC = new IdURIEncoderDecoder( new JavaUrlCodec()); /** User id of the user of the backend (encapsulating both user and server). */ private final ParticipantId userId; /** Waves this backend is aware of. */ private final Map<WaveId, ClientWaveView> waves = Maps.newHashMap(); /** RPC controllers for the open wave connections. */ private final Map<WaveId, RpcController> waveControllers = Maps.newHashMap(); /** Listeners waiting on wave updates. */ private final Set<WaveletOperationListener> waveletOperationListeners = Sets.newHashSet(); /** Id generator used for this (server, user) pair. */ private final IdGenerator idGenerator; /** RPC interface for communicating with server. */ private final ProtocolWaveClientRpc.Interface rpcServer; /** RPC channel for communicating with server. */ private final ClientRpcChannel rpcChannel; /** RPC utility for authenticating the user. */ private final ClientAuthenticator authenticator; /** The user's authentication token. Null until authentication is complete. */ private HttpCookie authenticationToken; /** Producer/consumer event queue. */ private final BlockingQueue<WaveletEventData> eventQueue = new LinkedBlockingQueue<WaveletEventData>(); /** For creating hashes when the client creates a wavelet. */ private final HashedVersionFactory hashedVersionFactory; /** For creating operation contexts. */ private final BasicWaveletOperationContextFactory contextFactory; /** * Create new client backend tied permanently to a given server and user, using a default * {@link RpcObjectFactory} implementation. Open the client's index, and begin managing waves it * has access to. * * @param userAtDomain the user and their domain (for example, foo@bar.org). * @param server to connect to (for example, acmewave.com:9898). * @throws IOException if we can't connect to the server. */ public ClientBackend(final String userAtDomain, String server) throws IOException { this(userAtDomain, server, new RpcObjectFactory() { @Override public ClientRpcChannel createClientChannel(InetSocketAddress serverAddress) throws IOException { return new WebSocketClientRpcChannel(serverAddress); } @Override public ProtocolWaveClientRpc.Interface createServerInterface(ClientRpcChannel channel) { return ProtocolWaveClientRpc.newStub(channel); } }, new HashedVersionZeroFactoryImpl(URI_CODEC), new ClientAuthenticator("http://" + server + SessionManager.SIGN_IN_URL)); } /** * Create new client backend tied permanently to a given server and user, open * that client's index, and begin managing waves it has access to. * * @param userAtDomain the user and their domain (for example, foo@bar.org). * @param server to connect to (for example, acmewave.com:9898). * @param rpcObjectFactory to use for creating RPC objects. * @throws IOException if we can't connect to the server. */ @Inject public ClientBackend(final String userAtDomain, String server, RpcObjectFactory rpcObjectFactory, HashedVersionFactory hashedVersionFactory, ClientAuthenticator authenticator) throws IOException { Preconditions.checkNotNull(server, "Server not specified"); this.userId = ParticipantId.ofUnsafe(userAtDomain); this.idGenerator = new IdGeneratorImpl(userId.getDomain(), new IdGeneratorImpl.Seed() { Random r = new Random(); @Override public String get() { return Long.toString(Math.abs(r.nextLong()), 36); } }); this.hashedVersionFactory = hashedVersionFactory; this.contextFactory = new BasicWaveletOperationContextFactory(this.userId); this.authenticator = authenticator; // Start pushing events to listeners in a separate thread. new Thread() { @Override public void run() { for (;;) { try { WaveletEventData nextEvent = eventQueue.take(); if (nextEvent.isDeltaSequenceEnd()) { for (WaveletOperationListener listener : waveletOperationListeners) { listener.onDeltaSequenceEnd(nextEvent.getWaveletData()); } } else if (nextEvent.isCommitNotice()) { for (WaveletOperationListener listener : waveletOperationListeners) { listener.onCommitNotice(nextEvent.getWaveletData(), nextEvent.getHashedVersion()); } } else { notifyWaveletOperationListeners(nextEvent.getAuthor(), nextEvent.getWaveletData(), nextEvent.getWaveletOperation()); } } catch (InterruptedException e) { // TODO: stop? } } } }.start(); // Connect to the specfied server address. this.rpcChannel = rpcObjectFactory.createClientChannel(NetUtils.parseHttpAddress(server)); this.rpcServer = rpcObjectFactory.createServerInterface(rpcChannel); } /** * Authenticate the user and pass the authentication information through the websocket * connection. This is required before sending authenticated RPCs to the backend. * * This RPC blocks until authentication is complete. * * @param password the user's password * @throws IOException * @return true if authentication succeeded, false otherwise. */ public boolean authenticate(char[] password) throws IOException { authenticationToken = authenticator.authenticate(userId.getAddress(), password); if (authenticationToken == null) { // The server rejected our authentication attempt. return false; } // Sending the auth token to the server is asynchronous. We'll use a latch to block until its // complete. final CountDownLatch latch = new CountDownLatch(1); final RpcController rpcController = rpcChannel.newRpcController(); ProtocolAuthenticate request = ProtocolAuthenticate.newBuilder() .setToken(authenticationToken.getValue()) .build(); rpcServer.authenticate(rpcController, request, new RpcCallback<ProtocolAuthenticationResult>() { @Override public void run(ProtocolAuthenticationResult result) { // The result is meaningless. We've authenticated. latch.countDown(); } }); try { latch.await(); } catch (InterruptedException e) { // This should never happen. throw new RuntimeException(e); } LOG.info("Authenticated."); // Opening the index wave will kickstart the process of receiving waves. openWave(INDEX_WAVE_ID, ""); return true; } /** * @return the client backend log. */ public static String getLog() { for (Handler handler : LOG.getLogger().getHandlers()) { handler.flush(); } return logOutput.toString(); } /** * Clear the log. */ public static void clearLog() { logOutput.reset(); } /** * Gracefully shut down the backend, closing all connections to the server and clearing the * collection of waves (since they will no longer be valid). This renders the backend relatively * useless since no more updates from the index wave will be received, but it is still valid. */ public void shutdown() { for (RpcController rpcController : waveControllers.values()) { LOG.info("Cancelling RpcController " + rpcController); rpcController.startCancel(); } waves.clear(); waveControllers.clear(); } /** * Open a wave. This method will return immediately and updates will be delivered internally * from the RPC interface, and externally to {@link WaveletOperationListener}s. * * @param waveId of wave to open * @param waveletIdPrefix filter such that the server will send wavelet updates for ids that * match any of this prefix */ private void openWave(WaveId waveId, String waveletIdPrefix) { if (waveControllers.containsKey(waveId)) { throw new IllegalArgumentException(waveId + " is already open"); } else { // The wave may already be there if created with createNewWave. if (!waves.containsKey(waveId)) { createWave(waveId); } } ProtocolOpenRequest.Builder openRequest = ProtocolOpenRequest.newBuilder(); openRequest.setParticipantId(getUserId().getAddress()); openRequest.setWaveId(ModernIdSerialiser.INSTANCE.serialiseWaveId(waveId)); openRequest.addWaveletIdPrefix(waveletIdPrefix); final RpcController rpcController = rpcChannel.newRpcController(); waveControllers.put(waveId, rpcController); LOG.info("Opening wave " + waveId + " for prefix \"" + waveletIdPrefix + '"'); rpcServer.open( rpcController, openRequest.build(), new RpcCallback<ProtocolWaveletUpdate>() { @Override public void run(ProtocolWaveletUpdate update) { if (update == null) { LOG.warning("RPC failed: " + rpcController.errorText()); } else { receiveWaveletUpdate(update); } } } ); } /** * Create a new conversation wave and tell the server about it, by adding ourselves as a * participant on the conversation root. * * @param callback callback invoked when the server rpc is complete * @return the {@link ClientWaveView} created */ public ClientWaveView createConversationWave( SuccessFailCallback<ProtocolSubmitResponse, String> callback) { return createConversationWave(getIdGenerator().newWaveId(), callback); } /** * Create a new conversation wave with a given wave id and tell the server about it, by adding * ourselves as a participant on the conversation root. * * @param newWaveId the id to give the new wave * @param callback callback invoked when the server rpc is complete * @return the {@link ClientWaveView} created */ private ClientWaveView createConversationWave(WaveId newWaveId, SuccessFailCallback<ProtocolSubmitResponse, String> callback) { ClientWaveView waveView = createWave(newWaveId); WaveletId waveletId = getIdGenerator().newConversationRootWaveletId(); // Add ourselves then create a conversation manifest. sendWaveletOperations(WaveletName.of(newWaveId, waveletId), callback, new AddParticipant(contextFactory.createContext(), getUserId()), new WaveletBlipOperation(DocumentConstants.MANIFEST_DOCUMENT_ID, new BlipContentOperation(contextFactory.createContext(), ClientUtils.createManifest()))); return waveView; } /** * @param id of wave to get * @return wave with the given id, or null if not found */ public ClientWaveView getWave(WaveId id) { return waves.get(id); } /** * @return the special wave containing the index data */ public ClientWaveView getIndexWave() { return getWave(INDEX_WAVE_ID); } /** * Sends wavelet operations over the wire. * * @param waveletName of the wavelet to apply the operation to * @param callback callback invoked when the server rpc is complete * @param ops to send */ public void sendWaveletOperations(WaveletName waveletName, SuccessFailCallback<ProtocolSubmitResponse, String> callback, WaveletOperation... ops) { WaveletDelta delta = makeDelta(waveletName, ops); sendWaveletDelta(waveletName, delta, callback); } /** * Sends wavelet operations over the wire and waits for the roundtrip success: the submit * callback from our local server, and the wavelet to be updated to the version given in the * response. * * @param waveletName of the wavelet to apply the operation to * @param timeout used twice, so real timeout may be up to twice that specified * @param unit of timeout * @param ops to send * @return true if the roundtrip trip was successful, false otherwise */ public boolean sendAndAwaitWaveletOperations(WaveletName waveletName, long timeout, TimeUnit unit, WaveletOperation... ops) { WaveletDelta delta = makeDelta(waveletName, ops); return sendAndAwaitWaveletDelta(waveletName, delta, timeout, unit); } /** * Send a wavelet delta over the wire. * * @param waveletName of the wavelet that the delta applies to * @param delta to send * @param callback callback invoked when the server rpc is complete */ private void sendWaveletDelta(WaveletName waveletName, WaveletDelta delta, final SuccessFailCallback<ProtocolSubmitResponse, String> callback) { // Build the submit request. ProtocolSubmitRequest.Builder submitRequest = ProtocolSubmitRequest.newBuilder(); submitRequest.setWaveletName(ModernIdSerialiser.INSTANCE.serialiseWaveletName(waveletName)); submitRequest.setDelta(CoreWaveletOperationSerializer.serialize(delta)); final RpcController rpcController = rpcChannel.newRpcController(); LOG.info("Sending delta " + delta + " for " + waveletName); rpcServer.submit(rpcController, submitRequest.build(), new RpcCallback<ProtocolSubmitResponse>() { @Override public void run(ProtocolSubmitResponse response) { if (response == null) { callback.onFailure("null response"); } else if (rpcController.failed()) { callback.onFailure(rpcController.errorText()); } else { callback.onSuccess(response); } } }); } /** * Send a wavelet delta over the wire and wait for the roundtrip success: the submit callback * from our local server, and the wavelet to be updated to the version given in the response. * * @param waveletName of the wavelet that the delta applies to * @param delta to send * @param timeout used twice, so real timeout may be up to twice that specified * @param unit of timeout * @return true if the roundtrip trip was successful, false otherwise */ public boolean sendAndAwaitWaveletDelta(WaveletName waveletName, WaveletDelta delta, long timeout, TimeUnit unit) { BlockingSuccessFailCallback<ProtocolSubmitResponse, String> callback = BlockingSuccessFailCallback.create(); sendWaveletDelta(waveletName, delta, callback); Pair<ProtocolSubmitResponse, String> result = callback.await(timeout, unit); if (result == null) { LOG.warning("Error: submit result pair for " + waveletName + " was null, timed out?"); return false; } else if (result.getFirst() == null) { LOG.warning("Error: submit response to " + waveletName + " was null: " + result.getSecond()); return false; } else { return getWave(waveletName.waveId).awaitWaveletVersion(waveletName.waveletId, result.getFirst().getHashedVersionAfterApplication().getVersion(), timeout, unit); } } /** * Receive a protocol wavelet update from the wave server. * * @param waveletUpdate the wavelet update */ public void receiveWaveletUpdate(ProtocolWaveletUpdate waveletUpdate) { LOG.info("Received update " + waveletUpdate); WaveletName waveletName = getWaveletName(waveletUpdate); // Apply operations to the wavelet. List<WaveletEventData> events = Lists.newArrayList(); ObservableWaveletData wavelet; if (waveletUpdate.hasSnapshot()) { List<WaveletEventData> snapshotEvents = Lists.newArrayList(); wavelet = receiveSnapshot(waveletUpdate, snapshotEvents); events.addAll(snapshotEvents); } else if (!waveletUpdate.getAppliedDeltaList().isEmpty()) { List<WaveletEventData> deltaEvents = Lists.newArrayList(); wavelet = receiveDeltas(waveletUpdate, events); events.addAll(deltaEvents); } else if (waveletUpdate.hasMarker()) { // Don't do anything at the moment. return; } else { // TODO(ljvderijk): Might be simplified, might be removed when protocol // update is complete. ClientWaveView wave = getExistingWaveView(waveletName); wavelet = wave.getWavelet(waveletName.waveletId); } ClientWaveView wave = getExistingWaveView(waveletName); if (waveletUpdate.hasResultingVersion()) { wave.setWaveletVersion(waveletName.waveletId, CoreWaveletOperationSerializer.deserialize(waveletUpdate.getResultingVersion())); } if (wavelet == null) { // The update is not associated with any wavelet (e.g. a channel id). return; } // If we have been removed from this wavelet then remove the data too, // since if we're re-added then we will get a fresh snapshot or deltas // from version 0, not the latest version we've seen. if (!wavelet.getParticipants().contains(getUserId())) { wave.removeWavelet(waveletName.waveletId); } // If it was an update to the index wave, might need to open/close some // more waves. if (IndexWave.isIndexWave(wave.getWaveId())) { syncWithIndexWave(wave); } if (waveletUpdate.hasCommitNotice()) { events.add(new WaveletEventData( wavelet, CoreWaveletOperationSerializer.deserialize(waveletUpdate.getCommitNotice()))); } events.add(new WaveletEventData(wavelet)); // Push all events to the eventQueue. for (WaveletEventData waveletEventData : events) { eventQueue.offer(waveletEventData); } } /** * Handles the receiving of a snapshot contained in a * {@link ProtocolWaveletUpdate}. * * The wavelet must not exist yet in the {@link ClientWaveView}. * * @param waveletUpdate the update proto to handle. * @param events the list where this method keeps track of the events it has * triggered. * @return the deserialized {@link ObservableWaveletData} stored in the * snapshot. */ private ObservableWaveletData receiveSnapshot( ProtocolWaveletUpdate waveletUpdate, List<WaveletEventData> events) { Preconditions.checkArgument(waveletUpdate.hasResultingVersion()); WaveletName waveletName = getWaveletName(waveletUpdate); ClientWaveView wave = getExistingWaveView(waveletName); ObservableWaveletData wavelet = wave.getWavelet(waveletName.waveletId); Preconditions.checkState(wavelet == null, "Wavelet must be null"); WaveletSnapshot snapshot = waveletUpdate.getSnapshot(); try { wavelet = CoreWaveletOperationSerializer.deserializeSnapshot( snapshot, waveletUpdate.getResultingVersion(), waveletName); wave.addWavelet( wavelet, CoreWaveletOperationSerializer.deserialize(waveletUpdate.getResultingVersion())); // Push the snapshot as operations to the event list SnapshotOperationContextFactory contextFactory = new SnapshotOperationContextFactory(wavelet); for (ParticipantId participant : wavelet.getParticipants()) { events.add(new WaveletEventData( wavelet.getCreator().getAddress(), wavelet, new AddParticipant(contextFactory.createContext(), participant))); } for (String documentId : wavelet.getDocumentIds()) { BlipData doc = wavelet.getDocument(documentId); DocOp docOp = doc.getContent().asOperation(); events.add( new WaveletEventData(wavelet.getCreator().getAddress(), wavelet, new WaveletBlipOperation(documentId, new BlipContentOperation(contextFactory.createContext(), docOp)))); } } catch (OperationException e) { // It should be okay (if cheeky) for the client to just ignore failed // ops. In any case, this should never happen if our server is // behaving correctly. LOG.severe("OperationException when creating snapshot ", e); } return wavelet; } /** * Handles the deltas contained in a {@link ProtocolWaveletUpdate}. * * @param waveletUpdate the update proto to handle. * @param events the list where this method keeps track of the events it has * triggered. * @return the {@link ObservableWaveletData} updated with the deltas present * in the {@link ProtocolWaveletUpdate}. */ private ObservableWaveletData receiveDeltas( ProtocolWaveletUpdate waveletUpdate, List<WaveletEventData> events) { Preconditions.checkArgument(waveletUpdate.hasResultingVersion()); Preconditions.checkArgument(!waveletUpdate.getAppliedDeltaList().isEmpty()); WaveletName waveletName = getWaveletName(waveletUpdate); ClientWaveView wave = getExistingWaveView(waveletName); ObservableWaveletData wavelet = wave.getWavelet(waveletName.waveletId); // TODO(ljvderijk): enable when protocol is fixed so that we always get a // snapshot first. // Preconditions.checkState(wavelet != null, "Wavelet must be present!"); for (ProtocolWaveletDelta protobufDelta : waveletUpdate.getAppliedDeltaList()) { // TODO(anorth): Plumb timestamp, hash to here when the protocol is fixed. HashedVersion dummyEndVersion = HashedVersion.unsigned( protobufDelta.getHashedVersion().getVersion() + protobufDelta.getOperationCount()); long dummyTimestamp = Constants.NO_TIMESTAMP; TransformedWaveletDelta delta = CoreWaveletOperationSerializer.deserialize(protobufDelta, dummyEndVersion, dummyTimestamp); if (wavelet == null) { // TODO(ljvderijk): This should never happen, but it currently does. // Snapshot should be received first. // Instantiate a new wavelet HashedVersion zero = hashedVersionFactory.createVersionZero(waveletName); wavelet = WaveletDataUtil.createEmptyWavelet(waveletName, delta.getAuthor(), zero, delta.getApplicationTimestamp()); wave.addWavelet(wavelet, zero); } Preconditions.checkState(delta.getAppliedAtVersion() == wavelet.getVersion(), "Delta at version %s doesn't apply to wavelet %s at %s", delta.getAppliedAtVersion(), waveletName, wavelet.getVersion()); try { WaveletDataUtil.applyWaveletDelta(delta, wavelet); } catch (OperationException e) { LOG.severe("Operations failed to apply", e); } // All operations were successful so put them in the list of events. for (WaveletOperation op : delta) { events.add(new WaveletEventData(delta.getAuthor().getAddress(), wavelet, op)); } } return wavelet; } /** * Creates a new, empty wave view and stores it in {@code waves}. * @param waveId the new wave id * @return the new wave's {@link ClientWaveView} */ private ClientWaveView createWave(WaveId waveId) { ClientWaveView wave = new ClientWaveView(hashedVersionFactory, waveId); waves.put(waveId, wave); return wave; } /** * Returns the {@link WaveletName} stored in the * {@link ProtocolWaveletUpdate}. * * @param waveletUpdate the update containing the name * @throws IllegalArgumentException if the data in the * {@link ProtocolWaveletUpdate} could not be decoded. */ private WaveletName getWaveletName(ProtocolWaveletUpdate waveletUpdate) { try { return ModernIdSerialiser.INSTANCE.deserialiseWaveletName(waveletUpdate.getWaveletName()); } catch (InvalidIdException e) { throw new IllegalArgumentException(e); } } /** * Returns a {@link ClientWaveView} for the given {@link WaveletName}. The * {@link ClientWaveView} for this wave must already exist. * * @param waveletName contains the waveId of the {@link ClientWaveView} to be * returned. * @throws IllegalArgumentException if the {@link ClientWaveView} does not * exist. */ private ClientWaveView getExistingWaveView(WaveletName waveletName) { ClientWaveView wave = waves.get(waveletName.waveId); Preconditions.checkArgument( wave != null, "Request for ClientWaveView with absent waveId " + waveletName.waveId); return wave; } /** * Synchronize with the index wave by opening any waves that appear in the index but that we * don't have an RPC open request to. * * @param indexWave to synchronize with */ private void syncWithIndexWave(ClientWaveView indexWave) { List<IndexEntry> indexEntries = IndexWave.getIndexEntries(indexWave.getWavelets()); for (IndexEntry indexEntry : indexEntries) { if (!waveControllers.containsKey(indexEntry.getWaveId())) { WaveId waveId = indexEntry.getWaveId(); openWave(waveId, ClientUtils.getConversationRootId(waveId).getId()); } } } /** * Notify all wavelet operation listeners of a wavelet operation. * * @param author the author who caused the operation * @param wavelet operated on * @param op the operation */ private void notifyWaveletOperationListeners(String author, WaveletData wavelet, WaveletOperation op) { for (WaveletOperationListener listener : waveletOperationListeners) { try { if (op instanceof WaveletBlipOperation) { WaveletBlipOperation waveletBlipOp = (WaveletBlipOperation) op; listener.waveletDocumentUpdated(author, wavelet, waveletBlipOp.getBlipId(), waveletBlipOp.getBlipOp()); } else if (op instanceof AddParticipant) { listener.participantAdded(author, wavelet, ((AddParticipant) op).getParticipantId()); } else if (op instanceof RemoveParticipant) { listener.participantRemoved(author, wavelet, ((RemoveParticipant) op).getParticipantId()); } else if (op instanceof NoOp) { listener.noOp(author, wavelet); } } catch (RuntimeException e) { LOG.severe("RuntimeException for listener " + listener, e); } } } /** * @return the {@link ParticipantId} id of the user */ public ParticipantId getUserId() { return userId; } /** * @return the id generator which generates wave, wavelet, and document ids */ public IdGenerator getIdGenerator() { return idGenerator; } /** * Returns the set of wave IDs of the currently open waves, optionally including the index wave. * * @param includeIndexWave should the index wave be included in the returned set? * @return the set of wave Ids of the open waves. */ public Set<WaveId> getOpenWaveIds(boolean includeIndexWave) { Set<WaveId> waveIds = waves.keySet(); if (!includeIndexWave) { waveIds = Sets.newHashSet(waveIds); waveIds.remove(INDEX_WAVE_ID); } return Collections.unmodifiableSet(waveIds); } /** Creates a context for a client-generated operation. */ public WaveletOperationContext createOperationContext() { return contextFactory.createContext(); } /** * @return the list of currently registered operation listeners */ @VisibleForTesting public Set<WaveletOperationListener> getListeners() { return Collections.unmodifiableSet(waveletOperationListeners); } /** * Add a {@link WaveletOperationListener} to be notified whenever a wave is updated. * * @param listener new listener */ public void addWaveletOperationListener(WaveletOperationListener listener) { waveletOperationListeners.add(listener); } /** * @param listener listener to be removed */ public void removeWaveletOperationListener(WaveletOperationListener listener) { waveletOperationListeners.remove(listener); } /** * Waits until all accumulated events have been processed in the events thread. */ @VisibleForTesting public void waitForAccumulatedEventsToProcess() { while (!eventQueue.isEmpty()) { Thread.yield(); } } private WaveletDelta makeDelta(WaveletName wavelet, WaveletOperation... ops) { ClientWaveView wave = waves.get(wavelet.waveId); HashedVersion targetVersion = wave.getWaveletVersion(wavelet.waveletId); return new WaveletDelta(getUserId(), targetVersion, Arrays.asList(ops)); } }