Merge branch '382-message-dependencies-2' into 'master'

Message Dependencies

This MR adds a new table to the database to hold message dependencies.
It introduces two more message states  (pending and delivered) and completely changes the validation and message delivery logic.

Since this is a **very invasive** change, please **review carefully**. It is better to catch issues now than having to debug things later. If you notice an area without tests, please let me know. I tried to have tests for all changes.

Unfortunately, this MR turned out rather large, but most of it are actually tests and you can review one commit after the other without needing to see the full changelog.

In order to reflect the new states, things that were previously true for `VALID` messages have been changed to now be true for `DELIVERED` messages. This might have changed the semantics in some places and can have unindented side-effects.

The `DatabaseComponent` was extended with methods for:
* adding dependencies
* getting dependencies and states of a message
* getting messages that depend on a message (dependents) and their states
* getting messages to be delivered (by startup hook)
* getting pending messages to be possibly delivered (by startup hook)

The `valid` column in the database was renamed to `state` along with the associated methods to better reflect its new extended meaning.

Since pending messages should not be available to clients, many database queries have been modified to only return results for delivered messages. I might have missed some. Please check carefully. Also you might want to think about how you could create arbitrarily large transactions by sending malicious messages.

Please note that the database version was bumped in this MR.

Closes #382

See merge request !198
This commit is contained in:
str4d
2016-05-28 00:20:39 +00:00
21 changed files with 2234 additions and 251 deletions

View File

@@ -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());
}
}

View File

@@ -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();
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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());

View File

@@ -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());
}

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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.

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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.
@@ -353,15 +397,6 @@ interface Database<T> {
Collection<MessageId> getMessagesToSend(T txn, ContactId c, int maxLength)
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 any messages that need to be validated by the given
* client.
@@ -371,6 +406,15 @@ interface Database<T> {
Collection<MessageId> getMessagesToValidate(T 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(T txn, ClientId c)
throws DbException;
/**
* Returns the message with the given ID, in serialised form, or null if
* the message has been deleted.
@@ -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

View File

@@ -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,

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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,18 @@ 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,
@@ -120,6 +165,163 @@ public class ValidationManagerImplTest extends BriarTestCase {
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,
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());
}
@Test
public void testValidationContinuesAfterNoSuchMessageException()
throws Exception {
@@ -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());
}
}