/** * 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.conversation; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.waveprotocol.wave.model.testing.ExtraAsserts.assertStructureEquivalent; import org.waveprotocol.wave.model.conversation.Conversation.Anchor; import org.waveprotocol.wave.model.conversation.testing.BlipTestUtils; import org.waveprotocol.wave.model.document.MutableDocument; import org.waveprotocol.wave.model.document.ObservableDocument; import org.waveprotocol.wave.model.document.ObservableMutableDocument; import org.waveprotocol.wave.model.document.MutableDocument.Action; import org.waveprotocol.wave.model.document.util.DefaultDocumentEventRouter; import org.waveprotocol.wave.model.document.util.DocHelper; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.document.util.XmlStringBuilder; import org.waveprotocol.wave.model.id.IdGenerator; import org.waveprotocol.wave.model.testing.FakeIdGenerator; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.wave.Blip; import org.waveprotocol.wave.model.wave.Wavelet; import org.waveprotocol.wave.model.wave.opbased.ObservableWaveView; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; /** * Conversation tests for the wavelet-based conversation. * * These tests mostly check the conversation interprets and generates the * correct manifest schema. * * @author anorth@google.com (Alex North) */ public class WaveletBasedConversationTest extends ConversationTestBase { private IdGenerator idGenerator; private ObservableWaveView waveView; private WaveBasedConversationView conversationView; private WaveletBasedConversation target; private ObservableMutableDocument<?, ?, ?> manifestDoc; private WaveletBasedConversationBlip.Listener blipListener; private WaveletBasedConversationThread.Listener threadListener; @Override protected void setUp() throws Exception { idGenerator = FakeIdGenerator.create(); waveView = ConversationTestUtils.createWaveView(idGenerator); conversationView = WaveBasedConversationView.create(waveView, idGenerator); target = makeConversation(); manifestDoc = WaveletBasedConversation.getManifestDocument(target.getWavelet()); blipListener = mock(WaveletBasedConversationBlip.Listener.class); threadListener = mock(WaveletBasedConversationThread.Listener.class); super.setUp(); } @Override protected void tearDown() throws Exception { super.tearDown(); } @Override protected WaveletBasedConversation makeConversation() { return conversationView.createConversation(); } @Override protected WaveletBasedConversation mirrorConversation(ObservableConversation toMirror) { WaveletBasedConversation backer = (WaveletBasedConversation) toMirror; ObservableDocument backerManifestDoc = WaveletBasedConversation.getManifestDocument( backer.getWavelet()); ObservableManifest manifest = backerManifestDoc.with( new ObservableMutableDocument.Method<ObservableManifest>() { @Override public <N, E extends N, T extends N> ObservableManifest exec( ObservableMutableDocument<N, E, T> doc) { E top = DocHelper.expectAndGetFirstTopLevelElement(doc, DocumentBasedManifest.MANIFEST_TOP_TAG); return DocumentBasedManifest.createOnExisting(DefaultDocumentEventRouter.create(doc), top); } }); return WaveletBasedConversation.create(conversationView, backer.getWavelet(), manifest, idGenerator); } @Override protected void assertBlipValid(ConversationBlip blip) { ((WaveletBasedConversationBlip)blip).checkIsUsable(); } @Override protected void assertBlipInvalid(ConversationBlip blip) { try { ((WaveletBasedConversationBlip)blip).checkIsUsable(); fail("Expected blip to be invalid"); } catch (IllegalStateException expected) { } } @Override protected void assertThreadInvalid(ConversationThread thread) { try { ((WaveletBasedConversationThread)thread).checkIsUsable(); fail("Expected thread to be invalid"); } catch (IllegalStateException expected) { } } @Override protected void assertThreadValid(ConversationThread thread) { ((WaveletBasedConversationThread)thread).checkIsUsable(); } // // Initialization // /** * Tests that an empty wavelet is recognised as having no conversation * structure. */ public void testNewWaveletHasNoConversation() { Wavelet wavelet = waveView.createWavelet(); assertFalse(WaveletBasedConversation.waveletHasConversation(wavelet)); } /** * Tests that a wavelet with an initialised manifest is recognised as having * conversation structure. */ public void testWaveletWithManifestHasConversation() { assertTrue(WaveletBasedConversation.waveletHasConversation(target.getWavelet())); } /** * Tests that makeWaveletConversational does so. */ public void testHackMakeWaveletConversationalMakesConversation() { Wavelet wavelet = waveView.createWavelet(); WaveletBasedConversation.makeWaveletConversational(wavelet); assertTrue(WaveletBasedConversation.waveletHasConversation(wavelet)); } /** * Tests that WaveletBasedConversation does not die if an additional * conversation element is added to the manifest dynamically. */ public void testDynamicAdditionOfExtraConversationElementDoesNotFail() { // target is currently listening to the manifest, so we only need to poke it. manifestDoc.with(new Action() { @Override public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) { doc.createChildElement(doc.getDocumentElement(), DocumentBasedManifest.MANIFEST_TOP_TAG, Collections.<String, String>emptyMap()); } }); } /** * Tests that WaveletBasedConversation does not die if it is loaded on a * manifest document with multiple conversation elements. */ public void testMultipleConversationElementsDoesNotPreventLoad() { manifestDoc.with(new Action() { @Override public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) { doc.createChildElement(doc.getDocumentElement(), DocumentBasedManifest.MANIFEST_TOP_TAG, Collections.<String, String>emptyMap()); } }); // Manifest now has multiple elements. Re-load conversation view. conversationView = WaveBasedConversationView.create(waveView, idGenerator); } // // Anchors // /** * Tests that an empty manifest document means no anchor. */ public void testEmptyManifestHasNoAnchor() { assertFalse(target.hasAnchor()); } /** * Tests that a wavelet with a manifest but no anchor attributes is not * anchored. */ public void testMissingAnchorAttributesMeansNoAnchor() { setManifestAttribute(manifestDoc, "sort", "a-value"); assertFalse(target.hasAnchor()); } /** * Tests that setting an anchor updates the conversation manifest correctly. */ public void testSetAnchorUpdatesManifest() { // Anchor target(wavelet1) in alternate (wavelet2). WaveletBasedConversation conversation2 = makeConversation(); populate(conversation2); ConversationBlip firstBlip = getFirstBlip(conversation2); Anchor anchor = conversation2.createAnchor(firstBlip); target.setAnchor(anchor); assertEquals(WaveletBasedConversation.idFor(conversation2.getWavelet().getId()), getManifestAttribute(manifestDoc, "anchorWavelet")); assertEquals(firstBlip.getId(), getManifestAttribute(manifestDoc, "anchorBlip")); } /** * Tests that setting a null anchor updates the manifest and makes the wavelet * un-anchored. */ public void testClearAnchorClearsManifest() { WaveletBasedConversation conversation2 = makeConversation(); populate(conversation2); Anchor anchor = conversation2.createAnchor(getFirstBlip(conversation2)); target.setAnchor(anchor); target.setAnchor(null); assertNull(getManifestAttribute(manifestDoc, "anchorWavelet")); assertNull(getManifestAttribute(manifestDoc, "anchorBlip")); } // // Threads and blips. // public void testAbstractBlipIdMatchesConcreteBlipId() { populate(target); WaveletBasedConversationBlip convBlip = target.getRootThread().getFirstBlip(); assertEquals(convBlip.getId(), convBlip.getBlip().getId()); } /** * Tests that the wavelet-based conversation reads meta-data from the * underlying wavelet structures. This test will go away when meta-data is * stored in the conversation documents. */ public void testConversationBlipMetadataMatchesWavelet() { populate(target); ConversationBlip convBlip = target.getRootThread().getFirstBlip(); Wavelet wavelet = target.getWavelet(); Blip blip = wavelet.getBlip(convBlip.getId()); assertEquals(blip.getId(), convBlip.getId()); assertEquals(blip.getLastModifiedVersion().longValue(), convBlip.getLastModifiedVersion()); assertEquals(blip.getLastModifiedTime().longValue(), convBlip.getLastModifiedTime()); assertEquals(blip.getAuthorId(), convBlip.getAuthorId()); assertEquals(blip.getContributorIds(), convBlip.getContributorIds()); } public void testAppendBlipsInRootThreadUpdatesManifest() { WaveletBasedConversationBlip first = target.getRootThread().appendBlip(); assertManifestXml("<blip id=\"" + first.getId() + "\"></blip>"); WaveletBasedConversationBlip second = target.getRootThread().appendBlip(); assertManifestXml("<blip id=\"" + first.getId() + "\"></blip>" + "<blip id=\"" + second.getId() + "\"></blip>"); assertMirrorConversationEquivalent(); } public void testAppendNonInlineRepliesUpdatesManifest() { WaveletBasedConversationBlip blip = target.getRootThread().appendBlip(); WaveletBasedConversationThread firstReply = blip.addReplyThread(); WaveletBasedConversationBlip firstReplyBlip = firstReply.appendBlip(); assertManifestXml("<blip id=\"" + blip.getId() + "\">" + "<thread id=\"" + firstReply.getId() + "\">" + "<blip id=\"" + firstReplyBlip.getId() + "\"></blip>" + "</thread>" + "</blip>"); WaveletBasedConversationThread secondReply = blip.addReplyThread(); WaveletBasedConversationBlip secondReplyBlip = secondReply.appendBlip(); assertManifestXml("<blip id=\"" + blip.getId() + "\">" + "<thread id=\"" + firstReply.getId() + "\">" + "<blip id=\"" + firstReplyBlip.getId() + "\"></blip>" + "</thread>" + "<thread id=\"" + secondReply.getId() + "\">" + "<blip id=\"" + secondReplyBlip.getId() + "\"></blip>" + "</thread>" + "</blip>"); assertMirrorConversationEquivalent(); } public void testAppendBlipsInReplyThreadsUpdatesManifest() { WaveletBasedConversationBlip blip = target.getRootThread().appendBlip(); WaveletBasedConversationThread reply = blip.addReplyThread(); WaveletBasedConversationBlip firstReplyBlip = reply.appendBlip(); WaveletBasedConversationBlip secondReplyBlip = reply.appendBlip(); assertManifestXml("<blip id=\"" + blip.getId() + "\">" + "<thread id=\"" + reply.getId() + "\">" + "<blip id=\"" + firstReplyBlip.getId() + "\"></blip>" + "<blip id=\"" + secondReplyBlip.getId() + "\"></blip>" + "</thread>" + "</blip>"); assertMirrorConversationEquivalent(); } public void testAppendInlineReplyUpdatesManifest() { WaveletBasedConversationBlip blip = target.getRootThread().appendBlip(); WaveletBasedConversationThread reply = blip.addReplyThread(locateAfterLineElement( blip.getContent())); WaveletBasedConversationBlip replyBlip = reply.appendBlip(); assertManifestXml("<blip id=\"" + blip.getId() + "\">" + "<thread id=\"" + reply.getId() + "\" inline=\"true\">" + "<blip id=\"" + replyBlip.getId() + "\"></blip>" + "</thread>" + "</blip>"); assertEquals(Blips.INITIAL_HEAD + "<body><line></line><reply id=\"" + reply.getId() + "\"></reply></body>", XmlStringBuilder.innerXml(blip.getContent()).toString()); assertMirrorConversationEquivalent(); } public void testDeleteBlipNoRepliesUpdatesManifest() { WaveletBasedConversationBlip blip = target.getRootThread().appendBlip(); blip.delete(); assertManifestXml(""); assertStructureEquivalent(XmlStringBuilder.createEmpty(), blip.getContent()); assertMirrorConversationEquivalent(); } public void testDeleteBlipWithInlineReplyUpdatesManifest() { WaveletBasedConversationBlip blip = target.getRootThread().appendBlip(); WaveletBasedConversationThread reply = blip.addReplyThread( BlipTestUtils.getBodyPosition(blip) + 3); WaveletBasedConversationBlip replyBlip = reply.appendBlip(); blip.delete(); // The first blip is gone, and the inline reply and its blip are gone too. // Both blips' content is gone. assertManifestXml(""); assertStructureEquivalent(XmlStringBuilder.createEmpty(), blip.getContent()); assertStructureEquivalent(XmlStringBuilder.createEmpty(), replyBlip.getContent()); assertMirrorConversationEquivalent(); } // Bug 2220263. public void testDeleteLastBlipInThreadRemovesThread() { ConversationBlip rootBlip = target.getRootThread().appendBlip(); ConversationThread topThread = rootBlip.addReplyThread(); ConversationBlip topBlip = topThread.appendBlip(); ConversationThread firstReply = topBlip.addReplyThread(); firstReply.appendBlip().delete(); assertNull(topBlip.getReplyThread(firstReply.getId())); assertManifestXml("<blip id=\"" + rootBlip.getId() + "\">" + "<thread id=\"" + topThread.getId() + "\">" + "<blip id=\"" + topBlip.getId() + "\"></blip>" + "</thread>" + "</blip>"); } /** * Test that producing a complex conversation structure programatically * results in the expected manifest, and that that manifest is parsed * back into an equivalent conversation structure. */ public void testComplexManifestProducesEquivalentConversation() { // Set up wavelet structure. The root wavelet has just a single blip. // The second (target) wavelet is a private reply to the root. It has // two blips in the root thread. The first has two reply threads with one // blip each. The first reply is inline, with an anchor in the content // of the root thread's first blip. WaveletBasedConversation rootConv = makeConversation(); WaveletBasedConversationBlip rootWaveletRootBlip = rootConv.getRootThread().appendBlip(); target.setAnchor(rootConv.createAnchor(rootConv.getRootThread().getFirstBlip())); WaveletBasedConversationBlip firstBlip = target.getRootThread().appendBlip(); WaveletBasedConversationBlip secondBlip = target.getRootThread().appendBlip(); WaveletBasedConversationThread firstReply = firstBlip.addReplyThread(); WaveletBasedConversationBlip firstReplyFirstBlip = firstReply.appendBlip(); final WaveletBasedConversationThread secondReply = firstBlip.addReplyThread( BlipTestUtils.getBodyPosition(firstBlip) + 1); WaveletBasedConversationBlip secondReplyFirstBlip = secondReply.appendBlip(); firstBlip.getContent().with(new Action() { @Override public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) { int location = locateAfterLineElement(doc); Point<N> point = doc.locate(location); E element = doc.createElement(point, "reply", Collections.singletonMap("id", secondReply.getId())); } }); // Clear attributes on the manifest since we're not interested in testing those. manifestDoc.with(new Action() { @Override public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) { E top = DocHelper.getFirstChildElement(doc, doc.getDocumentElement()); doc.setElementAttribute(top, "anchorBlip", null); doc.setElementAttribute(top, "anchorWavelet", null); } }); assertManifestXml( "<blip id=\"" + firstBlip.getId() + "\">" + "<thread id=\"" + firstReply.getId() + "\">" + "<blip id=\"" + firstReplyFirstBlip.getId() + "\"></blip>" + "</thread>" + "<thread id=\"" + secondReply.getId() + "\" inline=\"true\">" + "<blip id=\"" + secondReplyFirstBlip.getId() + "\"></blip>" + "</thread>" + "</blip>" + "<blip id=\"" + secondBlip.getId() + "\"></blip>"); assertMirrorConversationEquivalent(); } /** * Tests that empty threads are not ignored. */ public void testCreateWithEmptyManifestThreadNotIgnored() { ConversationBlip blip = target.getRootThread().appendBlip(); ConversationThread thread = blip.addReplyThread(); WaveletBasedConversation another = mirrorConversation(target); assertNotNull(another.getRootThread().getFirstBlip()); assertTrue(another.getRootThread().getFirstBlip().getReplyThreads().iterator().hasNext()); } /** * Test that a conversation can be created on a manifest that contains blips * that are not backed by the wavelet. */ public void testBlipMissingFromWavelet() { WaveletBasedConversation empty = target; WaveletBasedConversation nonEmpty = makeConversation(); nonEmpty.getRootThread().appendBlip(); WaveletBasedConversation conversation = WaveletBasedConversation.create(conversationView, empty.getWavelet(), nonEmpty.getManifest(), idGenerator); assertNull(conversation.getRootThread().getFirstBlip()); assertEquals(Collections.emptyList(), getBlipList(conversation.getRootThread())); } /** * Test that we can cope with blips being added to the manifest but not to the * wavelet. */ public void testAddingBlipMissingFromWavelet() { manifestDoc.with(new Action() { @Override public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) { N rootThreadNode = doc.getFirstChild(doc.getDocumentElement()); E rootThread = doc.asElement(rootThreadNode); doc.createChildElement(rootThread, "blip", Collections.singletonMap( "id", idGenerator.newBlipId())); } }); assertNull(target.getRootThread().getFirstBlip()); assertEquals(Collections.emptyList(), getBlipList(target.getRootThread())); } /** * Test that iterating a thread whose manifest contains blips not backed by the * wavelet skips those blips. */ public void testMissingBlipIteration() { WaveletBasedConversationThread thread = target.getRootThread(); WaveletBasedConversationBlip first = thread.appendBlip(); manifestDoc.with(new Action() { @Override public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) { N rootThreadNode = doc.getFirstChild(doc.getDocumentElement()); E rootThread = doc.asElement(rootThreadNode); doc.createChildElement(rootThread, "blip", Collections.singletonMap( "id", idGenerator.newBlipId())); } }); WaveletBasedConversationBlip third = thread.appendBlip(); assertEquals(3, CollectionUtils.newArrayList(thread.getManifestThread().getBlips()).size()); assertEquals(Arrays.asList(first, third), getBlipList(thread)); } // Bug 2268864. public void testObsoleteThreadThenRestoreRemoveBlipDoesntDie() { WaveletBasedConversationBlip first = target.getRootThread().appendBlip(); WaveletBasedConversationThread willBecomeEmpty = first.addReplyThread(); ConversationBlip toggleBlip = willBecomeEmpty.appendBlip(); String toggleBlipId = toggleBlip.getId(); // Make the thread empty by remotely removing its blip. ManifestBlip manifestRootBlip = target.getManifest().getRootThread().getBlip(0); ManifestThread manifestReply = manifestRootBlip.getReply(0); manifestReply.removeBlip(manifestReply.getBlip(0)); assertThreadValid(willBecomeEmpty); // Re-add then remove the blip, as can happen in playback. manifestReply.appendBlip(toggleBlipId); manifestReply.removeBlip(manifestReply.getBlip(0)); } public void testRemoveRestoreThreadAfterObsoleteThreadDoesntDie() { WaveletBasedConversationBlip first = target.getRootThread().appendBlip(); WaveletBasedConversationThread reply11 = first.addReplyThread(); ConversationBlip blip1 = reply11.appendBlip(); String blip1Id = blip1.getId(); WaveletBasedConversationThread reply2 = first.addReplyThread(); // Make first thread empty by removing its blip. ManifestBlip manifestRootBlip = target.getManifest().getRootThread().getBlip(0); ManifestThread manifestReply1 = manifestRootBlip.getReply(0); manifestReply1.removeBlip(manifestReply1.getBlip(0)); assertThreadValid(reply11); // Remove and re-add second thread, as can happen in playback. String thread2Id = reply2.getId(); ManifestThread manifestReply2 = manifestRootBlip.getReply(1); manifestRootBlip.removeReply(manifestReply2); manifestRootBlip.appendReply(thread2Id, false); assertThreadInvalid(reply2); } // // Non-interface methods. // public void testGetBlipRetrievesBlip() { WaveletBasedConversationBlip blip = target.getRootThread().appendBlip(); WaveletBasedConversationThread reply = blip.addReplyThread(); WaveletBasedConversationBlip replyBlip = reply.appendBlip(); assertSame(blip, target.getBlip(blip.getId())); assertSame(replyBlip, target.getBlip(replyBlip.getId())); assertNull(target.getBlip("foobar")); } // // Concurrent behaviour. // public void testConcrrentDeletionOfFinalBlipsLeavesEmptyThread() { WaveletBasedConversationBlip first = target.getRootThread().appendBlip(); WaveletBasedConversationThread replyThread = first.addReplyThread(); WaveletBasedConversationBlip b1 = replyThread.appendBlip(); WaveletBasedConversationBlip b2 = replyThread.appendBlip(); // Locally delete b1, remotely delete b2. b1.delete(); replyThread.addListener(threadListener); b2.addListener(blipListener); target.addListener(convListener); remoteRemoveBlip(b2); // Expect blip deletion events and it to be invalid. verify(blipListener).onDeleted(); verify(convListener).onBlipDeleted(b2); assertBlipInvalid(b2); assertThreadValid(replyThread); assertEquals(Arrays.asList(replyThread), CollectionUtils.newArrayList(first.getReplyThreads())); // The manifest now has a thread with no blips. assertEquals(1, first.getManifestBlip().numReplies()); assertEquals(0, first.getManifestBlip().getReply(0).numBlips()); // Still there after the next mutation. ObservableConversationBlip second = target.getRootThread().appendBlip(); assertThreadValid(replyThread); assertEquals(Arrays.asList(replyThread), CollectionUtils.newArrayList(first.getReplyThreads())); verify(convListener).onBlipAdded(second); verifyNoMoreInteractions(blipListener, threadListener, convListener); } public void testConcurrentDeletionOfFinalThreadsLeavesEmptyBlip() { WaveletBasedConversationBlip first = target.getRootThread().appendBlip(); WaveletBasedConversationThread t1 = first.addReplyThread(); WaveletBasedConversationBlip t1b = t1.appendBlip(); WaveletBasedConversationThread t2 = first.addReplyThread(); WaveletBasedConversationBlip t2b = t2.appendBlip(); // Locally delete t1, remotely delete t2. t1b.delete(); first.addListener(blipListener); t2.addListener(threadListener); target.addListener(convListener); remoteRemoveBlip(t2b); remoteRemoveThread(t2); // Expect thread t2 deletion events and it to be invalid. verify(threadListener).onDeleted(); verify(convListener).onBlipDeleted(t2b); verify(convListener).onThreadDeleted(t2); assertBlipInvalid(t2b); assertThreadInvalid(t2); assertBlipValid(first); assertNotNull(target.getRootThread().getFirstBlip()); // The manifest now has an empty blip. assertEquals(0, first.getManifestBlip().numReplies()); // Still there after next write. WaveletBasedConversationBlip second = target.getRootThread().appendBlip(); assertBlipValid(first); verify(convListener).onBlipAdded(second); verifyNoMoreInteractions(blipListener, threadListener, convListener); } /** * Sets an attribute value on the conversation element in a manifest document. * * @param doc manifest document * @param attribute attribute to set * @param value value to set */ private static void setManifestAttribute(MutableDocument<?, ?, ?> doc, final String attribute, final String value) { doc.with(new Action() { @Override public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) { E top = DocHelper.expectAndGetFirstTopLevelElement( doc, DocumentBasedManifest.MANIFEST_TOP_TAG); doc.setElementAttribute(top, attribute, value); } }); } /** * Gets an attribute value from the conversation element in a manifest * document. * * @param doc manifest document * @param attribute attribute to get * @return value of {@code attribute} */ private static <N> String getManifestAttribute(MutableDocument<N, ?, ?> doc, String attribute) { // NOTE(user): two-stage generic unfunging is required for Sun's compiler. return getManifestAttributeInner(doc, attribute); } private static <E> String getManifestAttributeInner(MutableDocument<? super E, E, ?> doc, String attribute) { E top = DocHelper.expectAndGetFirstTopLevelElement(doc, DocumentBasedManifest.MANIFEST_TOP_TAG); return doc.getAttribute(top, attribute); } /** * Removes a blip from the manifest, as though a remote client had done so. */ private void remoteRemoveBlip(WaveletBasedConversationBlip blip) { ManifestThread parentThread = blip.getThread().getManifestThread(); ManifestBlip manifestBlip = blip.getManifestBlip(); parentThread.removeBlip(manifestBlip); } /** * Removes a thread from the manifest, as though a remote client had done so. */ private void remoteRemoveThread(WaveletBasedConversationThread thread) { ManifestBlip parentBlip = thread.getParentBlip().getManifestBlip(); ManifestThread manifestThread = thread.getManifestThread(); parentBlip.removeReply(manifestThread); } /** * Asserts that the manifest content within the "conversation" tag matches an * expected string. */ private void assertManifestXml(final String expected) { manifestDoc.with(new Action() { @Override public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) { XmlStringBuilder exp = XmlStringBuilder.createFromXmlString(expected); assertStructureEquivalent(exp.wrap("conversation"), doc); } }); } /** * Asserts that a new conversation model built on top of the target * conversation's substrate matches the structure of that. */ private void assertMirrorConversationEquivalent() { Conversation copy = mirrorConversation(target); assertThreadsEquivalent(target.getRootThread(), copy.getRootThread()); } private static void assertThreadsEquivalent(ConversationThread expected, ConversationThread actual) { assertEquals("Mismatched id in constructed conversation thread", expected.getId(), actual.getId()); assertEquals("Mismatched first blip in constructed conversation thread", expected.getFirstBlip() == null, actual.getFirstBlip() == null); Iterator<? extends ConversationBlip> expectedBlips = expected.getBlips().iterator(); Iterator<? extends ConversationBlip> actualBlips = actual.getBlips().iterator(); while (expectedBlips.hasNext()) { assertTrue("Missing blip in reconstructed conversation", actualBlips.hasNext()); ConversationBlip expectedBlip = expectedBlips.next(); ConversationBlip actualBlip = actualBlips.next(); assertBlipsEquivalent(expectedBlip, actualBlip); } assertFalse("Extra blip in reconstructed conversation", actualBlips.hasNext()); } private static void assertBlipsEquivalent(ConversationBlip expected, ConversationBlip actual) { assertEquals("Mismatched id in constructed conversation blip", expected.getId(), actual.getId()); assertEquals("Mismatched author in constructed conversation blip", expected.getAuthorId(), actual.getAuthorId()); assertEquals("Mismatched timestamp in constructed conversation blip", expected.getLastModifiedTime(), actual.getLastModifiedTime()); assertEquals("Mismatched contributors in constructed conversation blip", expected.getContributorIds(), actual.getContributorIds()); Iterator<? extends ConversationThread> expectedThreads = expected.getReplyThreads().iterator(); Iterator<? extends ConversationThread> actualThreads = actual.getReplyThreads().iterator(); while (expectedThreads.hasNext()) { assertTrue("Missing thread in reconstructed conversation", actualThreads.hasNext()); ConversationThread expectedThread = expectedThreads.next(); ConversationThread actualThread = actualThreads.next(); assertThreadsEquivalent(expectedThread, actualThread); } assertFalse("Extra thread in reconstructed conversation", actualThreads.hasNext()); } }