// Copyright 2012 Google Inc. All Rights Reserved. // // 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 com.google.collide.shared.ot; import com.google.collide.dto.DocOp; import com.google.collide.dto.DocOpComponent; import com.google.collide.dto.shared.DocOpFactory; import com.google.collide.json.shared.JsonArray; import com.google.common.base.Preconditions; /* * Influenced by Wave's Transformer and Composer classes. Generally, we can't * use Wave's because we introduce the RetainLine doc op component, and don't * support a few extra components that Wave has. We didn't fork Wave's * Transformer because its design wasn't amenable to the changes required by * RetainLine. Instead, we wrote this from scratch to be able to handle that * component easily. * * The operations being transformed are A and B. Both are intended to be applied * to the same document. The output of the transformation will be A' and B'. For * example, A' will be A transformed so it can be cleanly applied after B has * been applied. * * The Processor class maintains the state for a document operation component. * Each method in the Processor handles a component from the other document * operation. As each method is executed, it outputs to its output document * operation and to the other processor's output document operation. It also * marks the general state into the ProcessorResult. */ /** * Transforms document operations for the code editor. * */ public class Transformer { /** * Exception that is thrown when there is a problem transforming two document * operations. */ public static class TransformException extends RuntimeException { public TransformException(String message, Throwable cause) { super(message, cause); } } private static class DeleteProcessor extends Processor { static String performDelete(DocOpCapturer output, String text, int deleteLength, ProcessorResult result) { output.delete(text.substring(0, deleteLength)); if (text.length() == deleteLength) { result.markMyStateFinished(); } return text.substring(deleteLength); } private String text; DeleteProcessor(DocOpCapturer output, String text) { super(output); this.text = text; } @Override void handleOtherDelete(DeleteProcessor other, ProcessorResult result) { /* * The transformed op shouldn't know about the deletes, so don't output * anything */ if (text.length() == other.text.length()) { result.markMyStateFinished(); result.markOtherStateFinished(); } else if (text.length() < other.text.length()) { other.text = other.text.substring(text.length()); result.markMyStateFinished(); } else { text = text.substring(other.text.length()); result.markOtherStateFinished(); } } @Override void handleOtherFinished(Processor other, ProcessorResult result) { throw new IllegalStateException("Cannot delete if the other side is finished"); } @Override void handleOtherInsert(InsertProcessor other, ProcessorResult result) { /* * Look at comments in InsertProcessor on why it needs to always handle * these components */ result.flip(); other.handleOtherDelete(this, result); } @Override void handleOtherRetain(RetainProcessor other, ProcessorResult result) { /* * The other transformed op won't have anything to retain, so output * nothing for it. Our transformed op needs to delete though, since the * other original op is just retaining. */ int minCount = Math.min(text.length(), other.count); other.count -= minCount; text = performDelete(output, text, minCount, result); if (other.count == 0) { result.markOtherStateFinished(); } } @Override void handleOtherRetainLine(RetainLineProcessor other, ProcessorResult result) { // Let RetainLineProcessor handle this result.flip(); other.handleOtherDelete(this, result); } } private static class FinishedProcessor extends Processor { FinishedProcessor(DocOpCapturer output) { super(output); } @Override void handleOtherDelete(DeleteProcessor other, ProcessorResult result) { result.flip(); other.handleOtherFinished(this, result); } @Override void handleOtherFinished(Processor other, ProcessorResult result) { throw new IllegalStateException("Both should not be finished"); } @Override void handleOtherInsert(InsertProcessor other, ProcessorResult result) { result.flip(); other.handleOtherFinished(this, result); } @Override void handleOtherRetain(RetainProcessor other, ProcessorResult result) { result.flip(); other.handleOtherFinished(this, result); } @Override void handleOtherRetainLine(RetainLineProcessor other, ProcessorResult result) { result.flip(); other.handleOtherFinished(this, result); } } private static class InsertProcessor extends Processor { private static void performInsert(DocOpCapturer output, DocOpCapturer otherOutput, String text, ProcessorResult result) { output.insert(text); boolean endsWithNewline = text.endsWith(NEWLINE); otherOutput.retain(text.length(), endsWithNewline); if (endsWithNewline) { result.markMyCurrentComponentInsertOfNewline(); } result.markMyStateFinished(); } private String text; InsertProcessor(DocOpCapturer output, String text) { super(output); this.text = text; } @Override void handleOtherDelete(DeleteProcessor other, ProcessorResult result) { // Handle insertion performInsert(output, other.output, text, result); // The delete will be handled by the successor for this processor } @Override void handleOtherFinished(Processor other, ProcessorResult result) { performInsert(output, other.output, text, result); } @Override void handleOtherInsert(InsertProcessor other, ProcessorResult result) { /* * Instead of inserting both, we only insert one so contiguous insertions * by one side will end up being contiguous in the transformed op. * (Otherwise, you get interleaved I, RL, I, RL, ...) */ performInsert(output, other.output, text, result); } @Override void handleOtherRetain(RetainProcessor other, ProcessorResult result) { performInsert(output, other.output, text, result); // The retain will be handled by the successor for this processor } @Override void handleOtherRetainLine(RetainLineProcessor other, ProcessorResult result) { result.flip(); other.handleOtherInsert(this, result); } } private abstract static class Processor { final DocOpCapturer output; Processor(DocOpCapturer output) { this.output = output; } abstract void handleOtherDelete(DeleteProcessor other, ProcessorResult result); abstract void handleOtherFinished(Processor other, ProcessorResult result); abstract void handleOtherInsert(InsertProcessor other, ProcessorResult result); abstract void handleOtherRetain(RetainProcessor other, ProcessorResult result); abstract void handleOtherRetainLine(RetainLineProcessor other, ProcessorResult result); } private static class ProcessorFactory implements DocOpCursor { private DocOpCapturer curOutput; private Processor returnProcessor; @Override public void delete(String text) { returnProcessor = new DeleteProcessor(curOutput, text); } @Override public void insert(String text) { returnProcessor = new InsertProcessor(curOutput, text); } @Override public void retain(int count, boolean hasTrailingNewline) { returnProcessor = new RetainProcessor(curOutput, count, hasTrailingNewline); } @Override public void retainLine(int lineCount) { returnProcessor = new RetainLineProcessor(curOutput, lineCount); } Processor create(DocOpCapturer output, DocOpComponent component) { curOutput = output; DocOpUtils.acceptComponent(component, this); return returnProcessor; } } private static class ProcessorResult { private boolean isMyStateFinished; private boolean isOtherStateFinished; /* * We need to know the value of the previous component, but if we only tracked that then reset * would clear it. */ private boolean isMyCurrentComponentInsertOfNewline; private boolean isOtherCurrentComponentInsertOfNewline; private boolean isMyPreviousComponentInsertOfNewline; private boolean isOtherPreviousComponentInsertOfNewline; private boolean isFlipped; private ProcessorResult() { } /** * Flips the "my" and "other" states. This should be called before one * processor is handing over execution to the other processor, including * passing this instance to the other processor. */ void flip() { boolean origMyStateFinished = isMyStateFinished; isMyStateFinished = isOtherStateFinished; isOtherStateFinished = origMyStateFinished; boolean origMyPreviousComponentInsertOfNewline = isMyPreviousComponentInsertOfNewline; isMyPreviousComponentInsertOfNewline = isOtherPreviousComponentInsertOfNewline; isOtherPreviousComponentInsertOfNewline = origMyPreviousComponentInsertOfNewline; boolean origMyCurrentComponentInsertOfNewline = isMyCurrentComponentInsertOfNewline; isMyCurrentComponentInsertOfNewline = isOtherCurrentComponentInsertOfNewline; isOtherCurrentComponentInsertOfNewline = origMyCurrentComponentInsertOfNewline; isFlipped = !isFlipped; } void markMyStateFinished() { if (!isFlipped) { isMyStateFinished = true; } else { isOtherStateFinished = true; } } void markOtherStateFinished() { if (!isFlipped) { isOtherStateFinished = true; } else { isMyStateFinished = true; } } void markMyCurrentComponentInsertOfNewline() { if (!isFlipped) { isMyCurrentComponentInsertOfNewline = true; } else { isOtherCurrentComponentInsertOfNewline = true; } } void markOtherCurrentComponentInsertOfNewline() { if (!isFlipped) { isOtherCurrentComponentInsertOfNewline = true; } else { isMyCurrentComponentInsertOfNewline = true; } } void reset() { isMyPreviousComponentInsertOfNewline = isMyCurrentComponentInsertOfNewline; isOtherPreviousComponentInsertOfNewline = isOtherCurrentComponentInsertOfNewline; isFlipped = isMyStateFinished = isOtherStateFinished = isMyCurrentComponentInsertOfNewline = isOtherCurrentComponentInsertOfNewline = false; } } private static class RetainLineProcessor extends Processor { private int lineCount; /** * In the event that we need to expand the retain line, we need to know * exactly how many retains it should be expanded to. This tracks that * number. */ private int substituteRetainCount; RetainLineProcessor(DocOpCapturer output, int lineCount) { super(output); this.lineCount = lineCount; } @Override void handleOtherDelete(DeleteProcessor other, ProcessorResult result) { other.output.delete(other.text); if (other.text.endsWith(NEWLINE)) { handleOtherLineEnd(false, result); } else { // My transformed op won't see the delete, so do nothing } result.markOtherStateFinished(); } @Override void handleOtherFinished(Processor other, ProcessorResult result) { Preconditions.checkState( lineCount == 1, "Cannot retain more than one line if other side is finished"); if (result.isMyPreviousComponentInsertOfNewline) { other.output.retainLine(1); } lineCount = 0; output.retainLine(1); result.markMyStateFinished(); } @Override void handleOtherInsert(InsertProcessor other, ProcessorResult result) { other.output.insert(other.text); if (other.text.endsWith(NEWLINE)) { // Retain the line just inserted by other lineCount++; handleOtherLineEnd(true, result); result.markOtherCurrentComponentInsertOfNewline(); } else { substituteRetainCount += other.text.length(); } result.markOtherStateFinished(); } void handleOtherLineEnd(boolean canUseRetainLine, ProcessorResult result) { if (canUseRetainLine) { output.retainLine(1); } else { if (substituteRetainCount > 0) { output.retain(substituteRetainCount, false); } } lineCount--; substituteRetainCount = 0; if (lineCount == 0) { result.markMyStateFinished(); } } @Override void handleOtherRetain(RetainProcessor other, ProcessorResult result) { other.output.retain(other.count, other.hasTrailingNewline); substituteRetainCount += other.count; if (other.hasTrailingNewline) { handleOtherLineEnd(true, result); } result.markOtherStateFinished(); } @Override void handleOtherRetainLine(RetainLineProcessor other, ProcessorResult result) { int minLineCount = Math.min(lineCount, other.lineCount); output.retainLine(minLineCount); lineCount -= minLineCount; other.output.retainLine(minLineCount); other.lineCount -= minLineCount; if (lineCount == 0) { result.markMyStateFinished(); } if (other.lineCount == 0) { result.markOtherStateFinished(); } } } private static class RetainProcessor extends Processor { static int performRetain(DocOpCapturer output, int fullCount, int retainCount, boolean hasTrailingNewline, ProcessorResult result, boolean useOtherInResult) { output.retain(retainCount, fullCount == retainCount ? hasTrailingNewline : false); if (retainCount == fullCount) { if (useOtherInResult) { result.markOtherStateFinished(); } else { result.markMyStateFinished(); } } return fullCount - retainCount; } private int count; private final boolean hasTrailingNewline; RetainProcessor(DocOpCapturer output, int count, boolean hasTrailingNewline) { super(output); this.count = count; this.hasTrailingNewline = hasTrailingNewline; } @Override void handleOtherDelete(DeleteProcessor other, ProcessorResult result) { result.flip(); other.handleOtherRetain(this, result); } @Override void handleOtherFinished(Processor other, ProcessorResult result) { throw new IllegalStateException("Cannot retain if other side is finished"); } @Override void handleOtherInsert(InsertProcessor other, ProcessorResult result) { result.flip(); other.handleOtherRetain(this, result); } @Override void handleOtherRetain(RetainProcessor other, ProcessorResult result) { int minCount = Math.min(count, other.count); count = performRetain(output, count, minCount, hasTrailingNewline, result, false); other.count = performRetain(other.output, other.count, minCount, other.hasTrailingNewline, result, true); } @Override void handleOtherRetainLine(RetainLineProcessor other, ProcessorResult result) { result.flip(); other.handleOtherRetain(this, result); } } private static final String NEWLINE = "\n"; private static final ProcessorFactory PROCESSOR_FACTORY = new ProcessorFactory(); public static OperationPair transform(DocOpFactory factory, DocOp clientOp, DocOp serverOp) throws TransformException { try { return new Transformer(factory).transformImpl(clientOp, serverOp); } catch (Throwable t) { throw new TransformException("Could not transform doc ops:\nClient: " + DocOpUtils.toString(clientOp, false) + "\nServer: " + DocOpUtils.toString(serverOp, false) + "\n", t); } } private static void dispatchProcessor(Processor a, Processor b, ProcessorResult result) { if (b instanceof DeleteProcessor) { a.handleOtherDelete((DeleteProcessor) b, result); } else if (b instanceof InsertProcessor) { a.handleOtherInsert((InsertProcessor) b, result); } else if (b instanceof RetainProcessor) { a.handleOtherRetain((RetainProcessor) b, result); } else if (b instanceof RetainLineProcessor) { a.handleOtherRetainLine((RetainLineProcessor) b, result); } else if (b instanceof FinishedProcessor) { a.handleOtherFinished(b, result); } } private final DocOpFactory factory; private Transformer(DocOpFactory factory) { this.factory = factory; } private OperationPair transformImpl(DocOp clientOp, DocOp serverOp) { /* * These capturers will create the respective side's doc op which will be * transformed from the respective side's original doc op to apply to the * document *after* the other side's original doc op. */ DocOpCapturer clientOutput = new DocOpCapturer(factory, true); DocOpCapturer serverOutput = new DocOpCapturer(factory, true); JsonArray<DocOpComponent> clientComponents = clientOp.getComponents(); JsonArray<DocOpComponent> serverComponents = serverOp.getComponents(); int clientIndex = 0; int serverIndex = 0; boolean clientComponentsFinished = false; boolean serverComponentsFinished = false; Processor client = null; Processor server = null; ProcessorResult result = new ProcessorResult(); while (!clientComponentsFinished || !serverComponentsFinished) { if (client == null) { if (clientIndex < clientComponents.size()) { client = PROCESSOR_FACTORY.create(clientOutput, clientComponents.get(clientIndex++)); } else { client = new FinishedProcessor(clientOutput); clientComponentsFinished = true; } } if (server == null) { if (serverIndex < serverComponents.size()) { server = PROCESSOR_FACTORY.create(serverOutput, serverComponents.get(serverIndex++)); } else { server = new FinishedProcessor(serverOutput); serverComponentsFinished = true; } } if (!clientComponentsFinished || !serverComponentsFinished) { dispatchProcessor(client, server, result); } if (result.isMyStateFinished) { client = null; } if (result.isOtherStateFinished) { server = null; } result.reset(); } return new OperationPair(clientOutput.getDocOp(), serverOutput.getDocOp()); } }