/** * 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.DocOpCursor; import org.waveprotocol.wave.model.operation.TransformException; import java.util.HashSet; import java.util.Set; /** * A utility class for checking the tameness of a structure-preserving operation * with respect to a deletion operation. * * A structure-preserving operation is considered to be tamed by a deletion * operation if it contains no attribute modifications and any regions with * annotations changes are deleted by the deletion operation. * * Note that the deletion operation may apply to either the document to which * the structure-preserving operation applies, or the document which results * from applying the structure-preserving operation. * * This class is used by {@link ReferenceTransformer} to aid in determining * whether a pair of structure-preserving operations is tamed by a pair of * deletion operations. * * If the composition of p1 and d1 is being transformed against the composition * of p2 and d2, where p1 and p2 are structure-preserving operations, and d1 and * d2 are deletion operations, then if each of p1 and p2 are tamed by each of d1 * and d2, the result of the transform will be equivalent to the transform of d1 * and d2. * * @author Alexandre Mah */ final class AnnotationTamenessChecker { /** * 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 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 position in the preservation operation relative * to the position in the deletion operation. */ RelativePosition getPreservationPosition() { return new RelativePosition() { public void increase(int amount) { position += amount; } public int get() { return position; } }; } /** * @return A RelativePosition representing the position in the deletion operation relative to * the position in the preservation operation. */ RelativePosition getDeletionPosition() { return new RelativePosition() { public void increase(int amount) { position -= amount; } public int get() { return -position; } }; } } private abstract class Target implements DocOpCursor { /** * 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. */ final RelativePosition relativePosition; Target(RelativePosition relativePosition) { this.relativePosition = relativePosition; } @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."); } } private final class PreservationTarget extends Target { PreservationTarget(RelativePosition relativePosition) { super(relativePosition); } @Override public void retain(int itemCount) { if (relativePosition.get() < 0) { checkAnnotations(); } relativePosition.increase(itemCount); } @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) { annotationsAreTame = false; relativePosition.increase(1); } @Override public void updateAttributes(AttributesUpdate attrUpdate) { annotationsAreTame = false; relativePosition.increase(1); } @Override public void annotationBoundary(AnnotationBoundaryMap map) { for (int i = 0; i < map.endSize(); ++i) { activeAnnotations.remove(map.getEndKey(i)); } for (int i = 0; i < map.changeSize(); ++i) { activeAnnotations.add(map.getChangeKey(i)); } } } private final class DeletionTarget extends Target { DeletionTarget(RelativePosition relativePosition) { super(relativePosition); } @Override public void retain(int itemCount) { process(itemCount, false); } @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) { process(chars.length(), true); } @Override public void deleteElementStart(String tag, Attributes attrs) { process(1, true); } @Override public void deleteElementEnd() { process(1, true); } @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."); } void process(int itemCount, boolean newDeletionState) { deletionState = newDeletionState; checkAnnotations(); relativePosition.increase(itemCount); } } private boolean annotationsAreTame = true; private boolean deletionState = false; private final Set<String> activeAnnotations = new HashSet<String>(); private AnnotationTamenessChecker() {} private void checkAnnotations() { if (deletionState == false && !activeAnnotations.isEmpty()) { annotationsAreTame = false; } } /** * Determines whether annotations are tame. * * @param preservationOp the structure-preserving operation * @param deletionOp the deletion operation * @return whether the annotations are tame */ private boolean annotationsAreTame( DocOp preservationOp, DocOp deletionOp) throws TransformException { PositionTracker positionTracker = new PositionTracker(); RelativePosition preservationPosition = positionTracker.getPreservationPosition(); RelativePosition deletionPosition = positionTracker.getDeletionPosition(); // 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); // 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); } return annotationsAreTame; } /** * Determines whether a structure-preserving operation is tame relative to a * deletion operation. * * @param preservationOp the structure-preserving operation * @param deletionOp the deletion operation * @return whether the structure-preserving operation is tame * @throws TransformException if a problem is encountered */ private static boolean checkTameness( DocOp preservationOp, DocOp deletionOp) throws TransformException { return new AnnotationTamenessChecker().annotationsAreTame(preservationOp, deletionOp); } /** * Determines whether a pair of structure-preserving operations are tame * relative to each of two deletion operations. * * @param p1 the first structure-preserving operation * @param p2 the second structure-preserving operation * @param d1 the first deletion operation * @param d2 the second deletion operation * @return whether the structure-preserving operations are tame * @throws TransformException if a problem is encountered */ static boolean checkTameness(DocOp p1, DocOp p2, DocOp d1, DocOp d2) throws TransformException { return checkTameness(p1, d1) && checkTameness(p1, d2) && checkTameness(p2, d1) && checkTameness(p2, d2); } }