/** * 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.server.frontend; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.MapMaker; import com.google.common.collect.Sets; import org.waveprotocol.box.common.CommonConstants; import org.waveprotocol.box.common.DeltaSequence; import org.waveprotocol.box.common.ExceptionalIterator; import org.waveprotocol.box.common.IndexWave; import org.waveprotocol.box.common.Snippets; import org.waveprotocol.box.common.comms.WaveClientRpc; import org.waveprotocol.box.server.waveserver.WaveBus; import org.waveprotocol.box.server.waveserver.WaveServerException; import org.waveprotocol.box.server.waveserver.WaveletProvider; import org.waveprotocol.box.server.waveserver.WaveletProvider.SubmitRequestListener; import org.waveprotocol.wave.federation.Proto.ProtocolWaveletDelta; 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.AddParticipant; import org.waveprotocol.wave.model.operation.wave.RemoveParticipant; import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletOperation; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.version.HashedVersionFactory; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.data.ReadableWaveletData; import org.waveprotocol.wave.util.logging.Log; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; /** * Implements the client front-end. * * This class maintains a list of wavelets accessible by local participants by * inspecting all updates it receives (there is no need to inspect historic * deltas as they would have been received as updates had there been an * addParticipant). Updates are aggregated in a special index Wave which is * stored with the WaveServer. * * When a wavelet is added and it's not at version 0, buffer updates until a * request for the wavelet's history has completed. */ public class ClientFrontendImpl implements ClientFrontend, WaveBus.Subscriber { private static final Log LOG = Log.get(ClientFrontendImpl.class); private final static AtomicInteger channel_counter = new AtomicInteger(0); /** Information we hold in memory for each wavelet, including index wavelets. */ private static class PerWavelet { private final HashedVersion version0; private final Set<ParticipantId> participants; private HashedVersion currentVersion; private String digest; PerWavelet(WaveletName waveletName, HashedVersion hashedVersionZero) { this.participants = Collections.synchronizedSet(Sets.<ParticipantId>newHashSet()); this.version0 = hashedVersionZero; this.currentVersion = version0; this.digest = ""; } synchronized HashedVersion getCurrentVersion() { return currentVersion; } synchronized void setCurrentVersion(HashedVersion version) { this.currentVersion = version; } } @VisibleForTesting final Map<ParticipantId, UserManager> perUser; private final Map<WaveId, Map< WaveletId, PerWavelet>> perWavelet; private final WaveletProvider waveletProvider; /** * Creates a client frontend and subscribes it to the wave bus. * * @throws WaveServerException if the server fails during initialisation */ public static ClientFrontendImpl create(HashedVersionFactory hashedVersionFactory, WaveletProvider waveletProvider, WaveBus wavebus) throws WaveServerException { ClientFrontendImpl impl = new ClientFrontendImpl(hashedVersionFactory, waveletProvider); // Initialize index here until a separate index system exists. impl.initialiseAllWaves(); wavebus.subscribe(impl); return impl; } /** * Constructor. * * @param hashedVersionFactory * @param waveletProvider * @param waveDomain the server wave domain. It is assumed that the wave domain is valid. */ @VisibleForTesting ClientFrontendImpl(final HashedVersionFactory hashedVersionFactory, WaveletProvider waveletProvider) { this.waveletProvider = waveletProvider; final MapMaker mapMaker = new MapMaker(); perWavelet = mapMaker.makeComputingMap(new Function<WaveId, Map<WaveletId, PerWavelet>>() { @Override public Map<WaveletId, PerWavelet> apply(final WaveId waveId) { return mapMaker.makeComputingMap(new Function<WaveletId, PerWavelet>() { @Override public PerWavelet apply(WaveletId waveletId) { WaveletName waveletName = WaveletName.of(waveId, waveletId); return new PerWavelet(waveletName, hashedVersionFactory.createVersionZero(waveletName)); } }); } }); perUser = mapMaker.makeComputingMap(new Function<ParticipantId, UserManager>() { @Override public UserManager apply(ParticipantId from) { return new UserManager(); } }); } @Override public void openRequest(ParticipantId loggedInUser, WaveId waveId, IdFilter waveletIdFilter, Collection<WaveClientRpc.WaveletVersion> knownWavelets, OpenListener openListener) { LOG.info("received openRequest from " + loggedInUser + " for " + waveId + ", filter " + waveletIdFilter + ", known wavelets: " + knownWavelets); // TODO(josephg): Make it possible for this to succeed & return public waves. if (loggedInUser == null) { openListener.onFailure("Not logged in"); return; } if (!knownWavelets.isEmpty()) { openListener.onFailure("Known wavelets not supported"); return; } boolean isIndexWave = IndexWave.isIndexWave(waveId); try { if (!isIndexWave) { initialiseWave(waveId); } } catch (WaveServerException e) { LOG.severe("Wave server failed lookup for " + waveId, e); openListener.onFailure("Wave server failed to look up wave"); return; } String channelId = generateChannelID(); UserManager userManager = perUser.get(loggedInUser); synchronized (userManager) { WaveViewSubscription subscription = userManager.subscribe(waveId, waveletIdFilter, channelId, openListener); LOG.info("Subscribed " + loggedInUser + " to " + waveId + " channel " + channelId); Set<WaveletId> waveletIds; try { waveletIds = visibleWaveletsFor(subscription, loggedInUser); } catch (WaveServerException e1) { waveletIds = Sets.newHashSet(); LOG.warning("Failed to retrieve visible wavelets for " + loggedInUser, e1); } for (WaveletId waveletId : waveletIds) { WaveletName waveletName = WaveletName.of(waveId, waveletId); // The WaveletName by which the waveletProvider knows the relevant deltas // TODO(anorth): if the client provides known wavelets, calculate // where to start sending deltas from. DeltaSequence deltasToSend; CommittedWaveletSnapshot snapshotToSend; HashedVersion endVersion; if (isIndexWave) { // Fetch deltas from the real wave from which the index wavelet // is generated. // TODO(anorth): send a snapshot instead. WaveletName sourceWaveletName = IndexWave.waveletNameFromIndexWavelet(waveletName); endVersion = getWavelet(sourceWaveletName).currentVersion; HashedVersion startVersion = getWavelet(sourceWaveletName).version0; try { // Send deltas to bring the wavelet up to date DeltaSequence sourceWaveletDeltas = DeltaSequence.of( waveletProvider.getHistory(sourceWaveletName, startVersion, endVersion)); // Construct fake index wave deltas from the deltas String newDigest = getWavelet(sourceWaveletName).digest; deltasToSend = IndexWave.createIndexDeltas( startVersion.getVersion(), sourceWaveletDeltas, "", newDigest); } catch (WaveServerException e) { LOG.warning("Failed to retrieve history for wavelet " + sourceWaveletName, e); deltasToSend = DeltaSequence.empty(); } snapshotToSend = null; } else { // Send a snapshot of the current state. // TODO(anorth): calculate resync point if the client already knows // a snapshot. deltasToSend = DeltaSequence.empty(); try { snapshotToSend = waveletProvider.getSnapshot(waveletName); } catch (WaveServerException e) { LOG.warning("Failed to retrieve snapshot for wavelet " + waveletName, e); openListener.onFailure("Wave server failure retrieving wavelet"); return; } } LOG.info("snapshot in response is: " + (snapshotToSend != null)); if (snapshotToSend == null) { // Send deltas. openListener.onUpdate(waveletName, snapshotToSend, deltasToSend, null, null, channelId); } else { // Send the snapshot. openListener.onUpdate(waveletName, snapshotToSend, deltasToSend, snapshotToSend.committedVersion, null, channelId); } } WaveletName dummyWaveletName = createDummyWaveletName(waveId); if (waveletIds.size() == 0) { // Send message with just the channel id. LOG.info("sending just a channel id for " + dummyWaveletName); openListener.onUpdate(dummyWaveletName, null, DeltaSequence.empty(), null, null, channelId); } LOG.info("sending marker for " + dummyWaveletName); openListener.onUpdate(dummyWaveletName, null, DeltaSequence.empty(), null, true, null); } } private String generateChannelID() { return "ch" + channel_counter.addAndGet(1); } /** * Initialises in-memory state for all waves, and the index wave, * by scanning the wavelet provider. * * The index wave is the main driver of this behaviour; when it's factored * out this should not be necessary. */ @VisibleForTesting void initialiseAllWaves() throws WaveServerException { ExceptionalIterator<WaveId, WaveServerException> witr = waveletProvider.getWaveIds(); Map<WaveletId, PerWavelet> indexWavelets = perWavelet.get(CommonConstants.INDEX_WAVE_ID); while (witr.hasNext()) { WaveId waveId = witr.next(); Preconditions.checkState(!IndexWave.isIndexWave(waveId), "Index wave should not persist"); initialiseWave(waveId); WaveletName indexWaveletName = IndexWave.indexWaveletNameFor(waveId); // IndexWavelets is a computing map, so get() initialises the entry. // Because the index wavelets are not persistent wavelets there's // no need to initialise participant or digest information. indexWavelets.get(indexWaveletName.waveletId); } } /** * Initialises front-end information from the wave store, if necessary. */ private void initialiseWave(WaveId waveId) throws WaveServerException { Preconditions.checkArgument(!IndexWave.isIndexWave(waveId), "Late initialisation of index wave"); if (!perWavelet.containsKey(waveId)) { Map<WaveletId, PerWavelet> wavelets = perWavelet.get(waveId); for (WaveletId waveletId : waveletProvider.getWaveletIds(waveId)) { ReadableWaveletData wavelet = waveletProvider.getSnapshot(WaveletName.of(waveId, waveletId)).snapshot; // Wavelets is a computing map, so get() initialises the entry. PerWavelet waveletInfo = wavelets.get(waveletId); synchronized (waveletInfo) { waveletInfo.currentVersion = wavelet.getHashedVersion(); waveletInfo.digest = digest(Snippets.renderSnippet(wavelet, 80)); waveletInfo.participants.addAll(wavelet.getParticipants()); } } } } private boolean isWaveletWritable(WaveletName waveletName) { return !IndexWave.isIndexWave(waveletName.waveId); } @Override public void submitRequest(ParticipantId loggedInUser, final WaveletName waveletName, final ProtocolWaveletDelta delta, final String channelId, final SubmitRequestListener listener) { final ParticipantId author = new ParticipantId(delta.getAuthor()); if (!author.equals(loggedInUser)) { listener.onFailure("Author field on delta must match logged in user"); return; } if (!isWaveletWritable(waveletName)) { listener.onFailure("Wavelet " + waveletName + " is readonly"); } else { perUser.get(author).submitRequest(channelId, waveletName); waveletProvider.submitRequest(waveletName, delta, new SubmitRequestListener() { @Override public void onSuccess(int operationsApplied, HashedVersion hashedVersionAfterApplication, long applicationTimestamp) { listener.onSuccess(operationsApplied, hashedVersionAfterApplication, applicationTimestamp); perUser.get(author).submitResponse(channelId, waveletName, hashedVersionAfterApplication); } @Override public void onFailure(String error) { listener.onFailure(error); perUser.get(author).submitResponse(channelId, waveletName, null); } }); } } @Override public void waveletCommitted(WaveletName waveletName, HashedVersion version) { for (ParticipantId participant : getWavelet(waveletName).participants) { // TODO(arb): commits? channelId perUser.get(participant).onCommit(waveletName, version, null); } } private void participantAddedToWavelet(WaveletName waveletName, ParticipantId participant) { getWavelet(waveletName).participants.add(participant); } private void participantRemovedFromWavelet(WaveletName waveletName, ParticipantId participant) { getWavelet(waveletName).participants.remove(participant); } /** * Sends new deltas to a particular user on a particular wavelet, and also * generates fake deltas for the index wavelet. Updates the participants of * the specified wavelet if the participant was added or removed. * * @param waveletName which the deltas belong to * @param participant on the wavelet * @param newDeltas newly arrived deltas of relevance for participant. Must * not be empty. * @param add whether the participant is added by the first delta * @param remove whether the participant is removed by the last delta * @param oldDigest The digest text of the wavelet before the deltas are * applied (but including all changes from preceding deltas) * @param newDigest The digest text of the wavelet after the deltas are * applied */ private void participantUpdate(WaveletName waveletName, ParticipantId participant, DeltaSequence newDeltas, boolean add, boolean remove, String oldDigest, String newDigest) { if (add) { participantAddedToWavelet(waveletName, participant); } perUser.get(participant).onUpdate(waveletName, newDeltas); if (remove) { participantRemovedFromWavelet(waveletName, participant); } // Construct and publish fake index wave deltas if (IndexWave.canBeIndexed(waveletName)) { WaveletName indexWaveletName = IndexWave.indexWaveletNameFor(waveletName.waveId); long startVersion = newDeltas.getStartVersion(); if (add) { participantAddedToWavelet(indexWaveletName, participant); startVersion = 0; } DeltaSequence indexDeltas = IndexWave.createIndexDeltas(startVersion, newDeltas, oldDigest, newDigest); if (!indexDeltas.isEmpty()) { perUser.get(participant).onUpdate(indexWaveletName, indexDeltas); } if (remove) { participantRemovedFromWavelet(indexWaveletName, participant); } } } /** * Based on deltas we receive from the wave server, pass the appropriate * membership changes and deltas from both the affected wavelets and the * corresponding index wave wavelets on to the UserManagers. */ @Override public void waveletUpdate(ReadableWaveletData wavelet, DeltaSequence newDeltas) { if (newDeltas.isEmpty()) { return; } WaveletName waveletName = WaveletName.of(wavelet.getWaveId(), wavelet.getWaveletId()); PerWavelet waveletInfo = getWavelet(waveletName); HashedVersion expectedVersion; String oldDigest; Set<ParticipantId> remainingParticipants; synchronized (waveletInfo) { expectedVersion = waveletInfo.getCurrentVersion(); oldDigest = waveletInfo.digest; remainingParticipants = Sets.newHashSet(waveletInfo.participants); } Preconditions.checkState(expectedVersion.getVersion() == newDeltas.getStartVersion(), "Expected deltas starting at version %s, got %s", expectedVersion, newDeltas.getStartVersion()); String newDigest = digest(Snippets.renderSnippet(wavelet, 80)); synchronized (waveletInfo) { waveletInfo.setCurrentVersion(newDeltas.getEndVersion()); waveletInfo.digest = newDigest; } // Participants added during the course of newDeltas Set<ParticipantId> newParticipants = Sets.newHashSet(); for (int i = 0; i < newDeltas.size(); i++) { TransformedWaveletDelta delta = newDeltas.get(i); // Participants added or removed in this delta get the whole delta for (WaveletOperation op : delta) { if (op instanceof AddParticipant) { ParticipantId p = ((AddParticipant) op).getParticipantId(); remainingParticipants.add(p); newParticipants.add(p); } if (op instanceof RemoveParticipant) { ParticipantId p = ((RemoveParticipant) op).getParticipantId(); remainingParticipants.remove(p); participantUpdate(waveletName, p, newDeltas.subList(0, i + 1), newParticipants.remove(p), true, oldDigest, ""); } } } // Send out deltas to those who end up being participants at the end // (either because they already were, or because they were added). for (ParticipantId p : remainingParticipants) { boolean isNew = newParticipants.contains(p); participantUpdate(waveletName, p, newDeltas, isNew, false, oldDigest, newDigest); } } private PerWavelet getWavelet(WaveletName name) { return perWavelet.get(name.waveId).get(name.waveletId); } private Set<WaveletId> visibleWaveletsFor(WaveViewSubscription subscription, ParticipantId loggedInUser) throws WaveServerException { Set<WaveletId> visible = Sets.newHashSet(); Set<Entry<WaveletId, PerWavelet>> entrySet = perWavelet.get(subscription.getWaveId()).entrySet(); for (Entry<WaveletId, PerWavelet> entry : entrySet) { WaveletName waveletName = WaveletName.of(subscription.getWaveId(), entry.getKey()); if (subscription.includes(entry.getKey()) && waveletProvider.checkAccessPermission(waveletName, loggedInUser)) { visible.add(entry.getKey()); } } return visible; } /** Constructs a digest of the specified String. */ private static String digest(String text) { int digestEndPos = text.indexOf('\n'); if (digestEndPos < 0) { return text; } else { return text.substring(0, Math.min(80, digestEndPos)); } } @VisibleForTesting static WaveletName createDummyWaveletName(WaveId waveId) { final WaveletName dummyWaveletName = WaveletName.of(waveId, WaveletId.of(waveId.getDomain(), "dummy+root")); return dummyWaveletName; } }