/** * 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.document.operation.algorithm; 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.BufferedDocOp; import org.waveprotocol.wave.model.document.operation.DocOpCursor; import org.waveprotocol.wave.model.document.operation.EvaluatingDocOpCursor; import org.waveprotocol.wave.model.document.operation.impl.AnnotationBoundaryMapImpl; import org.waveprotocol.wave.model.document.operation.impl.AttributesUpdateImpl; import org.waveprotocol.wave.model.document.operation.impl.DocOpBuffer; import org.waveprotocol.wave.model.operation.OperationPair; import org.waveprotocol.wave.model.operation.TransformException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * A utility class for transforming document operations. * * TODO: Make detection of illegal transformations more thorough. * TODO: Reorganise this class to be a little less klunky. */ public final class Transformer { /** * For internal error propagation. It is a RuntimeException to facilitate * error propagation through document.operation interfaces from outside * this package. */ private static class InternalTransformException extends RuntimeException { InternalTransformException(String message) { super(message); } } private static abstract class AnnotationTracker { final Map<String, ValueUpdate> active = new HashMap<String, ValueUpdate>(); private final List<AnnotationBoundaryMap> maps = new ArrayList<AnnotationBoundaryMap>(); private final AnnotationProcessor processor; AnnotationTracker(AnnotationProcessor processor) { this.processor = processor; } final void buffer(AnnotationBoundaryMap map) { maps.add(map); } final void flush() { processAll(); } final void sync() { processor.sync(); } final void startDeletion() { opposingTracker().processAll(); processor.cursor.annotationBoundary(opposingTracker().processor.toSynced(processor.active)); } final void endDeletion() { processor.cursor.annotationBoundary(opposingTracker().processor.fromSynced(processor.active)); } final void processAll() { for (AnnotationBoundaryMap map : maps) { process(map); } maps.clear(); } private final void process(AnnotationBoundaryMap map) { for (int i = 0; i < map.endSize(); ++i) { active.remove(map.getEndKey(i)); } for (int i = 0; i < map.changeSize(); ++i) { active.put(map.getChangeKey(i), new ValueUpdate(map.getOldValue(i), map.getNewValue(i))); } processUpdate(map); } abstract void processUpdate(AnnotationBoundaryMap map); abstract AnnotationTracker opposingTracker(); } private static final class AnnotationProcessor { final DocOpCursor cursor; private final Map<String, ValueUpdate> active = new HashMap<String, ValueUpdate>(); private final Map<String, ValueUpdate> unsynced = new HashMap<String, ValueUpdate>(); AnnotationProcessor(DocOpCursor cursor) { this.cursor = cursor; } void process(AnnotationBoundaryMap map) { for (int i = 0; i < map.endSize(); ++i) { String key = map.getEndKey(i); if (!unsynced.containsKey(key)) { unsynced.put(key, active.get(key)); } active.remove(key); } for (int i = 0; i < map.changeSize(); ++i) { String key = map.getChangeKey(i); if (!unsynced.containsKey(key)) { unsynced.put(key, active.get(key)); } active.put(key, new ValueUpdate(map.getOldValue(i), map.getNewValue(i))); } cursor.annotationBoundary(map); } void sync() { unsynced.clear(); } AnnotationBoundaryMap toSynced(Map<String, ValueUpdate> toCombine) { // TODO: This seems pretty awkward. Perhaps we should give // AnnotationBoundaryMapImpl an easier builder to use. List<String> changeKeys = new ArrayList<String>(); List<String> changeOldValues = new ArrayList<String>(); List<String> changeNewValues = new ArrayList<String>(); for (Map.Entry<String, ValueUpdate> entry : unsynced.entrySet()) { String key = entry.getKey(); ValueUpdate values = entry.getValue(); if (values == null) { ValueUpdate update = active.get(key); if (update != null) { ValueUpdate forCombining = toCombine.get(key); changeKeys.add(key); changeOldValues.add((forCombining != null) ? forCombining.oldValue : update.newValue); changeNewValues.add(update.oldValue); } } else { ValueUpdate forCombining = toCombine.get(key); changeKeys.add(key); if (forCombining != null) { changeOldValues.add(forCombining.oldValue); } else { ValueUpdate update = active.get(key); changeOldValues.add((update != null) ? update.newValue : values.oldValue); } changeNewValues.add(values.newValue); } } return new AnnotationBoundaryMapImpl( new String[0], changeKeys.toArray(new String[0]), changeOldValues.toArray(new String[0]), changeNewValues.toArray(new String[0])); } AnnotationBoundaryMap fromSynced(Map<String, ValueUpdate> toCombine) { // TODO: This seems pretty awkward. Perhaps we should give // AnnotationBoundaryMapImpl an easier builder to use. List<String> endKeys = new ArrayList<String>(); List<String> changeKeys = new ArrayList<String>(); List<String> changeOldValues = new ArrayList<String>(); List<String> changeNewValues = new ArrayList<String>(); for (Map.Entry<String, ValueUpdate> entry : unsynced.entrySet()) { String key = entry.getKey(); ValueUpdate values = entry.getValue(); if (values != null || active.containsKey(key)) { ValueUpdate update = toCombine.get(key); if (update != null) { changeKeys.add(key); changeOldValues.add(update.oldValue); changeNewValues.add(update.newValue); } else { endKeys.add(key); } } } return new AnnotationBoundaryMapImpl( endKeys.toArray(new String[0]), changeKeys.toArray(new String[0]), changeOldValues.toArray(new String[0]), changeNewValues.toArray(new String[0])); } } /** * The relative position of one cursor relative to a second cursor. */ private interface RelativePosition { /** * Increase the relative position of the cursor. * * @param amount The amount by which to increase the relative position. */ void increase(int amount); /** * @return The relative position. */ int get(); } /** * A cache for the effect of a component of a document mutation that affects a * range of the document. */ private static abstract class RangeCache { abstract void resolveRetain(int retain); void resolveDeleteCharacters(String characters) { throw new InternalTransformException("Incompatible operations in transformation"); } void resolveDeleteElementStart(String type, Attributes attributes) { throw new InternalTransformException("Incompatible operations in transformation"); } void resolveDeleteElementEnd() { throw new InternalTransformException("Incompatible operations in transformation"); } void resolveReplaceAttributes(Attributes oldAttributes, Attributes newAttributes) { throw new InternalTransformException("Incompatible operations in transformation"); } void resolveUpdateAttributes(AttributesUpdate update) { throw new InternalTransformException("Incompatible operations in transformation"); } } /** * A resolver for mutation components which affects ranges. */ private interface RangeResolver { /** * Resolves a mutation component with a cached mutation component from a * different document mutation. * * @param size The size of the range affected by the range modifications to * resolve. * @param cache The cached range. */ void resolve(int size, RangeCache cache); } /** * A tracker that tracks the positions of two cursors relative to each other. */ private static final class PositionTracker { int position = 0; /** * @return A RelativePosition representing the client's position relative to * the server's position. */ RelativePosition getClientPosition() { return new RelativePosition() { public void increase(int amount) { position += amount; } public int get() { return position; } }; } /** * @return A RelativePosition representing the server's position relative to * the client's position. */ RelativePosition getServerPosition() { return new RelativePosition() { public void increase(int amount) { position -= amount; } public int get() { return -position; } }; } } /** * A resolver for "deleteCharacters" mutation components. */ private static final class DeleteCharactersResolver implements RangeResolver { private final String characters; DeleteCharactersResolver(String characters) { this.characters = characters; } @Override public void resolve(int size, RangeCache range) { range.resolveDeleteCharacters(characters.substring(0, size)); } } /** * A resolver for "deleteElementStart" mutation components. */ private static final class DeleteElementStartResolver implements RangeResolver { private final String type; private final Attributes attributes; DeleteElementStartResolver(String type, Attributes attributes) { this.type = type; this.attributes = attributes; } @Override public void resolve(int size, RangeCache range) { range.resolveDeleteElementStart(type, attributes); } } /** * A resolver for "replaceAttributes" mutation components. */ private static final class ReplaceAttributesResolver implements RangeResolver { private final Attributes oldAttributes; private final Attributes newAttributes; ReplaceAttributesResolver(Attributes oldAttributes, Attributes newAttributes) { this.oldAttributes = oldAttributes; this.newAttributes = newAttributes; } @Override public void resolve(int size, RangeCache range) { range.resolveReplaceAttributes(oldAttributes, newAttributes); } } /** * A resolver for "updateAttributes" mutation components. */ private static final class UpdateAttributesResolver implements RangeResolver { private final AttributesUpdate update; UpdateAttributesResolver(AttributesUpdate update) { this.update = update; } @Override public void resolve(int size, RangeCache range) { range.resolveUpdateAttributes(update); } } /** * A resolver for "retain" mutation components. */ private static final RangeResolver retainResolver = new RangeResolver() { @Override public void resolve(int size, RangeCache range) { range.resolveRetain(size); } }; /** * A resolver for "deleteElementEnd" mutation components. */ private static final RangeResolver deleteElementEndResolver = new RangeResolver() { @Override public void resolve(int size, RangeCache range) { range.resolveDeleteElementEnd(); } }; /** * A target of a document mutation which can be used to transform document * mutations by making use primarily of information from one mutation with the * help of auxiliary information from a second mutation. These targets should * be used in pairs. */ private static final class Target implements EvaluatingDocOpCursor<BufferedDocOp> { private final class DeleteCharactersCache extends RangeCache { private String characters; DeleteCharactersCache(String characters) { this.characters = characters; } @Override void resolveRetain(int itemCount) { doDeleteCharacters(characters.substring(0, itemCount)); characters = characters.substring(itemCount); } @Override void resolveDeleteCharacters(String characters) { dualDeletion(); this.characters = this.characters.substring(characters.length()); } } private final class DeleteElementStartCache extends RangeCache { private final String type; private final Attributes attributes; DeleteElementStartCache(String type, Attributes attributes) { this.type = type; this.attributes = attributes; } @Override void resolveRetain(int itemCount) { doDeleteElementStart(type, attributes); ++depth; } @Override void resolveDeleteElementStart(String type, Attributes attributes) { dualDeletion(); ++depth; ++otherTarget.depth; } @Override void resolveReplaceAttributes(Attributes oldAttributes, Attributes newAttributes) { doDeleteElementStart(type, newAttributes); ++depth; } @Override void resolveUpdateAttributes(AttributesUpdate update) { doDeleteElementStart(type, attributes.updateWith(update)); ++depth; } } private final class DeleteElementEndCache extends RangeCache { @Override void resolveRetain(int itemCount) { doDeleteElementEnd(); --depth; } @Override void resolveDeleteElementEnd() { dualDeletion(); --depth; --otherTarget.depth; } } private final class ReplaceAttributesCache extends RangeCache { private final Attributes oldAttributes; private final Attributes newAttributes; ReplaceAttributesCache(Attributes oldAttributes, Attributes newAttributes) { this.oldAttributes = oldAttributes; this.newAttributes = newAttributes; } @Override void resolveRetain(int itemCount) { flushAnnotations(); targetDocument.replaceAttributes(oldAttributes, newAttributes); otherTarget.targetDocument.retain(1); } @Override void resolveDeleteElementStart(String type, Attributes attributes) { otherTarget.doDeleteElementStart(type, newAttributes); ++otherTarget.depth; } @Override void resolveReplaceAttributes(Attributes oldAttributes, Attributes newAttributes) { flushAnnotations(); targetDocument.replaceAttributes(newAttributes, this.newAttributes); otherTarget.targetDocument.retain(1); } @Override void resolveUpdateAttributes(AttributesUpdate update) { flushAnnotations(); targetDocument.replaceAttributes(this.oldAttributes.updateWith(update), this.newAttributes); otherTarget.targetDocument.retain(1); } } private final class UpdateAttributesCache extends RangeCache { private final AttributesUpdate update; UpdateAttributesCache(AttributesUpdate update) { this.update = update; } @Override void resolveRetain(int itemCount) { flushAnnotations(); targetDocument.updateAttributes(update); otherTarget.targetDocument.retain(1); } @Override void resolveDeleteElementStart(String type, Attributes attributes) { otherTarget.doDeleteElementStart(type, attributes.updateWith(update)); ++otherTarget.depth; } @Override void resolveReplaceAttributes(Attributes oldAttributes, Attributes newAttributes) { flushAnnotations(); targetDocument.retain(1); otherTarget.targetDocument.replaceAttributes(oldAttributes.updateWith(update), newAttributes); } @Override void resolveUpdateAttributes(AttributesUpdate update) { flushAnnotations(); Map<String, String> updated = new HashMap<String, String>(); for (int i = 0; i < update.changeSize(); ++i) { updated.put(update.getChangeKey(i), update.getNewValue(i)); } AttributesUpdate newUpdate = new AttributesUpdateImpl(); // TODO: This is a little silly. We should do this a better way. for (int i = 0; i < this.update.changeSize(); ++i) { String key = this.update.getChangeKey(i); String newOldValue = updated.containsKey(key) ? updated.get(key) : this.update.getOldValue(i); newUpdate = newUpdate.composeWith(new AttributesUpdateImpl(key, newOldValue, this.update.getNewValue(i))); } targetDocument.updateAttributes(newUpdate); Set<String> keySet = new HashSet<String>(); for (int i = 0; i < this.update.changeSize(); ++i) { keySet.add(this.update.getChangeKey(i)); } AttributesUpdate transformedAttributes = update.exclude(keySet); otherTarget.targetDocument.updateAttributes(transformedAttributes); } } private final RangeCache retainCache = new RangeCache() { @Override void resolveRetain(int itemCount) { flushAnnotations(); targetDocument.retain(itemCount); otherTarget.targetDocument.retain(itemCount); } @Override void resolveDeleteCharacters(String characters) { otherTarget.doDeleteCharacters(characters); } @Override void resolveDeleteElementStart(String type, Attributes attributes) { otherTarget.doDeleteElementStart(type, attributes); ++otherTarget.depth; } @Override void resolveDeleteElementEnd() { otherTarget.doDeleteElementEnd(); --otherTarget.depth; } @Override void resolveReplaceAttributes(Attributes oldAttributes, Attributes newAttributes) { flushAnnotations(); targetDocument.retain(1); otherTarget.targetDocument.replaceAttributes(oldAttributes, newAttributes); } @Override void resolveUpdateAttributes(AttributesUpdate update) { flushAnnotations(); targetDocument.retain(1); otherTarget.targetDocument.updateAttributes(update); } }; /** * The target to which to write the transformed mutation. */ private final EvaluatingDocOpCursor<BufferedDocOp> targetDocument; /** * The position of the processing cursor associated with this target * relative to the position of the processing cursor associated to the * opposing target. All positional calculations are based on cursor * positions in the original document on which the two original operations * apply. */ private final RelativePosition relativePosition; /** * An annotation tracker that tracks annotation modifications at the current * cursor position. */ private final AnnotationTracker annotationTracker; /** * The target that is used opposite this target in the transformation. */ private Target otherTarget; /** * A cache for the effect of mutation components which affect ranges. */ private RangeCache rangeCache = retainCache; /** * The current depth of element deletions. */ private int depth = 0; Target(EvaluatingDocOpCursor<BufferedDocOp> targetDocument, RelativePosition relativePosition, AnnotationTracker annotationTracker) { this.targetDocument = targetDocument; this.relativePosition = relativePosition; this.annotationTracker = annotationTracker; } // TODO: See if we can remove this explicit method and find a // better way to do this using a constructor or factory. public void setOtherTarget(Target otherTarget) { this.otherTarget = otherTarget; } public BufferedDocOp finish() { annotationTracker.flush(); return targetDocument.finish(); } @Override public void retain(int itemCount) { resolveRange(itemCount, retainResolver); rangeCache = retainCache; } @Override public void characters(String chars) { if (otherTarget.depth > 0) { otherTarget.annotationTracker.startDeletion(); otherTarget.targetDocument.deleteCharacters(chars); otherTarget.annotationTracker.endDeletion(); } else { prepareForInsertion(); targetDocument.characters(chars); otherTarget.targetDocument.retain(chars.length()); } } @Override public void elementStart(String tag, Attributes attrs) { if (otherTarget.depth > 0) { otherTarget.annotationTracker.startDeletion(); otherTarget.targetDocument.deleteElementStart(tag, attrs); otherTarget.annotationTracker.endDeletion(); } else { prepareForInsertion(); targetDocument.elementStart(tag, attrs); otherTarget.targetDocument.retain(1); } } @Override public void elementEnd() { if (otherTarget.depth > 0) { otherTarget.annotationTracker.startDeletion(); otherTarget.targetDocument.deleteElementEnd(); otherTarget.annotationTracker.endDeletion(); } else { prepareForInsertion(); targetDocument.elementEnd(); otherTarget.targetDocument.retain(1); } } @Override public void deleteCharacters(String chars) { int resolutionSize = resolveRange(chars.length(), new DeleteCharactersResolver(chars)); if (resolutionSize >= 0) { rangeCache = new DeleteCharactersCache(chars.substring(resolutionSize)); } } @Override public void deleteElementStart(String tag, Attributes attrs) { if (resolveRange(1, new DeleteElementStartResolver(tag, attrs)) == 0) { rangeCache = new DeleteElementStartCache(tag, attrs); } } @Override public void deleteElementEnd() { if (resolveRange(1, deleteElementEndResolver) == 0) { rangeCache = new DeleteElementEndCache(); } } @Override public void replaceAttributes(Attributes oldAttrs, Attributes newAttrs) { if (resolveRange(1, new ReplaceAttributesResolver(oldAttrs, newAttrs)) == 0) { rangeCache = new ReplaceAttributesCache(oldAttrs, newAttrs); } } @Override public void updateAttributes(AttributesUpdate attrUpdate) { if (resolveRange(1, new UpdateAttributesResolver(attrUpdate)) == 0) { rangeCache = new UpdateAttributesCache(attrUpdate); } } @Override public void annotationBoundary(AnnotationBoundaryMap map) { annotationTracker.buffer(map); } /** * Resolves the transformation of a range. * * @param size the requested size to resolve * @param resolver the resolver to use * @return the portion of the requested size that was resolved, or -1 to * indicate that the entire range was resolved */ private int resolveRange(int size, RangeResolver resolver) { int oldPosition = relativePosition.get(); relativePosition.increase(size); if (relativePosition.get() > 0) { if (oldPosition < 0) { resolver.resolve(-oldPosition, otherTarget.rangeCache); } return -oldPosition; } else { resolver.resolve(size, otherTarget.rangeCache); return -1; } } private void flushAnnotations() { annotationTracker.flush(); otherTarget.annotationTracker.flush(); annotationTracker.sync(); otherTarget.annotationTracker.sync(); } private void doDeleteCharacters(String chars) { annotationTracker.flush(); annotationTracker.sync(); annotationTracker.startDeletion(); targetDocument.deleteCharacters(chars); annotationTracker.endDeletion(); } private void doDeleteElementStart(String type, Attributes attrs) { annotationTracker.flush(); annotationTracker.sync(); annotationTracker.startDeletion(); targetDocument.deleteElementStart(type, attrs); annotationTracker.endDeletion(); } private void doDeleteElementEnd() { annotationTracker.flush(); annotationTracker.sync(); annotationTracker.startDeletion(); targetDocument.deleteElementEnd(); annotationTracker.endDeletion(); } private void prepareForInsertion() { annotationTracker.flush(); annotationTracker.sync(); otherTarget.annotationTracker.sync(); } private void dualDeletion() { annotationTracker.processAll(); otherTarget.annotationTracker.processAll(); } } private final EvaluatingDocOpCursor<BufferedDocOp> clientOperation = OperationNormalizer.createNormalizer(new DocOpBuffer()); private final EvaluatingDocOpCursor<BufferedDocOp> serverOperation = OperationNormalizer.createNormalizer(new DocOpBuffer()); private final AnnotationProcessor clientAnnotationProcessor = new AnnotationProcessor(clientOperation); private final AnnotationProcessor serverAnnotationProcessor = new AnnotationProcessor(serverOperation); private final AnnotationTracker clientAnnotationTracker = new AnnotationTracker(clientAnnotationProcessor) { @Override void processUpdate(AnnotationBoundaryMap map) { // TODO: This seems pretty awkward. Perhaps we should give // AnnotationBoundaryMapImpl an easier builder to use. List<String> clientEndKeys = new ArrayList<String>(); List<String> clientChangeKeys = new ArrayList<String>(); List<String> clientChangeOldValues = new ArrayList<String>(); List<String> clientChangeNewValues = new ArrayList<String>(); List<String> serverEndKeys = new ArrayList<String>(); List<String> serverChangeKeys = new ArrayList<String>(); List<String> serverChangeOldValues = new ArrayList<String>(); List<String> serverChangeNewValues = new ArrayList<String>(); for (int i = 0; i < map.endSize(); ++i) { String key = map.getEndKey(i); ValueUpdate serverValues = serverAnnotationTracker.active.get(key); clientEndKeys.add(key); if (serverValues != null) { serverChangeKeys.add(key); serverChangeOldValues.add(serverValues.oldValue); serverChangeNewValues.add(serverValues.newValue); } } for (int i = 0; i < map.changeSize(); ++i) { String key = map.getChangeKey(i); String oldValue = map.getOldValue(i); String newValue = map.getNewValue(i); ValueUpdate serverValues = serverAnnotationTracker.active.get(key); clientChangeKeys.add(key); clientChangeNewValues.add(newValue); if (serverValues != null) { clientChangeOldValues.add(serverValues.newValue); serverEndKeys.add(key); } else { clientChangeOldValues.add(oldValue); } } clientAnnotationProcessor.process(new AnnotationBoundaryMapImpl( clientEndKeys.toArray(new String[0]), clientChangeKeys.toArray(new String[0]), clientChangeOldValues.toArray(new String[0]), clientChangeNewValues.toArray(new String[0]))); serverAnnotationProcessor.process(new AnnotationBoundaryMapImpl( serverEndKeys.toArray(new String[0]), serverChangeKeys.toArray(new String[0]), serverChangeOldValues.toArray(new String[0]), serverChangeNewValues.toArray(new String[0]))); } @Override AnnotationTracker opposingTracker() { return serverAnnotationTracker; } }; private final AnnotationTracker serverAnnotationTracker = new AnnotationTracker(serverAnnotationProcessor) { @Override void processUpdate(AnnotationBoundaryMap map) { // TODO: This seems pretty awkward. Perhaps we should give // AnnotationBoundaryMapImpl an easier builder to use. List<String> serverEndKeys = new ArrayList<String>(); List<String> serverChangeKeys = new ArrayList<String>(); List<String> serverChangeOldValues = new ArrayList<String>(); List<String> serverChangeNewValues = new ArrayList<String>(); List<String> clientEndKeys = new ArrayList<String>(); List<String> clientChangeKeys = new ArrayList<String>(); List<String> clientChangeOldValues = new ArrayList<String>(); List<String> clientChangeNewValues = new ArrayList<String>(); for (int i = 0; i < map.endSize(); ++i) { String key = map.getEndKey(i); ValueUpdate clientValues = clientAnnotationTracker.active.get(key); if (clientValues != null) { clientChangeKeys.add(key); clientChangeOldValues.add(clientValues.oldValue); clientChangeNewValues.add(clientValues.newValue); } else { serverEndKeys.add(key); } } for (int i = 0; i < map.changeSize(); ++i) { String key = map.getChangeKey(i); String oldValue = map.getOldValue(i); String newValue = map.getNewValue(i); ValueUpdate clientValues = clientAnnotationTracker.active.get(key); if (clientValues != null) { clientChangeKeys.add(key); clientChangeOldValues.add(newValue); clientChangeNewValues.add(clientValues.newValue); } else { serverChangeKeys.add(key); serverChangeOldValues.add(oldValue); serverChangeNewValues.add(newValue); } } serverAnnotationProcessor.process(new AnnotationBoundaryMapImpl( serverEndKeys.toArray(new String[0]), serverChangeKeys.toArray(new String[0]), serverChangeOldValues.toArray(new String[0]), serverChangeNewValues.toArray(new String[0]))); clientAnnotationProcessor.process(new AnnotationBoundaryMapImpl( clientEndKeys.toArray(new String[0]), clientChangeKeys.toArray(new String[0]), clientChangeOldValues.toArray(new String[0]), clientChangeNewValues.toArray(new String[0]))); } @Override AnnotationTracker opposingTracker() { return clientAnnotationTracker; } }; /** * Transform a pair of operations. * * @param clientOp The operation from the client. * @param serverOp The operation from the server. * @return The transformed pair of operations. * @throws TransformException if a problem was encountered during the * transformation process. */ public OperationPair<BufferedDocOp> transformOperations(BufferedDocOp clientOp, BufferedDocOp serverOp) throws TransformException { try { PositionTracker positionTracker = new PositionTracker(); RelativePosition clientPosition = positionTracker.getClientPosition(); RelativePosition serverPosition = positionTracker.getServerPosition(); // The target responsible for processing components of the client operation. Target clientTarget = new Target(clientOperation, clientPosition, clientAnnotationTracker); // The target responsible for processing components of the server operation. Target serverTarget = new Target(serverOperation, serverPosition, serverAnnotationTracker); clientTarget.setOtherTarget(serverTarget); serverTarget.setOtherTarget(clientTarget); // Incrementally apply the two operations in a linearly-ordered interleaving // fashion. int clientIndex = 0; int serverIndex = 0; while (clientIndex < clientOp.size()) { clientOp.applyComponent(clientIndex++, clientTarget); while (clientPosition.get() > 0) { if (serverIndex >= serverOp.size()) { throw new TransformException("Ran out of " + serverOp.size() + " server op components after " + clientIndex + " of " + clientOp.size() + " client op components, with " + clientPosition.get() + " spare positions"); } serverOp.applyComponent(serverIndex++, serverTarget); } } while (serverIndex < serverOp.size()) { serverOp.applyComponent(serverIndex++, serverTarget); } clientOp = clientTarget.finish(); serverOp = serverTarget.finish(); } catch (InternalTransformException e) { throw new TransformException(e.getMessage()); } return new OperationPair<BufferedDocOp>(clientOp, serverOp); } /** * Transform a pair of operations. * * @param clientOp The operation from the client. * @param serverOp The operation from the server. * @return The transformed pair of operations. * @throws TransformException if a problem was encountered during the * transformation process. */ public static OperationPair<BufferedDocOp> transform(BufferedDocOp clientOp, BufferedDocOp serverOp) throws TransformException { return new Transformer().transformOperations(clientOp, serverOp); } }