/**
* Copyright 2010 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.common;
import static org.waveprotocol.box.common.CommonConstants.INDEX_WAVE_ID;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
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.InvalidIdException;
import org.waveprotocol.wave.model.id.ModernIdSerialiser;
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.BlipContentOperation;
import org.waveprotocol.wave.model.operation.wave.BlipOperation;
import org.waveprotocol.wave.model.operation.wave.NoOp;
import org.waveprotocol.wave.model.operation.wave.RemoveParticipant;
import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta;
import org.waveprotocol.wave.model.operation.wave.WaveletBlipOperation;
import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
import org.waveprotocol.wave.model.operation.wave.WaveletOperationContext;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.data.ReadableWaveletData;
import java.util.List;
/**
* Utilities for mutating the index wave, a list of waves visible to a user. The
* index wave has a wavelet for each wave in the index. That wavelet has a
* digest document containing a snippet of text.
*
* TODO(anorth): replace this with a more canonical use of the wave model.
*
* @author anorth@google.com (Alex North)
* @author mk.mateng@gmail.com (Michael Kuntzman)
*/
public final class IndexWave {
@VisibleForTesting
public static final ParticipantId DIGEST_AUTHOR = new ParticipantId("digest-author@example.com");
@VisibleForTesting
public static final String DIGEST_DOCUMENT_ID = "digest";
/**
* @return true if the specified wave can be indexed in an index wave.
*/
public static boolean canBeIndexed(WaveId waveId) {
return !isIndexWave(waveId);
}
/**
* @return true if the specified wavelet name can be encoded into an index
* wave.
*/
public static boolean canBeIndexed(WaveletName waveletName) {
WaveId waveId = waveletName.waveId;
WaveletId waveletId = waveletName.waveletId;
return canBeIndexed(waveId) && waveId.getDomain().equals(waveletId.getDomain())
&& IdUtil.isConversationRootWaveletId(waveletId);
}
/**
* Constructs the deltas for an index wave wavelet in response to deltas on an
* original conversation wavelet.
*
* The created delta sequence will have the same effect on the participants as
* the original deltas and will update the digest as a function of oldDigest
* and newDigest. The sequence may contain no-ops to pad it to the same length
* as the source deltas.
*
* @param sourceDeltas conversational deltas to process
* @param oldDigest the current index digest
* @param newDigest the new digest
* @return deltas to apply to the index wavelet to achieve the same change in
* participants, and the specified change in digest text
*/
public static DeltaSequence createIndexDeltas(long targetVersion, DeltaSequence sourceDeltas,
String oldDigest, String newDigest) {
long deltaTargetVersion = targetVersion; // Target for the next delta.
long timestamp = 0L; // No timestamp so that this is testable.
long numSourceOps = sourceDeltas.getEndVersion().getVersion() - targetVersion;
List<TransformedWaveletDelta> indexDeltas =
createParticipantDeltas(sourceDeltas, deltaTargetVersion);
long numIndexOps = numOpsInDeltas(indexDeltas);
deltaTargetVersion += numIndexOps;
if (numIndexOps < numSourceOps) {
indexDeltas.add(createDigestDelta(deltaTargetVersion, timestamp, oldDigest, newDigest));
numIndexOps += 1;
deltaTargetVersion += 1;
}
if (numIndexOps < numSourceOps) {
// Append no-ops.
long numNoOps = numSourceOps - numIndexOps;
List<WaveletOperation> noOps = Lists.newArrayList();
WaveletOperationContext initialContext =
new WaveletOperationContext(DIGEST_AUTHOR, timestamp, 1);
for (long i = 0; i < numNoOps - 1; ++i) {
noOps.add(new NoOp(initialContext));
}
HashedVersion resultingVersion = HashedVersion.unsigned(deltaTargetVersion + numNoOps);
WaveletOperationContext finalContext =
new WaveletOperationContext(DIGEST_AUTHOR, timestamp, 1, resultingVersion);
noOps.add(new NoOp(finalContext));
TransformedWaveletDelta noOpDelta =
new TransformedWaveletDelta(DIGEST_AUTHOR, resultingVersion, 0L, noOps);
indexDeltas.add(noOpDelta);
}
return DeltaSequence.of(indexDeltas);
}
/**
* Retrieve a list of index entries from an index wave.
*
* @param wavelets wavelets to retrieve the index from.
* @return list of index entries.
*/
public static List<IndexEntry> getIndexEntries(Iterable<? extends ReadableWaveletData> wavelets) {
List<IndexEntry> indexEntries = Lists.newArrayList();
for (ReadableWaveletData wavelet : wavelets) {
WaveId waveId = waveIdFromIndexWavelet(wavelet);
String digest = Snippets.collateTextForWavelet(wavelet);
indexEntries.add(new IndexEntry(waveId, digest));
}
return indexEntries;
}
/**
* Constructs the name of the index wave wavelet that refers to the specified
* wave.
*
* @param waveId referent wave id
* @return WaveletName of the index wave wavelet referring to waveId
* @throws IllegalArgumentException if the wave cannot be indexed
*/
public static WaveletName indexWaveletNameFor(WaveId waveId) {
Preconditions.checkArgument(canBeIndexed(waveId), "Wave %s cannot be indexed", waveId);
return WaveletName.of(INDEX_WAVE_ID, WaveletId.of(waveId.getDomain(), waveId.getId()));
}
/**
* @return true if the specified wave ID is an index wave ID.
*/
public static boolean isIndexWave(WaveId waveId) {
return waveId.equals(INDEX_WAVE_ID);
}
/**
* Extracts the wave id referred to by an index wavelet's wavelet name.
*
* @param indexWavelet the index wavelet.
* @return the wave id.
* @throws IllegalArgumentException if the wavelet is not from an index wave.
*/
public static WaveId waveIdFromIndexWavelet(ReadableWaveletData indexWavelet) {
return waveIdFromIndexWavelet(
WaveletName.of(indexWavelet.getWaveId(), indexWavelet.getWaveletId()));
}
/**
* Extracts the wave id referred to by an index wavelet name.
*
* @param indexWaveletName of the index wavelet.
* @return the wave id.
* @throws IllegalArgumentException if the wavelet is not from an index wave.
*/
public static WaveId waveIdFromIndexWavelet(WaveletName indexWaveletName) {
WaveId waveId = indexWaveletName.waveId;
Preconditions.checkArgument(isIndexWave(waveId), waveId + " is not an index wave");
try {
return ModernIdSerialiser.INSTANCE.deserialiseWaveId(
ModernIdSerialiser.INSTANCE.serialiseWaveletId(indexWaveletName.waveletId));
} catch (InvalidIdException e) {
throw new IllegalStateException(e);
}
}
/**
* Extracts a wavelet name referring to the conversational root wavelet in the
* wave referred to by an index wavelet name.
*/
public static WaveletName waveletNameFromIndexWavelet(WaveletName indexWaveletName) {
return WaveletName.of(IndexWave.waveIdFromIndexWavelet(indexWaveletName), WaveletId.of(
indexWaveletName.waveletId.getDomain(), IdConstants.CONVERSATION_ROOT_WAVELET));
}
/**
* Counts the ops in a sequence of deltas
*/
private static long numOpsInDeltas(Iterable<TransformedWaveletDelta> deltas) {
long sum = 0;
for (TransformedWaveletDelta d : deltas) {
sum += d.size();
}
return sum;
}
/**
* Constructs a delta with one op which transforms the digest document from
* one digest string to another.
*/
private static TransformedWaveletDelta createDigestDelta(long targetVersion, long timestamp,
String oldDigest, String newDigest) {
HashedVersion resultingVersion = HashedVersion.unsigned(targetVersion + 1);
WaveletOperationContext context =
new WaveletOperationContext(DIGEST_AUTHOR, timestamp, 1, resultingVersion);
WaveletOperation op =
new WaveletBlipOperation(DIGEST_DOCUMENT_ID, createEditOp(oldDigest, newDigest, context));
TransformedWaveletDelta delta = new TransformedWaveletDelta(DIGEST_AUTHOR, resultingVersion,
timestamp, ImmutableList.of(op));
return delta;
}
/** Constructs a DocOp that transforms source into target. */
private static BlipOperation createEditOp(String source, String target,
WaveletOperationContext context) {
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 new BlipContentOperation(context, builder.build());
}
/** Extracts participant change operations from a delta sequence. */
private static List<TransformedWaveletDelta> createParticipantDeltas(
Iterable<TransformedWaveletDelta> deltas, long targetVersion) {
List<TransformedWaveletDelta> participantDeltas = Lists.newArrayList();
for (TransformedWaveletDelta delta : deltas) {
List<WaveletOperation> receivedParticipantOps = Lists.newArrayList();
for (WaveletOperation op : delta) {
if (op instanceof AddParticipant || op instanceof RemoveParticipant) {
receivedParticipantOps.add(op);
}
}
if (!receivedParticipantOps.isEmpty()) {
HashedVersion endVersion =
HashedVersion.unsigned(targetVersion + receivedParticipantOps.size());
participantDeltas.add(TransformedWaveletDelta.cloneOperations(delta.getAuthor(),
endVersion, delta.getApplicationTimestamp(), receivedParticipantOps));
targetVersion += receivedParticipantOps.size();
}
}
return participantDeltas;
}
/**
* 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;
}
}