// Copyright 2010 Google Inc. All Rights Reserved. package org.waveprotocol.box.server.frontend; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.MapMaker; import com.google.common.collect.Sets; import org.waveprotocol.box.common.DeltaSequence; import org.waveprotocol.wave.model.id.IdFilter; 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.wave.TransformedWaveletDelta; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.util.logging.Log; import java.util.Collection; import java.util.List; import java.util.concurrent.ConcurrentMap; /** * A client's subscription to a wave view. * * @author anorth@google.com (Alex North) */ final class WaveViewSubscription { /** * State of a wavelet endpoint. */ private static final class WaveletChannelState { /** * Resulting versions of deltas submitted on this wavelet for which * the outbound delta has not yet been seen. */ public final Collection<Long> submittedEndVersions = Sets.newHashSet(); /** * Resulting version of the most recent outbound delta. */ public HashedVersion lastVersion = null; /** * Whether a submit request is awaiting a response. */ public boolean hasOutstandingSubmit = false; /** * Outbound deltas held back while a submit is in-flight. */ public List<TransformedWaveletDelta> heldBackDeltas = Lists.newLinkedList(); } private static final Log LOG = Log.get(WaveViewSubscription.class); private final WaveId waveId; private final IdFilter waveletIdFilter; private final ClientFrontend.OpenListener openListener; private final String channelId; private final ConcurrentMap<WaveletId, WaveletChannelState> channels = new MapMaker().makeComputingMap(new Function<WaveletId, WaveletChannelState>() { @Override public WaveletChannelState apply(WaveletId id) { return new WaveletChannelState(); } }); public WaveViewSubscription(WaveId waveId, IdFilter waveletIdFilter, String channelId, ClientFrontend.OpenListener openListener) { Preconditions.checkNotNull(waveId, "null wave id"); Preconditions.checkNotNull(waveletIdFilter, "null filter"); Preconditions.checkNotNull(openListener, "null listener"); Preconditions.checkNotNull(channelId, "null channel id"); this.waveId = waveId; this.waveletIdFilter = waveletIdFilter; this.channelId = channelId; this.openListener = openListener; } public WaveId getWaveId() { return waveId; } public ClientFrontend.OpenListener getOpenListener() { return openListener; } public String getChannelId() { return channelId; } /** * Checks whether the subscription includes a wavelet. */ public boolean includes(WaveletId waveletId) { return IdFilter.accepts(waveletIdFilter, waveletId); } /** This client sent a submit request */ public synchronized void submitRequest(WaveletName waveletName) { // A given client can only have one outstanding submit per wavelet. WaveletChannelState state = channels.get(waveletName.waveletId); Preconditions.checkState(!state.hasOutstandingSubmit, "Received overlapping submit requests to subscription %s", this); LOG.info("Submit oustandinding on channel " + channelId); state.hasOutstandingSubmit = true; } /** * A submit response for the given wavelet and version has been sent to this * client. */ public synchronized void submitResponse(WaveletName waveletName, HashedVersion version) { Preconditions.checkNotNull(version, "Null delta application version"); WaveletId waveletId = waveletName.waveletId; WaveletChannelState state = channels.get(waveletId); Preconditions.checkState(state.hasOutstandingSubmit); state.submittedEndVersions.add(version.getVersion()); state.hasOutstandingSubmit = false; LOG.info("Submit resolved on channel " + channelId); // Forward any queued deltas. List<TransformedWaveletDelta> filteredDeltas = filterOwnDeltas(state.heldBackDeltas, state); if (!filteredDeltas.isEmpty()) { sendUpdate(waveletName, filteredDeltas, null); } state.heldBackDeltas.clear(); } /** * Sends deltas for this subscription (if appropriate). * * If the update contains a delta for a wavelet where the delta is actually * from this client, the delta is dropped. If there's an outstanding submit * request the delta is queued until the submit finishes. */ public synchronized void onUpdate(WaveletName waveletName, DeltaSequence deltas) { Preconditions.checkArgument(!deltas.isEmpty()); WaveletChannelState state = channels.get(waveletName.waveletId); checkUpdateVersion(waveletName, deltas, state); state.lastVersion = deltas.getEndVersion(); if (state.hasOutstandingSubmit) { state.heldBackDeltas.addAll(deltas); } else { List<TransformedWaveletDelta> filteredDeltas = filterOwnDeltas(deltas, state); if (!filteredDeltas.isEmpty()) { sendUpdate(waveletName, filteredDeltas, null); } } } /** * Filters any deltas sent by this client from a list of received deltas. * * @param deltas received deltas * @param state channel state * @return deltas, if none are from this client, or a copy with own client's * deltas removed */ private List<TransformedWaveletDelta> filterOwnDeltas(List<TransformedWaveletDelta> deltas, WaveletChannelState state) { List<TransformedWaveletDelta> filteredDeltas = deltas; if (!state.submittedEndVersions.isEmpty()) { filteredDeltas = Lists.newArrayList(); for (TransformedWaveletDelta delta : deltas) { long deltaEndVersion = delta.getResultingVersion().getVersion(); if (!state.submittedEndVersions.remove(deltaEndVersion)) { filteredDeltas.add(delta); } } } return filteredDeltas; } /** * Sends a commit notice for this subscription. */ public synchronized void onCommit(WaveletName waveletName, HashedVersion committedVersion) { sendUpdate(waveletName, ImmutableList.<TransformedWaveletDelta>of(), committedVersion); } /** * Sends an update to the client. */ private void sendUpdate(WaveletName waveletName, List<TransformedWaveletDelta> deltas, HashedVersion committedVersion) { // Channel id needs to be sent with every message until views can be // closed, see bug 128. openListener.onUpdate(waveletName, null, deltas, committedVersion, null, channelId); } /** * Checks the update targets the next expected version. */ private void checkUpdateVersion(WaveletName waveletName, DeltaSequence deltas, WaveletChannelState state) { if (state.lastVersion != null) { long expectedVersion = state.lastVersion.getVersion(); long targetVersion = deltas.getStartVersion(); Preconditions.checkState(targetVersion == expectedVersion, "Subscription expected delta for %s targeting %s, was %s", waveletName, expectedVersion, targetVersion); } } @Override public String toString() { return "[WaveViewSubscription wave: " + waveId + ", channel: " + channelId + "]"; } }