/** * 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.indexed; import junit.framework.TestCase; import org.waveprotocol.wave.model.document.MutableDocument; import org.waveprotocol.wave.model.document.MutableDocumentImpl; import org.waveprotocol.wave.model.document.ReadableDocument; import org.waveprotocol.wave.model.document.indexed.DocumentEvent.AttributesModified; import org.waveprotocol.wave.model.document.indexed.DocumentEvent.ContentDeleted; import org.waveprotocol.wave.model.document.indexed.DocumentEvent.ContentInserted; import org.waveprotocol.wave.model.document.indexed.DocumentEvent.TextDeleted; import org.waveprotocol.wave.model.document.indexed.DocumentEvent.TextInserted; import org.waveprotocol.wave.model.document.indexed.DocumentHandler.EventBundle; import org.waveprotocol.wave.model.document.operation.Attributes; import org.waveprotocol.wave.model.document.operation.Nindo; import org.waveprotocol.wave.model.document.operation.Nindo.Builder; import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema; import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl; import org.waveprotocol.wave.model.document.raw.impl.Element; import org.waveprotocol.wave.model.document.raw.impl.Node; import org.waveprotocol.wave.model.document.raw.impl.RawDocumentImpl; import org.waveprotocol.wave.model.document.raw.impl.Text; import org.waveprotocol.wave.model.document.util.DocHelper; import org.waveprotocol.wave.model.document.util.DocProviders; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.OperationRuntimeException; import org.waveprotocol.wave.model.operation.OperationSequencer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Basic tests for observable indexed doc * * @author danilatos@google.com (Daniel Danilatos) */ public class ObservableIndexedDocumentTest extends TestCase { EventBundle<Node, Element, Text> events, events2; IndexedDocument<Node, Element, Text> indexed; IndexedDocument<Node, Element, Text> indexedCopy; MutableDocument<Node, Element, Text> doc; Element root; /** Empty attributes map. */ private static final Map<String, String> NA = new HashMap<String, String>(); DocumentHandler<Node, Element, Text> handler = new DocumentHandler<Node, Element, Text>() { @Override public void onDocumentEvents(EventBundle<Node, Element, Text> eventBundle) { events = eventBundle; } }; DocumentHandler<Node, Element, Text> handler2 = new DocumentHandler<Node, Element, Text>() { @Override public void onDocumentEvents(EventBundle<Node, Element, Text> eventBundle) { events2 = eventBundle; } }; public void testDeletes() throws OperationException { for (int i = 0; i < 2; i++) { testNindoConsume = i == 0; doTestDeletes(); } } @SuppressWarnings("unchecked") public void doTestDeletes() { String a = "<p>abc<b>def</b></p>"; String b = "<p><b>deH</b>abG</p>"; String txt = "alksjdflk"; create(a); doc.deleteRange(l(0), l(doc.size())); checkEvents(cd(0, 10, dl(st("p", NA), tt("abc"), st("b", NA), tt("def"), et("b"), et("p")))); create(a + b); List<Element> deletions = new ArrayList<Element>(); deletions.addAll( elementsOuter(doc, DocHelper.getFirstChildElement(doc, doc.getDocumentElement()))); deletions.addAll( elementsOuter(doc, DocHelper.getLastChildElement(doc, doc.getDocumentElement()))); doc.deleteRange(l(0), l(doc.size())); checkEvents( cd(0, 10, dl(st("p", NA), tt("abc"), st("b", NA), tt("def"), et("b"), et("p"))), cd(0, 10, dl(st("p", NA), st("b", NA), tt("deH"), et("b"), tt("abG"), et("p"))) ); checkDeletions(deletions); deletions.clear(); create(txt); doc.deleteRange(l(0), l(doc.size())); checkEvents(td(0, txt)); create(a + txt + b); deletions.addAll( elementsOuter(doc, DocHelper.getFirstChildElement(doc, doc.getDocumentElement()))); deletions.addAll( elementsOuter(doc, DocHelper.getLastChildElement(doc, doc.getDocumentElement()))); doc.deleteRange(l(0), l(doc.size())); checkEvents( cd(0, 10, dl(st("p", NA), tt("abc"), st("b", NA), tt("def"), et("b"), et("p"))), td(0, txt), cd(0, 10, dl(st("p", NA), st("b", NA), tt("deH"), et("b"), tt("abG"), et("p"))) ); checkDeletions(deletions); deletions.clear(); } private <E> List<E> elements(ReadableDocument<? super E, E, ?> doc) { return elementsInner(doc, doc.getDocumentElement()); } private <E> List<E> elementsInner(ReadableDocument<? super E, E, ?> doc, E e) { List<E> els = new ArrayList<E>(); buildElements(doc, e, els); return els; } private <E> List<E> elementsOuter(ReadableDocument<? super E, E, ?> doc, E e) { List<E> els = new ArrayList<E>(); els.add(e); buildElements(doc, e, els); return els; } private <E> void buildElements(ReadableDocument<? super E, E, ?> doc, E e, List<E> els) { E child = DocHelper.getFirstChildElement(doc, e); while (child != null) { els.add(child); buildElements(doc, child, els); child = DocHelper.getNextSiblingElement(doc, child); } } public void testAttributes() throws OperationException { for (int i = 0; i < 2; i++) { testNindoConsume = i == 0; doTestAttributes(); } } @SuppressWarnings("unchecked") public void doTestAttributes() { String a = "<p>abc<b>def</b></p>"; create(a); Element elem = (Element) doc.getDocumentElement().getFirstChild(); Attributes attrs = attrs("x", "1", "y", "2", "z", "3"); doc.setElementAttributes(elem, attrs); checkEvents(am(elem, pairs("x", null, "y", null, "z", null), attrs)); Attributes updates = attrs("w", "4", "x", "1", "y", "5"); doc.updateElementAttributes(elem, updates); // x omitted because no change. checkEvents(am(elem, pairs("w", null, "y", "2"), attrs("w", "4", "y", "5"))); Map<String, String> updates2 = pairs("w", null); doc.updateElementAttributes(elem, updates2); // x omitted because no change. checkEvents(am(elem, pairs("w", "4"), pairs("w", null))); Attributes sets = attrs("x", "1", "y", "6", "v", "7"); doc.setElementAttributes(elem, sets); // x omitted because no change. checkEvents(am(elem, pairs("y", "5", "z", "3", "v", null), pairs("y", "6", "z", null, "v", "7"))); // tests lack of concurrent modification exception: Attributes sets2 = attrs("x", "1", "y", "6", "v", "7"); doc.setElementAttributes(elem, sets2); checkEvents(am(elem, pairs(), pairs())); } public void testInserts() throws OperationException { for (int i = 0; i < 2; i++) { testNindoConsume = i == 0; doTestInserts(); } } @SuppressWarnings("unchecked") public void doTestInserts() throws OperationException { String a = "<p>abc<b>def</b></p>"; create(a); Element firstEl = (Element) doc.getFirstChild(doc.getDocumentElement()); Builder b = at(0); Attributes attrs = attrs("x", "1", "y", "2"); b.replaceAttributes(attrs); b.elementStart("x", attrs("a", "1")); b.characters("hello"); b.elementStart("y", attrs("b", "2", "c", "3")); b.characters("yeah"); b.elementEnd(); b.elementEnd(); String moreText = "more text"; b.characters(moreText); consumeNindo(b.build()); checkEvents( am(firstEl, pairs("x", null, "y", null), attrs), ci(doc.asElement(firstEl.getFirstChild())), ti(14, moreText)); } public void testAdjacentContentInsertsAndDeletes1() throws OperationException { testNindoConsume = true; doTestAdjacentContentInsertsAndDeletes(); } public void testAdjacentContentInsertsAndDeletes2() throws OperationException { testNindoConsume = false; doTestAdjacentContentInsertsAndDeletes(); } @SuppressWarnings("unchecked") public void doTestAdjacentContentInsertsAndDeletes() throws OperationException { String a1 = "<zz><p x=\"y\">abc<b a=\"b\">def</b></p></zz>"; String a = a1 + "<p>blah</p>"; create(a); // GWT doesn't define subList. Element top = (Element) doc.getDocumentElement().getFirstChild(); Node n1 = doc.getFirstChild(top); Node n2 = doc.getLastChild(top); List<Element> all = elementsInner(doc, top); List<Element> els = Arrays.asList(all.get(0), all.get(1)); Builder b = at(0); Attributes attrs = attrs("x", "1", "y", "2"); b.replaceAttributes(attrs); b.elementStart("x", attrs("a", "1")); b.characters("hello"); b.elementStart("y", attrs("b", "2", "c", "3")); b.characters("yeah"); b.elementEnd(); b.elementEnd(); String moreText = "more text"; b.characters(moreText); b.deleteElementStart(); b.deleteCharacters(3); b.deleteElementStart(); b.deleteCharacters(3); b.deleteElementEnd(); b.deleteElementEnd(); String moreText2 = "more"; b.characters(moreText2); consumeNindo(b.build()); checkEvents( am(top, pairs("x", null, "y", null), attrs), ci(doc.asElement(top.getFirstChild())), ti(14, moreText), cd(23, 10, dl(st("p", attrs("x", "y")), tt("abc"), st("b", attrs("a", "b")), tt("def"), et("b"), et("p"))), ti(14 + moreText.length(), moreText2)); checkDeletions(els); } private Attributes attrs(String ... strs) { return new AttributesImpl(pairs(strs)); } private Map<String, String> pairs(String ... strs) { assert strs.length % 2 == 0; Map<String, String> map = new HashMap<String, String>(); for (int i = 0; i < strs.length; i += 2) { map.put(strs[i], strs[i + 1]); } return map; } private DocumentEvent<Node, Element, Text> ti(int loc, String text) { return new TextInserted<Node, Element, Text>(loc, text); } private DocumentEvent<Node, Element, Text> ci(Element e) { return new ContentInserted<Node, Element, Text>(e); } private DocumentEvent<Node, Element, Text> am(Element e, Map<String, String> oldVals, Map<String, String> newVals) { return new AttributesModified<Node, Element, Text>(e, oldVals, newVals); } private DocumentEvent<Node, Element, Text> td(int loc, String text) { return new TextDeleted<Node, Element, Text>(loc, text); } private static List<ContentDeleted.Token> dl(ContentDeleted.Token... tokens) { List<ContentDeleted.Token> tokenList = new ArrayList<ContentDeleted.Token>(); for (ContentDeleted.Token token : tokens) { tokenList.add(token); } return tokenList; } private static ContentDeleted.Token tt(String text) { return ContentDeleted.Token.textToken(text); } private static ContentDeleted.Token st(String tagName, Map<String, String> attributes) { return ContentDeleted.Token.elementStartToken(tagName, attributes); } private static ContentDeleted.Token et(String tagName) { return ContentDeleted.Token.elementEndToken(tagName); } private DocumentEvent<Node, Element, Text> cd(int loc, int size, List<ContentDeleted.Token> tokens) { return new ContentDeleted<Node, Element, Text>(loc, size, tokens, null); } private void checkDeletions(Collection<Element> expected) { for (Element expectedDeletion : expected) { assertTrue(getEvents().wasDeleted(expectedDeletion)); } for (Element realDeletion : getEvents().getDeletedElements()) { assertTrue(expected.contains(realDeletion)); } } private void checkEvents(Object ... expectedEvents) { assertEquals(flatten(Arrays.asList(expectedEvents)), getEvents().getEventComponents()); } private List<Object> flatten(List<?> objects) { List<Object> list = new ArrayList<Object>(); for (Object o : objects) { if (o instanceof List) { list.addAll(flatten((List<?>) o)); } else { list.add(o); } } return list; } private Point<Node> l(int location) { if (location < 0) { location = doc.size() + location; } return doc.locate(location); } private void consumeNindo(Nindo op) throws OperationException { indexedCopy.consume(indexed.consumeAndReturnInvertible(op)); } boolean testNindoConsume; private void create(String xml) { RawDocumentImpl raw = DocProviders.ROJO.parse("<d>" + xml + "</d>"); RawDocumentImpl raw2 = DocProviders.ROJO.parse("<d>" + xml + "</d>"); indexed = new ObservableIndexedDocument<Node, Element, Text, Void>(handler, raw, null, DocumentSchema.NO_SCHEMA_CONSTRAINTS); indexedCopy = new ObservableIndexedDocument<Node, Element, Text, Void>( handler2, raw2, null, DocumentSchema.NO_SCHEMA_CONSTRAINTS); doc = new MutableDocumentImpl<Node, Element, Text>( new OperationSequencer<Nindo>() { @Override public void begin() { } @Override public void consume(Nindo op) { try { consumeNindo(op); } catch (OperationException e) { throw new OperationRuntimeException("Bug!", e); } } @Override public void end() { } }, testNindoConsume ? indexed : indexedCopy); root = doc.getDocumentElement(); } EventBundle<Node, Element, Text> getEvents() { return testNindoConsume ? events : events2; } private Builder at(int location) { Builder b = new Builder(); if (location > 0) { b.skip(location); } return b; } }