/** * 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.wave.model.wave.undo; import com.google.common.annotations.VisibleForTesting; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.operation.OperationPair; import org.waveprotocol.wave.model.operation.TransformException; import org.waveprotocol.wave.model.operation.core.CoreAddParticipant; import org.waveprotocol.wave.model.operation.core.CoreRemoveParticipant; import org.waveprotocol.wave.model.operation.core.CoreWaveletDocumentOperation; import org.waveprotocol.wave.model.operation.core.CoreWaveletOperation; import org.waveprotocol.wave.model.operation.wave.AddParticipant; import org.waveprotocol.wave.model.operation.wave.BlipContentOperation; import org.waveprotocol.wave.model.operation.wave.RemoveParticipant; 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.util.Preconditions; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.wave.ParticipantId; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * An aggregate operation similar to @see AggregateOperation, but with an * additional field to specify the creator of its component ops. * */ class WaveAggregateOp { /** List of op, creator pairs */ private final List<OpCreatorPair> opPairs; private static class OpCreatorPair { final ParticipantId creator; final AggregateOperation op; OpCreatorPair(AggregateOperation op, ParticipantId creator) { Preconditions.checkNotNull(op, "op must be non-null"); Preconditions.checkNotNull(creator, "creator must be non-null"); this.op = op; this.creator = creator; } } /** * Constructs a WaveAggregateOp from a Wavelet op. * @param op */ static WaveAggregateOp createAggregate(WaveletOperation op) { Preconditions.checkNotNull(op, "op must be non-null"); Preconditions.checkNotNull(op.getContext(), "context must be non-null"); ParticipantId creator = op.getContext().getCreator(); AggregateOperation aggOp = AggregateOperation.createAggregate(op); return new WaveAggregateOp(aggOp, creator); } /** * Compose a list of WaveAggregateOps. * * NOTE(user): Consider adding some checks for operations that span different * creators, i.e. a compose of addParticipant(personA) by creator1 and * creator2 should be invalid. * * @param operations */ static WaveAggregateOp compose(List<WaveAggregateOp> operations) { return new WaveAggregateOp(composeDocumentOps(flatten(operations))); } /** * Transform the given operations. * * @param clientOp * @param serverOp * * @throws TransformException */ static OperationPair<WaveAggregateOp> transform(WaveAggregateOp clientOp, WaveAggregateOp serverOp) throws TransformException { // This gets filled with transformed server ops. List<OpCreatorPair> transformedServerOps = new ArrayList<OpCreatorPair>(); // This starts with the original client ops, and gets transformed with each server op. List<OpCreatorPair> transformedClientOps = new ArrayList<OpCreatorPair>(clientOp.opPairs); for (OpCreatorPair sPair : serverOp.opPairs) { transformedServerOps.add(transformAndUpdate(transformedClientOps, sPair)); } return new OperationPair<WaveAggregateOp>(new WaveAggregateOp(transformedClientOps), new WaveAggregateOp(transformedServerOps)); } private static void maybeCollectOps(List<AggregateOperation> ops, ParticipantId creator, List<OpCreatorPair> dest) { if (ops != null && ops.size() > 0) { assert creator != null; dest.add(new OpCreatorPair(AggregateOperation.compose(ops), creator)); } } private static List<OpCreatorPair> composeDocumentOps(List<OpCreatorPair> ops) { List<OpCreatorPair> ret = new ArrayList<OpCreatorPair>(); ParticipantId currentCreator = null; List<AggregateOperation> currentOps = null; // Group sequences of ops under same creator. for (OpCreatorPair op : ops) { if (!op.creator.equals(currentCreator)) { // If the creator is different, compose and finish with the current // group, and start the next group. maybeCollectOps(currentOps, currentCreator, ret); currentOps = null; currentCreator = op.creator; } if (currentOps == null) { currentOps = new ArrayList<AggregateOperation>(); } currentOps.add(op.op); } // Collect the last batch of ops. maybeCollectOps(currentOps, currentCreator, ret); return ret; } /** * Flatten a sequence of WaveAggregate operations into a list of * OpCreatorPairs. * * @param operations */ private static List<OpCreatorPair> flatten(List<WaveAggregateOp> operations) { List<OpCreatorPair> ret = new ArrayList<OpCreatorPair>(); for (WaveAggregateOp aggOp : operations) { ret.addAll(aggOp.opPairs); } return ret; } /** * Transform stream S against streamC, updating streamC and returning the * transform of s. * * @param streamC * @param s * @throws TransformException */ private static OpCreatorPair transformAndUpdate(List<OpCreatorPair> streamC, OpCreatorPair s) throws TransformException { // Makes a copy of streamC and clear the original, so that it can be filled with the // transformed version. List<OpCreatorPair> streamCCopy = new ArrayList<OpCreatorPair>(streamC); streamC.clear(); for (OpCreatorPair c : streamCCopy) { OperationPair<OpCreatorPair> transformed = transform(c, s); streamC.add(transformed.clientOp()); s = transformed.serverOp(); } return s; } private static OperationPair<OpCreatorPair> transform(OpCreatorPair c, OpCreatorPair s) throws TransformException { OperationPair<AggregateOperation> transformed = AggregateOperation.transform(c.op, s.op); return new OperationPair<OpCreatorPair>(new OpCreatorPair(transformed.clientOp(), c.creator), new OpCreatorPair(transformed.serverOp(), s.creator)); } @VisibleForTesting WaveAggregateOp(AggregateOperation op, ParticipantId creator) { opPairs = Collections.singletonList(new OpCreatorPair(op, creator)); } private WaveAggregateOp(List<OpCreatorPair> pairs) { Preconditions.checkNotNull(pairs, "pairs must be non-null"); this.opPairs = pairs; } /** * @return wavelet operations corresponding to this WaveAggregateOp. */ public List<WaveletOperation> toWaveletOperations() { return toWaveletOperationsWithVersions(0, null); } /** * Special case where we populate the last op in the list with the given versions. * This is necessary to preserve the WaveletOperationContext from the server. * * @param versionIncrement * @param hashedVersion */ public List<WaveletOperation> toWaveletOperationsWithVersions(long versionIncrement, HashedVersion hashedVersion) { List<WaveletOperation> ret = new ArrayList<WaveletOperation>(); for (int i = 0; i < opPairs.size(); ++i) { OpCreatorPair pair = opPairs.get(i); boolean isLastOfOuter = (i == opPairs.size() - 1); List<CoreWaveletOperation> coreWaveletOperations = pair.op.toCoreWaveletOperations(); for (int j = 0; j < coreWaveletOperations.size(); ++j) { boolean isLast = isLastOfOuter && (j == coreWaveletOperations.size() - 1); WaveletOperationContext opContext = contextForCreator(pair.creator, versionIncrement, hashedVersion, isLast); WaveletOperation waveletOps = coreWaveletOpsToWaveletOps(coreWaveletOperations.get(j), opContext); ret.add(waveletOps); } } return ret; } WaveAggregateOp invert() { List<OpCreatorPair> invertedPairs = new ArrayList<OpCreatorPair>(); for (OpCreatorPair pair : opPairs) { invertedPairs.add(new OpCreatorPair(pair.op.invert(), pair.creator)); } Collections.reverse(invertedPairs); return new WaveAggregateOp(invertedPairs); } private WaveletOperation coreWaveletOpsToWaveletOps(CoreWaveletOperation op, WaveletOperationContext context) { if (op instanceof CoreRemoveParticipant) { ParticipantId participantId = ((CoreRemoveParticipant) op).getParticipantId(); return new RemoveParticipant(context, participantId); } else if (op instanceof CoreAddParticipant) { ParticipantId participantId = ((CoreAddParticipant) op).getParticipantId(); return new AddParticipant(context, participantId); } else if (op instanceof CoreWaveletDocumentOperation) { CoreWaveletDocumentOperation waveletDocOp = (CoreWaveletDocumentOperation) op; String documentId = waveletDocOp.getDocumentId(); DocOp operation = waveletDocOp.getOperation(); return new WaveletBlipOperation(documentId, new BlipContentOperation(context, operation)); } throw new RuntimeException("unhandled operation type"); } /** * @param creator * @param isLastOfSeq * @param hashedVersion * @param versionIncrement * @return a WaveletOperationContext with the specified participant as the * creator. */ private WaveletOperationContext contextForCreator(ParticipantId creator, long versionIncrement, HashedVersion hashedVersion, boolean isLastOfSeq) { if (isLastOfSeq) { return new WaveletOperationContext(creator, System.currentTimeMillis(), versionIncrement, hashedVersion); } else { // NOTE(user): The timestamp and version field are not relevant in the // client for outgoing ops, but may need to be filled out properly on the // server. return new WaveletOperationContext(creator, System.currentTimeMillis(), 0L); } } }