/**
* 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.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import static org.waveprotocol.wave.examples.fedone.common.CommonConstants.INDEX_WAVE_ID;
import org.waveprotocol.wave.examples.fedone.common.HashedVersion;
import org.waveprotocol.wave.examples.fedone.common.WaveletOperationSerializer;
import static org.waveprotocol.wave.examples.fedone.common.WaveletOperationSerializer.serialize;
import org.waveprotocol.wave.examples.fedone.waveclient.common.ClientUtils;
import org.waveprotocol.wave.examples.fedone.waveserver.WaveClientRpc.WaveletSnapshot;
import org.waveprotocol.wave.examples.fedone.waveserver.WaveClientRpc.WaveletSnapshot.DocumentSnapshot;
import org.waveprotocol.wave.federation.FederationErrorProto.FederationError;
import org.waveprotocol.wave.federation.FederationErrors;
import org.waveprotocol.wave.federation.Proto.ProtocolHashedVersion;
import org.waveprotocol.wave.federation.Proto.ProtocolWaveletDelta;
import org.waveprotocol.wave.federation.Proto.ProtocolWaveletOperation;
import org.waveprotocol.wave.model.document.operation.BufferedDocOp;
import org.waveprotocol.wave.model.document.operation.impl.DocOpBuilder;
import org.waveprotocol.wave.model.id.IdConstants;
import org.waveprotocol.wave.model.id.IdUtil;
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.WaveletDelta;
import org.waveprotocol.wave.model.operation.wave.WaveletDocumentOperation;
import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
import org.waveprotocol.wave.model.util.Pair;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.data.WaveletData;
import org.waveprotocol.wave.waveserver.SubmitResultListener;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicLong;
/**
* Implements the Client Frontend.
*
* 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 it stores with the
* WaveServer.
*
*
*
*/
public class ClientFrontendImpl implements ClientFrontend {
@VisibleForTesting
static final ParticipantId DIGEST_AUTHOR = new ParticipantId("digest-author");
@VisibleForTesting
static final String DIGEST_DOCUMENT_ID = "digest";
/** Information we hold in memory for each wavelet, including index wavelets. */
private static class PerWavelet {
private final Set<ParticipantId> participants;
final AtomicLong timestamp; // last modified time
private final ProtocolHashedVersion version0;
private ProtocolHashedVersion currentVersion;
private String digest;
PerWavelet(WaveletName waveletName) {
this.participants = Collections.synchronizedSet(Sets.<ParticipantId>newHashSet());
this.timestamp = new AtomicLong(0);
this.version0 = WaveletOperationSerializer.serialize(HashedVersion.versionZero(waveletName));
this.currentVersion = version0;
this.digest = "";
}
synchronized ProtocolHashedVersion getCurrentVersion() {
return currentVersion;
}
synchronized void setCurrentVersion(ProtocolHashedVersion version) {
this.currentVersion = version;
}
}
/**
* Constructs the name of the index wave wavelet that refers to the
* specified conversation root wavelet.
*
* @param waveletName to refer to
* @return WaveletName of the index wave wavelet referring to waveId
* @throws IllegalArgumentException if waveId is the WaveId of the index wave
* @throws NullPointerException if waveId is null
*/
@VisibleForTesting
static WaveletName indexWaveletNameFor(WaveletName waveletName) {
if (!isConversationRootWavelet(waveletName)) {
throw new IllegalArgumentException("Not a conversation root wavelet: " + waveletName);
} else {
WaveId waveId = waveletName.waveId;
if (waveId.equals(INDEX_WAVE_ID)) { // throws NPE if waveId is null
throw new IllegalArgumentException(
"There is no index wave wavelet for the index wave itself: " + waveId);
} else {
return WaveletName.of(INDEX_WAVE_ID, WaveletId.deserialise(waveId.serialise()));
}
}
}
private static boolean isConversationRootWavelet(WaveletName waveletName) {
return waveletName.waveId.getDomain().equals(waveletName.waveletId.getDomain())
&& IdUtil.isConversationRootWaveletId(waveletName.waveletId);
}
@VisibleForTesting
static WaveletName waveletNameForIndexWavelet(WaveletName indexWaveletName) {
WaveId waveId = indexWaveletName.waveId;
Preconditions.checkArgument(
waveId.equals(INDEX_WAVE_ID), "Expected " + INDEX_WAVE_ID + ", got " + waveId);
WaveletId waveletId = indexWaveletName.waveletId;
return WaveletName.of(WaveId.deserialise(waveletId.serialise()),
new WaveletId(waveletId.getDomain(), IdConstants.CONVERSATION_ROOT_WAVELET));
}
/** Maps wavelets to the participants currently on that wavelet */
private final Map<ParticipantId, UserManager> perUser;
private final Map<WaveletName, PerWavelet> perWavelet;
private final WaveletProvider waveletProvider;
@Inject
public ClientFrontendImpl(WaveletProvider waveletProvider) {
this.waveletProvider = waveletProvider;
waveletProvider.setListener(this);
MapMaker mapMaker = new MapMaker();
perWavelet = mapMaker.makeComputingMap(new Function<WaveletName, PerWavelet>() {
@Override public PerWavelet apply(WaveletName wn) { return new PerWavelet(wn); }
});
perUser = mapMaker.makeComputingMap(new Function<ParticipantId, UserManager>() {
@Override public UserManager apply(ParticipantId from) { return new UserManager(); }
});
}
@Override
public void openRequest(ParticipantId participant, WaveId waveId, Set<String> waveletIdPrefixes,
int maximumInitialWavelets, boolean snapshotsEnabled, OpenListener openListener) {
if (waveletIdPrefixes == null || waveletIdPrefixes.isEmpty()) {
waveletIdPrefixes = ImmutableSet.of("");
}
final boolean isIndexWave = waveId.equals(INDEX_WAVE_ID);
UserManager userManager = perUser.get(participant);
synchronized(userManager) {
Set<WaveletId> waveletIds = userManager.subscribe(waveId, waveletIdPrefixes, openListener);
// Send this listener all deltas on relevant wavelets that we've already
// sent out to other listeners, so that the listener can catch up with
// those.
// TODO: Because of the way we create the fake digest edit ops for the
// index wave, if the listener is subscribing to the index wave then it
// may be that this requires fewer ops than the existing listeners have
// seen, leaving this listener at a different end version than other
// listeners on the same wavelet. Fix!
for (WaveletId waveletId : waveletIds) {
WaveletName waveletName = WaveletName.of(waveId, waveletId);
// The WaveletName by which the waveletProvider knows the relevant deltas
WaveletName sourceWaveletName =
(isIndexWave ? waveletNameForIndexWavelet(waveletName) : waveletName);
ProtocolHashedVersion startVersion = perWavelet.get(sourceWaveletName).version0;
ProtocolHashedVersion endVersion = userManager.getWaveletVersion(sourceWaveletName);
List<ProtocolWaveletDelta> deltaList;
WaveletSnapshotAndVersions snapshot;
// TODO(arb): can I snapshot the indexWave?
if (isIndexWave || !snapshotsEnabled) {
DeltaSequence deltaSequence = new DeltaSequence(
waveletProvider.getHistory(sourceWaveletName, startVersion, endVersion),
endVersion);
if (isIndexWave) { // Construct fake index wave deltas from the deltas
String newDigest = perWavelet.get(sourceWaveletName).digest;
deltaSequence = createIndexDeltas(startVersion, deltaSequence, "", newDigest);
}
deltaList = deltaSequence;
endVersion = deltaSequence.getEndVersion();
snapshot = null;
} else {
// TODO(arb): when we have uncommitted deltas, look them up here.
deltaList = Collections.emptyList();
WaveletSnapshotBuilder<WaveletSnapshotAndVersions> snapshotBuilder =
new WaveletSnapshotBuilder<WaveletSnapshotAndVersions>() {
@Override
public WaveletSnapshotAndVersions build(WaveletData waveletData,
HashedVersion currentVersion, ProtocolHashedVersion committedVersion) {
return new WaveletSnapshotAndVersions(serializeSnapshot(waveletData),
currentVersion, committedVersion);
}
};
snapshot = waveletProvider.getSnapshot(sourceWaveletName, snapshotBuilder);
}
// TODO: Once we've made sure that all listeners have received the
// same number of ops for the index wavelet, enable the following check:
//if (!deltaList.getEndVersion().equals(userManager.getWaveletVersion(waveletName))) {
// throw new IllegalStateException(..)
// }
if (snapshot == null) {
// TODO(arb): get the LCV - maybe add to waveletProvider?
openListener.onUpdate(waveletName, snapshot, deltaList, endVersion,
null);
} else {
openListener.onUpdate(waveletName, snapshot, deltaList, endVersion,
snapshot.committedVersion);
}
}
}
}
/**
* Serializes a WaveletData into a WaveletSnapshot protobuffer.
*
* @param snapshot the snapshot
* @return the new protobuffer
*/
private WaveletSnapshot serializeSnapshot(WaveletData snapshot) {
WaveletSnapshot.Builder snapshotBuilder = WaveletSnapshot.newBuilder();
Map<String, BufferedDocOp> documentMap = snapshot.getDocuments();
for (Entry<String,BufferedDocOp> document : documentMap.entrySet()) {
DocumentSnapshot.Builder documentBuilder = DocumentSnapshot.newBuilder();
documentBuilder.setDocumentId(document.getKey());
documentBuilder.setDocumentOperation(
WaveletOperationSerializer.serialize(document.getValue()));
snapshotBuilder.addDocument(documentBuilder.build());
}
for (ParticipantId participant : snapshot.getParticipants()) {
snapshotBuilder.addParticipantId(participant.toString());
}
return snapshotBuilder.build();
}
private static class SubmitResultListenerAdapter implements SubmitResultListener {
private final SubmitResultListener listener;
public SubmitResultListenerAdapter(SubmitResultListener listener) {
this.listener = listener;
}
@Override
public void onFailure(FederationError error) {
listener.onFailure(error);
}
@Override
public void onSuccess(int operationsApplied,
ProtocolHashedVersion hashedVersionAfterApplication,
long applicationTimestamp) {
listener.onSuccess(operationsApplied, hashedVersionAfterApplication, applicationTimestamp);
}
}
private boolean isWaveletWritable(WaveletName waveletName) {
return !waveletName.waveId.equals(INDEX_WAVE_ID);
}
@Override
public void submitRequest(final WaveletName waveletName, ProtocolWaveletDelta delta,
final SubmitResultListener listener) {
if (!isWaveletWritable(waveletName)) {
listener.onFailure(FederationErrors.badRequest("Wavelet " + waveletName + " is readonly"));
} else {
waveletProvider.submitRequest(waveletName, delta, new SubmitResultListenerAdapter(listener) {
@Override
public void onSuccess(int operationsApplied,
ProtocolHashedVersion hashedVersionAfterApplication, long applicationTimestamp) {
super.onSuccess(operationsApplied, hashedVersionAfterApplication, applicationTimestamp);
perWavelet.get(waveletName).timestamp.set(applicationTimestamp);
}
});
}
}
@Override
public void waveletCommitted(WaveletName waveletName, ProtocolHashedVersion version) {
for (ParticipantId participant : perWavelet.get(waveletName).participants) {
perUser.get(participant).onCommit(waveletName, version);
}
}
private void onAdd(WaveletName waveletName, ParticipantId participant) {
perWavelet.get(waveletName).participants.add(participant);
perUser.get(participant).addWavelet(waveletName);
}
private void onRemove(WaveletName waveletName, ParticipantId participant) {
perWavelet.get(waveletName).participants.remove(participant);
perUser.get(participant).removeWavelet(waveletName);
}
/**
* Sends new deltas to a particular user on a particular wavelet, and also
* generates fake deltas for the index wavelet. If the user was added,
* requests missing deltas from the waveletProvider. 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
*/
@VisibleForTesting
void participantUpdate(WaveletName waveletName, ParticipantId participant,
DeltaSequence newDeltas, boolean add, boolean remove, String oldDigest, String newDigest) {
final DeltaSequence deltasToSend;
if (add && newDeltas.getStartVersion().getVersion() > 0) {
ProtocolHashedVersion version0 = perWavelet.get(waveletName).version0;
ProtocolHashedVersion firstKnownDelta = newDeltas.getStartVersion();
deltasToSend = newDeltas.prepend(
waveletProvider.getHistory(waveletName, version0, firstKnownDelta));
oldDigest = "";
} else {
deltasToSend = newDeltas;
}
if (add) {
onAdd(waveletName, participant);
}
perUser.get(participant).onUpdate(waveletName, deltasToSend);
if (remove) {
onRemove(waveletName, participant);
}
// Construct and publish fake index wave deltas
if (isConversationRootWavelet(waveletName)) {
WaveletName indexWaveletName = indexWaveletNameFor(waveletName);
if (add) {
onAdd(indexWaveletName, participant);
}
ProtocolHashedVersion indexVersion =
perUser.get(participant).getWaveletVersion(indexWaveletName);
DeltaSequence indexDeltas = createIndexDeltas(indexVersion, deltasToSend,
oldDigest, newDigest);
if (!indexDeltas.isEmpty()) {
perUser.get(participant).onUpdate(indexWaveletName, indexDeltas);
}
if (remove) {
onRemove(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(WaveletName waveletName, List<ProtocolWaveletDelta> newDeltas,
ProtocolHashedVersion endVersion, Map<String, BufferedDocOp> documentState) {
if (newDeltas.isEmpty()) {
return;
}
final PerWavelet waveletInfo = perWavelet.get(waveletName);
final ProtocolHashedVersion expectedVersion;
final String oldDigest;
final Set<ParticipantId> remainingParticipants;
synchronized(waveletInfo) {
expectedVersion = waveletInfo.getCurrentVersion();
oldDigest = waveletInfo.digest;
remainingParticipants = Sets.newHashSet(waveletInfo.participants);
}
DeltaSequence deltaSequence = new DeltaSequence(newDeltas, endVersion);
if (!expectedVersion.equals(deltaSequence.getStartVersion())) {
throw new IllegalStateException("Expected deltas starting at version " +
expectedVersion + ", got " +
deltaSequence.getStartVersion().getVersion());
}
String newDigest = digest(ClientUtils.render(documentState.values()));
// Participants added during the course of newDeltas
Set<ParticipantId> newParticipants = Sets.newHashSet();
for (int i = 0; i < newDeltas.size(); i++) {
ProtocolWaveletDelta delta = newDeltas.get(i);
// Participants added or removed in this delta get the whole delta
for (ProtocolWaveletOperation op : delta.getOperationList()) {
if (op.hasAddParticipant()) {
ParticipantId p = new ParticipantId(op.getAddParticipant());
remainingParticipants.add(p);
newParticipants.add(p);
}
if (op.hasRemoveParticipant()) {
ParticipantId p = new ParticipantId(op.getRemoveParticipant());
remainingParticipants.remove(p);
participantUpdate(waveletName, p,
deltaSequence.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, deltaSequence, isNew, false, oldDigest, newDigest);
}
synchronized(waveletInfo) {
waveletInfo.setCurrentVersion(deltaSequence.getEndVersion());
waveletInfo.digest = newDigest;
}
}
/**
* Determines the length (in number of characters) of the longest common
* prefix of the specified two CharSequences. E.g. ("", "foo") -> 0.
* ("foo", "bar) -> 0. ("foo", "foobar") -> 3. ("bar", "baz") -> 2.
*
* (Does this utility method already exist anywhere?)
*
* @throws NullPointerException if a or b is null
*/
private static int lengthOfCommonPrefix(CharSequence a, CharSequence b) {
int result = 0;
int minLength = Math.min(a.length(), b.length());
while (result < minLength && a.charAt(result) == b.charAt(result)) {
result++;
}
return result;
}
/** 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 DeltaSequence createUnsignedDeltas(List<ProtocolWaveletDelta> deltas) {
Preconditions.checkArgument(!deltas.isEmpty(), "No deltas specified");
ProtocolWaveletDelta lastDelta = Iterables.getLast(deltas);
long endVersion = lastDelta.getHashedVersion().getVersion() + lastDelta.getOperationCount();
return new DeltaSequence(deltas, serialize(HashedVersion.unsigned(endVersion)));
}
private static DeltaSequence participantDeltasOnly(long version,
Iterable<ProtocolWaveletDelta> deltas) {
List<ProtocolWaveletDelta> result = Lists.newArrayList();
// Filter out operations that are of interest to the index wave wavelet
// (add/remove participant operations):
for (ProtocolWaveletDelta protoDelta : deltas) {
Pair<WaveletDelta, HashedVersion> deltaAndVersion =
WaveletOperationSerializer.deserialize(protoDelta);
WaveletDelta delta = deltaAndVersion.first;
List<WaveletOperation> indexOps = Lists.newArrayList();
for (WaveletOperation op : delta.getOperations()) {
if (op instanceof AddParticipant || op instanceof RemoveParticipant) {
indexOps.add(op);
}
}
if (!indexOps.isEmpty()) {
WaveletDelta indexDelta = new WaveletDelta(delta.getAuthor(), indexOps);
result.add(serialize(indexDelta, HashedVersion.unsigned(version)));
version += indexDelta.getOperations().size();
}
}
return new DeltaSequence(result, serialize(HashedVersion.unsigned(version)));
}
/** Constructs a BufferedDocOp that transforms source into target. */
private static BufferedDocOp createEditOp(String source, String target) {
int commonPrefixLength = lengthOfCommonPrefix(source, target);
DocOpBuilder builder = new DocOpBuilder();
if (commonPrefixLength > 0) {
builder.retain(commonPrefixLength);
}
if (source.length() > commonPrefixLength) {
builder.deleteCharacters(source.substring(commonPrefixLength));
}
if (target.length() > commonPrefixLength) {
builder.characters(target.substring(commonPrefixLength));
}
return builder.build();
}
@VisibleForTesting
static ProtocolWaveletDelta createDigestDelta(HashedVersion version,
String oldDigest, String newDigest) {
if (oldDigest.equals(newDigest)) {
return null;
} else {
WaveletOperation op = new WaveletDocumentOperation(DIGEST_DOCUMENT_ID,
createEditOp(oldDigest, newDigest));
WaveletDelta indexDelta = new WaveletDelta(DIGEST_AUTHOR, ImmutableList.of(op));
return WaveletOperationSerializer.serialize(indexDelta, version);
}
}
/**
* Constructs the deltas that should be passed on to the index wave wavelet,
* when the corresponding target wavelet receives the specified deltas
* and the original wave's digest has changed as specified.
*
* The returned deltas will have the same effect on the participants as
* the original deltas. The effect of the returned deltas on the document's
* digest are purely a function of oldDigest and newDigest, which should
* represent the change implied by deltas.
*
* @param indexVersion the returned deltas should start at
* @param deltas The deltas whose effect on the participants to determine
* @return deltas to apply to the index wavelet to achieve the same change
* in participants, and the specified change in digest text
*/
private static DeltaSequence createIndexDeltas(ProtocolHashedVersion indexVersion,
DeltaSequence deltas, String oldDigest, String newDigest) {
long version = indexVersion.getVersion();
ProtocolWaveletDelta digestDelta =
createDigestDelta(HashedVersion.unsigned(version), oldDigest, newDigest);
if (digestDelta != null) {
version += digestDelta.getOperationCount();
}
DeltaSequence participantDeltas = participantDeltasOnly(version, deltas);
if (digestDelta == null) {
return participantDeltas;
} else {
return participantDeltas.prepend(ImmutableList.of(digestDelta));
}
}
}