/** * 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.common; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.protobuf.ByteString; import org.waveprotocol.box.common.comms.WaveClientRpc.DocumentSnapshot; import org.waveprotocol.box.common.comms.WaveClientRpc.ProtocolWaveletUpdate; import org.waveprotocol.box.common.comms.WaveClientRpc.WaveletSnapshot; import org.waveprotocol.box.server.util.WaveletDataUtil; import org.waveprotocol.wave.federation.Proto.ProtocolDocumentOperation; 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.AnnotationBoundaryMap; import org.waveprotocol.wave.model.document.operation.Attributes; import org.waveprotocol.wave.model.document.operation.AttributesUpdate; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.document.operation.DocOpCursor; import org.waveprotocol.wave.model.document.operation.impl.AnnotationBoundaryMapImpl; import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl; import org.waveprotocol.wave.model.document.operation.impl.AttributesUpdateImpl; import org.waveprotocol.wave.model.document.operation.impl.DocOpBuilder; import org.waveprotocol.wave.model.id.WaveletName; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.core.CoreAddParticipant; 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.BlipOperationVisitor; import org.waveprotocol.wave.model.operation.wave.NoOp; import org.waveprotocol.wave.model.operation.wave.RemoveParticipant; import org.waveprotocol.wave.model.operation.wave.SubmitBlip; import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletBlipOperation; import org.waveprotocol.wave.model.operation.wave.WaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletOperation; import org.waveprotocol.wave.model.operation.wave.WaveletOperationContext; import org.waveprotocol.wave.model.schema.SchemaCollection; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.wave.Constants; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.data.ObservableWaveletData; import org.waveprotocol.wave.model.wave.data.core.CoreWaveletData; import org.waveprotocol.wave.model.wave.data.core.impl.CoreWaveletDataImpl; import org.waveprotocol.wave.model.wave.data.impl.DataUtil; import java.util.List; import java.util.Map; /** * Utility class for serialising/deserialising wavelet operations (and their components) to/from * their protocol buffer representations (and their components). * * */ public class CoreWaveletOperationSerializer { private CoreWaveletOperationSerializer() { } /** * Serializes an untransformed delta as a {@link ProtocolWaveletDelta}. * * @param delta to serialize * @return serialized protocol buffer wavelet delta */ public static ProtocolWaveletDelta serialize(WaveletDelta delta) { ProtocolWaveletDelta.Builder protobufDelta = ProtocolWaveletDelta.newBuilder(); for (WaveletOperation waveletOp : delta) { protobufDelta.addOperation(serialize(waveletOp)); } protobufDelta.setAuthor(delta.getAuthor().getAddress()); protobufDelta.setHashedVersion(serialize(delta.getTargetVersion())); return protobufDelta.build(); } /** * Serializes a transformed delta as a {@link ProtocolWaveletDelta}. * * The target version of the result does not have an accompanying hash; * server delta hashes are redundant in the client/server protocol. * * @param delta to serialize * @return serialized protocol buffer wavelet delta */ public static ProtocolWaveletDelta serialize(TransformedWaveletDelta delta) { ProtocolWaveletDelta.Builder protobufDelta = ProtocolWaveletDelta.newBuilder(); for (WaveletOperation waveletOp : delta) { protobufDelta.addOperation(serialize(waveletOp)); } protobufDelta.setAuthor(delta.getAuthor().getAddress()); protobufDelta.setHashedVersion(serialize(HashedVersion.unsigned(delta.getAppliedAtVersion()))); return protobufDelta.build(); } /** * Serializes a wavelet operation as a {@link ProtocolWaveletOperation}. * * @param waveletOp wavelet operation to serialize * @return serialized protocol buffer wavelet operation */ public static ProtocolWaveletOperation serialize(WaveletOperation waveletOp) { ProtocolWaveletOperation.Builder protobufOp = ProtocolWaveletOperation.newBuilder(); if (waveletOp instanceof NoOp) { protobufOp.setNoOp(true); } else if (waveletOp instanceof AddParticipant) { protobufOp.setAddParticipant( ((AddParticipant) waveletOp).getParticipantId().getAddress()); } else if (waveletOp instanceof RemoveParticipant) { protobufOp.setRemoveParticipant( ((RemoveParticipant) waveletOp).getParticipantId().getAddress()); } else if (waveletOp instanceof WaveletBlipOperation) { final WaveletBlipOperation wbOp = (WaveletBlipOperation) waveletOp; final ProtocolWaveletOperation.MutateDocument.Builder mutation = ProtocolWaveletOperation.MutateDocument.newBuilder(); mutation.setDocumentId(wbOp.getBlipId()); wbOp.getBlipOp().acceptVisitor(new BlipOperationVisitor() { @Override public void visitBlipContentOperation(BlipContentOperation blipOp) { mutation.setDocumentOperation(serialize(blipOp.getContentOp())); } @Override public void visitSubmitBlip(SubmitBlip op) { throw new IllegalArgumentException("Unsupported blip operation: " + wbOp.getBlipOp()); } }); protobufOp.setMutateDocument(mutation.build()); } else { throw new IllegalArgumentException("Unsupported wavelet operation: " + waveletOp); } return protobufOp.build(); } /** * Serialize a {@link DocOp} as a {@link ProtocolDocumentOperation}. * * @param inputOp document operation to serialize * @return serialized protocol buffer document operation */ public static ProtocolDocumentOperation serialize(DocOp inputOp) { final ProtocolDocumentOperation.Builder output = ProtocolDocumentOperation.newBuilder(); inputOp.apply(new DocOpCursor() { private ProtocolDocumentOperation.Component.Builder newComponentBuilder() { return ProtocolDocumentOperation.Component.newBuilder(); } @Override public void retain(int itemCount) { output.addComponent(newComponentBuilder().setRetainItemCount(itemCount).build()); } @Override public void characters(String characters) { output.addComponent(newComponentBuilder().setCharacters(characters).build()); } @Override public void deleteCharacters(String characters) { output.addComponent(newComponentBuilder().setDeleteCharacters(characters).build()); } @Override public void elementStart(String type, Attributes attributes) { ProtocolDocumentOperation.Component.ElementStart e = makeElementStart(type, attributes); output.addComponent(newComponentBuilder().setElementStart(e).build()); } @Override public void deleteElementStart(String type, Attributes attributes) { ProtocolDocumentOperation.Component.ElementStart e = makeElementStart(type, attributes); output.addComponent(newComponentBuilder().setDeleteElementStart(e).build()); } private ProtocolDocumentOperation.Component.ElementStart makeElementStart( String type, Attributes attributes) { ProtocolDocumentOperation.Component.ElementStart.Builder e = ProtocolDocumentOperation.Component.ElementStart.newBuilder(); e.setType(type); for (String name : attributes.keySet()) { e.addAttribute(ProtocolDocumentOperation.Component.KeyValuePair.newBuilder() .setKey(name).setValue(attributes.get(name)).build()); } return e.build(); } @Override public void elementEnd() { output.addComponent(newComponentBuilder().setElementEnd(true).build()); } @Override public void deleteElementEnd() { output.addComponent(newComponentBuilder().setDeleteElementEnd(true).build()); } @Override public void replaceAttributes(Attributes oldAttributes, Attributes newAttributes) { ProtocolDocumentOperation.Component.ReplaceAttributes.Builder r = ProtocolDocumentOperation.Component.ReplaceAttributes.newBuilder(); if (oldAttributes.isEmpty() && newAttributes.isEmpty()) { r.setEmpty(true); } else { for (String name : oldAttributes.keySet()) { r.addOldAttribute(ProtocolDocumentOperation.Component.KeyValuePair.newBuilder() .setKey(name).setValue(oldAttributes.get(name)).build()); } for (String name : newAttributes.keySet()) { r.addNewAttribute(ProtocolDocumentOperation.Component.KeyValuePair.newBuilder() .setKey(name).setValue(newAttributes.get(name)).build()); } } output.addComponent(newComponentBuilder().setReplaceAttributes(r.build()).build()); } @Override public void updateAttributes(AttributesUpdate attributes) { ProtocolDocumentOperation.Component.UpdateAttributes.Builder u = ProtocolDocumentOperation.Component.UpdateAttributes.newBuilder(); if (attributes.changeSize() == 0) { u.setEmpty(true); } else { for (int i = 0; i < attributes.changeSize(); i++) { u.addAttributeUpdate(makeKeyValueUpdate( attributes.getChangeKey(i), attributes.getOldValue(i), attributes.getNewValue(i))); } } output.addComponent(newComponentBuilder().setUpdateAttributes(u.build()).build()); } @Override public void annotationBoundary(AnnotationBoundaryMap map) { ProtocolDocumentOperation.Component.AnnotationBoundary.Builder a = ProtocolDocumentOperation.Component.AnnotationBoundary.newBuilder(); if (map.endSize() == 0 && map.changeSize() == 0) { a.setEmpty(true); } else { for (int i = 0; i < map.endSize(); i++) { a.addEnd(map.getEndKey(i)); } for (int i = 0; i < map.changeSize(); i++) { a.addChange(makeKeyValueUpdate( map.getChangeKey(i), map.getOldValue(i), map.getNewValue(i))); } } output.addComponent(newComponentBuilder().setAnnotationBoundary(a.build()).build()); } private ProtocolDocumentOperation.Component.KeyValueUpdate makeKeyValueUpdate( String key, String oldValue, String newValue) { ProtocolDocumentOperation.Component.KeyValueUpdate.Builder kvu = ProtocolDocumentOperation.Component.KeyValueUpdate.newBuilder(); kvu.setKey(key); if (oldValue != null) { kvu.setOldValue(oldValue); } if (newValue != null) { kvu.setNewValue(newValue); } return kvu.build(); } }); return output.build(); } /** * Deserializes a {@link ProtocolWaveletDelta} as an untransformed wavelet * delta. */ public static WaveletDelta deserialize(ProtocolWaveletDelta delta) { List<WaveletOperation> ops = Lists.newArrayList(); for (ProtocolWaveletOperation op : delta.getOperationList()) { WaveletOperationContext context = new WaveletOperationContext( ParticipantId.ofUnsafe(delta.getAuthor()), Constants.NO_TIMESTAMP, 1); ops.add(deserialize(op, context)); } HashedVersion hashedVersion = deserialize(delta.getHashedVersion()); return new WaveletDelta(new ParticipantId(delta.getAuthor()), deserialize(delta.getHashedVersion()), ops); } /** * Deserializes a {@link ProtocolWaveletDelta} as a transformed wavelet delta. */ public static TransformedWaveletDelta deserialize(ProtocolWaveletDelta delta, HashedVersion resultingVersion, long applicationTimestamp) { ParticipantId author = ParticipantId.ofUnsafe(delta.getAuthor()); int count = delta.getOperationCount(); Preconditions.checkArgument(count > 0, "Cannot deserialize an empty delta"); List<WaveletOperation> ops = Lists.newArrayListWithCapacity(count); if (count > 1) { WaveletOperationContext context = new WaveletOperationContext(author, applicationTimestamp, 1); for (int i = 0; i < count - 1; i++) { ProtocolWaveletOperation op = delta.getOperation(i); ops.add(deserialize(op, context)); } } WaveletOperationContext context = new WaveletOperationContext(author, applicationTimestamp, 1, resultingVersion); ops.add(deserialize(delta.getOperation(count - 1), context)); return new TransformedWaveletDelta(author, resultingVersion, applicationTimestamp, ops); } /** Deserializes a protobuf to a HashedVersion POJO. */ public static HashedVersion deserialize(ProtocolHashedVersion hashedVersion) { final ByteString historyHash = hashedVersion.getHistoryHash(); return HashedVersion.of(hashedVersion.getVersion(), historyHash.toByteArray()); } /** Serializes a HashedVersion POJO to a protobuf. */ public static ProtocolHashedVersion serialize(HashedVersion hashedVersion) { return ProtocolHashedVersion.newBuilder().setVersion(hashedVersion.getVersion()). setHistoryHash(ByteString.copyFrom(hashedVersion.getHistoryHash())).build(); } /** * Deserialize a {@link ProtocolWaveletOperation} as a {@link WaveletOperation}. * * @param protobufOp protocol buffer wavelet operation to deserialize * @return deserialized wavelet operation */ public static WaveletOperation deserialize(ProtocolWaveletOperation protobufOp, WaveletOperationContext context) { if (protobufOp.hasNoOp()) { return new NoOp(context); } else if (protobufOp.hasAddParticipant()) { return new AddParticipant(context, new ParticipantId(protobufOp.getAddParticipant())); } else if (protobufOp.hasRemoveParticipant()) { return new RemoveParticipant(context, new ParticipantId(protobufOp.getRemoveParticipant())); } else if (protobufOp.hasMutateDocument()) { return new WaveletBlipOperation(protobufOp.getMutateDocument().getDocumentId(), new BlipContentOperation(context, deserialize(protobufOp.getMutateDocument().getDocumentOperation()))); } else { throw new IllegalArgumentException("Unsupported operation: " + protobufOp); } } /** * Deserialize a {@link WaveletSnapshot} into a list of * {@link WaveletOperation}s. * * @param snapshot snapshot protocol buffer to deserialize * @return a list of operations */ public static List<CoreWaveletOperation> deserialize(WaveletSnapshot snapshot) { List<CoreWaveletOperation> ops = Lists.newArrayList(); for (String participant : snapshot.getParticipantIdList()) { CoreAddParticipant addOp = new CoreAddParticipant(new ParticipantId(participant)); ops.add(addOp); } for (DocumentSnapshot document : snapshot.getDocumentList()) { CoreWaveletDocumentOperation docOp = new CoreWaveletDocumentOperation( document.getDocumentId(), deserialize(document.getDocumentOperation())); ops.add(docOp); } return ops; } /** * Deserialize a {@link ProtocolDocumentOperation} into a {@link DocOp}. * * @param op protocol buffer document operation to deserialize * @return deserialized DocOp */ public static DocOp deserialize(ProtocolDocumentOperation op) { DocOpBuilder output = new DocOpBuilder(); for (ProtocolDocumentOperation.Component c : op.getComponentList()) { if (c.hasAnnotationBoundary()) { if (c.getAnnotationBoundary().getEmpty()) { output.annotationBoundary(AnnotationBoundaryMapImpl.EMPTY_MAP); } else { String[] ends = new String[c.getAnnotationBoundary().getEndCount()]; String[] changeKeys = new String[c.getAnnotationBoundary().getChangeCount()]; String[] oldValues = new String[c.getAnnotationBoundary().getChangeCount()]; String[] newValues = new String[c.getAnnotationBoundary().getChangeCount()]; if (c.getAnnotationBoundary().getEndCount() > 0) { c.getAnnotationBoundary().getEndList().toArray(ends); } for (int i = 0; i < changeKeys.length; i++) { ProtocolDocumentOperation.Component.KeyValueUpdate kvu = c.getAnnotationBoundary().getChange(i); changeKeys[i] = kvu.getKey(); oldValues[i] = kvu.hasOldValue() ? kvu.getOldValue() : null; newValues[i] = kvu.hasNewValue() ? kvu.getNewValue() : null; } output.annotationBoundary( new AnnotationBoundaryMapImpl(ends, changeKeys, oldValues, newValues)); } } else if (c.hasCharacters()) { output.characters(c.getCharacters()); } else if (c.hasElementStart()) { Map<String, String> attributesMap = Maps.newHashMap(); for (ProtocolDocumentOperation.Component.KeyValuePair pair : c.getElementStart().getAttributeList()) { attributesMap.put(pair.getKey(), pair.getValue()); } output.elementStart(c.getElementStart().getType(), new AttributesImpl(attributesMap)); } else if (c.hasElementEnd()) { output.elementEnd(); } else if (c.hasRetainItemCount()) { output.retain(c.getRetainItemCount()); } else if (c.hasDeleteCharacters()) { output.deleteCharacters(c.getDeleteCharacters()); } else if (c.hasDeleteElementStart()) { Map<String, String> attributesMap = Maps.newHashMap(); for (ProtocolDocumentOperation.Component.KeyValuePair pair : c.getDeleteElementStart().getAttributeList()) { attributesMap.put(pair.getKey(), pair.getValue()); } output.deleteElementStart(c.getDeleteElementStart().getType(), new AttributesImpl(attributesMap)); } else if (c.hasDeleteElementEnd()) { output.deleteElementEnd(); } else if (c.hasReplaceAttributes()) { if (c.getReplaceAttributes().getEmpty()) { output.replaceAttributes(AttributesImpl.EMPTY_MAP, AttributesImpl.EMPTY_MAP); } else { Map<String, String> oldAttributesMap = Maps.newHashMap(); Map<String, String> newAttributesMap = Maps.newHashMap(); for (ProtocolDocumentOperation.Component.KeyValuePair pair : c.getReplaceAttributes().getOldAttributeList()) { oldAttributesMap.put(pair.getKey(), pair.getValue()); } for (ProtocolDocumentOperation.Component.KeyValuePair pair : c.getReplaceAttributes().getNewAttributeList()) { newAttributesMap.put(pair.getKey(), pair.getValue()); } output.replaceAttributes(new AttributesImpl(oldAttributesMap), new AttributesImpl(newAttributesMap)); } } else if (c.hasUpdateAttributes()) { if (c.getUpdateAttributes().getEmpty()) { output.updateAttributes(AttributesUpdateImpl.EMPTY_MAP); } else { String[] triplets = new String[c.getUpdateAttributes().getAttributeUpdateCount()*3]; for (int i = 0, j = 0; i < c.getUpdateAttributes().getAttributeUpdateCount(); i++) { ProtocolDocumentOperation.Component.KeyValueUpdate kvu = c.getUpdateAttributes().getAttributeUpdate(i); triplets[j++] = kvu.getKey(); triplets[j++] = kvu.hasOldValue() ? kvu.getOldValue() : null; triplets[j++] = kvu.hasNewValue() ? kvu.getNewValue() : null; } output.updateAttributes(new AttributesUpdateImpl(triplets)); } } else { //throw new IllegalArgumentException("Unsupported operation component: " + c); } } return output.build(); } /** * Deserializes the snapshot contained in the {@link ProtocolWaveletUpdate} * into a {@link ObservableWaveletData}. * * @param snapshot the {@link WaveletSnapshot} to deserialize. * @param version the version of the wavelet after all deltas have been * applied. * @param waveletName the name of the wavelet contained in the update. * @throws OperationException if the ops in the snapshot can not be applied. */ public static ObservableWaveletData deserializeSnapshot( WaveletSnapshot snapshot, ProtocolHashedVersion version, WaveletName waveletName) throws OperationException { // TODO(ljvderijk): This method does too many steps to get to the // ObservableWaveletData. // We need something simpler when operations and the protocol have been // edited. // Creating a CoreWaveletData because the current protocol lacks the // meta-data required to construct an ObservableWaveletData directly. // But this results in unnecessary object creation and copies. CoreWaveletData coreWavelet = new CoreWaveletDataImpl(waveletName.waveId, waveletName.waveletId); Preconditions.checkArgument(snapshot.getParticipantIdCount() > 0); // Have to add a single participant for the copying to complete without a // NPE. coreWavelet.addParticipant(ParticipantId.ofUnsafe(snapshot.getParticipantId(0))); for (DocumentSnapshot document : snapshot.getDocumentList()) { DocOp op = CoreWaveletOperationSerializer.deserialize(document.getDocumentOperation()); coreWavelet.modifyDocument(document.getDocumentId(), op); } HashedVersion hashedVersion = CoreWaveletOperationSerializer.deserialize(version); ObservableWaveletData immutableWaveletData = DataUtil.fromCoreWaveletData(coreWavelet, hashedVersion, SchemaCollection.empty()); ObservableWaveletData wavelet = WaveletDataUtil.copyWavelet(immutableWaveletData); for (String participant : snapshot.getParticipantIdList()) { wavelet.addParticipant(new ParticipantId(participant)); } return wavelet; } }