/** * 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.operation.testing.reference; 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.EvaluatingDocOpCursor; import org.waveprotocol.wave.model.document.operation.algorithm.OperationNormalizer; import org.waveprotocol.wave.model.document.operation.impl.AnnotationBoundaryMapImpl; import org.waveprotocol.wave.model.document.operation.impl.DocOpBuffer; import org.waveprotocol.wave.model.operation.TransformException; import org.waveprotocol.wave.model.operation.testing.reference.PositionTracker.RelativePosition; import org.waveprotocol.wave.model.util.Pair; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * A utility class for transforming a structure-preserving operation with a * deletion operation. * * @author Alexandre Mah */ final class PreservationDeletionTransformer { /** * 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); } } /** * 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 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 abstract class Target<T> implements EvaluatingDocOpCursor<T> { /** * 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; /** * 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; Target(RelativePosition relativePosition) { this.relativePosition = relativePosition; } // 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; } @Override public void retain(int itemCount) { resolveRange(itemCount, RETAIN_RESOLVER); rangeCache = retainCache; } @Override public void characters(String chars) { throw new UnsupportedOperationException("This method should never be called."); } @Override public void elementStart(String tag, Attributes attrs) { throw new UnsupportedOperationException("This method should never be called."); } @Override public void elementEnd() { throw new UnsupportedOperationException("This method should never be called."); } /** * 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 */ 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; } } void setRangeCache(RangeCache rangeCache) { this.rangeCache = rangeCache; } } /** * 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 final class PreservationTarget extends Target<DocOp> { 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) { processReplaceAttributes(oldAttributes, newAttributes); } @Override void resolveDeleteElementStart(String type, Attributes attributes) { processDeleteElementStart(type, newAttributes); } } private final class UpdateAttributesCache extends RangeCache { private final AttributesUpdate update; UpdateAttributesCache(AttributesUpdate update) { this.update = update; } @Override void resolveRetain(int itemCount) { processUpdateAttributes(update); } @Override void resolveDeleteElementStart(String type, Attributes attributes) { processDeleteElementStart(type, attributes.updateWith(update)); } } PreservationTarget(RelativePosition relativePosition) { super(relativePosition); } @Override public DocOp finish() { return preservationOperation.finish(); } @Override public void deleteCharacters(String chars) { throw new UnsupportedOperationException("This method should never be called."); } @Override public void deleteElementStart(String tag, Attributes attrs) { throw new UnsupportedOperationException("This method should never be called."); } @Override public void deleteElementEnd() { throw new UnsupportedOperationException("This method should never be called."); } @Override public void replaceAttributes(Attributes oldAttrs, Attributes newAttrs) { if (resolveRange(1, new ReplaceAttributesResolver(oldAttrs, newAttrs)) == 0) { setRangeCache(new ReplaceAttributesCache(oldAttrs, newAttrs)); } } @Override public void updateAttributes(AttributesUpdate attrUpdate) { if (resolveRange(1, new UpdateAttributesResolver(attrUpdate)) == 0) { setRangeCache(new UpdateAttributesCache(attrUpdate)); } } @Override public void annotationBoundary(AnnotationBoundaryMap map) { preservationOperation.annotationBoundary(map); for (int i = 0; i < map.endSize(); ++i) { String key = map.getEndKey(i); if (!propagatingAnnotations.containsKey(key)) { propagatingAnnotations.put(key, activeAnnotations.get(key)); } activeAnnotations.remove(key); } for (int i = 0; i < map.changeSize(); ++i) { String key = map.getChangeKey(i); if (!propagatingAnnotations.containsKey(key)) { propagatingAnnotations.put(key, activeAnnotations.get(key)); } activeAnnotations.put(key, new ValueUpdate(map.getOldValue(i), map.getNewValue(i))); } } } /** * 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 final class DeletionTarget extends Target<Pair<DocOp, DocOp>> { private final class DeleteCharactersCache extends RangeCache { private String characters; DeleteCharactersCache(String characters) { this.characters = characters; } @Override void resolveRetain(int itemCount) { processDeleteCharacters(characters.substring(0, itemCount)); characters = characters.substring(itemCount); } @Override void resolveDeleteCharacters(String characters) { 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) { processDeleteElementStart(type, attributes); } @Override void resolveDeleteElementStart(String type, Attributes attributes) {} @Override void resolveReplaceAttributes(Attributes oldAttributes, Attributes newAttributes) { // This point should be unreachable. assert false; processDeleteElementStart(type, newAttributes); } @Override void resolveUpdateAttributes(AttributesUpdate update) { // This point should be unreachable. assert false; processDeleteElementStart(type, attributes.updateWith(update)); } } private final class DeleteElementEndCache extends RangeCache { @Override void resolveRetain(int itemCount) { processDeleteElementEnd(); } @Override void resolveDeleteElementEnd() {} } DeletionTarget(RelativePosition relativePosition) { super(relativePosition); } @Override public Pair<DocOp, DocOp> finish() { return new Pair<DocOp, DocOp>( annotationResidue.finish(), deletionOperation.finish()); } @Override public void retain(int itemCount) { resolveRange(itemCount, RETAIN_RESOLVER); setRangeCache(retainCache); } @Override public void characters(String chars) { throw new UnsupportedOperationException("This method should never be called."); } @Override public void elementStart(String tag, Attributes attrs) { throw new UnsupportedOperationException("This method should never be called."); } @Override public void elementEnd() { throw new UnsupportedOperationException("This method should never be called."); } @Override public void deleteCharacters(String chars) { int resolutionSize = resolveRange(chars.length(), new DeleteCharactersResolver(chars)); if (resolutionSize >= 0) { setRangeCache(new DeleteCharactersCache(chars.substring(resolutionSize))); } } @Override public void deleteElementStart(String tag, Attributes attrs) { if (resolveRange(1, new DeleteElementStartResolver(tag, attrs)) == 0) { setRangeCache(new DeleteElementStartCache(tag, attrs)); } } @Override public void deleteElementEnd() { if (resolveRange(1, DELETE_ELEMENT_END_RESOLVER) == 0) { setRangeCache(new DeleteElementEndCache()); } } @Override public void replaceAttributes(Attributes oldAttrs, Attributes newAttrs) { throw new UnsupportedOperationException("This method should never be called."); } @Override public void updateAttributes(AttributesUpdate attrUpdate) { throw new UnsupportedOperationException("This method should never be called."); } @Override public void annotationBoundary(AnnotationBoundaryMap map) { throw new UnsupportedOperationException("This method should never be called."); } } /** * 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 RETAIN_RESOLVER = new RangeResolver() { @Override public void resolve(int size, RangeCache range) { range.resolveRetain(size); } }; /** * A resolver for "deleteElementEnd" mutation components. */ private static final RangeResolver DELETE_ELEMENT_END_RESOLVER = new RangeResolver() { @Override public void resolve(int size, RangeCache range) { range.resolveDeleteElementEnd(); } }; private final RangeCache retainCache = new RangeCache() { @Override void resolveRetain(int itemCount) { preservationOperation.retain(itemCount); annotationResidue.retain(itemCount); deletionOperation.retain(itemCount); propagatingAnnotations.clear(); } @Override void resolveDeleteCharacters(String characters) { processDeleteCharacters(characters); } @Override void resolveDeleteElementStart(String type, Attributes attributes) { processDeleteElementStart(type, attributes); } @Override void resolveDeleteElementEnd() { processDeleteElementEnd(); } @Override void resolveReplaceAttributes(Attributes oldAttributes, Attributes newAttributes) { processReplaceAttributes(oldAttributes, newAttributes); } @Override void resolveUpdateAttributes(AttributesUpdate update) { processUpdateAttributes(update); } }; private final EvaluatingDocOpCursor<DocOp> preservationOperation = OperationNormalizer.createNormalizer(new DocOpBuffer()); private final EvaluatingDocOpCursor<DocOp> annotationResidue = OperationNormalizer.createNormalizer(new DocOpBuffer()); private final EvaluatingDocOpCursor<DocOp> deletionOperation = OperationNormalizer.createNormalizer(new DocOpBuffer()); private final Map<String, ValueUpdate> activeAnnotations = new HashMap<String, ValueUpdate>(); private final Map<String, ValueUpdate> propagatingAnnotations = new HashMap<String, ValueUpdate>(); private void processDeleteCharacters(String characters) { deletionOperation.deleteCharacters(characters); delete(characters.length()); } private void processDeleteElementStart(String type, Attributes attributes) { deletionOperation.deleteElementStart(type, attributes); delete(1); } private void processDeleteElementEnd() { deletionOperation.deleteElementEnd(); delete(1); } private void processReplaceAttributes(Attributes oldAttributes, Attributes newAttributes) { annotationResidue.retain(1); deletionOperation.retain(1); preservationOperation.replaceAttributes(oldAttributes, newAttributes); propagatingAnnotations.clear(); } private void processUpdateAttributes(AttributesUpdate update) { annotationResidue.retain(1); deletionOperation.retain(1); preservationOperation.updateAttributes(update); propagatingAnnotations.clear(); } private void delete(int size) { List<String> keys = new ArrayList<String>(); List<String> oldValues = new ArrayList<String>(); List<String> newValues = new ArrayList<String>(); for (Map.Entry<String, ValueUpdate> entry : propagatingAnnotations.entrySet()) { String key = entry.getKey(); ValueUpdate update = entry.getValue(); ValueUpdate activeUpdate = activeAnnotations.get(key); if (update != null) { keys.add(key); oldValues.add(activeUpdate != null ? activeUpdate.newValue : update.oldValue); newValues.add(update.newValue); } else if (activeUpdate != null) { keys.add(key); oldValues.add(activeUpdate.newValue); newValues.add(activeUpdate.oldValue); } } annotationResidue.annotationBoundary(new AnnotationBoundaryMapImpl( new String[0], keys.toArray(new String[0]), oldValues.toArray(new String[0]), newValues.toArray(new String[0]))); annotationResidue.retain(size); annotationResidue.annotationBoundary(new AnnotationBoundaryMapImpl( keys.toArray(new String[0]), new String[0], new String[0], new String[0])); } /** * Transforms a structure-preserving operation with a deletion operation. * * @param preservationOp the structure-preserving operation * @param deletionOp the deletion operation * @return the transformed preservation operation and the transformed * transformed deletion operation * @throws TransformException if a problem was encountered during the * transformation process */ Pair<DocOp, Pair<DocOp, DocOp>> transformOperations( DocOp preservationOp, DocOp deletionOp) throws TransformException { Pair<DocOp, DocOp> transformedDeletionOp; try { PositionTracker positionTracker = new PositionTracker(); RelativePosition preservationPosition = positionTracker.getPosition1(); RelativePosition deletionPosition = positionTracker.getPosition2(); // The target responsible for processing components of the preservation operation. PreservationTarget preservationTarget = new PreservationTarget(preservationPosition); // The target responsible for processing components of the deletion operation. DeletionTarget deletionTarget = new DeletionTarget(deletionPosition); preservationTarget.setOtherTarget(deletionTarget); deletionTarget.setOtherTarget(preservationTarget); // Incrementally apply the two operations in a linearly-ordered interleaving // fashion. int preservationIndex = 0; int deletionIndex = 0; while (preservationIndex < preservationOp.size()) { preservationOp.applyComponent(preservationIndex++, preservationTarget); while (preservationPosition.get() > 0) { if (deletionIndex >= deletionOp.size()) { throw new TransformException("Ran out of " + deletionOp.size() + " deletion op components after " + preservationIndex + " of " + preservationOp.size() + " preservation op components, with " + preservationPosition.get() + " spare positions"); } deletionOp.applyComponent(deletionIndex++, deletionTarget); } } while (deletionIndex < deletionOp.size()) { deletionOp.applyComponent(deletionIndex++, deletionTarget); } preservationOp = preservationTarget.finish(); transformedDeletionOp = deletionTarget.finish(); } catch (InternalTransformException e) { throw new TransformException(e.getMessage()); } return new Pair<DocOp, Pair<DocOp, DocOp>>(preservationOp, transformedDeletionOp); } }