/** * 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.model.supplement; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.CopyOnWriteSet; import org.waveprotocol.wave.model.util.Preconditions; import org.waveprotocol.wave.model.util.ReadableStringMap; import org.waveprotocol.wave.model.util.StringMap; import org.waveprotocol.wave.model.version.HashedVersion; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Canonical implementation of a {@link PrimitiveSupplement}. * */ public final class PrimitiveSupplementImpl implements ObservablePrimitiveSupplement { /** * Holds thread state relevant to a particular wavelet. */ private final static class WaveletThreadState { /** Collapsed state for threads */ private final Map<String, ThreadState> threadCollapsed = new HashMap<String, ThreadState>(); void setThreadState(String tid, ThreadState collapsed) { if (collapsed == null) { threadCollapsed.remove(tid); } else { threadCollapsed.put(tid, collapsed); } } ThreadState getThreadState(String tid) { return threadCollapsed.get(tid); } public Iterable<String> getStatefulThreads() { return threadCollapsed.keySet(); } } /** * Holds read state relevant to a particular wavelet. */ private final static class WaveletReadState { /** Blip Last-read versions. */ private final Map<String, Integer> blipVersions = new HashMap<String, Integer>(); /** Wavelet override last-read version. Null means unspecified. */ private Integer waveletVersion; /** Participants-set last-read version. Null means unspecified. */ private Integer participantVersion; /** Tags document last-read version. Null means unspecified. */ private Integer tagsVersion; WaveletReadState() { } void setBlipVersion(String id, int version) { // Monotonic guard if (!blipVersions.containsKey(id) || blipVersions.get(id) < version) { blipVersions.put(id, version); } } void clearBlipVersion(String id) { blipVersions.remove(id); } void setParticipantVersion(int version) { // Monotonic guard if (this.participantVersion == null || this.participantVersion < version) { this.participantVersion = version; } } void setTagsVersion(int version) { // Monotonic guard if (this.tagsVersion == null || this.tagsVersion < version) { this.tagsVersion = version; } } void setWaveletVersion(int version) { // Monotonic guard if (this.waveletVersion == null || this.waveletVersion < participantVersion) { this.waveletVersion = version; } } Integer getBlipVersion(String id) { return blipVersions.get(id); } Integer getParticipantVersion() { return participantVersion; } Integer getTagsVersion() { return tagsVersion; } Integer getWaveletVersion() { return waveletVersion; } Iterable<String> getReadBlips() { return blipVersions.keySet(); } } /** Maps wavelet ids to their read state. */ private final Map<WaveletId, WaveletReadState> waveletReadStates = new HashMap<WaveletId, WaveletReadState>(); /** Maps wavelet ids to their collapsed state */ private final Map<WaveletId, WaveletThreadState> waveletThreadStates = new HashMap<WaveletId, WaveletThreadState>(); /** Folder allocations. */ private final Set<Integer> folders = new HashSet<Integer>(); /** Maps wavelet ids to their read state. */ private final Map<WaveletId, Integer> waveletArchiveVersions = new HashMap<WaveletId, Integer>(); /** Maps wavelet ids to their last seen version and signature. */ private final Map<WaveletId, HashedVersion> waveletSeenVersions = CollectionUtils.newHashMap(); /** All the wanted evaluations known to this supplement. */ private final Set<WantedEvaluation> wantedEvaluations = new HashSet<WantedEvaluation>(); /** Listeners attached to this supplement. */ private final CopyOnWriteSet<Listener> listeners = CopyOnWriteSet.create(); /** Optional specification of follow state. */ private Boolean follow; /** Optional specification of notification state. */ private boolean pendingNotification = false; /** Maps wavelet ids to their notified version. */ private final Map<WaveletId, Integer> waveletNotifiedVersions = new HashMap<WaveletId, Integer>(); /** Maps gadget ids to their state maps. */ private final StringMap<StringMap<String>> gadgetStates = CollectionUtils.<StringMap<String>>createStringMap(); private static int unboxVersion (Integer version) { return version == null ? NO_VERSION : version; } /** * Creates an empty primitive supplement. */ public PrimitiveSupplementImpl() { } /** * Creates a primitive supplement as a copy of another. * * @param other primitive supplement to copy */ public PrimitiveSupplementImpl(PrimitiveSupplement other) { for (WaveletId wavelet : other.getReadWavelets()) { WaveletReadState readState = getWaveletReadState(wavelet); if (other.getLastReadWaveletVersion(wavelet) != NO_VERSION) { readState.setWaveletVersion(other.getLastReadWaveletVersion(wavelet)); } if (other.getLastReadParticipantsVersion(wavelet) != NO_VERSION) { readState.setParticipantVersion(other.getLastReadParticipantsVersion(wavelet)); } if (other.getLastReadTagsVersion(wavelet) != NO_VERSION) { readState.setTagsVersion(other.getLastReadTagsVersion(wavelet)); } for (String blip : other.getReadBlips(wavelet)) { readState.setBlipVersion(blip, other.getLastReadBlipVersion(wavelet, blip)); } } for (WaveletId wavelet : other.getWaveletsWithThreadState()) { WaveletThreadState threadState = getWaveletThreadState(wavelet); for (String thread : other.getStatefulThreads(wavelet)) { threadState.setThreadState(thread, other.getThreadState(wavelet, thread)); } } for (WaveletId waveletId : other.getArchiveWavelets()) { waveletArchiveVersions.put(waveletId, other.getArchiveWaveletVersion(waveletId)); } for (int folder : other.getFolders()) { folders.add(folder); } for (WantedEvaluation evaluation: other.getWantedEvaluations()) { addWantedEvaluation(evaluation); } for (WaveletId waveletId : other.getSeenWavelets()) { waveletSeenVersions.put(waveletId, other.getSeenVersion(waveletId)); } for (WaveletId waveletId : other.getNotifiedWavelets()) { waveletNotifiedVersions.put(waveletId, other.getNotifiedVersion(waveletId)); } follow = other.getFollowed(); pendingNotification = other.getPendingNotification(); } /** * Gets a per-wavelet read-state tracker for a wavelet id, creating one if it * does not already exist * * @param id wavelet id * @return the read-state tracker for a wavelet (never null). */ private WaveletReadState getWaveletReadState(WaveletId id) { WaveletReadState s = waveletReadStates.get(id); if (s == null) { s = new WaveletReadState(); waveletReadStates.put(id, s); } return s; } /** * Gets a per-wavelet thread-state tracker for a wavelet id, creating one if it * does not already exist * * @param id wavelet id * @return the thread-state tracker for a wavelet (never null). */ private WaveletThreadState getWaveletThreadState(WaveletId id) { WaveletThreadState s = waveletThreadStates.get(id); if (s == null) { s = new WaveletThreadState(); waveletThreadStates.put(id, s); } return s; } @Override public int getLastReadBlipVersion(WaveletId waveletId, String blipId) { Integer version = waveletReadStates.containsKey(waveletId) ? waveletReadStates.get(waveletId).getBlipVersion(blipId) : null; return unboxVersion(version); } @Override public int getLastReadParticipantsVersion(WaveletId waveletId) { Integer version = waveletReadStates.containsKey(waveletId) ? waveletReadStates.get(waveletId).getParticipantVersion() : null; return unboxVersion(version); } @Override public int getLastReadTagsVersion(WaveletId waveletId) { Integer version = waveletReadStates.containsKey(waveletId) ? waveletReadStates.get(waveletId).getTagsVersion() : null; return unboxVersion(version); } @Override public int getLastReadWaveletVersion(WaveletId waveletId) { Integer version = waveletReadStates.containsKey(waveletId) ? waveletReadStates.get(waveletId).getWaveletVersion() : null; return unboxVersion(version); } @Override public Iterable<WaveletId> getReadWavelets() { return waveletReadStates.keySet(); } @Override public Iterable<String> getReadBlips(WaveletId waveletId) { if (waveletReadStates.containsKey(waveletId)) { return waveletReadStates.get(waveletId).getReadBlips(); } else { return Collections.<String>emptyList(); } } @Override public void setLastReadBlipVersion(WaveletId waveletId, String blipId, int version) { WaveletReadState state = getWaveletReadState(waveletId); int oldVersion = unboxVersion(state.getBlipVersion(blipId)); state.setBlipVersion(blipId, version); if (version != oldVersion) { for (Listener listener : listeners) { listener.onLastReadBlipVersionChanged(waveletId, blipId, oldVersion, version); } } } @Override public void setLastReadParticipantsVersion(WaveletId waveletId, int version) { WaveletReadState states = getWaveletReadState(waveletId); int oldVersion= unboxVersion(states.getParticipantVersion()); states.setParticipantVersion(version); if (version != oldVersion) { for (Listener listener : listeners) { listener.onLastReadParticipantsVersionChanged(waveletId, oldVersion, version); } } } @Override public void setLastReadTagsVersion(WaveletId waveletId, int version) { WaveletReadState states = getWaveletReadState(waveletId); int oldVersion = unboxVersion(states.getTagsVersion()); states.setTagsVersion(version); if (version != oldVersion) { for (Listener listener : listeners) { listener.onLastReadTagsVersionChanged(waveletId, oldVersion, version); } } } @Override public void setLastReadWaveletVersion(WaveletId waveletId, int version) { WaveletReadState states = getWaveletReadState(waveletId); int oldVersion = unboxVersion(states.getWaveletVersion()); states.setWaveletVersion(version); if (version != oldVersion) { for (Listener listener : listeners) { listener.onLastReadWaveletVersionChanged(waveletId, oldVersion, version); } } } @Override public void clearReadState() { waveletReadStates.clear(); } @Override public void clearBlipReadState(WaveletId waveletId, String blipId) { WaveletReadState readState = getWaveletReadState(waveletId); int oldVersion = unboxVersion(readState.getBlipVersion(blipId)); readState.clearBlipVersion(blipId); if (oldVersion != NO_VERSION) { for (Listener listener :listeners ) { listener.onLastReadBlipVersionChanged(waveletId, blipId, oldVersion, NO_VERSION); } } } @Override public ThreadState getThreadState(WaveletId waveletId, String threadId) { return getWaveletThreadState(waveletId).getThreadState(threadId); } @Override public void setThreadState(WaveletId waveletId, String threadId, ThreadState newState) { WaveletThreadState threads = getWaveletThreadState(waveletId); ThreadState oldState = threads.getThreadState(threadId); threads.setThreadState(threadId, newState); if (newState != oldState) { for (Listener listener : listeners) { listener.onThreadStateChanged(waveletId, threadId, oldState, newState); } } } @Override public Iterable<String> getStatefulThreads(WaveletId waveletId) { return getWaveletThreadState(waveletId).getStatefulThreads(); } @Override public Iterable<WaveletId> getWaveletsWithThreadState() { return waveletThreadStates.keySet(); } @Override public void addFolder(int id) { if (folders.add(id)) { for (Listener listener : listeners) { listener.onFolderAdded(id); } } } @Override public void removeAllFolders() { for (Integer folder : CollectionUtils.newHashSet(folders)) { removeFolder(folder); } } @Override public void removeFolder(int id) { if (folders.remove(id)) { for (Listener listener : listeners) { listener.onFolderRemoved(id); } } } @Override public Set<Integer> getFolders() { return folders; } @Override public boolean isInFolder(int id) { return folders.contains(id); } @Override public void follow() { if (follow != Boolean.TRUE) { follow = true; for (Listener listener : listeners) { listener.onFollowed(); } } } @Override public void unfollow() { if (follow != Boolean.FALSE) { follow = false; for (Listener listener : listeners) { listener.onUnfollowed(); } } } @Override public void clearFollow() { if (follow != null) { follow = null; for (Listener listener : listeners) { listener.onFollowCleared(); } } } @Override public Boolean getFollowed() { return follow; } @Override public int getArchiveWaveletVersion(WaveletId waveletId) { if (waveletArchiveVersions.containsKey(waveletId)) { return unboxVersion(waveletArchiveVersions.get(waveletId)); } else { return NO_VERSION; } } @Override public void archiveAtVersion(WaveletId waveletId, int waveletVersion) { int oldVersion = unboxVersion(waveletArchiveVersions.get(waveletId)); waveletArchiveVersions.put(waveletId, waveletVersion); for (Listener listener : listeners) { listener.onArchiveVersionChanged(waveletId, oldVersion, waveletVersion); } } @Override public void clearArchiveState() { waveletArchiveVersions.clear(); for (Listener listener : listeners) { listener.onArchiveClearChanged(false, true); } } @Override public Iterable<WaveletId> getArchiveWavelets() { return waveletArchiveVersions.keySet(); } @Override public HashedVersion getSeenVersion(WaveletId waveletId) { HashedVersion seenSignature = waveletSeenVersions.get(waveletId); if (null == seenSignature) { return HashedVersion.unsigned(0); } return seenSignature; } @Override public void setSeenVersion(WaveletId waveletId, HashedVersion signature) { waveletSeenVersions.put(waveletId, signature); } @Override public void clearSeenVersion(WaveletId waveletId) { waveletSeenVersions.remove(waveletId); } @Override public Set<WaveletId> getSeenWavelets() { return Collections.unmodifiableSet(waveletSeenVersions.keySet()); } @Override public Set<WantedEvaluation> getWantedEvaluations() { return CollectionUtils.immutableSet(wantedEvaluations); } @Override public void addWantedEvaluation(WantedEvaluation evaluation) { wantedEvaluations.add(evaluation); for (Listener listener : listeners) { listener.onWantedEvaluationsChanged(evaluation.getWaveletId()); } } @Override public boolean getPendingNotification() { return pendingNotification; } @Override public int getNotifiedVersion(WaveletId waveletId) { Integer version = waveletNotifiedVersions.containsKey(waveletId) ? waveletNotifiedVersions.get(waveletId) : null; return unboxVersion(version); } @Override public Set<WaveletId> getNotifiedWavelets() { return Collections.unmodifiableSet(waveletNotifiedVersions.keySet()); } @Override public void setNotifiedVersion(WaveletId waveletId, int version) { waveletNotifiedVersions.put(waveletId, version); } @Override public void clearPendingNotification() { pendingNotification = false; } @Override public void addListener(Listener listener) { listeners.add(listener); } @Override public void removeListener(Listener listener) { listeners.remove(listener); } @Override public ReadableStringMap<String> getGadgetState(String gadgetId) { if (!gadgetStates.containsKey(gadgetId)) { gadgetStates.put(gadgetId, CollectionUtils.<String> createStringMap()); } return gadgetStates.get(gadgetId); } @Override public void setGadgetState(String gadgetId, String key, String value) { Preconditions.checkNotNull(key, "Private gadget state key is null."); StringMap<String> gadgetState; if (!gadgetStates.containsKey(gadgetId)) { gadgetState = CollectionUtils.<String> createStringMap(); gadgetStates.put(gadgetId, gadgetState); } else { gadgetState = gadgetStates.get(gadgetId); } if (value != null) { gadgetState.put(key, value); } else { gadgetState.remove(key); } } }