/** * 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.wave.examples.fedone.waveserver; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.waveprotocol.wave.examples.fedone.common.HashedVersion; import org.waveprotocol.wave.examples.fedone.common.WaveletOperationSerializer; import org.waveprotocol.wave.examples.fedone.waveserver.ClientFrontend.OpenListener; import org.waveprotocol.wave.federation.Proto.ProtocolHashedVersion; import org.waveprotocol.wave.federation.Proto.ProtocolWaveletDelta; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.id.WaveletName; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; /** * Per-participant state (wavelets the user is subscribed to, and * active subscription channels). * * This class has no special knowledge of the Index Wave and treats * it no different from other waves. * * None of the methods in this class accept null arguments. * * Note: Because this class has several maps keyed by waveId or waveletName, * we could potentially replace it with a WaveParticipant class that manages * all wavelets of a single participant on a particular wave. * * */ final class UserManager { /** * Result of a {@link #subscribe(WaveId, Set, OpenListener)} request. * Stores a set of waveletIdPrefixes and the listener to inform of changes * to any wavelet matching one of the prefixes. * * Each subscription belongs to a particular waveId (the waveId is not * stored as part of the subscription). */ private static class Subscription { private final List<String> waveletIdPrefixes; private final OpenListener openListener; public Subscription(Set<String> waveletIdPrefixes, OpenListener openListener) { Preconditions.checkNotNull(waveletIdPrefixes); Preconditions.checkNotNull(openListener); this.waveletIdPrefixes = ImmutableList.copyOf(waveletIdPrefixes); this.openListener = openListener; } /** * Returns true if the listener should be informed of changes to the * specified wavelet. */ boolean matches(WaveletId waveletId) { // TODO: Could be made more efficient with a trie List<OpenListener> result = Lists.newArrayList(); String waveletIdStr = waveletId.serialise(); for (String prefix : waveletIdPrefixes) { if (waveletIdStr.startsWith(prefix)) { return true; } } return false; } } private final ListMultimap<WaveId, Subscription> subscriptions; /** Wavelets that this user is a participant of. */ private final HashMultimap<WaveId, WaveletId> waveletIds; /** * The current version of the specified wavelet, as per the deltas received * for it so far. This contains a waveletName if and only if we are a * participant on that wavelet. Invariant: At the start and end of * onUpdate(), the listeners from all active subscriptions have received * the deltas up to this version. */ private final Map<WaveletName, ProtocolHashedVersion> currentVersion; UserManager() { this.subscriptions = LinkedListMultimap.create(); this.waveletIds = HashMultimap.create(); this.currentVersion = Maps.newHashMap(); } /** Whether this user is a participant on the specified wavelet. */ synchronized boolean isParticipant(WaveletName waveletName) { return currentVersion.containsKey(waveletName); // Alternatively, could do: //return waveletIds.get(waveletName.waveId).contains(waveletName.waveletId); } /** The listeners interested in the specified wavelet. */ @VisibleForTesting synchronized List<OpenListener> matchSubscriptions(WaveletName waveletName) { List<OpenListener> result = Lists.newArrayList(); for (Subscription subscription : subscriptions.get(waveletName.waveId)) { if (subscription.matches(waveletName.waveletId)) { result.add(subscription.openListener); } } return result; } synchronized ProtocolHashedVersion getWaveletVersion(WaveletName waveletName) { Preconditions.checkArgument(isParticipant(waveletName), "Not a participant of " + waveletName); return currentVersion.get(waveletName); } /** * Receives additional deltas for the specified wavelet, of which we * must be a participant. * * @throws NullPointerException if waveletName or deltas is null * @throws IllegalStateException if we're not a participant of the wavelet * @throws IllegalArgumentException if the version numbering of the deltas is * not properly contiguous from another or from deltas we previously * received for this delta. */ synchronized void onUpdate(WaveletName waveletName, DeltaSequence deltas) { Preconditions.checkNotNull(waveletName); if (deltas.isEmpty()) { return; } if (!isParticipant(waveletName)) { throw new IllegalStateException("Not a participant of wavelet " + waveletName); } long version = deltas.getStartVersion().getVersion(); long expectedVersion = currentVersion.get(waveletName).getVersion(); Preconditions.checkArgument(expectedVersion == version, "Expected startVersion " + expectedVersion + ", got " + version); currentVersion.put(waveletName, deltas.getEndVersion()); List<OpenListener> listeners = matchSubscriptions(waveletName); for (OpenListener listener : listeners) { try { listener.onUpdate(waveletName, null, deltas, deltas.getEndVersion(), null); } catch (IllegalStateException e) { // TODO: remove the listener } } } /** * Receives notification that the specified wavelet has been committed at the * specified version. * * @throws NullPointerException if waveletName or version is null * @throws IllegalStateException if we're not a participant of the wavelet */ void onCommit(WaveletName waveletName, ProtocolHashedVersion version) { Preconditions.checkNotNull(waveletName); Preconditions.checkNotNull(version); if (!isParticipant(waveletName)) { throw new IllegalStateException("Not a participant of wavelet " + waveletName); } List<ProtocolWaveletDelta> emptyList = Collections.emptyList(); List<OpenListener> listeners = matchSubscriptions(waveletName); for (OpenListener listener : listeners) { listener.onUpdate(waveletName, null, emptyList, null, version); } } /** * Subscribes to updates from the specified waveId and waveletIds with * the specified prefixes, using the specified listener to receive updates. * * @return All subscribed waveletIds that are of interest to the listener. * The caller must ensure that the listener gets deltas 0 (inclusive) * through getWaveletVersion(WaveletName.of(waveId, waveletId)) * exclusive before onUpdate() is next called on this UserManager. * This is to ensure that the listener catches up with all other * listeners on those wavelets before further deltas are broadcast * to all listeners. */ synchronized Set<WaveletId> subscribe( WaveId waveId, Set<String> waveletIdPrefixes, OpenListener listener) { Preconditions.checkNotNull(waveId); Subscription subscription = new Subscription(waveletIdPrefixes, listener); subscriptions.put(waveId, subscription); Set<WaveletId> result = Sets.newHashSet(); for (WaveletId waveletId : getWaveletIds(waveId)) { if (subscription.matches(waveletId)) { result.add(waveletId); } } return result; } /** * Notifies that the user has been added to the specified wavelet. */ synchronized void addWavelet(WaveletName waveletName) { if (isParticipant(waveletName)) { throw new IllegalStateException("Already a participant of " + waveletName); } waveletIds.get(waveletName.waveId).add(waveletName.waveletId); currentVersion.put(waveletName, WaveletOperationSerializer.serialize(HashedVersion.versionZero(waveletName))); } /** * Notifies that the user has been removed from the specified wavelet * @param waveletName we were removed from */ synchronized void removeWavelet(WaveletName waveletName) { if (!isParticipant(waveletName)) { throw new IllegalStateException("Not a participant of " + waveletName); } waveletIds.get(waveletName.waveId).remove(waveletName.waveletId); currentVersion.remove(waveletName); } synchronized Set<WaveletId> getWaveletIds(WaveId waveId) { Preconditions.checkNotNull(waveId); return Collections.unmodifiableSet(waveletIds.get(waveId)); } }