/**
* 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.wave.undo;
import org.waveprotocol.wave.model.document.operation.DocOp;
import org.waveprotocol.wave.model.document.operation.algorithm.DocOpInverter;
import org.waveprotocol.wave.model.document.operation.algorithm.Transformer;
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.CoreNoOp;
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.NoOp;
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.wave.ParticipantId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* An aggregate operation, built up from wavelet operations.
*
*/
final class AggregateOperation {
private static final class DocumentOperations {
final String id;
final DocOpList operations;
DocumentOperations(String id, DocOpList operations) {
this.id = id;
this.operations = operations;
}
}
private static final Comparator<ParticipantId> participantComparator =
new Comparator<ParticipantId>() {
@Override
public int compare(ParticipantId o1, ParticipantId o2) {
return o1.getAddress().compareTo(o2.getAddress());
}
};
/**
* Creates an aggregate operation from a <code>CoreWaveletOperation</code>.
*
* @param operation The wavelet operation whose behaviour the aggregate
* operation should have.
* @return The aggregate operation.
*/
static AggregateOperation createAggregate(CoreWaveletOperation operation) {
if (operation instanceof CoreWaveletDocumentOperation) {
return new AggregateOperation((CoreWaveletDocumentOperation) operation);
} else if (operation instanceof CoreRemoveParticipant) {
return new AggregateOperation((CoreRemoveParticipant) operation);
} else if (operation instanceof CoreAddParticipant) {
return new AggregateOperation((CoreAddParticipant) operation);
}
assert operation instanceof CoreNoOp;
return new AggregateOperation();
}
/**
* Creates an aggregate operation from a <code>WaveletOperation</code>.
*
* @param operation The wavelet operation whose behaviour the aggregate
* operation should have.
* @return The aggregate operation.
*/
static AggregateOperation createAggregate(WaveletOperation operation) {
if (operation instanceof WaveletBlipOperation) {
return new AggregateOperation((WaveletBlipOperation) operation);
} else if (operation instanceof RemoveParticipant) {
return new AggregateOperation((RemoveParticipant) operation);
} else if (operation instanceof AddParticipant) {
return new AggregateOperation((AddParticipant) operation);
}
assert operation instanceof NoOp : "Operation is an unhandled type: " + operation.getClass();
return new AggregateOperation();
}
private static DocOpList invert(DocOpList docOpList) {
return new DocOpList.Singleton(DocOpInverter.invert(docOpList.composeAll()));
}
/**
* Composes the given aggregate operations.
*
* @param operations The aggregate operations to compose.
* @return The composition of the given operations.
*/
static AggregateOperation compose(Iterable<AggregateOperation> operations) {
// NOTE(user): It's possible to replace the following two sets with a single map.
Set<ParticipantId> toRemove = new TreeSet<ParticipantId>(participantComparator);
Set<ParticipantId> toAdd = new TreeSet<ParticipantId>(participantComparator);
Map<String, DocOpList> docOps = new TreeMap<String, DocOpList>();
for (AggregateOperation operation : operations) {
for (ParticipantId participant : operation.participantsToRemove) {
if (toAdd.contains(participant)) {
toAdd.remove(participant);
} else {
toRemove.add(participant);
}
}
for (ParticipantId participant : operation.participantsToAdd) {
if (toRemove.contains(participant)) {
toRemove.remove(participant);
} else {
toAdd.add(participant);
}
}
for (DocumentOperations documentOps : operation.docOps) {
DocOpList docOpList = docOps.get(documentOps.id);
if (docOpList != null) {
docOps.put(documentOps.id, docOpList.concatenateWith(documentOps.operations));
} else {
docOps.put(documentOps.id, documentOps.operations);
}
}
}
return new AggregateOperation(
new ArrayList<ParticipantId>(toRemove),
new ArrayList<ParticipantId>(toAdd),
mapToList(docOps));
}
/**
* Transforms the given aggregate operations.
*
* @param clientOp The client operation to transform.
* @param serverOp The server operation to transform.
*
* @return The transform of the two operations.
* @throws TransformException
*/
static OperationPair<AggregateOperation> transform(AggregateOperation clientOp,
AggregateOperation serverOp) throws TransformException {
List<ParticipantId> clientParticipantsToRemove = new ArrayList<ParticipantId>();
List<ParticipantId> serverParticipantsToRemove = new ArrayList<ParticipantId>();
List<ParticipantId> clientParticipantsToAdd = new ArrayList<ParticipantId>();
List<ParticipantId> serverParticipantsToAdd = new ArrayList<ParticipantId>();
List<DocumentOperations> clientDocOps = new ArrayList<DocumentOperations>();
List<DocumentOperations> serverDocOps = new ArrayList<DocumentOperations>();
removeCommonParticipants(clientOp.participantsToRemove, serverOp.participantsToRemove,
clientParticipantsToRemove, serverParticipantsToRemove);
removeCommonParticipants(clientOp.participantsToAdd, serverOp.participantsToAdd,
clientParticipantsToAdd, serverParticipantsToAdd);
transformDocumentOperations(clientOp.docOps, serverOp.docOps,
clientDocOps, serverDocOps);
AggregateOperation transformedClientOp = new AggregateOperation(
clientParticipantsToRemove, clientParticipantsToAdd, clientDocOps);
AggregateOperation transformedServerOp = new AggregateOperation(
serverParticipantsToRemove, serverParticipantsToAdd, serverDocOps);
return new OperationPair<AggregateOperation>(transformedClientOp, transformedServerOp);
}
private static List<DocumentOperations> mapToList(Map<String, DocOpList> map) {
List<DocumentOperations> list = new ArrayList<DocumentOperations>();
for (Map.Entry<String, DocOpList> entry : map.entrySet()) {
list.add(new DocumentOperations(entry.getKey(), entry.getValue()));
}
return list;
}
static private void removeCommonParticipants(List<ParticipantId> ids1, List<ParticipantId> ids2,
List<ParticipantId> outputIds1, List<ParticipantId> outputIds2) {
int index = 0;
outerLoop:
for (ParticipantId id1 : ids1) {
while (index < ids2.size()) {
ParticipantId id2 = ids2.get(index);
int comparison = participantComparator.compare(id1, id2);
if (comparison < 0) {
break;
}
++index;
if (comparison > 0) {
outputIds2.add(id2);
} else {
continue outerLoop;
}
}
outputIds1.add(id1);
}
for (; index < ids2.size(); ++index) {
outputIds2.add(ids2.get(index));
}
}
static private void transformDocumentOperations(
List<DocumentOperations> clientOps,
List<DocumentOperations> serverOps,
List<DocumentOperations> transformedClientOps,
List<DocumentOperations> transformedServerOps) throws TransformException {
int index = 0;
outerLoop:
for (DocumentOperations fromClient : clientOps) {
while (index < serverOps.size()) {
DocumentOperations fromServer = serverOps.get(index);
int comparison = fromClient.id.compareTo(fromServer.id);
if (comparison < 0) {
break;
}
++index;
if (comparison > 0) {
transformedServerOps.add(fromServer);
} else {
DocOp clientOp = fromClient.operations.composeAll();
DocOp serverOp = fromServer.operations.composeAll();
OperationPair<DocOp> transformedOps = Transformer.transform(clientOp, serverOp);
transformedClientOps.add(new DocumentOperations(fromClient.id,
new DocOpList.Singleton(transformedOps.clientOp())));
transformedServerOps.add(new DocumentOperations(fromClient.id,
new DocOpList.Singleton(transformedOps.serverOp())));
continue outerLoop;
}
}
transformedClientOps.add(fromClient);
}
for (; index < serverOps.size(); ++index) {
transformedServerOps.add(serverOps.get(index));
}
}
private final List<ParticipantId> participantsToRemove;
private final List<ParticipantId> participantsToAdd;
private final List<DocumentOperations> docOps;
private AggregateOperation(List<ParticipantId> toRemove, List<ParticipantId> toAdd,
List<DocumentOperations> docOps) {
participantsToRemove = toRemove;
participantsToAdd = toAdd;
this.docOps = docOps;
}
/**
* Constructs an aggregate operation that does nothing.
*/
AggregateOperation() {
participantsToRemove = Collections.emptyList();
participantsToAdd = Collections.emptyList();
docOps = Collections.emptyList();
}
// The "Core" operations are simpler variants of the regular operations,
// used in the open source org.waveprotocol federation implementation.
/**
* Constructs an aggregate operation that has the same behaviour as a
* <code>CoreWaveletDocumentOperation</code>.
*
* @param waveletDocumentOperation The wavelet document operation.
*/
AggregateOperation(CoreWaveletDocumentOperation waveletDocumentOperation) {
participantsToRemove = Collections.emptyList();
participantsToAdd = Collections.emptyList();
docOps = Collections.singletonList(
new DocumentOperations(
waveletDocumentOperation.getDocumentId(),
new DocOpList.Singleton(waveletDocumentOperation.getOperation())));
}
/**
* Constructs an aggregate operation that has the same behaviour as a
* <code>CoreRemoveParticipant</code>.
*
* @param removeParticipant
*/
AggregateOperation(CoreRemoveParticipant removeParticipant) {
participantsToRemove = Collections.singletonList(removeParticipant.getParticipantId());
participantsToAdd = Collections.emptyList();
docOps = Collections.emptyList();
}
/**
* Constructs an aggregate operation that has the same behaviour as an
* <code>CoreAddParticipant</code>.
*
* @param addParticipant
*/
AggregateOperation(CoreAddParticipant addParticipant) {
participantsToRemove = Collections.emptyList();
participantsToAdd = Collections.singletonList(addParticipant.getParticipantId());
docOps = Collections.emptyList();
}
/**
* Constructs an aggregate operation that has the same behaviour as a
* <code>WaveletBlipOperation</code>.
*
* @param op The wavelet blip operation.
*/
AggregateOperation(WaveletBlipOperation op) {
participantsToRemove = Collections.emptyList();
participantsToAdd = Collections.emptyList();
if (op.getBlipOp() instanceof BlipContentOperation) {
docOps = Collections.singletonList(
new DocumentOperations(
op.getBlipId(),
new DocOpList.Singleton(((BlipContentOperation) op.getBlipOp()).getContentOp())));
} else {
docOps = Collections.emptyList();
}
}
/**
* Constructs an aggregate operation that has the same behaviour as a
* <code>RemoveParticipant</code>.
*
* @param removeParticipant
*/
AggregateOperation(RemoveParticipant removeParticipant) {
ParticipantId participant = new ParticipantId(
removeParticipant.getParticipantId().getAddress());
participantsToRemove = Collections.singletonList(participant);
participantsToAdd = Collections.emptyList();
docOps = Collections.emptyList();
}
/**
* Constructs an aggregate operation that has the same behaviour as an
* <code>AddParticipant</code>.
*
* @param addParticipant
*/
AggregateOperation(AddParticipant addParticipant) {
ParticipantId participant = new ParticipantId(addParticipant.getParticipantId().getAddress());
participantsToRemove = Collections.emptyList();
participantsToAdd = Collections.singletonList(participant);
docOps = Collections.emptyList();
}
/**
* Inverts this aggregate operation.
*
* @return this aggregate operation.
*/
AggregateOperation invert() {
List<DocumentOperations> invertedDocOps = new ArrayList<DocumentOperations>(docOps.size());
for (DocumentOperations operations : docOps) {
invertedDocOps.add(new DocumentOperations(operations.id, invert(operations.operations)));
}
return new AggregateOperation(participantsToAdd, participantsToRemove, invertedDocOps);
}
/**
* Creates a list of wavelet operations representing the behaviour of this
* aggregate operation.
*
* @return The list of wavelet operations representing the behaviour of this
* aggregate operation.
*/
List<CoreWaveletOperation> toCoreWaveletOperations() {
List<CoreWaveletOperation> operations = new ArrayList<CoreWaveletOperation>();
for (ParticipantId participant : participantsToRemove) {
operations.add(new CoreRemoveParticipant(participant));
}
for (DocumentOperations documentOps : docOps) {
operations.add(new CoreWaveletDocumentOperation(documentOps.id,
documentOps.operations.composeAll()));
}
for (ParticipantId participant : participantsToAdd) {
operations.add(new CoreAddParticipant(participant));
}
return operations;
}
}