/** * 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.client.editor.content; import org.waveprotocol.wave.client.editor.testing.TestEditors; import org.waveprotocol.wave.model.document.ReadableDocument; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.document.operation.impl.DocOpBuffer; import org.waveprotocol.wave.model.document.operation.impl.DocOpUtil; import org.waveprotocol.wave.model.document.util.DocHelper; import org.waveprotocol.wave.model.document.util.DocProviders; import org.waveprotocol.wave.model.document.util.DomOperationUtil; import org.waveprotocol.wave.model.document.util.LocalDocument; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.document.util.ReadableTreeWalker; import org.waveprotocol.wave.model.operation.SilentOperationSink; import java.util.Collections; import java.util.LinkedList; import java.util.Map; import java.util.Queue; /** * Tests some transparent/persistent operations on a ContentDocument * * @author patcoleman@google.com (Pat Coleman) */ public class LazyPersistentContentDocumentGwtTest extends EditorGwtTestCase { /** Simplest use of persistence, creating a node locally then persisting it. */ public void testSimpleForcedPersist() { // set up document, with queue for outgoing messages. final Queue<DocOp> opMessageQueue = new LinkedList<DocOp>(); ContentDocument doc = TestEditors.createTestDocument(); // Note: this replaces the output sink that the editor has injected into the // document, breaking an editor assumption. doc.replaceOutgoingSink(new SilentOperationSink<DocOp>(){ @Override public void consume(DocOp op) { opMessageQueue.add(op); } }); // initial document, with simple content: doc.consume(DocProviders.POJO.parse("<root></root>").asOperation()); assertTrue(opMessageQueue.isEmpty()); // came from server, nothing sent out LocalDocument<ContentNode, ContentElement, ContentTextNode> local = doc.getContext().annotatableContent(); ContentElement root = local.getFirstChild(local.getDocumentElement()).asElement(); // create local text node: ContentElement child = local.transparentCreate("child", Collections.<String, String>emptyMap(), root, null); assertTrue(opMessageQueue.isEmpty()); // local, nothing used. assertFalse(checkDomEqual(doc.getMutableDoc(), local)); // forcibly persist: local.markNodeForPersistence(child, false); popServerOp(opMessageQueue, "__1; << child {}; >>; __1; "); // sent assertTrue(checkDomEqual(doc.getMutableDoc(), local)); ContentElement mutableChild = doc.getMutableDoc().getFirstChild(root).asElement(); assertTrue(child == mutableChild); // exact same object // persist again, just to make sure it's fine local.markNodeForPersistence(child, false); assertTrue(opMessageQueue.isEmpty()); // nothing new to send assertTrue(checkDomEqual(doc.getMutableDoc(), local)); } /** * Simple use of lazy persistence, creating a node locally, marking to persist, * then persisting due to (say) a user action. */ public void testSimpleLazyPersist() { // set up document, with queue for outgoing messages. final Queue<DocOp> opMessageQueue = new LinkedList<DocOp>(); ContentDocument doc = TestEditors.createTestDocument(); // Note: this replaces the output sink that the editor has injected into the // document, breaking an editor assumption. doc.replaceOutgoingSink(new SilentOperationSink<DocOp>(){ @Override public void consume(DocOp op) { opMessageQueue.add(op); } }); // initial document, with simple content: doc.consume(DocProviders.POJO.parse("<root></root>").asOperation()); assertTrue(opMessageQueue.isEmpty()); // came from server, nothing sent out LocalDocument<ContentNode, ContentElement, ContentTextNode> local = doc.getContext().annotatableContent(); ContentElement root = local.getFirstChild(local.getDocumentElement()).asElement(); // create local text node: ContentElement child = local.transparentCreate("child", Collections.<String, String>emptyMap(), root, null); assertTrue(opMessageQueue.isEmpty()); // local, nothing used. assertFalse(checkDomEqual(doc.getMutableDoc(), local)); // Make sure it doesn't get included in a filter: Point<ContentNode> filtered = DocHelper.getFilteredPoint( doc.getPersistentView(), Point.start(doc.getFullContentView(), child)); // filter container to be the root persisted node assertTrue(filtered.getContainer() == root); // mark to lazily persist: local.markNodeForPersistence(child, true); assertTrue(opMessageQueue.isEmpty()); // still local, nothing used. assertFalse(checkDomEqual(doc.getMutableDoc(), local)); // now require it to be persisted: filtered = DocHelper.getFilteredPoint( doc.getPersistentView(), Point.start(doc.getFullContentView(), child)); popServerOp(opMessageQueue, "__1; << child {}; >>; __1; "); // sent assertTrue(checkDomEqual(doc.getMutableDoc(), local)); ContentElement mutableChild = doc.getMutableDoc().getFirstChild(root).asElement(); assertTrue(child == mutableChild); // exact same object assertTrue(filtered.getContainer() == child); // different container now. // persist again, just to make sure it's fine local.markNodeForPersistence(child, false); assertTrue(opMessageQueue.isEmpty()); // nothing new to send assertTrue(checkDomEqual(doc.getMutableDoc(), local)); } /** Using a complex tree, make sure state is correct before and after an opaque persist call. */ public void testComplexSingleOpaquePersist() { // set up document, with queue for outgoing messages. final Queue<DocOp> opMessageQueue = new LinkedList<DocOp>(); ContentDocument doc = TestEditors.createTestDocument(); // Note: this replaces the output sink that the editor has injected into the // document, breaking an editor assumption. doc.replaceOutgoingSink(new SilentOperationSink<DocOp>(){ @Override public void consume(DocOp op) { opMessageQueue.add(op); } }); // initial document, with content and annotation: doc.consume(DocProviders.POJO.parse("<a><b>hi</b> world</a>").asOperation()); assertTrue(opMessageQueue.isEmpty()); // came from server, nothing sent out doc.getMutableDoc().setAnnotation(1, 5, "a", "b"); popServerOp(opMessageQueue, "__1; || { \"a\": null -> \"b\" }; __4; || { \"a\" }; __7; "); // save some places: LocalDocument<ContentNode, ContentElement, ContentTextNode> local = doc.getContext().annotatableContent(); ContentElement A = local.getDocumentElement().getFirstChild().asElement(); ContentNode B = local.getFirstChild(A); // writing to mutable sends to server: doc.getMutableDoc().insertText(2, "!"); popServerOp(opMessageQueue, "__2; ++\"!\"; __10; "); // local mutations, nothing sent to server: ContentNode afterB = local.getNextSibling(B); ContentElement X = local.transparentCreate("c", Collections.singletonMap("K", "V"), A, afterB); ContentTextNode T = local.transparentCreate(":)", X, null); assertTrue(opMessageQueue.isEmpty()); // documents are different: assertFalse(checkDomEqual(doc.getMutableDoc(), local)); // persist the local subtree local.markNodeForPersistence(X, true); assertTrue(opMessageQueue.isEmpty()); // marked for persistence, not actually used yet. doc.getPersistentView().onBeforeFilter(Point.inElement((ContentNode) X, null)); popServerOp(opMessageQueue, "__6; << c { K=\"V\" }; ++\":)\"; >>; __7; "); assertTrue(checkDomEqual(doc.getMutableDoc(), local)); // change attribute on newly persisted doc: doc.getMutableDoc().setElementAttribute(X, "K", "V2"); popServerOp(opMessageQueue, "__6; u@ { K: \"V\" -> \"V2\" }; __10; "); // check annotations moved correctly: int sz = doc.getMutableDoc().size(); assertEquals( 1, doc.getMutableDoc().firstAnnotationChange(0, sz, "a", null)); assertEquals(10, doc.getMutableDoc().firstAnnotationChange(3, sz, "a", "b")); // check *exact* equality ContentElement mA = doc.getMutableDoc().getDocumentElement().getFirstChild().asElement(); ContentNode mB = doc.getMutableDoc().getFirstChild(mA); ContentNode mX = doc.getMutableDoc().getNextSibling(mB); ContentNode mT = doc.getMutableDoc().getFirstChild(mX); assertTrue(mA == A); assertTrue(mB == B); assertTrue(mX == X); assertTrue(mT == T); // try inserting text through mutable document too: int at = doc.getMutableDoc().getLocation(X); doc.getMutableDoc().insertText(at + 1, "#"); popServerOp(opMessageQueue, "__7; ++\"#\"; __10; "); assertTrue(checkDomEqual(doc.getMutableDoc(), local)); } /** Checks multiple calls to opaque persist with related nodes. */ public void testMultipleChildOpaquePersists() { // set up document, with queue for outgoing messages. final Queue<DocOp> opMessageQueue = new LinkedList<DocOp>(); ContentDocument doc = TestEditors.createTestDocument(); // Note: this replaces the output sink that the editor has injected into the // document, breaking an editor assumption. doc.replaceOutgoingSink(new SilentOperationSink<DocOp>(){ @Override public void consume(DocOp op) { opMessageQueue.add(op); } }); Map<String, String> empty = Collections.<String, String>emptyMap(); // initial document, many child nodes doc.consume(DocProviders.POJO.parse("<P></P>").asOperation()); // just parent assertTrue(opMessageQueue.isEmpty()); // came from server, nothing sent out LocalDocument<ContentNode, ContentElement, ContentTextNode> local = doc.getContext().annotatableContent(); CMutableDocument mutable = doc.getMutableDoc(); ContentElement P = local.getDocumentElement().getFirstChild().asElement(); // parent // create four children locally, one after the other. final ContentElement C1 = local.transparentCreate("C1", empty, P, null); ContentElement C2 = local.transparentCreate("C2", Collections.singletonMap("K", "V"), P, null); ContentElement C3 = local.transparentCreate("C3", empty, P, null); ContentElement C4 = local.transparentCreate("C4", Collections.singletonMap("X", "Y"), P, null); local.transparentCreate("a", C1, null); local.transparentCreate("b", C4, null); String localDom = "<doc><P><C1>a</C1><C2 K=\"V\"/><C3/><C4 X=\"Y\">b</C4></P></doc>"; assertEquals(localDom, toSimpleString(local)); assertEquals("<doc><P/></doc>", toSimpleString(mutable)); // persist C4 local.markNodeForPersistence(C4, false); assertEquals(localDom, toSimpleString(local)); assertEquals("<doc><P><C4 X=\"Y\">b</C4></P></doc>", toSimpleString(mutable)); assertTrue(mutable.getFirstChild(mutable.getFirstChild(mutable.getDocumentElement())) == C4); popServerOp(opMessageQueue, "__1; << C4 { X=\"Y\" }; ++\"b\"; >>; __1; "); // persist C1's child local.markNodeForPersistence(C1.getFirstChild(), false); assertEquals(localDom, toSimpleString(local)); assertEquals("<doc><P>a<C4 X=\"Y\">b</C4></P></doc>", toSimpleString(mutable)); assertTrue(mutable.getFirstChild(mutable.getFirstChild(mutable.getDocumentElement())) == C1.getFirstChild()); popServerOp(opMessageQueue, "__1; ++\"a\"; __4; "); // forcibly persist C3 local.markNodeForPersistence(C3, false); assertEquals(localDom, toSimpleString(local)); assertEquals("<doc><P>a<C3/><C4 X=\"Y\">b</C4></P></doc>", toSimpleString(mutable)); popServerOp(opMessageQueue, "__2; << C3 {}; >>; __4; "); // forcibly persist C2 local.markNodeForPersistence(C2, false); assertEquals(localDom, toSimpleString(local)); assertEquals("<doc><P>a<C2 K=\"V\"/><C3/><C4 X=\"Y\">b</C4></P></doc>", toSimpleString(mutable)); popServerOp(opMessageQueue, "__2; << C2 { K=\"V\" }; >>; __6; "); // check that they're different by the C1 tag: assertTrue(new ReadableTreeWalker<ContentNode, ContentElement, ContentTextNode>( local, local.getDocumentElement()) { @Override protected void progress() { super.progress(); if (currentNode == C1) { super.progress(); // skip C1 } } }.checkWalk(mutable, mutable.getDocumentElement())); } // // Utilities // // removes serialized op from the server queue and checks against expected string. private static void popServerOp(Queue<DocOp> queue, String op) { assertTrue("Empty queue, " + op + " expected", !queue.isEmpty()); String next = DocOpUtil.toConciseString(queue.poll()); assertEquals(next + " sent, " + op + " expected", next, op); } // makes sure the two given documents have identical DOM structure. private static boolean checkDomEqual( ReadableDocument<ContentNode, ContentElement, ContentTextNode> A, ReadableDocument<ContentNode, ContentElement, ContentTextNode> B) { return new ReadableTreeWalker<ContentNode, ContentElement, ContentTextNode>( A, A.getDocumentElement()).checkWalk(B, B.getDocumentElement()); } // converts the DOM to a string. private static String toSimpleString(ReadableDocument<ContentNode, ?, ?> doc) { DocOpBuffer opBuffer = new DocOpBuffer(); DomOperationUtil.buildDomInitializationFromSubtree(doc, doc.getDocumentElement(), opBuffer); return DocOpUtil.toXmlString(DocOpUtil.asInitialization(opBuffer.finish())); } }