diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java index 59c88d4d7fe03b492aad603783d4718ee1abf50b..1714a49ea9b6861671d83be2e1313cbbe423262c 100644 --- a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java @@ -2,13 +2,30 @@ package org.briarproject; import junit.framework.Assert; -import org.briarproject.api.db.DatabaseComponent; +import net.jodah.concurrentunit.Waiter; + +import org.briarproject.api.contact.Contact; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.db.DbException; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPost; import org.briarproject.api.forum.ForumPostFactory; import org.briarproject.api.forum.ForumPostHeader; +import org.briarproject.api.forum.ForumSharingManager; +import org.briarproject.api.identity.AuthorFactory; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.lifecycle.LifecycleManager; import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.SyncSession; +import org.briarproject.api.sync.SyncSessionFactory; +import org.briarproject.api.system.Clock; import org.briarproject.contact.ContactModule; import org.briarproject.crypto.CryptoModule; import org.briarproject.forum.ForumModule; @@ -17,55 +34,101 @@ import org.briarproject.properties.PropertiesModule; import org.briarproject.sync.SyncModule; import org.briarproject.transport.TransportModule; import org.briarproject.util.StringUtils; +import org.junit.After; import org.junit.Before; import org.junit.Test; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; import java.util.Collection; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; import javax.inject.Inject; import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.TestCase.assertFalse; +import static org.briarproject.TestPluginsModule.MAX_LATENCY; +import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.PENDING; +import static org.briarproject.api.sync.ValidationManager.State.VALID; import static org.junit.Assert.assertTrue; public class ForumManagerTest { + private LifecycleManager lifecycleManager0, lifecycleManager1; + private SyncSessionFactory sync0, sync1; + private ForumManager forumManager0, forumManager1; + private ContactManager contactManager0, contactManager1; + private ContactId contactId0,contactId1; + private IdentityManager identityManager0, identityManager1; + private LocalAuthor author0, author1; + private Forum forum0; + @Inject - protected ForumManager forumManager; + Clock clock; @Inject - protected ForumPostFactory forumPostFactory; + AuthorFactory authorFactory; @Inject - protected DatabaseComponent db; + ForumPostFactory forumPostFactory; + + // objects accessed from background threads need to be volatile + private volatile ForumSharingManager forumSharingManager0; + private volatile ForumSharingManager forumSharingManager1; + private volatile Waiter validationWaiter; + private volatile Waiter deliveryWaiter; private final File testDir = TestUtils.getTestDirectory(); + private final SecretKey master = TestUtils.getSecretKey(); + private final int TIMEOUT = 15000; + private final String SHARER = "Sharer"; + private final String INVITEE = "Invitee"; + + private static final Logger LOG = + Logger.getLogger(ForumSharingIntegrationTest.class.getName()); + + private ForumManagerTestComponent t0, t1; @Before public void setUp() throws Exception { + ForumManagerTestComponent component = + DaggerForumManagerTestComponent.builder().build(); + component.inject(this); + injectEagerSingletons(component); assertTrue(testDir.mkdirs()); - File tDir = new File(testDir, "db"); + File t0Dir = new File(testDir, SHARER); + t0 = DaggerForumManagerTestComponent.builder() + .testDatabaseModule(new TestDatabaseModule(t0Dir)).build(); + injectEagerSingletons(t0); + File t1Dir = new File(testDir, INVITEE); + t1 = DaggerForumManagerTestComponent.builder() + .testDatabaseModule(new TestDatabaseModule(t1Dir)).build(); + injectEagerSingletons(t1); - ForumManagerTestComponent component = - DaggerForumManagerTestComponent.builder() - .testDatabaseModule(new TestDatabaseModule(tDir)) - .build(); + identityManager0 = t0.getIdentityManager(); + identityManager1 = t1.getIdentityManager(); + contactManager0 = t0.getContactManager(); + contactManager1 = t1.getContactManager(); + forumManager0 = t0.getForumManager(); + forumManager1 = t1.getForumManager(); + forumSharingManager0 = t0.getForumSharingManager(); + forumSharingManager1 = t1.getForumSharingManager(); + sync0 = t0.getSyncSessionFactory(); + sync1 = t1.getSyncSessionFactory(); - component.inject(new LifecycleModule.EagerSingletons()); - component.inject(new ForumModule.EagerSingletons()); - component.inject(new CryptoModule.EagerSingletons()); - component.inject(new ContactModule.EagerSingletons()); - component.inject(new TransportModule.EagerSingletons()); - component.inject(new SyncModule.EagerSingletons()); - component.inject(new PropertiesModule.EagerSingletons()); - component.inject(this); + // initialize waiters fresh for each test + validationWaiter = new Waiter(); + deliveryWaiter = new Waiter(); } - ForumPost createForumPost(GroupId groupId, ForumPost parent, String body, - long ms) - throws Exception { + private ForumPost createForumPost(GroupId groupId, ForumPost parent, + String body, long ms) throws Exception { return forumPostFactory.createAnonymousPost(groupId, ms, parent == null ? null : parent.getMessage().getId(), "text/plain", StringUtils.toUtf8(body)); @@ -73,13 +136,12 @@ public class ForumManagerTest { @Test public void testForumPost() throws Exception { - assertFalse(db.open()); - assertNotNull(forumManager); - Forum forum = forumManager.addForum("TestForum"); - assertEquals(1, forumManager.getForums().size()); - final long ms1 = System.currentTimeMillis() - 1000L; + startLifecycles(); + Forum forum = forumManager0.addForum("TestForum"); + assertEquals(1, forumManager0.getForums().size()); + final long ms1 = clock.currentTimeMillis() - 1000L; final String body1 = "some forum text"; - final long ms2 = System.currentTimeMillis(); + final long ms2 = clock.currentTimeMillis(); final String body2 = "some other forum text"; ForumPost post1 = createForumPost(forum.getGroup().getId(), null, body1, ms1); @@ -87,16 +149,16 @@ public class ForumManagerTest { ForumPost post2 = createForumPost(forum.getGroup().getId(), post1, body2, ms2); assertEquals(ms2, post2.getMessage().getTimestamp()); - forumManager.addLocalPost(post1); - forumManager.setReadFlag(post1.getMessage().getId(), true); - forumManager.addLocalPost(post2); - forumManager.setReadFlag(post2.getMessage().getId(), false); + forumManager0.addLocalPost(post1); + forumManager0.setReadFlag(post1.getMessage().getId(), true); + forumManager0.addLocalPost(post2); + forumManager0.setReadFlag(post2.getMessage().getId(), false); Collection<ForumPostHeader> headers = - forumManager.getPostHeaders(forum.getGroup().getId()); + forumManager0.getPostHeaders(forum.getGroup().getId()); assertEquals(2, headers.size()); for (ForumPostHeader h : headers) { final String hBody = - StringUtils.fromUtf8(forumManager.getPostBody(h.getId())); + StringUtils.fromUtf8(forumManager0.getPostBody(h.getId())); boolean isPost1 = h.getId().equals(post1.getMessage().getId()); boolean isPost2 = h.getId().equals(post2.getMessage().getId()); @@ -114,8 +176,258 @@ public class ForumManagerTest { assertFalse(h.isRead()); } } - forumManager.removeForum(forum); - assertEquals(0, forumManager.getForums().size()); - db.close(); + forumManager0.removeForum(forum); + assertEquals(0, forumManager0.getForums().size()); + stopLifecycles(); } + + @Test + public void testForumPostDelivery() throws Exception { + startLifecycles(); + defaultInit(); + + // share forum + GroupId g = forum0.getId(); + forumSharingManager0.sendForumInvitation(g, contactId1, null); + sync0To1(); + deliveryWaiter.await(TIMEOUT, 1); + Contact c0 = contactManager1.getContact(contactId0); + forumSharingManager1.respondToInvitation(forum0, c0, true); + sync1To0(); + deliveryWaiter.await(TIMEOUT, 1); + + // add one forum post + long time = clock.currentTimeMillis(); + ForumPost post1 = createForumPost(g, null, "a", time); + forumManager0.addLocalPost(post1); + assertEquals(1, forumManager0.getPostHeaders(g).size()); + assertEquals(0, forumManager1.getPostHeaders(g).size()); + + // send post to 1 + sync0To1(); + deliveryWaiter.await(TIMEOUT, 1); + assertEquals(1, forumManager1.getPostHeaders(g).size()); + + stopLifecycles(); + } + + @Test + public void testForumPostDeliveredAfterParent() throws Exception { + startLifecycles(); + defaultInit(); + + // share forum + GroupId g = forum0.getId(); + forumSharingManager0.sendForumInvitation(g, contactId1, null); + sync0To1(); + deliveryWaiter.await(TIMEOUT, 1); + Contact c0 = contactManager1.getContact(contactId0); + forumSharingManager1.respondToInvitation(forum0, c0, true); + sync1To0(); + deliveryWaiter.await(TIMEOUT, 1); + + // add one forum post without the parent + long time = clock.currentTimeMillis(); + ForumPost post1 = createForumPost(g, null, "a", time); + ForumPost post2 = createForumPost(g, post1, "a", time); + forumManager0.addLocalPost(post2); + assertEquals(1, forumManager0.getPostHeaders(g).size()); + assertEquals(0, forumManager1.getPostHeaders(g).size()); + + // send post to 1 without waiting for message delivery + sync0To1(); + validationWaiter.await(TIMEOUT, 1); + assertEquals(0, forumManager1.getPostHeaders(g).size()); + + // now add the parent post as well + forumManager0.addLocalPost(post1); + assertEquals(2, forumManager0.getPostHeaders(g).size()); + assertEquals(0, forumManager1.getPostHeaders(g).size()); + + // and send it over to 1 and wait for a second message to be delivered + sync0To1(); + deliveryWaiter.await(TIMEOUT, 2); + assertEquals(2, forumManager1.getPostHeaders(g).size()); + + stopLifecycles(); + } + + @Test + public void testForumPostWithParentInOtherGroup() throws Exception { + startLifecycles(); + defaultInit(); + + // share forum + GroupId g = forum0.getId(); + forumSharingManager0.sendForumInvitation(g, contactId1, null); + sync0To1(); + deliveryWaiter.await(TIMEOUT, 1); + Contact c0 = contactManager1.getContact(contactId0); + forumSharingManager1.respondToInvitation(forum0, c0, true); + sync1To0(); + deliveryWaiter.await(TIMEOUT, 1); + + // share a second forum + Forum forum1 = forumManager0.addForum("Test Forum1"); + GroupId g1 = forum1.getId(); + forumSharingManager0.sendForumInvitation(g1, contactId1, null); + sync0To1(); + deliveryWaiter.await(TIMEOUT, 1); + forumSharingManager1.respondToInvitation(forum1, c0, true); + sync1To0(); + deliveryWaiter.await(TIMEOUT, 1); + + // add one forum post with a parent in another forum + long time = clock.currentTimeMillis(); + ForumPost post1 = createForumPost(g1, null, "a", time); + ForumPost post = createForumPost(g, post1, "b", time); + forumManager0.addLocalPost(post); + assertEquals(1, forumManager0.getPostHeaders(g).size()); + assertEquals(0, forumManager1.getPostHeaders(g).size()); + + // send posts to 1 + sync0To1(); + validationWaiter.await(TIMEOUT, 1); + assertEquals(1, forumManager0.getPostHeaders(g).size()); + assertEquals(0, forumManager1.getPostHeaders(g).size()); + + // now also add the parent post which is in another group + forumManager0.addLocalPost(post1); + assertEquals(1, forumManager0.getPostHeaders(g1).size()); + assertEquals(0, forumManager1.getPostHeaders(g1).size()); + + // send posts to 1 + sync0To1(); + deliveryWaiter.await(TIMEOUT, 1); + assertEquals(1, forumManager0.getPostHeaders(g).size()); + assertEquals(1, forumManager0.getPostHeaders(g1).size()); + // the next line is critical, makes sure post doesn't show up + assertEquals(0, forumManager1.getPostHeaders(g).size()); + assertEquals(1, forumManager1.getPostHeaders(g1).size()); + + stopLifecycles(); + } + + @After + public void tearDown() throws Exception { + TestUtils.deleteTestDirectory(testDir); + } + + private class Listener implements EventListener { + public void eventOccurred(Event e) { + if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent event = (MessageStateChangedEvent) e; + if (!event.isLocal()) { + if (event.getState() == DELIVERED) { + deliveryWaiter.resume(); + } else if (event.getState() == VALID || + event.getState() == INVALID || + event.getState() == PENDING) { + validationWaiter.resume(); + } + } + } + } + } + + private void defaultInit() throws DbException { + addDefaultIdentities(); + addDefaultContacts(); + addForum(); + listenToEvents(); + } + + private void addDefaultIdentities() throws DbException { + author0 = authorFactory.createLocalAuthor(SHARER, + TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH), + TestUtils.getRandomBytes(123)); + identityManager0.addLocalAuthor(author0); + author1 = authorFactory.createLocalAuthor(INVITEE, + TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH), + TestUtils.getRandomBytes(123)); + identityManager1.addLocalAuthor(author1); + } + + private void addDefaultContacts() throws DbException { + // sharer adds invitee as contact + contactId1 = contactManager0.addContact(author1, + author0.getId(), master, clock.currentTimeMillis(), true, + true + ); + // invitee adds sharer back + contactId0 = contactManager1.addContact(author0, + author1.getId(), master, clock.currentTimeMillis(), true, + true + ); + } + + private void addForum() throws DbException { + forum0 = forumManager0.addForum("Test Forum"); + } + + private void listenToEvents() { + Listener listener0 = new Listener(); + t0.getEventBus().addListener(listener0); + Listener listener1 = new Listener(); + t1.getEventBus().addListener(listener1); + } + + private void sync0To1() throws IOException, TimeoutException { + deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1"); + } + + private void sync1To0() throws IOException, TimeoutException { + deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0"); + } + + private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId, + SyncSessionFactory toSync, ContactId toId, String debug) + throws IOException, TimeoutException { + + if (debug != null) LOG.info("TEST: Sending message from " + debug); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Create an outgoing sync session + SyncSession sessionFrom = + fromSync.createSimplexOutgoingSession(toId, MAX_LATENCY, out); + // Write whatever needs to be written + sessionFrom.run(); + out.close(); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + // Create an incoming sync session + SyncSession sessionTo = toSync.createIncomingSession(fromId, in); + // Read whatever needs to be read + sessionTo.run(); + in.close(); + } + + private void startLifecycles() throws InterruptedException { + // Start the lifecycle manager and wait for it to finish + lifecycleManager0 = t0.getLifecycleManager(); + lifecycleManager1 = t1.getLifecycleManager(); + lifecycleManager0.startServices(); + lifecycleManager1.startServices(); + lifecycleManager0.waitForStartup(); + lifecycleManager1.waitForStartup(); + } + + private void stopLifecycles() throws InterruptedException { + // Clean up + lifecycleManager0.stopServices(); + lifecycleManager1.stopServices(); + lifecycleManager0.waitForShutdown(); + lifecycleManager1.waitForShutdown(); + } + + private void injectEagerSingletons(ForumManagerTestComponent component) { + component.inject(new LifecycleModule.EagerSingletons()); + component.inject(new ForumModule.EagerSingletons()); + component.inject(new CryptoModule.EagerSingletons()); + component.inject(new ContactModule.EagerSingletons()); + component.inject(new TransportModule.EagerSingletons()); + component.inject(new SyncModule.EagerSingletons()); + component.inject(new PropertiesModule.EagerSingletons()); + } + } diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java index cac69fb0df0754b6af1ada181fb29512d27d2e8b..8a9e5fc6db41a653e6e9737a39874cb89dce6b4c 100644 --- a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java +++ b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java @@ -1,5 +1,12 @@ package org.briarproject; +import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.forum.ForumManager; +import org.briarproject.api.forum.ForumSharingManager; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.lifecycle.LifecycleManager; +import org.briarproject.api.sync.SyncSessionFactory; import org.briarproject.clients.ClientsModule; import org.briarproject.contact.ContactModule; import org.briarproject.crypto.CryptoModule; @@ -55,4 +62,18 @@ public interface ForumManagerTestComponent { void inject(TransportModule.EagerSingletons init); + LifecycleManager getLifecycleManager(); + + EventBus getEventBus(); + + IdentityManager getIdentityManager(); + + ContactManager getContactManager(); + + ForumSharingManager getForumSharingManager(); + + ForumManager getForumManager(); + + SyncSessionFactory getSyncSessionFactory(); + } diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java index 87cfec0977269ba321f496c532fa72d616a56e7a..79809f444b7fa97999576616b48fa58182f7278e 100644 --- a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java @@ -18,7 +18,7 @@ import org.briarproject.api.event.Event; import org.briarproject.api.event.EventListener; import org.briarproject.api.event.ForumInvitationReceivedEvent; import org.briarproject.api.event.ForumInvitationResponseReceivedEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumInvitationMessage; import org.briarproject.api.forum.ForumManager; @@ -27,9 +27,12 @@ import org.briarproject.api.identity.AuthorFactory; import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.lifecycle.LifecycleManager; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.SyncSession; import org.briarproject.api.sync.SyncSessionFactory; +import org.briarproject.api.sync.ValidationManager; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.system.Clock; import org.briarproject.contact.ContactModule; import org.briarproject.crypto.CryptoModule; @@ -60,6 +63,8 @@ import static org.briarproject.TestPluginsModule.MAX_LATENCY; import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH; import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION; import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -669,14 +674,15 @@ public class ForumSharingIntegrationTest extends BriarTestCase { public volatile boolean responseReceived = false; public void eventOccurred(Event e) { - if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent event = (MessageValidatedEvent) e; - if (event.getClientId() - .equals(forumSharingManager0.getClientId()) && + if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent event = (MessageStateChangedEvent) e; + State s = event.getState(); + ClientId c = event.getClientId(); + if ((s == DELIVERED || s == INVALID) && + c.equals(forumSharingManager0.getClientId()) && !event.isLocal()) { LOG.info("TEST: Sharer received message in group " + - ((MessageValidatedEvent) e).getMessage() - .getGroupId().hashCode()); + event.getMessage().getGroupId().hashCode()); msgWaiter.resume(); } } else if (e instanceof ForumInvitationResponseReceivedEvent) { @@ -722,14 +728,15 @@ public class ForumSharingIntegrationTest extends BriarTestCase { } public void eventOccurred(Event e) { - if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent event = (MessageValidatedEvent) e; - if (event.getClientId() - .equals(forumSharingManager1.getClientId()) && + if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent event = (MessageStateChangedEvent) e; + State s = event.getState(); + ClientId c = event.getClientId(); + if ((s == DELIVERED || s == INVALID) && + c.equals(forumSharingManager0.getClientId()) && !event.isLocal()) { LOG.info("TEST: Invitee received message in group " + - ((MessageValidatedEvent) e).getMessage() - .getGroupId().hashCode()); + event.getMessage().getGroupId().hashCode()); msgWaiter.resume(); } } else if (e instanceof ForumInvitationReceivedEvent) { diff --git a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java index 259be1a9d4f5295d7ab36be98822125757cae24e..52fad50bbfb83256fd3af44f4e9b6c4ed827d970 100644 --- a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java @@ -19,7 +19,7 @@ import org.briarproject.api.event.IntroductionAbortedEvent; import org.briarproject.api.event.IntroductionRequestReceivedEvent; import org.briarproject.api.event.IntroductionResponseReceivedEvent; import org.briarproject.api.event.IntroductionSucceededEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.identity.AuthorFactory; import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.identity.LocalAuthor; @@ -29,10 +29,13 @@ import org.briarproject.api.introduction.IntroductionRequest; import org.briarproject.api.lifecycle.LifecycleManager; import org.briarproject.api.properties.TransportProperties; import org.briarproject.api.properties.TransportPropertyManager; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.SyncSession; import org.briarproject.api.sync.SyncSessionFactory; +import org.briarproject.api.sync.ValidationManager; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.system.Clock; import org.briarproject.contact.ContactModule; import org.briarproject.crypto.CryptoModule; @@ -70,6 +73,8 @@ import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID; import static org.briarproject.api.introduction.IntroductionConstants.TYPE; import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -1047,15 +1052,16 @@ public class IntroductionIntegrationTest extends BriarTestCase { @Override public void eventOccurred(Event e) { - if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent event = (MessageValidatedEvent) e; - if (event.getClientId() - .equals(introductionManager0.getClientId()) && + if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent event = (MessageStateChangedEvent) e; + State s = event.getState(); + ClientId c = event.getClientId(); + if ((s == DELIVERED || s == INVALID) && + c.equals(introductionManager0.getClientId()) && !event.isLocal()) { LOG.info("TEST: Introducee" + introducee + " received message in group " + - ((MessageValidatedEvent) e).getMessage() - .getGroupId().hashCode()); + event.getMessage().getGroupId().hashCode()); msgWaiter.resume(); } } else if (e instanceof IntroductionRequestReceivedEvent) { @@ -1114,14 +1120,13 @@ public class IntroductionIntegrationTest extends BriarTestCase { @Override public void eventOccurred(Event e) { - if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent event = (MessageValidatedEvent) e; - if (event.getClientId() + if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent event = (MessageStateChangedEvent) e; + if (event.getState() == DELIVERED && event.getClientId() .equals(introductionManager0.getClientId()) && !event.isLocal()) { LOG.info("TEST: Introducer received message in group " + - ((MessageValidatedEvent) e).getMessage() - .getGroupId().hashCode()); + event.getMessage().getGroupId().hashCode()); msgWaiter.resume(); } } else if (e instanceof IntroductionResponseReceivedEvent) { diff --git a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java index 166c8e84fb05ba68632d9d2921fe321bdc43fbf6..7e3d83c9df06f3710fdb934af8c1a44502bba1f9 100644 --- a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java +++ b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java @@ -24,7 +24,7 @@ import org.briarproject.api.event.ForumInvitationReceivedEvent; import org.briarproject.api.event.IntroductionRequestReceivedEvent; import org.briarproject.api.event.IntroductionResponseReceivedEvent; import org.briarproject.api.event.IntroductionSucceededEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.event.SettingsUpdatedEvent; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.lifecycle.Service; @@ -59,6 +59,7 @@ import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET; import static java.util.logging.Level.WARNING; import static org.briarproject.android.BriarActivity.GROUP_ID; import static org.briarproject.android.fragment.SettingsFragment.SETTINGS_NAMESPACE; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; class AndroidNotificationManagerImpl implements AndroidNotificationManager, Service, EventListener { @@ -156,9 +157,9 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, if (e instanceof SettingsUpdatedEvent) { SettingsUpdatedEvent s = (SettingsUpdatedEvent) e; if (s.getNamespace().equals(SETTINGS_NAMESPACE)) loadSettings(); - } else if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent m = (MessageValidatedEvent) e; - if (m.isValid() && !m.isLocal()) { + } else if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent m = (MessageStateChangedEvent) e; + if (!m.isLocal() && m.getState() == DELIVERED) { ClientId c = m.getClientId(); if (c.equals(messagingManager.getClientId())) showPrivateMessageNotification(m.getMessage().getGroupId()); diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java index d70f7d439cc2dd2facbcd91f45daeb8d71605c3d..7b21f28ac2950306b06673f50b64f200bd766971 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java @@ -30,7 +30,7 @@ import org.briarproject.api.event.ContactStatusChangedEvent; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventListener; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.forum.ForumInvitationMessage; import org.briarproject.api.forum.ForumSharingManager; import org.briarproject.api.identity.IdentityManager; @@ -54,6 +54,7 @@ import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAn import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.android.BriarActivity.GROUP_ID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; public class ContactListFragment extends BaseFragment implements EventListener { @@ -230,12 +231,13 @@ public class ContactListFragment extends BaseFragment implements EventListener { } else if (e instanceof ContactRemovedEvent) { LOG.info("Contact removed"); removeItem(((ContactRemovedEvent) e).getContactId()); - } else if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent m = (MessageValidatedEvent) e; + } else if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent m = (MessageStateChangedEvent) e; ClientId c = m.getClientId(); - if (m.isValid() && (c.equals(messagingManager.getClientId()) || - c.equals(introductionManager.getClientId()) || - c.equals(forumSharingManager.getClientId()))) { + if (m.getState() == DELIVERED && + (c.equals(messagingManager.getClientId()) || + c.equals(introductionManager.getClientId()) || + c.equals(forumSharingManager.getClientId()))) { LOG.info("Message added, reloading"); reloadConversation(m.getMessage().getGroupId()); } diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java index 2c8e3b5df4eda5a4c2d4d453f7da53b8904ae3be..f91d0644051705fffc3c8ce9e05f92de695e5d0a 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java @@ -47,7 +47,7 @@ import org.briarproject.api.event.EventListener; import org.briarproject.api.event.ForumInvitationReceivedEvent; import org.briarproject.api.event.IntroductionRequestReceivedEvent; import org.briarproject.api.event.IntroductionResponseReceivedEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.forum.ForumInvitationMessage; @@ -87,6 +87,7 @@ import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.android.contact.ConversationItem.IncomingItem; import static org.briarproject.android.contact.ConversationItem.OutgoingItem; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; public class ConversationActivity extends BriarActivity implements EventListener, OnClickListener, @@ -468,9 +469,10 @@ public class ConversationActivity extends BriarActivity LOG.info("Contact removed"); finishOnUiThread(); } - } else if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent m = (MessageValidatedEvent) e; - if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) { + } else if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent m = (MessageStateChangedEvent) e; + if (m.getState() == DELIVERED && + m.getMessage().getGroupId().equals(groupId)) { LOG.info("Message added, reloading"); // Mark new incoming messages as read directly if (m.isLocal()) loadMessages(); diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java index b2069a6f2d7d94bf04e1b234d9c2049ced398fbc..cfa26e8c8adf0b7e0fd68a6ec1ed4cbb4a185eba 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java @@ -30,7 +30,7 @@ import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventListener; import org.briarproject.api.event.GroupRemovedEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPostHeader; @@ -62,6 +62,7 @@ import static java.util.logging.Level.WARNING; import static org.briarproject.android.forum.ReadForumPostActivity.RESULT_PREV_NEXT; import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH; import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; public class ForumActivity extends BriarActivity implements EventListener, OnItemClickListener { @@ -356,9 +357,10 @@ public class ForumActivity extends BriarActivity implements EventListener, } public void eventOccurred(Event e) { - if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent m = (MessageValidatedEvent) e; - if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) { + if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent m = (MessageStateChangedEvent) e; + if (m.getState() == DELIVERED && + m.getMessage().getGroupId().equals(groupId)) { LOG.info("Message added, reloading"); loadHeaders(); } diff --git a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java index e6f0f6a26685e7feee22c25e8be0a15a322dbe2a..9c32079838aa9290bf4c9b19412fc9b67affbcd9 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java +++ b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java @@ -25,7 +25,7 @@ import org.briarproject.api.event.Event; import org.briarproject.api.event.ForumInvitationReceivedEvent; import org.briarproject.api.event.GroupAddedEvent; import org.briarproject.api.event.GroupRemovedEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPostHeader; @@ -42,6 +42,7 @@ import javax.inject.Inject; import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; public class ForumListFragment extends BaseEventFragment implements View.OnClickListener { @@ -224,14 +225,13 @@ public class ForumListFragment extends BaseEventFragment implements LOG.info("Forum removed, removing from list"); removeForum(g.getGroup().getId()); } - } else if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent m = (MessageValidatedEvent) e; - if (m.isValid()) { - ClientId c = m.getClientId(); - if (c.equals(forumManager.getClientId())) { - LOG.info("Forum post added, reloading"); - loadForumHeaders(m.getMessage().getGroupId()); - } + } else if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent m = (MessageStateChangedEvent) e; + ClientId c = m.getClientId(); + if (m.getState() == DELIVERED && + c.equals(forumManager.getClientId())) { + LOG.info("Forum post added, reloading"); + loadForumHeaders(m.getMessage().getGroupId()); } } else if (e instanceof ForumInvitationReceivedEvent) { loadAvailableForums(); diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java index e145d9e8442fd022e6ee1424429950231a658cf0..123d8e4b16ae98100ca856778314eacfdc4c98de 100644 --- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java +++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java @@ -22,6 +22,8 @@ import org.briarproject.api.transport.TransportKeys; import java.util.Collection; import java.util.Map; +import static org.briarproject.api.sync.ValidationManager.State; + /** * Encapsulates the database implementation and exposes high-level operations * to other components. @@ -233,6 +235,24 @@ public interface DatabaseComponent { Collection<MessageId> getMessagesToValidate(Transaction txn, ClientId c) throws DbException; + /** + * Returns the IDs of any messages that need to be delivered to the given + * client. + * <p/> + * Read-only. + */ + Collection<MessageId> getMessagesToDeliver(Transaction txn, ClientId c) + throws DbException; + + /** + * Returns the IDs of any messages that are still pending due to + * dependencies to other messages for the given client. + * <p/> + * Read-only. + */ + Collection<MessageId> getPendingMessages(Transaction txn, ClientId c) + throws DbException; + /** * Returns the message with the given ID, in serialised form, or null if * the message has been deleted. @@ -276,6 +296,22 @@ public interface DatabaseComponent { Collection<MessageStatus> getMessageStatus(Transaction txn, ContactId c, GroupId g) throws DbException; + /** + * Returns the dependencies of the given message. + * <p/> + * Read-only. + */ + Map<MessageId, State> getMessageDependencies(Transaction txn, MessageId m) + throws DbException; + + /** + * Returns all IDs of messages that depend on the given message. + * <p/> + * Read-only. + */ + Map<MessageId, State> getMessageDependents(Transaction txn, MessageId m) + throws DbException; + /** * Returns the status of the given message with respect to the given * contact. @@ -391,11 +427,17 @@ public interface DatabaseComponent { throws DbException; /** - * Marks the given message as valid or invalid. + * Sets the state of the message with respect to validation and delivery. */ - void setMessageValid(Transaction txn, Message m, ClientId c, boolean valid) + void setMessageState(Transaction txn, Message m, ClientId c, State valid) throws DbException; + /** + * Adds dependencies for a message + */ + void addMessageDependencies(Transaction txn, Message dependent, + Collection<MessageId> dependencies) throws DbException; + /** * Sets the reordering window for the given contact and transport in the * given rotation period. diff --git a/briar-api/src/org/briarproject/api/event/MessageStateChangedEvent.java b/briar-api/src/org/briarproject/api/event/MessageStateChangedEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..c2aafa0347525bb9160aa064e3ea8dc9a56d7e6a --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/MessageStateChangedEvent.java @@ -0,0 +1,42 @@ +package org.briarproject.api.event; + +import org.briarproject.api.sync.ClientId; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.ValidationManager; +import static org.briarproject.api.sync.ValidationManager.State; + +/** + * An event that is broadcast when a message state changed. + */ +public class MessageStateChangedEvent extends Event { + + private final Message message; + private final ClientId clientId; + private final boolean local; + private final State state; + + public MessageStateChangedEvent(Message message, ClientId clientId, + boolean local, State state) { + this.message = message; + this.clientId = clientId; + this.local = local; + this.state = state; + } + + public Message getMessage() { + return message; + } + + public ClientId getClientId() { + return clientId; + } + + public boolean isLocal() { + return local; + } + + public State getState() { + return state; + } + +} diff --git a/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java b/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java deleted file mode 100644 index 26216a873019ec9c6a1fcb5407fe06e718dd913f..0000000000000000000000000000000000000000 --- a/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.briarproject.api.event; - -import org.briarproject.api.db.Metadata; -import org.briarproject.api.sync.ClientId; -import org.briarproject.api.sync.Message; - -/** - * An event that is broadcast when a message has passed or failed validation. - */ -public class MessageValidatedEvent extends Event { - - private final Message message; - private final ClientId clientId; - private final boolean local, valid; - - public MessageValidatedEvent(Message message, ClientId clientId, - boolean local, boolean valid) { - this.message = message; - this.clientId = clientId; - this.local = local; - this.valid = valid; - } - - public Message getMessage() { - return message; - } - - public ClientId getClientId() { - return clientId; - } - - public boolean isLocal() { - return local; - } - - public boolean isValid() { - return valid; - } -} diff --git a/briar-api/src/org/briarproject/api/sync/ValidationManager.java b/briar-api/src/org/briarproject/api/sync/ValidationManager.java index 7304b2e038254426500eb2110889ec47f5a0bb5b..e92a6a6827f1fa4dc92df217a7863c1cd3225665 100644 --- a/briar-api/src/org/briarproject/api/sync/ValidationManager.java +++ b/briar-api/src/org/briarproject/api/sync/ValidationManager.java @@ -10,13 +10,13 @@ import org.briarproject.api.db.Transaction; */ public interface ValidationManager { - enum Validity { + enum State { - UNKNOWN(0), INVALID(1), VALID(2); + UNKNOWN(0), INVALID(1), PENDING(2), VALID(3), DELIVERED(4); private final int value; - Validity(int value) { + State(int value) { this.value = value; } @@ -24,8 +24,8 @@ public interface ValidationManager { return value; } - public static Validity fromValue(int value) { - for (Validity s : values()) if (s.value == value) return s; + public static State fromValue(int value) { + for (State s : values()) if (s.value == value) return s; throw new IllegalArgumentException(); } } diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java index 6589fbc81477147535e4af61402ae8944b2d8963..9779217904ba814b9aaa7deefb05edd6171300b8 100644 --- a/briar-core/src/org/briarproject/db/Database.java +++ b/briar-core/src/org/briarproject/db/Database.java @@ -16,7 +16,7 @@ import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageStatus; -import org.briarproject.api.sync.ValidationManager.Validity; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.transport.TransportKeys; import java.util.Collection; @@ -79,9 +79,15 @@ interface Database<T> { /** * Stores a message. */ - void addMessage(T txn, Message m, Validity validity, boolean shared) + void addMessage(T txn, Message m, State state, boolean shared) throws DbException; + /** + * Adds a dependency between two MessageIds + */ + void addMessageDependency(T txn, MessageId dependentId, + MessageId dependencyId) throws DbException; + /** * Records that a message has been offered by the given contact. */ @@ -266,6 +272,26 @@ interface Database<T> { */ Collection<LocalAuthor> getLocalAuthors(T txn) throws DbException; + /** + * Returns the dependencies of the given message. + * This method makes sure that dependencies in different groups + * are returned as {@link ValidationManager.State.INVALID}. Note that this + * is not set on the dependencies themselves; the returned states should + * only be taken in the context of the given message. + * <p/> + * Read-only. + */ + Map<MessageId, State> getMessageDependencies(T txn, MessageId m) + throws DbException; + + /** + * Returns all IDs of messages that depend on the given message. + * <p/> + * Read-only. + */ + Map<MessageId, State> getMessageDependents(T txn, MessageId m) + throws DbException; + /** * Returns the IDs of all messages in the given group. * <p/> @@ -335,6 +361,15 @@ interface Database<T> { Collection<MessageId> getMessagesToAck(T txn, ContactId c, int maxMessages) throws DbException; + /** + * Returns the IDs of any messages that need to be delivered to the given + * client. + * <p/> + * Read-only. + */ + Collection<MessageId> getMessagesToDeliver(T txn, ClientId c) + throws DbException; + /** * Returns the IDs of some messages that are eligible to be offered to the * given contact, up to the given number of messages. @@ -344,6 +379,15 @@ interface Database<T> { Collection<MessageId> getMessagesToOffer(T txn, ContactId c, int maxMessages) throws DbException; + /** + * Returns the IDs of some messages that are eligible to be requested from + * the given contact, up to the given number of messages. + * <p/> + * Read-only. + */ + Collection<MessageId> getMessagesToRequest(T txn, ContactId c, + int maxMessages) throws DbException; + /** * Returns the IDs of some messages that are eligible to be sent to the * given contact, up to the given total length. @@ -354,21 +398,21 @@ interface Database<T> { throws DbException; /** - * Returns the IDs of some messages that are eligible to be requested from - * the given contact, up to the given number of messages. + * Returns the IDs of any messages that need to be validated by the given + * client. * <p/> * Read-only. */ - Collection<MessageId> getMessagesToRequest(T txn, ContactId c, - int maxMessages) throws DbException; + Collection<MessageId> getMessagesToValidate(T txn, ClientId c) + throws DbException; /** - * Returns the IDs of any messages that need to be validated by the given - * client. + * Returns the IDs of any messages that are still pending due to + * dependencies to other messages for the given client. * <p/> * Read-only. */ - Collection<MessageId> getMessagesToValidate(T txn, ClientId c) + Collection<MessageId> getPendingMessages(T txn, ClientId c) throws DbException; /** @@ -432,14 +476,14 @@ interface Database<T> { void lowerRequestedFlag(T txn, ContactId c, Collection<MessageId> requested) throws DbException; - /* + /** * Merges the given metadata with the existing metadata for the given * group. */ void mergeGroupMetadata(T txn, GroupId g, Metadata meta) throws DbException; - /* + /** * Merges the given metadata with the existing metadata for the given * message. */ @@ -538,7 +582,8 @@ interface Database<T> { /** * Marks the given message as valid or invalid. */ - void setMessageValid(T txn, MessageId m, boolean valid) throws DbException; + void setMessageState(T txn, MessageId m, State state) + throws DbException; /** * Sets the reordering window for the given contact and transport in the diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java index 37c1f08849656f65599113db5264d502f1171928..4f84a04bd047e0a1054743cd7331a8e90ab7c3a0 100644 --- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java +++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java @@ -29,8 +29,8 @@ import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageSharedEvent; import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToRequestEvent; -import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.MessagesAckedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.event.SettingsUpdatedEvent; import org.briarproject.api.identity.Author; @@ -47,7 +47,7 @@ import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageStatus; import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.Request; -import org.briarproject.api.sync.ValidationManager.Validity; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.transport.TransportKeys; import java.util.ArrayList; @@ -64,8 +64,8 @@ import java.util.logging.Logger; import javax.inject.Inject; import static java.util.logging.Level.WARNING; -import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN; -import static org.briarproject.api.sync.ValidationManager.Validity.VALID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES; class DatabaseComponentImpl<T> implements DatabaseComponent { @@ -193,17 +193,18 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { if (!db.containsGroup(txn, m.getGroupId())) throw new NoSuchGroupException(); if (!db.containsMessage(txn, m.getId())) { - addMessage(txn, m, VALID, shared); + addMessage(txn, m, DELIVERED, shared); transaction.attach(new MessageAddedEvent(m, null)); - transaction.attach(new MessageValidatedEvent(m, c, true, true)); + transaction.attach(new MessageStateChangedEvent(m, c, true, + DELIVERED)); if (shared) transaction.attach(new MessageSharedEvent(m)); } db.mergeMessageMetadata(txn, m.getId(), meta); } - private void addMessage(T txn, Message m, Validity validity, boolean shared) + private void addMessage(T txn, Message m, State state, boolean shared) throws DbException { - db.addMessage(txn, m, validity, shared); + db.addMessage(txn, m, state, shared); for (ContactId c : db.getVisibility(txn, m.getGroupId())) { boolean offered = db.removeOfferedMessage(txn, c, m.getId()); db.addStatus(txn, c, m.getId(), offered, offered); @@ -411,6 +412,18 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { return db.getMessagesToValidate(txn, c); } + public Collection<MessageId> getMessagesToDeliver(Transaction transaction, + ClientId c) throws DbException { + T txn = unbox(transaction); + return db.getMessagesToDeliver(txn, c); + } + + public Collection<MessageId> getPendingMessages(Transaction transaction, + ClientId c) throws DbException { + T txn = unbox(transaction); + return db.getPendingMessages(txn, c); + } + public byte[] getRawMessage(Transaction transaction, MessageId m) throws DbException { T txn = unbox(transaction); @@ -463,6 +476,22 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { return db.getMessageStatus(txn, c, m); } + public Map<MessageId, State> getMessageDependencies(Transaction transaction, + MessageId m) throws DbException { + T txn = unbox(transaction); + if (!db.containsMessage(txn, m)) + throw new NoSuchMessageException(); + return db.getMessageDependencies(txn, m); + } + + public Map<MessageId, State> getMessageDependents(Transaction transaction, + MessageId m) throws DbException { + T txn = unbox(transaction); + if (!db.containsMessage(txn, m)) + throw new NoSuchMessageException(); + return db.getMessageDependents(txn, m); + } + public Settings getSettings(Transaction transaction, String namespace) throws DbException { T txn = unbox(transaction); @@ -664,14 +693,26 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { if (shared) transaction.attach(new MessageSharedEvent(m)); } - public void setMessageValid(Transaction transaction, Message m, ClientId c, - boolean valid) throws DbException { + public void setMessageState(Transaction transaction, Message m, ClientId c, + State state) throws DbException { if (transaction.isReadOnly()) throw new IllegalArgumentException(); T txn = unbox(transaction); if (!db.containsMessage(txn, m.getId())) throw new NoSuchMessageException(); - db.setMessageValid(txn, m.getId(), valid); - transaction.attach(new MessageValidatedEvent(m, c, false, valid)); + db.setMessageState(txn, m.getId(), state); + transaction.attach(new MessageStateChangedEvent(m, c, false, state)); + } + + public void addMessageDependencies(Transaction transaction, + Message dependent, Collection<MessageId> dependencies) + throws DbException { + if (transaction.isReadOnly()) throw new IllegalArgumentException(); + T txn = unbox(transaction); + if (!db.containsMessage(txn, dependent.getId())) + throw new NoSuchMessageException(); + for (MessageId dependencyId : dependencies) { + db.addMessageDependency(txn, dependent.getId(), dependencyId); + } } public void setReorderingWindow(Transaction transaction, ContactId c, diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java index e3e026f221b757029d6542927878bc8ed52e213c..a52fbe2a1da6c9729518c91fbb7e61cd5f39ddca 100644 --- a/briar-core/src/org/briarproject/db/JdbcDatabase.java +++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java @@ -19,7 +19,7 @@ import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageStatus; -import org.briarproject.api.sync.ValidationManager.Validity; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.system.Clock; import org.briarproject.api.transport.IncomingKeys; import org.briarproject.api.transport.OutgoingKeys; @@ -49,9 +49,11 @@ import java.util.logging.Logger; import static java.util.logging.Level.WARNING; import static org.briarproject.api.db.Metadata.REMOVE; -import static org.briarproject.api.sync.ValidationManager.Validity.INVALID; -import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN; -import static org.briarproject.api.sync.ValidationManager.Validity.VALID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.PENDING; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; +import static org.briarproject.api.sync.ValidationManager.State.VALID; import static org.briarproject.db.DatabaseConstants.DB_SETTINGS_NAMESPACE; import static org.briarproject.db.DatabaseConstants.DEVICE_ID_KEY; import static org.briarproject.db.DatabaseConstants.DEVICE_SETTINGS_NAMESPACE; @@ -65,8 +67,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry; */ abstract class JdbcDatabase implements Database<Connection> { - private static final int SCHEMA_VERSION = 24; - private static final int MIN_SCHEMA_VERSION = 24; + private static final int SCHEMA_VERSION = 25; + private static final int MIN_SCHEMA_VERSION = 25; private static final String CREATE_SETTINGS = "CREATE TABLE settings" @@ -131,7 +133,7 @@ abstract class JdbcDatabase implements Database<Connection> { + " (messageId HASH NOT NULL," + " groupId HASH NOT NULL," + " timestamp BIGINT NOT NULL," - + " valid INT NOT NULL," + + " state INT NOT NULL," + " shared BOOLEAN NOT NULL," + " length INT NOT NULL," + " raw BLOB," // Null if message has been deleted @@ -150,6 +152,14 @@ abstract class JdbcDatabase implements Database<Connection> { + " REFERENCES messages (messageId)" + " ON DELETE CASCADE)"; + private static final String CREATE_MESSAGE_DEPENDENCIES = + "CREATE TABLE messageDependencies" + + " (messageId HASH NOT NULL," + + " dependencyId HASH NOT NULL," // Not a foreign key + + " FOREIGN KEY (messageId)" + + " REFERENCES messages (messageId)" + + " ON DELETE CASCADE)"; + private static final String CREATE_OFFERS = "CREATE TABLE offers" + " (messageId HASH NOT NULL," // Not a foreign key @@ -320,6 +330,7 @@ abstract class JdbcDatabase implements Database<Connection> { s.executeUpdate(insertTypeNames(CREATE_GROUP_VISIBILITIES)); s.executeUpdate(insertTypeNames(CREATE_MESSAGES)); s.executeUpdate(insertTypeNames(CREATE_MESSAGE_METADATA)); + s.executeUpdate(insertTypeNames(CREATE_MESSAGE_DEPENDENCIES)); s.executeUpdate(insertTypeNames(CREATE_OFFERS)); s.executeUpdate(insertTypeNames(CREATE_STATUSES)); s.executeUpdate(insertTypeNames(CREATE_TRANSPORTS)); @@ -520,18 +531,18 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void addMessage(Connection txn, Message m, Validity validity, + public void addMessage(Connection txn, Message m, State state, boolean shared) throws DbException { PreparedStatement ps = null; try { String sql = "INSERT INTO messages (messageId, groupId, timestamp," - + " valid, shared, length, raw)" + + " state, shared, length, raw)" + " VALUES (?, ?, ?, ?, ?, ?, ?)"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getId().getBytes()); ps.setBytes(2, m.getGroupId().getBytes()); ps.setLong(3, m.getTimestamp()); - ps.setInt(4, validity.getValue()); + ps.setInt(4, state.getValue()); ps.setBoolean(5, shared); byte[] raw = m.getRaw(); ps.setInt(6, raw.length); @@ -596,6 +607,25 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public void addMessageDependency(Connection txn, MessageId dependentId, + MessageId dependencyId) throws DbException { + PreparedStatement ps = null; + try { + String sql = + "INSERT INTO messageDependencies (messageId, dependencyId)" + + " VALUES (?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, dependentId.getBytes()); + ps.setBytes(2, dependencyId.getBytes()); + int affected = ps.executeUpdate(); + if (affected != 1) throw new DbStateException(); + ps.close(); + } catch (SQLException e) { + tryToClose(ps); + throw new DbException(e); + } + } + public void addTransport(Connection txn, TransportId t, int maxLatency) throws DbException { PreparedStatement ps = null; @@ -1135,10 +1165,33 @@ abstract class JdbcDatabase implements Database<Connection> { } } + private Collection<MessageId> getMessageIds(Connection txn, GroupId g, + State state) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT messageId FROM messages" + + " WHERE state = ? AND groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, state.getValue()); + ps.setBytes(2, g.getBytes()); + rs = ps.executeQuery(); + List<MessageId> ids = new ArrayList<MessageId>(); + while (rs.next()) ids.add(new MessageId(rs.getBytes(1))); + rs.close(); + ps.close(); + return Collections.unmodifiableList(ids); + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public Collection<MessageId> getMessageIds(Connection txn, GroupId g, Metadata query) throws DbException { - // If there are no query terms, return all messages - if (query.isEmpty()) return getMessageIds(txn, g); + // If there are no query terms, return all delivered messages + if (query.isEmpty()) return getMessageIds(txn, g, DELIVERED); PreparedStatement ps = null; ResultSet rs = null; try { @@ -1148,12 +1201,14 @@ abstract class JdbcDatabase implements Database<Connection> { + " FROM messages AS m" + " JOIN messageMetadata AS md" + " ON m.messageId = md.messageId" - + " WHERE groupId = ? AND key = ? AND value = ?"; + + " WHERE state = ? AND groupId = ?" + + " AND key = ? AND value = ?"; for (Entry<String, byte[]> e : query.entrySet()) { ps = txn.prepareStatement(sql); - ps.setBytes(1, g.getBytes()); - ps.setString(2, e.getKey()); - ps.setBytes(3, e.getValue()); + ps.setInt(1, DELIVERED.getValue()); + ps.setBytes(2, g.getBytes()); + ps.setString(3, e.getKey()); + ps.setBytes(4, e.getValue()); rs = ps.executeQuery(); Set<MessageId> ids = new HashSet<MessageId>(); while (rs.next()) ids.add(new MessageId(rs.getBytes(1))); @@ -1182,10 +1237,11 @@ abstract class JdbcDatabase implements Database<Connection> { + " FROM messages AS m" + " JOIN messageMetadata AS md" + " ON m.messageId = md.messageId" - + " WHERE groupId = ?" + + " WHERE state = ? AND groupId = ?" + " ORDER BY m.messageId"; ps = txn.prepareStatement(sql); - ps.setBytes(1, g.getBytes()); + ps.setInt(1, DELIVERED.getValue()); + ps.setBytes(2, g.getBytes()); rs = ps.executeQuery(); Map<MessageId, Metadata> all = new HashMap<MessageId, Metadata>(); Metadata metadata = null; @@ -1223,23 +1279,38 @@ abstract class JdbcDatabase implements Database<Connection> { public Metadata getGroupMetadata(Connection txn, GroupId g) throws DbException { - return getMetadata(txn, g.getBytes(), "groupMetadata", "groupId"); + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT key, value FROM groupMetadata" + + " WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + rs = ps.executeQuery(); + Metadata metadata = new Metadata(); + while (rs.next()) metadata.put(rs.getString(1), rs.getBytes(2)); + rs.close(); + ps.close(); + return metadata; + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } } public Metadata getMessageMetadata(Connection txn, MessageId m) throws DbException { - return getMetadata(txn, m.getBytes(), "messageMetadata", "messageId"); - } - - private Metadata getMetadata(Connection txn, byte[] id, String tableName, - String columnName) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT key, value FROM " + tableName - + " WHERE " + columnName + " = ?"; + String sql = "SELECT key, value FROM messageMetadata AS md" + + " JOIN messages AS m" + + " ON m.messageId = md.messageId" + + " WHERE m.state = ? AND md.messageId = ?"; ps = txn.prepareStatement(sql); - ps.setBytes(1, id); + ps.setInt(1, DELIVERED.getValue()); + ps.setBytes(2, m.getBytes()); rs = ps.executeQuery(); Metadata metadata = new Metadata(); while (rs.next()) metadata.put(rs.getString(1), rs.getBytes(2)); @@ -1312,6 +1383,91 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public Map<MessageId, State> getMessageDependencies(Connection txn, + MessageId m) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT d.dependencyId, m.state, m.groupId" + + " FROM messageDependencies AS d" + + " LEFT OUTER JOIN messages AS m" + + " ON d.dependencyId = m.messageId" + + " WHERE d.messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + Map<MessageId, State> dependencies = new HashMap<MessageId, State>(); + while (rs.next()) { + MessageId messageId = new MessageId(rs.getBytes(1)); + State state = State.fromValue(rs.getInt(2)); + if (state != UNKNOWN) { + // set dependency invalid if it is in a different group + if (!hasGroupId(txn, m, rs.getBytes(3))) state = INVALID; + } + dependencies.put(messageId, state); + } + rs.close(); + ps.close(); + return Collections.unmodifiableMap(dependencies); + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + private boolean hasGroupId(Connection txn, MessageId m, byte[] g) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT NULL FROM messages" + + " WHERE messageId = ? AND groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + ps.setBytes(2, g); + rs = ps.executeQuery(); + boolean same = rs.next(); + if (rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + return same; + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + public Map<MessageId, State> getMessageDependents(Connection txn, + MessageId m) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT d.messageId, m.state" + + " FROM messageDependencies AS d" + + " LEFT OUTER JOIN messages AS m" + + " ON d.messageId = m.messageId" + + " WHERE dependencyId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + Map<MessageId, State> dependents = new HashMap<MessageId, State>(); + while (rs.next()) { + MessageId messageId = new MessageId(rs.getBytes(1)); + State state = State.fromValue(rs.getInt(2)); + dependents.put(messageId, state); + } + rs.close(); + ps.close(); + return Collections.unmodifiableMap(dependents); + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public Collection<MessageId> getMessagesToAck(Connection txn, ContactId c, int maxMessages) throws DbException { PreparedStatement ps = null; @@ -1346,13 +1502,13 @@ abstract class JdbcDatabase implements Database<Connection> { + " JOIN statuses AS s" + " ON m.messageId = s.messageId" + " WHERE contactId = ?" - + " AND valid = ? AND shared = TRUE AND raw IS NOT NULL" + + " AND state = ? AND shared = TRUE AND raw IS NOT NULL" + " AND seen = FALSE AND requested = FALSE" + " AND expiry < ?" + " ORDER BY timestamp DESC LIMIT ?"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); - ps.setInt(2, VALID.getValue()); + ps.setInt(2, DELIVERED.getValue()); ps.setLong(3, now); ps.setInt(4, maxMessages); rs = ps.executeQuery(); @@ -1402,13 +1558,13 @@ abstract class JdbcDatabase implements Database<Connection> { + " JOIN statuses AS s" + " ON m.messageId = s.messageId" + " WHERE contactId = ?" - + " AND valid = ? AND shared = TRUE AND raw IS NOT NULL" + + " AND state = ? AND shared = TRUE AND raw IS NOT NULL" + " AND seen = FALSE" + " AND expiry < ?" + " ORDER BY timestamp DESC"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); - ps.setInt(2, VALID.getValue()); + ps.setInt(2, DELIVERED.getValue()); ps.setLong(3, now); rs = ps.executeQuery(); List<MessageId> ids = new ArrayList<MessageId>(); @@ -1431,14 +1587,29 @@ abstract class JdbcDatabase implements Database<Connection> { public Collection<MessageId> getMessagesToValidate(Connection txn, ClientId c) throws DbException { + return getMessagesInState(txn, c, UNKNOWN); + } + + public Collection<MessageId> getMessagesToDeliver(Connection txn, + ClientId c) throws DbException { + return getMessagesInState(txn, c, VALID); + } + + public Collection<MessageId> getPendingMessages(Connection txn, + ClientId c) throws DbException { + return getMessagesInState(txn, c, PENDING); + } + + private Collection<MessageId> getMessagesInState(Connection txn, ClientId c, + State state) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { String sql = "SELECT messageId FROM messages AS m" + " JOIN groups AS g ON m.groupId = g.groupId" - + " WHERE valid = ? AND clientId = ? AND raw IS NOT NULL"; + + " WHERE state = ? AND clientId = ? AND raw IS NOT NULL"; ps = txn.prepareStatement(sql); - ps.setInt(1, UNKNOWN.getValue()); + ps.setInt(1, state.getValue()); ps.setBytes(2, c.getBytes()); rs = ps.executeQuery(); List<MessageId> ids = new ArrayList<MessageId>(); @@ -1485,13 +1656,13 @@ abstract class JdbcDatabase implements Database<Connection> { + " JOIN statuses AS s" + " ON m.messageId = s.messageId" + " WHERE contactId = ?" - + " AND valid = ? AND shared = TRUE AND raw IS NOT NULL" + + " AND state = ? AND shared = TRUE AND raw IS NOT NULL" + " AND seen = FALSE AND requested = TRUE" + " AND expiry < ?" + " ORDER BY timestamp DESC"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); - ps.setInt(2, VALID.getValue()); + ps.setInt(2, DELIVERED.getValue()); ps.setLong(3, now); rs = ps.executeQuery(); List<MessageId> ids = new ArrayList<MessageId>(); @@ -2081,13 +2252,13 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void setMessageValid(Connection txn, MessageId m, boolean valid) + public void setMessageState(Connection txn, MessageId m, State state) throws DbException { PreparedStatement ps = null; try { - String sql = "UPDATE messages SET valid = ? WHERE messageId = ?"; + String sql = "UPDATE messages SET state = ? WHERE messageId = ?"; ps = txn.prepareStatement(sql); - ps.setInt(1, valid ? VALID.getValue() : INVALID.getValue()); + ps.setInt(1, state.getValue()); ps.setBytes(2, m.getBytes()); int affected = ps.executeUpdate(); if (affected < 0 || affected > 1) throw new DbStateException(); diff --git a/briar-core/src/org/briarproject/forum/ForumPostValidator.java b/briar-core/src/org/briarproject/forum/ForumPostValidator.java index 6dbc68e96f6ff5ff8a8134d1b663ba5ea5f29ed8..e0a86a7b8c14a18560971f50477a8cc4cdbc2d4f 100644 --- a/briar-core/src/org/briarproject/forum/ForumPostValidator.java +++ b/briar-core/src/org/briarproject/forum/ForumPostValidator.java @@ -2,8 +2,8 @@ package org.briarproject.forum; import org.briarproject.api.FormatException; import org.briarproject.api.UniqueId; -import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.clients.BdfMessageContext; +import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.KeyParser; import org.briarproject.api.crypto.PublicKey; @@ -16,10 +16,13 @@ import org.briarproject.api.identity.AuthorFactory; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.InvalidMessageException; import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; import org.briarproject.api.system.Clock; import org.briarproject.clients.BdfMessageValidator; import java.security.GeneralSecurityException; +import java.util.Collection; +import java.util.Collections; import static org.briarproject.api.forum.ForumConstants.MAX_CONTENT_TYPE_LENGTH; import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH; @@ -96,10 +99,14 @@ class ForumPostValidator extends BdfMessageValidator { throw new InvalidMessageException("Invalid public key"); } } - // Return the metadata + // Return the metadata and dependencies BdfDictionary meta = new BdfDictionary(); + Collection<MessageId> dependencies = null; meta.put("timestamp", m.getTimestamp()); - if (parent != null) meta.put("parent", parent); + if (parent != null) { + meta.put("parent", parent); + dependencies = Collections.singletonList(new MessageId(parent)); + } if (author != null) { BdfDictionary authorMeta = new BdfDictionary(); authorMeta.put("id", author.getId()); @@ -109,6 +116,6 @@ class ForumPostValidator extends BdfMessageValidator { } meta.put("contentType", contentType); meta.put("read", false); - return new BdfMessageContext(meta); + return new BdfMessageContext(meta, dependencies); } } diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java index 1e85803a22aba9d836f5564b23b3fb8854f10917..f6aa74c3ab07fcd7bbb0ba5f211f027270980329 100644 --- a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java +++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java @@ -23,8 +23,10 @@ import org.briarproject.api.sync.ValidationManager; import org.briarproject.api.sync.MessageContext; import org.briarproject.util.ByteUtils; +import java.util.Collection; import java.util.LinkedList; import java.util.Map; +import java.util.Map.Entry; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; @@ -36,6 +38,10 @@ import javax.inject.Inject; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.PENDING; +import static org.briarproject.api.sync.ValidationManager.State.VALID; class ValidationManagerImpl implements ValidationManager, Service, EventListener { @@ -64,7 +70,10 @@ class ValidationManagerImpl implements ValidationManager, Service, @Override public void startService() { if (used.getAndSet(true)) throw new IllegalStateException(); - for (ClientId c : validators.keySet()) getMessagesToValidate(c); + for (ClientId c : validators.keySet()) { + validateOutstandingMessages(c); + deliverOutstandingMessages(c); + } } @Override @@ -82,7 +91,7 @@ class ValidationManagerImpl implements ValidationManager, Service, hooks.put(c, hook); } - private void getMessagesToValidate(final ClientId c) { + private void validateOutstandingMessages(final ClientId c) { dbExecutor.execute(new Runnable() { @Override public void run() { @@ -126,6 +135,7 @@ class ValidationManagerImpl implements ValidationManager, Service, LOG.info("Group removed before validation"); // Continue to next message } finally { + if (!txn.isComplete()) txn.setComplete(); db.endTransaction(txn); } if (m != null && g != null) validateMessage(m, g); @@ -138,6 +148,112 @@ class ValidationManagerImpl implements ValidationManager, Service, }); } + private void deliverOutstandingMessages(final ClientId c) { + dbExecutor.execute(new Runnable() { + @Override + public void run() { + try { + Queue<MessageId> validated = new LinkedList<MessageId>(); + Queue<MessageId> pending = new LinkedList<MessageId>(); + Transaction txn = db.startTransaction(true); + try { + validated.addAll(db.getMessagesToDeliver(txn, c)); + pending.addAll(db.getPendingMessages(txn, c)); + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + deliverNextMessage(validated); + deliverNextPendingMessage(pending); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + + private void deliverNextMessage(final Queue<MessageId> validated) { + if (validated.isEmpty()) return; + dbExecutor.execute(new Runnable() { + @Override + public void run() { + try { + Message m = null; + Group g = null; + Metadata meta = null; + Transaction txn = db.startTransaction(true); + try { + MessageId id = validated.poll(); + byte[] raw = db.getRawMessage(txn, id); + m = parseMessage(id, raw); + g = db.getGroup(txn, m.getGroupId()); + meta = db.getMessageMetadata(txn, id); + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + if (g != null) deliverMessage(m, g.getClientId(), meta); + deliverNextMessage(validated); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + + private void deliverNextPendingMessage(final Queue<MessageId> pending) { + if (pending.isEmpty()) return; + dbExecutor.execute(new Runnable() { + @Override + public void run() { + Message m = null; + ClientId c = null; + try { + boolean allDelivered = true; + Metadata meta = null; + Transaction txn = db.startTransaction(true); + try { + MessageId id = pending.poll(); + byte[] raw = db.getRawMessage(txn, id); + m = parseMessage(id, raw); + Group g = db.getGroup(txn, m.getGroupId()); + c = g.getClientId(); + + // check if a dependency is invalid + Map<MessageId, State> states = + db.getMessageDependencies(txn, id); + for (Entry<MessageId, State> d : states.entrySet()) { + if (d.getValue() == INVALID) { + throw new InvalidMessageException( + "Invalid Dependency"); + } + if (d.getValue() != DELIVERED) allDelivered = false; + } + if(allDelivered) { + meta = db.getMessageMetadata(txn, id); + } + txn.setComplete(); + } finally { + if (!txn.isComplete()) txn.setComplete(); + db.endTransaction(txn); + } + if (c != null && allDelivered) deliverMessage(m, c, meta); + deliverNextPendingMessage(pending); + } catch(InvalidMessageException e) { + if (LOG.isLoggable(INFO)) + LOG.log(INFO, e.toString(), e); + markMessageInvalid(m, c); + deliverNextPendingMessage(pending); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + private Message parseMessage(MessageId id, byte[] raw) { if (raw.length <= MESSAGE_HEADER_LENGTH) throw new IllegalArgumentException(); @@ -174,19 +290,105 @@ class ValidationManagerImpl implements ValidationManager, Service, @Override public void run() { try { + State newState = null; + Metadata meta = null; Transaction txn = db.startTransaction(false); try { - Metadata meta = result.getMetadata(); + // store dependencies + Collection<MessageId> dependencies = + result.getDependencies(); + if (dependencies != null && dependencies.size() > 0) { + db.addMessageDependencies(txn, m, dependencies); + } + // check if a dependency is invalid + // and if all dependencies have been delivered + Map<MessageId, State> states = + db.getMessageDependencies(txn, m.getId()); + newState = VALID; + for (Entry<MessageId, State> d : states.entrySet()) { + if (d.getValue() == INVALID) { + throw new InvalidMessageException( + "Dependency Invalid"); + } + if (d.getValue() != DELIVERED) { + newState = PENDING; + LOG.info("depend. undelivered, set to PENDING"); + break; + } + } + // save metadata and new message state + meta = result.getMetadata(); db.mergeMessageMetadata(txn, m.getId(), meta); - db.setMessageValid(txn, m, c, true); - db.setMessageShared(txn, m, true); + db.setMessageState(txn, m, c, newState); + txn.setComplete(); + } finally { + if (!txn.isComplete()) txn.setComplete(); + db.endTransaction(txn); + } + // deliver message if valid + if (newState == VALID) { + deliverMessage(m, c, meta); + } + } catch (InvalidMessageException e) { + if (LOG.isLoggable(INFO)) + LOG.log(INFO, e.toString(), e); + markMessageInvalid(m, c); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + + private void deliverMessage(final Message m, final ClientId c, + final Metadata meta) { + dbExecutor.execute(new Runnable() { + @Override + public void run() { + try { + Queue<MessageId> pending = new LinkedList<MessageId>(); + Transaction txn = db.startTransaction(false); + try { IncomingMessageHook hook = hooks.get(c); if (hook != null) hook.incomingMessage(txn, m, meta); + + // check if message was deleted by client + if (db.getRawMessage(txn, m.getId()) == null) { + throw new InvalidMessageException( + "Deleted by Client"); + } + + db.setMessageShared(txn, m, true); + db.setMessageState(txn, m, c, DELIVERED); + + // deliver pending dependents + Map<MessageId, State> dependents = + db.getMessageDependents(txn, m.getId()); + for (Entry<MessageId, State> i : dependents + .entrySet()) { + if (i.getValue() != PENDING) continue; + + // check that all dependencies are delivered + Map<MessageId, State> dependencies = + db.getMessageDependencies(txn, i.getKey()); + for (Entry<MessageId, State> j : dependencies + .entrySet()) { + if (j.getValue() != DELIVERED) return; + } + pending.add(i.getKey()); + } txn.setComplete(); } finally { + if (!txn.isComplete()) txn.setComplete(); db.endTransaction(txn); } + deliverNextMessage(pending); + } catch (InvalidMessageException e) { + if (LOG.isLoggable(INFO)) + LOG.log(INFO, e.toString(), e); + markMessageInvalid(m, c); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -200,13 +402,56 @@ class ValidationManagerImpl implements ValidationManager, Service, @Override public void run() { try { + Queue<MessageId> invalid = new LinkedList<MessageId>(); Transaction txn = db.startTransaction(false); try { - db.setMessageValid(txn, m, c, false); + Map<MessageId, State> dependents = + db.getMessageDependents(txn, m.getId()); + db.setMessageState(txn, m, c, INVALID); + db.deleteMessage(txn, m.getId()); + db.deleteMessageMetadata(txn, m.getId()); + + // recursively invalidate all messages that depend on m + // TODO check that cycles are properly taken care of + for (Entry<MessageId, State> i : dependents + .entrySet()) { + if (i.getValue() != INVALID) { + invalid.add(i.getKey()); + } + } + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + markNextMessageInvalid(invalid); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + + private void markNextMessageInvalid(final Queue<MessageId> invalid) { + if (invalid.isEmpty()) return; + dbExecutor.execute(new Runnable() { + @Override + public void run() { + try { + Message m = null; + Group g = null; + Transaction txn = db.startTransaction(true); + try { + MessageId id = invalid.poll(); + byte[] raw = db.getRawMessage(txn, id); + m = parseMessage(id, raw); + g = db.getGroup(txn, m.getGroupId()); txn.setComplete(); } finally { db.endTransaction(txn); } + if (g != null) markMessageInvalid(m, g.getClientId()); + markNextMessageInvalid(invalid); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java index ff6f7a918cc75941551be6c222aa64bade8358c5..6a4db39e815c184f835145d8a325832f45d5831e 100644 --- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java +++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java @@ -29,7 +29,7 @@ import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageSharedEvent; import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToRequestEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.event.SettingsUpdatedEvent; @@ -53,6 +53,7 @@ import org.jmock.Expectations; import org.jmock.Mockery; import org.junit.Test; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -60,8 +61,9 @@ import java.util.Map; import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; -import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN; -import static org.briarproject.api.sync.ValidationManager.Validity.VALID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; +import static org.briarproject.api.sync.ValidationManager.State.VALID; import static org.briarproject.api.transport.TransportConstants.REORDERING_WINDOW_SIZE; import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES; import static org.junit.Assert.assertEquals; @@ -264,7 +266,7 @@ public class DatabaseComponentImplTest extends BriarTestCase { will(returnValue(true)); oneOf(database).containsMessage(txn, messageId); will(returnValue(false)); - oneOf(database).addMessage(txn, message, VALID, true); + oneOf(database).addMessage(txn, message, DELIVERED, true); oneOf(database).mergeMessageMetadata(txn, messageId, metadata); oneOf(database).getVisibility(txn, groupId); will(returnValue(Collections.singletonList(contactId))); @@ -274,7 +276,7 @@ public class DatabaseComponentImplTest extends BriarTestCase { oneOf(database).commitTransaction(txn); // The message was added, so the listeners should be called oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class))); - oneOf(eventBus).broadcast(with(any(MessageValidatedEvent.class))); + oneOf(eventBus).broadcast(with(any(MessageStateChangedEvent.class))); oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class))); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, @@ -675,11 +677,11 @@ public class DatabaseComponentImplTest extends BriarTestCase { final EventBus eventBus = context.mock(EventBus.class); context.checking(new Expectations() {{ // Check whether the message is in the DB (which it's not) - exactly(8).of(database).startTransaction(); + exactly(10).of(database).startTransaction(); will(returnValue(txn)); - exactly(8).of(database).containsMessage(txn, messageId); + exactly(10).of(database).containsMessage(txn, messageId); will(returnValue(false)); - exactly(8).of(database).abortTransaction(txn); + exactly(10).of(database).abortTransaction(txn); // This is needed for getMessageStatus() to proceed exactly(1).of(database).containsContact(txn, contactId); will(returnValue(true)); @@ -759,7 +761,27 @@ public class DatabaseComponentImplTest extends BriarTestCase { transaction = db.startTransaction(false); try { - db.setMessageValid(transaction, message, clientId, true); + db.setMessageState(transaction, message, clientId, VALID); + fail(); + } catch (NoSuchMessageException expected) { + // Expected + } finally { + db.endTransaction(transaction); + } + + transaction = db.startTransaction(true); + try { + db.getMessageDependencies(transaction, messageId); + fail(); + } catch (NoSuchMessageException expected) { + // Expected + } finally { + db.endTransaction(transaction); + } + + transaction = db.startTransaction(true); + try { + db.getMessageDependents(transaction, messageId); fail(); } catch (NoSuchMessageException expected) { // Expected @@ -1595,4 +1617,79 @@ public class DatabaseComponentImplTest extends BriarTestCase { context.assertIsSatisfied(); } + + @Test + @SuppressWarnings("unchecked") + public void testMessageDependencies() throws Exception { + final int shutdownHandle = 12345; + Mockery context = new Mockery(); + final Database<Object> database = context.mock(Database.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final EventBus eventBus = context.mock(EventBus.class); + final MessageId messageId2 = new MessageId(TestUtils.getRandomId()); + context.checking(new Expectations() {{ + // open() + oneOf(database).open(); + will(returnValue(false)); + oneOf(shutdown).addShutdownHook(with(any(Runnable.class))); + will(returnValue(shutdownHandle)); + // startTransaction() + oneOf(database).startTransaction(); + will(returnValue(txn)); + // addLocalMessage() + oneOf(database).containsGroup(txn, groupId); + will(returnValue(true)); + oneOf(database).containsMessage(txn, messageId); + will(returnValue(false)); + oneOf(database).addMessage(txn, message, DELIVERED, true); + oneOf(database).getVisibility(txn, groupId); + will(returnValue(Collections.singletonList(contactId))); + oneOf(database).mergeMessageMetadata(txn, messageId, metadata); + oneOf(database).removeOfferedMessage(txn, contactId, messageId); + will(returnValue(false)); + oneOf(database).addStatus(txn, contactId, messageId, false, false); + // addMessageDependencies() + oneOf(database).containsMessage(txn, messageId); + will(returnValue(true)); + oneOf(database).addMessageDependency(txn, messageId, messageId1); + oneOf(database).addMessageDependency(txn, messageId, messageId2); + // getMessageDependencies() + oneOf(database).containsMessage(txn, messageId); + will(returnValue(true)); + oneOf(database).getMessageDependencies(txn, messageId); + // getMessageDependents() + oneOf(database).containsMessage(txn, messageId); + will(returnValue(true)); + oneOf(database).getMessageDependents(txn, messageId); + // broadcast for message added event + oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class))); + oneOf(eventBus).broadcast(with(any(MessageStateChangedEvent.class))); + oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class))); + // endTransaction() + oneOf(database).commitTransaction(txn); + // close() + oneOf(shutdown).removeShutdownHook(shutdownHandle); + oneOf(database).close(); + }}); + DatabaseComponent db = createDatabaseComponent(database, eventBus, + shutdown); + + assertFalse(db.open()); + Transaction transaction = db.startTransaction(false); + try { + db.addLocalMessage(transaction, message, clientId, metadata, true); + Collection<MessageId> dependencies = new ArrayList<>(2); + dependencies.add(messageId1); + dependencies.add(messageId2); + db.addMessageDependencies(transaction, message, dependencies); + db.getMessageDependencies(transaction, messageId); + db.getMessageDependents(transaction, messageId); + transaction.setComplete(); + } finally { + db.endTransaction(transaction); + } + db.close(); + + context.assertIsSatisfied(); + } } diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java index d967b815973b6bb6246dc23d6075b5621522e7b7..9d5b1f0b912e9cd94d5086a31d17b546859fb1e4 100644 --- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java +++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java @@ -19,6 +19,7 @@ import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageStatus; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.transport.IncomingKeys; import org.briarproject.api.transport.OutgoingKeys; import org.briarproject.api.transport.TransportKeys; @@ -46,8 +47,11 @@ import static org.briarproject.api.db.Metadata.REMOVE; import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_LENGTH; -import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN; -import static org.briarproject.api.sync.ValidationManager.Validity.VALID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.PENDING; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; +import static org.briarproject.api.sync.ValidationManager.State.VALID; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -114,7 +118,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addGroup(txn, group); assertTrue(db.containsGroup(txn, groupId)); assertFalse(db.containsMessage(txn, messageId)); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); assertTrue(db.containsMessage(txn, messageId)); db.commitTransaction(txn); db.close(); @@ -152,7 +156,7 @@ public class H2DatabaseTest extends BriarTestCase { // Add a group and a message db.addGroup(txn, group); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); // Removing the group should remove the message assertTrue(db.containsMessage(txn, messageId)); @@ -174,7 +178,7 @@ public class H2DatabaseTest extends BriarTestCase { true)); db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); // The message has no status yet, so it should not be sendable Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, @@ -202,7 +206,7 @@ public class H2DatabaseTest extends BriarTestCase { } @Test - public void testSendableMessagesMustBeValid() throws Exception { + public void testSendableMessagesMustBeDelivered() throws Exception { Database<Connection> db = open(false); Connection txn = db.startTransaction(); @@ -222,15 +226,31 @@ public class H2DatabaseTest extends BriarTestCase { ids = db.getMessagesToOffer(txn, contactId, 100); assertTrue(ids.isEmpty()); - // Marking the message valid should make it sendable - db.setMessageValid(txn, messageId, true); + // Marking the message delivered should make it sendable + db.setMessageState(txn, messageId, DELIVERED); ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); assertEquals(Collections.singletonList(messageId), ids); ids = db.getMessagesToOffer(txn, contactId, 100); assertEquals(Collections.singletonList(messageId), ids); // Marking the message invalid should make it unsendable - db.setMessageValid(txn, messageId, false); + db.setMessageState(txn, messageId, INVALID); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // Marking the message valid should make it unsendable + // TODO do we maybe want to already send valid messages? If we do, we need also to call db.setMessageShared() earlier. + db.setMessageState(txn, messageId, VALID); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // Marking the message pending should make it unsendable + // TODO do we maybe want to already send pending messages? If we do, we need also to call db.setMessageShared() earlier. + db.setMessageState(txn, messageId, PENDING); ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); assertTrue(ids.isEmpty()); ids = db.getMessagesToOffer(txn, contactId, 100); @@ -251,7 +271,7 @@ public class H2DatabaseTest extends BriarTestCase { true)); db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addMessage(txn, message, VALID, false); + db.addMessage(txn, message, DELIVERED, false); db.addStatus(txn, contactId, messageId, false, false); // The message is not shared, so it should not be sendable @@ -290,7 +310,7 @@ public class H2DatabaseTest extends BriarTestCase { true)); db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, false); // The message is sendable, but too large to send @@ -321,10 +341,10 @@ public class H2DatabaseTest extends BriarTestCase { // Add some messages to ack MessageId messageId1 = new MessageId(TestUtils.getRandomId()); Message message1 = new Message(messageId1, groupId, timestamp, raw); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, true); db.raiseAckFlag(txn, contactId, messageId); - db.addMessage(txn, message1, VALID, true); + db.addMessage(txn, message1, DELIVERED, true); db.addStatus(txn, contactId, messageId1, false, true); db.raiseAckFlag(txn, contactId, messageId1); @@ -354,7 +374,7 @@ public class H2DatabaseTest extends BriarTestCase { true)); db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, false); // Retrieve the message from the database and mark it as sent @@ -396,7 +416,7 @@ public class H2DatabaseTest extends BriarTestCase { // Storing a message should reduce the free space Connection txn = db.startTransaction(); db.addGroup(txn, group); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.commitTransaction(txn); assertTrue(db.getFreeSpace() < free); @@ -555,7 +575,7 @@ public class H2DatabaseTest extends BriarTestCase { assertEquals(contactId, db.addContact(txn, author, localAuthorId, true)); db.addGroup(txn, group); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, false); // The group is not visible @@ -866,7 +886,7 @@ public class H2DatabaseTest extends BriarTestCase { // Add a group and a message db.addGroup(txn, group); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); // Attach some metadata to the message Metadata metadata = new Metadata(); @@ -930,6 +950,67 @@ public class H2DatabaseTest extends BriarTestCase { db.close(); } + @Test + public void testMessageMetadataOnlyForDeliveredMessages() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + + // Attach some metadata to the message + Metadata metadata = new Metadata(); + metadata.put("foo", new byte[]{'b', 'a', 'r'}); + metadata.put("baz", new byte[]{'b', 'a', 'm'}); + db.mergeMessageMetadata(txn, messageId, metadata); + + // Retrieve the metadata for the message + Metadata retrieved = db.getMessageMetadata(txn, messageId); + assertEquals(2, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + Map<MessageId, Metadata> map = db.getMessageMetadata(txn, groupId); + assertEquals(1, map.size()); + assertTrue(map.get(messageId).containsKey("foo")); + assertArrayEquals(metadata.get("foo"), map.get(messageId).get("foo")); + assertTrue(map.get(messageId).containsKey("baz")); + assertArrayEquals(metadata.get("baz"), map.get(messageId).get("baz")); + + // No metadata for unknown messages + db.setMessageState(txn, messageId, UNKNOWN); + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + map = db.getMessageMetadata(txn, groupId); + assertTrue(map.isEmpty()); + + // No metadata for invalid messages + db.setMessageState(txn, messageId, INVALID); + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + map = db.getMessageMetadata(txn, groupId); + assertTrue(map.isEmpty()); + + // No metadata for valid messages + db.setMessageState(txn, messageId, VALID); + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + map = db.getMessageMetadata(txn, groupId); + assertTrue(map.isEmpty()); + + // No metadata for pending messages + db.setMessageState(txn, messageId, PENDING); + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + map = db.getMessageMetadata(txn, groupId); + assertTrue(map.isEmpty()); + + db.commitTransaction(txn); + db.close(); + } + @Test public void testMetadataQueries() throws Exception { MessageId messageId1 = new MessageId(TestUtils.getRandomId()); @@ -940,8 +1021,8 @@ public class H2DatabaseTest extends BriarTestCase { // Add a group and two messages db.addGroup(txn, group); - db.addMessage(txn, message, VALID, true); - db.addMessage(txn, message1, VALID, true); + db.addMessage(txn, message, DELIVERED, true); + db.addMessage(txn, message1, DELIVERED, true); // Attach some metadata to the messages Metadata metadata = new Metadata(); @@ -1034,6 +1115,257 @@ public class H2DatabaseTest extends BriarTestCase { db.close(); } + @Test + public void testMetadataQueriesOnlyForDeliveredMessages() throws Exception { + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new Message(messageId1, groupId, timestamp, raw); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and two messages + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + db.addMessage(txn, message1, DELIVERED, true); + + // Attach some metadata to the messages + Metadata metadata = new Metadata(); + metadata.put("foo", new byte[]{'b', 'a', 'r'}); + metadata.put("baz", new byte[]{'b', 'a', 'm'}); + db.mergeMessageMetadata(txn, messageId, metadata); + Metadata metadata1 = new Metadata(); + metadata1.put("foo", new byte[]{'b', 'a', 'r'}); + db.mergeMessageMetadata(txn, messageId1, metadata1); + + for (int i = 1; i <= 2; i++) { + Metadata query; + if (i == 1) { + // Query the metadata with an empty query + query = new Metadata(); + } else { + // Query for foo + query = new Metadata(); + query.put("foo", metadata.get("foo")); + } + + db.setMessageState(txn, messageId, DELIVERED); + db.setMessageState(txn, messageId1, DELIVERED); + Map<MessageId, Metadata> all = + db.getMessageMetadata(txn, groupId, query); + assertEquals(2, all.size()); + assertEquals(2, all.get(messageId).size()); + assertEquals(1, all.get(messageId1).size()); + + // No metadata for unknown messages + db.setMessageState(txn, messageId, UNKNOWN); + db.setMessageState(txn, messageId1, UNKNOWN); + all = db.getMessageMetadata(txn, groupId, query); + assertTrue(all.isEmpty()); + + // No metadata for invalid messages + db.setMessageState(txn, messageId, INVALID); + db.setMessageState(txn, messageId1, INVALID); + all = db.getMessageMetadata(txn, groupId, query); + assertTrue(all.isEmpty()); + + // No metadata for valid messages + db.setMessageState(txn, messageId, VALID); + db.setMessageState(txn, messageId1, VALID); + all = db.getMessageMetadata(txn, groupId, query); + assertTrue(all.isEmpty()); + + // No metadata for pending messages + db.setMessageState(txn, messageId, PENDING); + db.setMessageState(txn, messageId1, PENDING); + all = db.getMessageMetadata(txn, groupId, query); + assertTrue(all.isEmpty()); + } + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testMessageDependencies() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, VALID, true); + + // Create more messages + MessageId mId1 = new MessageId(TestUtils.getRandomId()); + MessageId mId2 = new MessageId(TestUtils.getRandomId()); + MessageId dId1 = new MessageId(TestUtils.getRandomId()); + MessageId dId2 = new MessageId(TestUtils.getRandomId()); + Message m1 = new Message(mId1, groupId, timestamp, raw); + Message m2 = new Message(mId2, groupId, timestamp, raw); + + // Add new messages + db.addMessage(txn, m1, VALID, true); + db.addMessage(txn, m2, INVALID, true); + + // Add dependencies + db.addMessageDependency(txn, messageId, mId1); + db.addMessageDependency(txn, messageId, mId2); + db.addMessageDependency(txn, mId1, dId1); + db.addMessageDependency(txn, mId2, dId2); + + Map<MessageId, State> dependencies; + + // Retrieve dependencies for root + dependencies = db.getMessageDependencies(txn, messageId); + assertEquals(2, dependencies.size()); + assertEquals(VALID, dependencies.get(mId1)); + assertEquals(INVALID, dependencies.get(mId2)); + + // Retrieve dependencies for m1 + dependencies = db.getMessageDependencies(txn, mId1); + assertEquals(1, dependencies.size()); + assertEquals(UNKNOWN, dependencies.get(dId1)); + + // Retrieve dependencies for m2 + dependencies = db.getMessageDependencies(txn, mId2); + assertEquals(1, dependencies.size()); + assertEquals(UNKNOWN, dependencies.get(dId2)); + + // Make sure d's have no dependencies + dependencies = db.getMessageDependencies(txn, dId1); + assertTrue(dependencies.isEmpty()); + dependencies = db.getMessageDependencies(txn, dId2); + assertTrue(dependencies.isEmpty()); + + Map<MessageId, State> dependents; + + // Root message does not have dependents + dependents = db.getMessageDependents(txn, messageId); + assertTrue(dependents.isEmpty()); + + // The root message depends on both m's + dependents = db.getMessageDependents(txn, mId1); + assertEquals(1, dependents.size()); + assertEquals(VALID, dependents.get(messageId)); + dependents = db.getMessageDependents(txn, mId2); + assertEquals(1, dependents.size()); + assertEquals(VALID, dependents.get(messageId)); + + // Both m's depend on the d's + dependents = db.getMessageDependents(txn, dId1); + assertEquals(1, dependents.size()); + assertEquals(VALID, dependents.get(mId1)); + dependents = db.getMessageDependents(txn, dId2); + assertEquals(1, dependents.size()); + assertEquals(INVALID, dependents.get(mId2)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testMessageDependenciesInSameGroup() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + + // Add a second group + GroupId groupId1 = new GroupId(TestUtils.getRandomId()); + Group group1 = new Group(groupId1, group.getClientId(), + TestUtils.getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH)); + db.addGroup(txn, group1); + + // Add a message to the second group + MessageId mId1 = new MessageId(TestUtils.getRandomId()); + Message m1 = new Message(mId1, groupId1, timestamp, raw); + db.addMessage(txn, m1, DELIVERED, true); + + // Create a fake dependency as well + MessageId mId2 = new MessageId(TestUtils.getRandomId()); + + // Create and add a real and proper dependency + MessageId mId3 = new MessageId(TestUtils.getRandomId()); + Message m3 = new Message(mId3, groupId, timestamp, raw); + db.addMessage(txn, m3, PENDING, true); + + // Add dependencies + db.addMessageDependency(txn, messageId, mId1); + db.addMessageDependency(txn, messageId, mId2); + db.addMessageDependency(txn, messageId, mId3); + + // Return invalid dependencies for delivered message m1 + Map<MessageId, State> dependencies; + dependencies = db.getMessageDependencies(txn, messageId); + assertEquals(INVALID, dependencies.get(mId1)); + assertEquals(UNKNOWN, dependencies.get(mId2)); + assertEquals(PENDING, dependencies.get(mId3)); + + // Return invalid dependencies for valid message m1 + db.setMessageState(txn, mId1, VALID); + dependencies = db.getMessageDependencies(txn, messageId); + assertEquals(INVALID, dependencies.get(mId1)); + assertEquals(UNKNOWN, dependencies.get(mId2)); + assertEquals(PENDING, dependencies.get(mId3)); + + // Return invalid dependencies for pending message m1 + db.setMessageState(txn, mId1, PENDING); + dependencies = db.getMessageDependencies(txn, messageId); + assertEquals(INVALID, dependencies.get(mId1)); + assertEquals(UNKNOWN, dependencies.get(mId2)); + assertEquals(PENDING, dependencies.get(mId3)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessagesForValidationAndDelivery() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, VALID, true); + + // Create more messages + MessageId mId1 = new MessageId(TestUtils.getRandomId()); + MessageId mId2 = new MessageId(TestUtils.getRandomId()); + MessageId mId3 = new MessageId(TestUtils.getRandomId()); + MessageId mId4 = new MessageId(TestUtils.getRandomId()); + Message m1 = new Message(mId1, groupId, timestamp, raw); + Message m2 = new Message(mId2, groupId, timestamp, raw); + Message m3 = new Message(mId3, groupId, timestamp, raw); + Message m4 = new Message(mId4, groupId, timestamp, raw); + + // Add new messages with different states + db.addMessage(txn, m1, UNKNOWN, true); + db.addMessage(txn, m2, INVALID, true); + db.addMessage(txn, m3, PENDING, true); + db.addMessage(txn, m4, DELIVERED, true); + + Collection<MessageId> result; + + // Retrieve messages to be validated + result = db.getMessagesToValidate(txn, group.getClientId()); + assertEquals(1, result.size()); + assertTrue(result.contains(mId1)); + + // Retrieve messages to be delivered + result = db.getMessagesToDeliver(txn, group.getClientId()); + assertEquals(1, result.size()); + assertTrue(result.contains(messageId)); + + // Retrieve pending messages + result = db.getPendingMessages(txn, group.getClientId()); + assertEquals(1, result.size()); + assertTrue(result.contains(mId3)); + + db.commitTransaction(txn); + db.close(); + } + @Test public void testGetMessageStatus() throws Exception { Database<Connection> db = open(false); @@ -1049,7 +1381,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addVisibility(txn, contactId, groupId); // Add a message to the group - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, false); // The message should not be sent or seen @@ -1176,7 +1508,7 @@ public class H2DatabaseTest extends BriarTestCase { true)); db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, false); // The message should be visible to the contact diff --git a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java index 1aea2e30b01fea7a8db15b210a8fb1fc5a77a345..0520b3b1fffe9b0be5bf1216e8a79487663dd738 100644 --- a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java +++ b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java @@ -16,23 +16,37 @@ import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.InvalidMessageException; import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageContext; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.ValidationManager.IncomingMessageHook; import org.briarproject.api.sync.ValidationManager.MessageValidator; -import org.briarproject.api.sync.MessageContext; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.util.ByteUtils; import org.jmock.Expectations; import org.jmock.Mockery; import org.junit.Test; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.Executor; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.PENDING; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; +import static org.briarproject.api.sync.ValidationManager.State.VALID; +import static org.junit.Assert.assertTrue; + public class ValidationManagerImplTest extends BriarTestCase { private final ClientId clientId = new ClientId(TestUtils.getRandomId()); private final MessageId messageId = new MessageId(TestUtils.getRandomId()); private final MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + private final MessageId messageId2 = new MessageId(TestUtils.getRandomId()); private final GroupId groupId = new GroupId(TestUtils.getRandomId()); private final byte[] descriptor = new byte[32]; private final Group group = new Group(groupId, clientId, descriptor); @@ -42,14 +56,23 @@ public class ValidationManagerImplTest extends BriarTestCase { raw); private final Message message1 = new Message(messageId1, groupId, timestamp, raw); + private final Message message2 = new Message(messageId2, groupId, timestamp, + raw); private final Metadata metadata = new Metadata(); - final MessageContext validResult = new MessageContext(metadata); + private final MessageContext validResult = new MessageContext(metadata); private final ContactId contactId = new ContactId(234); + private final Collection<MessageId> dependencies = new ArrayList<>(); + private final MessageContext validResultWithDependencies = + new MessageContext(metadata, dependencies); + private final Map<MessageId, State> states = new HashMap<>(); public ValidationManagerImplTest() { // Encode the messages System.arraycopy(groupId.getBytes(), 0, raw, 0, UniqueId.LENGTH); ByteUtils.writeUint64(timestamp, raw, UniqueId.LENGTH); + + dependencies.add(messageId1); + states.put(messageId1, INVALID); } @Test @@ -64,8 +87,10 @@ public class ValidationManagerImplTest extends BriarTestCase { final Transaction txn = new Transaction(null, false); final Transaction txn1 = new Transaction(null, false); final Transaction txn2 = new Transaction(null, false); + final Transaction txn2b = new Transaction(null, false); final Transaction txn3 = new Transaction(null, false); final Transaction txn4 = new Transaction(null, false); + final Transaction txn5 = new Transaction(null, true); context.checking(new Expectations() {{ // Get messages to validate oneOf(db).startTransaction(true); @@ -87,12 +112,22 @@ public class ValidationManagerImplTest extends BriarTestCase { // Store the validation result for the first message oneOf(db).startTransaction(false); will(returnValue(txn2)); + oneOf(db).getMessageDependencies(txn2, messageId); oneOf(db).mergeMessageMetadata(txn2, messageId, metadata); - oneOf(db).setMessageValid(txn2, message, clientId, true); - oneOf(db).setMessageShared(txn2, message, true); - // Call the hook for the first message - oneOf(hook).incomingMessage(txn2, message, metadata); + oneOf(db).setMessageState(txn2, message, clientId, VALID); oneOf(db).endTransaction(txn2); + // Async delivery + oneOf(db).startTransaction(false); + will(returnValue(txn2b)); + oneOf(db).setMessageShared(txn2b, message, true); + // Call the hook for the first message + oneOf(hook).incomingMessage(txn2b, message, metadata); + oneOf(db).getRawMessage(txn2b, messageId); + will(returnValue(raw)); + oneOf(db).setMessageState(txn2b, message, clientId, DELIVERED); + oneOf(db).getMessageDependents(txn2b, messageId); + will(returnValue(Collections.emptyMap())); + oneOf(db).endTransaction(txn2b); // Load the second raw message and group oneOf(db).startTransaction(true); will(returnValue(txn3)); @@ -107,8 +142,170 @@ public class ValidationManagerImplTest extends BriarTestCase { // Store the validation result for the second message oneOf(db).startTransaction(false); will(returnValue(txn4)); - oneOf(db).setMessageValid(txn4, message1, clientId, false); + oneOf(db).setMessageState(txn4, message1, clientId, INVALID); + // Recursively invalidate dependents + oneOf(db).getMessageDependents(txn4, messageId1); + oneOf(db).deleteMessage(txn4, messageId1); + oneOf(db).deleteMessageMetadata(txn4, messageId1); + oneOf(db).endTransaction(txn4); + // Get other messages to deliver + oneOf(db).startTransaction(true); + will(returnValue(txn5)); + oneOf(db).getMessagesToDeliver(txn5, clientId); + oneOf(db).getPendingMessages(txn5, clientId); + oneOf(db).endTransaction(txn5); + }}); + + ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor, + cryptoExecutor); + vm.registerMessageValidator(clientId, validator); + vm.registerIncomingMessageHook(clientId, hook); + vm.startService(); + + context.assertIsSatisfied(); + } + + @Test + public void testMessagesAreDeliveredAtStartup() throws Exception { + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + final Executor dbExecutor = new ImmediateExecutor(); + final Executor cryptoExecutor = new ImmediateExecutor(); + final MessageValidator validator = context.mock(MessageValidator.class); + final IncomingMessageHook hook = + context.mock(IncomingMessageHook.class); + final Transaction txn = new Transaction(null, true); + final Transaction txn1 = new Transaction(null, true); + final Transaction txn2 = new Transaction(null, true); + final Transaction txn3 = new Transaction(null, false); + final Transaction txn4 = new Transaction(null, true); + final Transaction txn5 = new Transaction(null, false); + + states.put(messageId1, PENDING); + + context.checking(new Expectations() {{ + // Get messages to validate + oneOf(db).startTransaction(true); + will(returnValue(txn)); + oneOf(db).getMessagesToValidate(txn, clientId); + oneOf(db).endTransaction(txn); + // Get IDs of messages to deliver + oneOf(db).startTransaction(true); + will(returnValue(txn1)); + oneOf(db).getMessagesToDeliver(txn1, clientId); + will(returnValue(Collections.singletonList(messageId))); + oneOf(db).getPendingMessages(txn1, clientId); + oneOf(db).endTransaction(txn1); + // Get message and its metadata to deliver + oneOf(db).startTransaction(true); + will(returnValue(txn2)); + oneOf(db).getRawMessage(txn2, messageId); + will(returnValue(message.getRaw())); + oneOf(db).getGroup(txn2, message.getGroupId()); + will(returnValue(group)); + oneOf(db).getMessageMetadata(txn2, messageId); + will(returnValue(metadata)); + oneOf(db).endTransaction(txn2); + // Deliver message in a new transaction + oneOf(db).startTransaction(false); + will(returnValue(txn3)); + oneOf(db).setMessageShared(txn3, message, true); + oneOf(db).setMessageState(txn3, message, clientId, DELIVERED); + oneOf(hook).incomingMessage(txn3, message, metadata); + oneOf(db).getRawMessage(txn3, messageId); + will(returnValue(message.getRaw())); + // Try to also deliver pending dependents + oneOf(db).getMessageDependents(txn3, messageId); + will(returnValue(states)); + oneOf(db).getMessageDependencies(txn3, messageId1); + will(returnValue(Collections.singletonMap(messageId2, DELIVERED))); + oneOf(db).endTransaction(txn3); + // Get the dependent to deliver + oneOf(db).startTransaction(true); + will(returnValue(txn4)); + oneOf(db).getRawMessage(txn4, messageId1); + will(returnValue(message1.getRaw())); + oneOf(db).getGroup(txn4, message.getGroupId()); + will(returnValue(group)); + oneOf(db).getMessageMetadata(txn4, messageId1); + will(returnValue(metadata)); oneOf(db).endTransaction(txn4); + // Deliver the dependent in a new transaction + oneOf(db).startTransaction(false); + will(returnValue(txn5)); + oneOf(db).setMessageShared(txn5, message1, true); + oneOf(db).setMessageState(txn5, message1, clientId, DELIVERED); + oneOf(hook).incomingMessage(txn5, message1, metadata); + oneOf(db).getRawMessage(txn5, messageId1); + will(returnValue(message1.getRaw())); + oneOf(db).getMessageDependents(txn5, messageId1); + oneOf(db).endTransaction(txn5); + }}); + + ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor, + cryptoExecutor); + vm.registerMessageValidator(clientId, validator); + vm.registerIncomingMessageHook(clientId, hook); + vm.startService(); + + context.assertIsSatisfied(); + + assertTrue(txn.isComplete()); + assertTrue(txn1.isComplete()); + assertTrue(txn2.isComplete()); + assertTrue(txn3.isComplete()); + assertTrue(txn4.isComplete()); + assertTrue(txn5.isComplete()); + } + + @Test + public void testPendingMessagesAreDeliveredAtStartup() throws Exception { + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + final Executor dbExecutor = new ImmediateExecutor(); + final Executor cryptoExecutor = new ImmediateExecutor(); + final MessageValidator validator = context.mock(MessageValidator.class); + final IncomingMessageHook hook = + context.mock(IncomingMessageHook.class); + final Transaction txn = new Transaction(null, true); + final Transaction txn1 = new Transaction(null, true); + final Transaction txn2 = new Transaction(null, true); + final Transaction txn3 = new Transaction(null, false); + + context.checking(new Expectations() {{ + // Get messages to validate + oneOf(db).startTransaction(true); + will(returnValue(txn)); + oneOf(db).getMessagesToValidate(txn, clientId); + oneOf(db).endTransaction(txn); + // Get IDs of messages to deliver + oneOf(db).startTransaction(true); + will(returnValue(txn1)); + oneOf(db).getMessagesToDeliver(txn1, clientId); + oneOf(db).getPendingMessages(txn1, clientId); + will(returnValue(Collections.singletonList(messageId))); + oneOf(db).endTransaction(txn1); + // Get message and its metadata to deliver + oneOf(db).startTransaction(true); + will(returnValue(txn2)); + oneOf(db).getRawMessage(txn2, messageId); + will(returnValue(message.getRaw())); + oneOf(db).getGroup(txn2, message.getGroupId()); + will(returnValue(group)); + oneOf(db).getMessageDependencies(txn2, messageId); + will(returnValue(Collections.singletonMap(messageId1, DELIVERED))); + oneOf(db).getMessageMetadata(txn2, messageId); + oneOf(db).endTransaction(txn2); + // Deliver the pending message + oneOf(db).startTransaction(false); + will(returnValue(txn3)); + oneOf(db).setMessageShared(txn3, message, true); + oneOf(db).setMessageState(txn3, message, clientId, DELIVERED); + oneOf(hook).incomingMessage(txn3, message, metadata); + oneOf(db).getRawMessage(txn3, messageId); + will(returnValue(message.getRaw())); + oneOf(db).getMessageDependents(txn3, messageId); + oneOf(db).endTransaction(txn3); }}); ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor, @@ -118,6 +315,11 @@ public class ValidationManagerImplTest extends BriarTestCase { vm.startService(); context.assertIsSatisfied(); + + assertTrue(txn.isComplete()); + assertTrue(txn1.isComplete()); + assertTrue(txn2.isComplete()); + assertTrue(txn3.isComplete()); } @Test @@ -134,6 +336,7 @@ public class ValidationManagerImplTest extends BriarTestCase { final Transaction txn1 = new Transaction(null, true); final Transaction txn2 = new Transaction(null, true); final Transaction txn3 = new Transaction(null, false); + final Transaction txn4 = new Transaction(null, true); context.checking(new Expectations() {{ // Get messages to validate oneOf(db).startTransaction(true); @@ -161,8 +364,18 @@ public class ValidationManagerImplTest extends BriarTestCase { // Store the validation result for the second message oneOf(db).startTransaction(false); will(returnValue(txn3)); - oneOf(db).setMessageValid(txn3, message1, clientId, false); + oneOf(db).setMessageState(txn3, message1, clientId, INVALID); + // recursively invalidate dependents + oneOf(db).getMessageDependents(txn3, messageId1); + oneOf(db).deleteMessage(txn3, messageId1); + oneOf(db).deleteMessageMetadata(txn3, messageId1); oneOf(db).endTransaction(txn3); + // Get other messages to deliver + oneOf(db).startTransaction(true); + will(returnValue(txn4)); + oneOf(db).getMessagesToDeliver(txn4, clientId); + oneOf(db).getPendingMessages(txn4, clientId); + oneOf(db).endTransaction(txn4); }}); ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor, @@ -172,6 +385,12 @@ public class ValidationManagerImplTest extends BriarTestCase { vm.startService(); context.assertIsSatisfied(); + + assertTrue(txn.isComplete()); + assertTrue(txn1.isComplete()); + assertTrue(txn2.isComplete()); + assertTrue(txn3.isComplete()); + assertTrue(txn4.isComplete()); } @Test @@ -188,6 +407,7 @@ public class ValidationManagerImplTest extends BriarTestCase { final Transaction txn1 = new Transaction(null, true); final Transaction txn2 = new Transaction(null, true); final Transaction txn3 = new Transaction(null, false); + final Transaction txn4 = new Transaction(null, true); context.checking(new Expectations() {{ // Get messages to validate oneOf(db).startTransaction(true); @@ -218,8 +438,18 @@ public class ValidationManagerImplTest extends BriarTestCase { // Store the validation result for the second message oneOf(db).startTransaction(false); will(returnValue(txn3)); - oneOf(db).setMessageValid(txn3, message1, clientId, false); + oneOf(db).setMessageState(txn3, message1, clientId, INVALID); + // recursively invalidate dependents + oneOf(db).getMessageDependents(txn3, messageId1); + oneOf(db).deleteMessage(txn3, messageId1); + oneOf(db).deleteMessageMetadata(txn3, messageId1); oneOf(db).endTransaction(txn3); + // Get other messages to deliver + oneOf(db).startTransaction(true); + will(returnValue(txn4)); + oneOf(db).getMessagesToDeliver(txn4, clientId); + oneOf(db).getPendingMessages(txn4, clientId); + oneOf(db).endTransaction(txn4); }}); ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor, @@ -229,6 +459,12 @@ public class ValidationManagerImplTest extends BriarTestCase { vm.startService(); context.assertIsSatisfied(); + + assertTrue(txn.isComplete()); + assertTrue(txn1.isComplete()); + assertTrue(txn2.isComplete()); + assertTrue(txn3.isComplete()); + assertTrue(txn4.isComplete()); } @Test @@ -242,6 +478,7 @@ public class ValidationManagerImplTest extends BriarTestCase { context.mock(IncomingMessageHook.class); final Transaction txn = new Transaction(null, true); final Transaction txn1 = new Transaction(null, false); + final Transaction txn2 = new Transaction(null, false); context.checking(new Expectations() {{ // Load the group oneOf(db).startTransaction(true); @@ -255,12 +492,21 @@ public class ValidationManagerImplTest extends BriarTestCase { // Store the validation result oneOf(db).startTransaction(false); will(returnValue(txn1)); + oneOf(db).getMessageDependencies(txn1, messageId); oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); - oneOf(db).setMessageValid(txn1, message, clientId, true); - oneOf(db).setMessageShared(txn1, message, true); - // Call the hook - oneOf(hook).incomingMessage(txn1, message, metadata); + oneOf(db).setMessageState(txn1, message, clientId, VALID); oneOf(db).endTransaction(txn1); + // async delivery + oneOf(db).startTransaction(false); + will(returnValue(txn2)); + oneOf(db).setMessageShared(txn2, message, true); + // Call the hook + oneOf(hook).incomingMessage(txn2, message, metadata); + oneOf(db).getRawMessage(txn2, messageId); + will(returnValue(raw)); + oneOf(db).setMessageState(txn2, message, clientId, DELIVERED); + oneOf(db).getMessageDependents(txn2, messageId); + oneOf(db).endTransaction(txn2); }}); ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor, @@ -270,6 +516,10 @@ public class ValidationManagerImplTest extends BriarTestCase { vm.eventOccurred(new MessageAddedEvent(message, contactId)); context.assertIsSatisfied(); + + assertTrue(txn.isComplete()); + assertTrue(txn1.isComplete()); + assertTrue(txn2.isComplete()); } @Test @@ -290,4 +540,402 @@ public class ValidationManagerImplTest extends BriarTestCase { context.assertIsSatisfied(); } + + @Test + public void testMessagesWithNonDeliveredDependenciesArePending() + throws Exception { + + states.put(messageId1, UNKNOWN); + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + final Executor dbExecutor = new ImmediateExecutor(); + final Executor cryptoExecutor = new ImmediateExecutor(); + final MessageValidator validator = context.mock(MessageValidator.class); + final IncomingMessageHook hook = + context.mock(IncomingMessageHook.class); + final Transaction txn = new Transaction(null, true); + final Transaction txn1 = new Transaction(null, false); + context.checking(new Expectations() {{ + // Load the group + oneOf(db).startTransaction(true); + will(returnValue(txn)); + oneOf(db).getGroup(txn, groupId); + will(returnValue(group)); + oneOf(db).endTransaction(txn); + // Validate the message: valid + oneOf(validator).validateMessage(message, group); + will(returnValue(validResultWithDependencies)); + // Store the validation result + oneOf(db).startTransaction(false); + will(returnValue(txn1)); + oneOf(db).addMessageDependencies(txn1, message, + validResultWithDependencies.getDependencies()); + oneOf(db).getMessageDependencies(txn1, messageId); + will(returnValue(states)); + oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); + oneOf(db).setMessageState(txn1, message, clientId, PENDING); + oneOf(db).endTransaction(txn1); + }}); + + ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor, + cryptoExecutor); + vm.registerMessageValidator(clientId, validator); + vm.registerIncomingMessageHook(clientId, hook); + vm.eventOccurred(new MessageAddedEvent(message, contactId)); + + context.assertIsSatisfied(); + + assertTrue(txn.isComplete()); + assertTrue(txn1.isComplete()); + } + + @Test + public void testMessagesWithDeliveredDependenciesGetDelivered() + throws Exception { + + states.put(messageId1, DELIVERED); + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + final Executor dbExecutor = new ImmediateExecutor(); + final Executor cryptoExecutor = new ImmediateExecutor(); + final MessageValidator validator = context.mock(MessageValidator.class); + final IncomingMessageHook hook = + context.mock(IncomingMessageHook.class); + final Transaction txn = new Transaction(null, true); + final Transaction txn1 = new Transaction(null, false); + final Transaction txn2 = new Transaction(null, false); + context.checking(new Expectations() {{ + // Load the group + oneOf(db).startTransaction(true); + will(returnValue(txn)); + oneOf(db).getGroup(txn, groupId); + will(returnValue(group)); + oneOf(db).endTransaction(txn); + // Validate the message: valid + oneOf(validator).validateMessage(message, group); + will(returnValue(validResultWithDependencies)); + // Store the validation result + oneOf(db).startTransaction(false); + will(returnValue(txn1)); + oneOf(db).addMessageDependencies(txn1, message, + validResultWithDependencies.getDependencies()); + oneOf(db).getMessageDependencies(txn1, messageId); + will(returnValue(states)); + oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); + oneOf(db).setMessageState(txn1, message, clientId, VALID); + oneOf(db).endTransaction(txn1); + // async delivery + oneOf(db).startTransaction(false); + will(returnValue(txn2)); + oneOf(db).setMessageShared(txn2, message, true); + // Call the hook + oneOf(hook).incomingMessage(txn2, message, metadata); + oneOf(db).getRawMessage(txn2, messageId); + will(returnValue(raw)); + oneOf(db).setMessageState(txn2, message, clientId, DELIVERED); + oneOf(db).getMessageDependents(txn2, messageId); + oneOf(db).endTransaction(txn2); + }}); + + ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor, + cryptoExecutor); + vm.registerMessageValidator(clientId, validator); + vm.registerIncomingMessageHook(clientId, hook); + vm.eventOccurred(new MessageAddedEvent(message, contactId)); + + context.assertIsSatisfied(); + + assertTrue(txn.isComplete()); + assertTrue(txn1.isComplete()); + assertTrue(txn2.isComplete()); + } + + @Test + public void testMessagesWithInvalidDependenciesAreInvalid() + throws Exception { + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + final Executor dbExecutor = new ImmediateExecutor(); + final Executor cryptoExecutor = new ImmediateExecutor(); + final MessageValidator validator = context.mock(MessageValidator.class); + final IncomingMessageHook hook = + context.mock(IncomingMessageHook.class); + final Transaction txn = new Transaction(null, true); + final Transaction txn1 = new Transaction(null, false); + final Transaction txn2 = new Transaction(null, false); + final Transaction txn3 = new Transaction(null, true); + final Transaction txn4 = new Transaction(null, false); + context.checking(new Expectations() {{ + // Load the group + oneOf(db).startTransaction(true); + will(returnValue(txn)); + oneOf(db).getGroup(txn, groupId); + will(returnValue(group)); + oneOf(db).endTransaction(txn); + // Validate the message: valid + oneOf(validator).validateMessage(message, group); + will(returnValue(validResultWithDependencies)); + // Store the validation result + oneOf(db).startTransaction(false); + will(returnValue(txn1)); + oneOf(db).addMessageDependencies(txn1, message, + validResultWithDependencies.getDependencies()); + oneOf(db).getMessageDependencies(txn1, messageId); + will(returnValue(states)); + oneOf(db).endTransaction(txn1); + // Invalidate message in a new transaction + oneOf(db).startTransaction(false); + will(returnValue(txn2)); + oneOf(db).getMessageDependents(txn2, messageId); + will(returnValue(Collections.singletonMap(messageId2, UNKNOWN))); + oneOf(db).setMessageState(txn2, message, clientId, INVALID); + oneOf(db).deleteMessage(txn2, messageId); + oneOf(db).deleteMessageMetadata(txn2, messageId); + oneOf(db).endTransaction(txn2); + // Get message to invalidate in a new transaction + oneOf(db).startTransaction(true); + will(returnValue(txn3)); + oneOf(db).getRawMessage(txn3, messageId2); + will(returnValue(message2.getRaw())); + oneOf(db).getGroup(txn3, message2.getGroupId()); + will(returnValue(group)); + oneOf(db).endTransaction(txn3); + // Invalidate dependent message in a new transaction + oneOf(db).startTransaction(false); + will(returnValue(txn4)); + oneOf(db).getMessageDependents(txn4, messageId2); + will(returnValue(Collections.emptyMap())); + oneOf(db).setMessageState(txn4, message2, clientId, INVALID); + oneOf(db).deleteMessage(txn4, messageId2); + oneOf(db).deleteMessageMetadata(txn4, messageId2); + oneOf(db).endTransaction(txn4); + }}); + + ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor, + cryptoExecutor); + vm.registerMessageValidator(clientId, validator); + vm.registerIncomingMessageHook(clientId, hook); + vm.eventOccurred(new MessageAddedEvent(message, contactId)); + + context.assertIsSatisfied(); + + assertTrue(txn.isComplete()); + assertTrue(txn1.isComplete()); + assertTrue(txn2.isComplete()); + assertTrue(txn3.isComplete()); + assertTrue(txn4.isComplete()); + } + + @Test + public void testPendingDependentsGetDelivered() throws Exception { + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + final Executor dbExecutor = new ImmediateExecutor(); + final Executor cryptoExecutor = new ImmediateExecutor(); + final MessageValidator validator = context.mock(MessageValidator.class); + final IncomingMessageHook hook = + context.mock(IncomingMessageHook.class); + final Transaction txn = new Transaction(null, true); + final Transaction txn1 = new Transaction(null, false); + final Transaction txn2 = new Transaction(null, false); + final Transaction txn3 = new Transaction(null, true); + final Transaction txn4 = new Transaction(null, false); + context.checking(new Expectations() {{ + // Load the group + oneOf(db).startTransaction(true); + will(returnValue(txn)); + oneOf(db).getGroup(txn, groupId); + will(returnValue(group)); + oneOf(db).endTransaction(txn); + // Validate the message: valid + oneOf(validator).validateMessage(message, group); + will(returnValue(validResult)); + // Store the validation result + oneOf(db).startTransaction(false); + will(returnValue(txn1)); + oneOf(db).getMessageDependencies(txn1, messageId); + will(returnValue(Collections.emptyMap())); + oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); + oneOf(db).setMessageState(txn1, message, clientId, VALID); + oneOf(db).endTransaction(txn1); + // Deliver first message + oneOf(db).startTransaction(false); + will(returnValue(txn2)); + oneOf(db).setMessageShared(txn2, message, true); + oneOf(hook).incomingMessage(txn2, message, metadata); + oneOf(db).getRawMessage(txn2, messageId); + will(returnValue(raw)); + oneOf(db).setMessageState(txn2, message, clientId, DELIVERED); + oneOf(db).getMessageDependents(txn2, messageId); + will(returnValue(Collections.singletonMap(messageId1, PENDING))); + oneOf(db).getMessageDependencies(txn2, messageId1); + will(returnValue(Collections.singletonMap(messageId2, DELIVERED))); + oneOf(db).endTransaction(txn2); + // Also get the pending message for delivery + oneOf(db).startTransaction(true); + will(returnValue(txn3)); + oneOf(db).getRawMessage(txn3, messageId1); + will(returnValue(message1.getRaw())); + oneOf(db).getGroup(txn3, message1.getGroupId()); + will(returnValue(group)); + oneOf(db).getMessageMetadata(txn3, messageId1); + will(returnValue(metadata)); + oneOf(db).endTransaction(txn3); + // Deliver the pending message + oneOf(db).startTransaction(false); + will(returnValue(txn4)); + oneOf(db).setMessageShared(txn4, message1, true); + oneOf(hook).incomingMessage(txn4, message1, metadata); + oneOf(db).getRawMessage(txn4, messageId1); + will(returnValue(raw)); + oneOf(db).setMessageState(txn4, message1, clientId, DELIVERED); + oneOf(db).getMessageDependents(txn4, messageId1); + will(returnValue(Collections.emptyMap())); + oneOf(db).endTransaction(txn4); + }}); + + ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor, + cryptoExecutor); + vm.registerMessageValidator(clientId, validator); + vm.registerIncomingMessageHook(clientId, hook); + vm.eventOccurred(new MessageAddedEvent(message, contactId)); + + context.assertIsSatisfied(); + + assertTrue(txn.isComplete()); + assertTrue(txn1.isComplete()); + assertTrue(txn2.isComplete()); + assertTrue(txn3.isComplete()); + assertTrue(txn4.isComplete()); + } + + @Test + public void testOnlyReadyPendingDependentsGetDelivered() throws Exception { + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + final Executor dbExecutor = new ImmediateExecutor(); + final Executor cryptoExecutor = new ImmediateExecutor(); + final MessageValidator validator = context.mock(MessageValidator.class); + final IncomingMessageHook hook = + context.mock(IncomingMessageHook.class); + final Transaction txn = new Transaction(null, true); + final Transaction txn1 = new Transaction(null, false); + final Transaction txn2 = new Transaction(null, false); + context.checking(new Expectations() {{ + // Load the group + oneOf(db).startTransaction(true); + will(returnValue(txn)); + oneOf(db).getGroup(txn, groupId); + will(returnValue(group)); + oneOf(db).endTransaction(txn); + // Validate the message: valid + oneOf(validator).validateMessage(message, group); + will(returnValue(validResult)); + // Store the validation result + oneOf(db).startTransaction(false); + will(returnValue(txn1)); + oneOf(db).getMessageDependencies(txn1, messageId); + will(returnValue(Collections.emptyMap())); + oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); + oneOf(db).setMessageState(txn1, message, clientId, VALID); + oneOf(db).endTransaction(txn1); + // Deliver first message + oneOf(db).startTransaction(false); + will(returnValue(txn2)); + oneOf(db).setMessageShared(txn2, message, true); + oneOf(hook).incomingMessage(txn2, message, metadata); + oneOf(db).getRawMessage(txn2, messageId); + will(returnValue(raw)); + oneOf(db).setMessageState(txn2, message, clientId, DELIVERED); + oneOf(db).getMessageDependents(txn2, messageId); + will(returnValue(Collections.singletonMap(messageId1, PENDING))); + oneOf(db).getMessageDependencies(txn2, messageId1); + will(returnValue(Collections.singletonMap(messageId2, VALID))); + oneOf(db).endTransaction(txn2); + }}); + + ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor, + cryptoExecutor); + vm.registerMessageValidator(clientId, validator); + vm.registerIncomingMessageHook(clientId, hook); + vm.eventOccurred(new MessageAddedEvent(message, contactId)); + + context.assertIsSatisfied(); + + assertTrue(txn.isComplete()); + assertTrue(txn1.isComplete()); + assertTrue(txn2.isComplete()); + } + + @Test + public void testMessageDependencyCycle() throws Exception { + states.put(messageId1, UNKNOWN); + final MessageContext cycleContext = new MessageContext(metadata, + Collections.singletonList(messageId)); + + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + final Executor dbExecutor = new ImmediateExecutor(); + final Executor cryptoExecutor = new ImmediateExecutor(); + final MessageValidator validator = context.mock(MessageValidator.class); + final IncomingMessageHook hook = + context.mock(IncomingMessageHook.class); + final Transaction txn = new Transaction(null, true); + final Transaction txn1 = new Transaction(null, false); + final Transaction txn2 = new Transaction(null, true); + final Transaction txn3 = new Transaction(null, false); + context.checking(new Expectations() {{ + // Load the group + oneOf(db).startTransaction(true); + will(returnValue(txn)); + oneOf(db).getGroup(txn, groupId); + will(returnValue(group)); + oneOf(db).endTransaction(txn); + // Validate the message: valid + oneOf(validator).validateMessage(message, group); + will(returnValue(validResultWithDependencies)); + // Store the validation result + oneOf(db).startTransaction(false); + will(returnValue(txn1)); + oneOf(db).addMessageDependencies(txn1, message, + validResultWithDependencies.getDependencies()); + oneOf(db).getMessageDependencies(txn1, messageId); + will(returnValue(states)); + oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); + oneOf(db).setMessageState(txn1, message, clientId, PENDING); + oneOf(db).endTransaction(txn1); + // Second message is coming in + oneOf(db).startTransaction(true); + will(returnValue(txn2)); + oneOf(db).getGroup(txn2, groupId); + will(returnValue(group)); + oneOf(db).endTransaction(txn2); + // Validate the message: valid + oneOf(validator).validateMessage(message1, group); + will(returnValue(cycleContext)); + // Store the validation result + oneOf(db).startTransaction(false); + will(returnValue(txn3)); + oneOf(db).addMessageDependencies(txn3, message1, + cycleContext.getDependencies()); + oneOf(db).getMessageDependencies(txn3, messageId1); + will(returnValue(Collections.singletonMap(messageId, PENDING))); + oneOf(db).mergeMessageMetadata(txn3, messageId1, metadata); + oneOf(db).setMessageState(txn3, message1, clientId, PENDING); + oneOf(db).endTransaction(txn3); + }}); + + ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor, + cryptoExecutor); + vm.registerMessageValidator(clientId, validator); + vm.registerIncomingMessageHook(clientId, hook); + vm.eventOccurred(new MessageAddedEvent(message, contactId)); + vm.eventOccurred(new MessageAddedEvent(message1, contactId)); + + context.assertIsSatisfied(); + + assertTrue(txn.isComplete()); + assertTrue(txn1.isComplete()); + } + }