From b03d0a206bc7c6a067bf5152bb9c6c10d52f81f4 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 18 May 2016 18:24:03 -0300 Subject: [PATCH 1/5] Add Message Dependencies to Database This adds a new table to the database to hold message dependencies. It introduces two more message states: pending and delivered The valid column in the database was renamed to state to better reflect its new extended meaning. The DatabaseComponent was extended with three methods for: * adding dependencies * getting dependencies of a message * getting messages that depend on a message (dependents) * getting messages to be delivered (by startup hook) * getting pending messages to be possibly delivered (by startup hook) 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. Since pending messages should not be available to clients, many database queries have been modified to only return results for delivered messages. All added methods and changes should come with updated unit tests. Please note that the database version was bumped in this commit. --- .../ForumSharingIntegrationTest.java | 21 +- .../IntroductionIntegrationTest.java | 21 +- .../AndroidNotificationManagerImpl.java | 9 +- .../android/contact/ContactListFragment.java | 14 +- .../android/contact/ConversationActivity.java | 10 +- .../android/forum/ForumActivity.java | 10 +- .../android/forum/ForumListFragment.java | 18 +- .../api/db/DatabaseComponent.java | 46 ++- .../api/event/MessageStateChangedEvent.java | 42 ++ .../api/event/MessageValidatedEvent.java | 39 -- .../api/sync/ValidationManager.java | 10 +- .../src/org/briarproject/db/Database.java | 49 ++- .../db/DatabaseComponentImpl.java | 65 ++- .../src/org/briarproject/db/JdbcDatabase.java | 247 ++++++++++-- .../sync/ValidationManagerImpl.java | 6 +- .../db/DatabaseComponentImplTest.java | 115 +++++- .../org/briarproject/db/H2DatabaseTest.java | 374 +++++++++++++++++- .../sync/ValidationManagerImplTest.java | 13 +- 18 files changed, 924 insertions(+), 185 deletions(-) create mode 100644 briar-api/src/org/briarproject/api/event/MessageStateChangedEvent.java delete mode 100644 briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java index 87cfec097..b4a9c78db 100644 --- a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java @@ -18,7 +18,7 @@ import org.briarproject.api.event.Event; import org.briarproject.api.event.EventListener; import org.briarproject.api.event.ForumInvitationReceivedEvent; import org.briarproject.api.event.ForumInvitationResponseReceivedEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumInvitationMessage; import org.briarproject.api.forum.ForumManager; @@ -60,6 +60,7 @@ 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.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -669,14 +670,13 @@ 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() + if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent event = (MessageStateChangedEvent) e; + if (event.getState() == DELIVERED && event.getClientId() .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 +722,13 @@ public class ForumSharingIntegrationTest extends BriarTestCase { } 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(forumSharingManager1.getClientId()) && !event.isLocal()) { LOG.info("TEST: Invitee received message in group " + - ((MessageValidatedEvent) e).getMessage() - .getGroupId().hashCode()); + event.getMessage().getGroupId().hashCode()); msgWaiter.resume(); } } else if (e instanceof ForumInvitationReceivedEvent) { diff --git a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java index 259be1a9d..043fcae7d 100644 --- a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java @@ -19,7 +19,7 @@ import org.briarproject.api.event.IntroductionAbortedEvent; import org.briarproject.api.event.IntroductionRequestReceivedEvent; import org.briarproject.api.event.IntroductionResponseReceivedEvent; import org.briarproject.api.event.IntroductionSucceededEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.identity.AuthorFactory; import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.identity.LocalAuthor; @@ -70,6 +70,7 @@ 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.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -1047,15 +1048,14 @@ 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: Introducee" + introducee + " received message in group " + - ((MessageValidatedEvent) e).getMessage() - .getGroupId().hashCode()); + event.getMessage().getGroupId().hashCode()); msgWaiter.resume(); } } else if (e instanceof IntroductionRequestReceivedEvent) { @@ -1114,14 +1114,13 @@ public class IntroductionIntegrationTest extends BriarTestCase { @Override public void eventOccurred(Event e) { - if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent event = (MessageValidatedEvent) e; - if (event.getClientId() + if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent event = (MessageStateChangedEvent) e; + if (event.getState() == DELIVERED && event.getClientId() .equals(introductionManager0.getClientId()) && !event.isLocal()) { LOG.info("TEST: Introducer received message in group " + - ((MessageValidatedEvent) e).getMessage() - .getGroupId().hashCode()); + event.getMessage().getGroupId().hashCode()); msgWaiter.resume(); } } else if (e instanceof IntroductionResponseReceivedEvent) { diff --git a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java index 166c8e84f..7e3d83c9d 100644 --- a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java +++ b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java @@ -24,7 +24,7 @@ import org.briarproject.api.event.ForumInvitationReceivedEvent; import org.briarproject.api.event.IntroductionRequestReceivedEvent; import org.briarproject.api.event.IntroductionResponseReceivedEvent; import org.briarproject.api.event.IntroductionSucceededEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.event.SettingsUpdatedEvent; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.lifecycle.Service; @@ -59,6 +59,7 @@ import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET; import static java.util.logging.Level.WARNING; import static org.briarproject.android.BriarActivity.GROUP_ID; import static org.briarproject.android.fragment.SettingsFragment.SETTINGS_NAMESPACE; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; class AndroidNotificationManagerImpl implements AndroidNotificationManager, Service, EventListener { @@ -156,9 +157,9 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, if (e instanceof SettingsUpdatedEvent) { SettingsUpdatedEvent s = (SettingsUpdatedEvent) e; if (s.getNamespace().equals(SETTINGS_NAMESPACE)) loadSettings(); - } else if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent m = (MessageValidatedEvent) e; - if (m.isValid() && !m.isLocal()) { + } else if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent m = (MessageStateChangedEvent) e; + if (!m.isLocal() && m.getState() == DELIVERED) { ClientId c = m.getClientId(); if (c.equals(messagingManager.getClientId())) showPrivateMessageNotification(m.getMessage().getGroupId()); diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java index d70f7d439..7b21f28ac 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java @@ -30,7 +30,7 @@ import org.briarproject.api.event.ContactStatusChangedEvent; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventListener; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.forum.ForumInvitationMessage; import org.briarproject.api.forum.ForumSharingManager; import org.briarproject.api.identity.IdentityManager; @@ -54,6 +54,7 @@ import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAn import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.android.BriarActivity.GROUP_ID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; public class ContactListFragment extends BaseFragment implements EventListener { @@ -230,12 +231,13 @@ public class ContactListFragment extends BaseFragment implements EventListener { } else if (e instanceof ContactRemovedEvent) { LOG.info("Contact removed"); removeItem(((ContactRemovedEvent) e).getContactId()); - } else if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent m = (MessageValidatedEvent) e; + } else if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent m = (MessageStateChangedEvent) e; ClientId c = m.getClientId(); - if (m.isValid() && (c.equals(messagingManager.getClientId()) || - c.equals(introductionManager.getClientId()) || - c.equals(forumSharingManager.getClientId()))) { + if (m.getState() == DELIVERED && + (c.equals(messagingManager.getClientId()) || + c.equals(introductionManager.getClientId()) || + c.equals(forumSharingManager.getClientId()))) { LOG.info("Message added, reloading"); reloadConversation(m.getMessage().getGroupId()); } diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java index 2c8e3b5df..f91d06440 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java @@ -47,7 +47,7 @@ import org.briarproject.api.event.EventListener; import org.briarproject.api.event.ForumInvitationReceivedEvent; import org.briarproject.api.event.IntroductionRequestReceivedEvent; import org.briarproject.api.event.IntroductionResponseReceivedEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.forum.ForumInvitationMessage; @@ -87,6 +87,7 @@ import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.android.contact.ConversationItem.IncomingItem; import static org.briarproject.android.contact.ConversationItem.OutgoingItem; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; public class ConversationActivity extends BriarActivity implements EventListener, OnClickListener, @@ -468,9 +469,10 @@ public class ConversationActivity extends BriarActivity LOG.info("Contact removed"); finishOnUiThread(); } - } else if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent m = (MessageValidatedEvent) e; - if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) { + } else if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent m = (MessageStateChangedEvent) e; + if (m.getState() == DELIVERED && + m.getMessage().getGroupId().equals(groupId)) { LOG.info("Message added, reloading"); // Mark new incoming messages as read directly if (m.isLocal()) loadMessages(); diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java index b2069a6f2..cfa26e8c8 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java @@ -30,7 +30,7 @@ import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventListener; import org.briarproject.api.event.GroupRemovedEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPostHeader; @@ -62,6 +62,7 @@ import static java.util.logging.Level.WARNING; import static org.briarproject.android.forum.ReadForumPostActivity.RESULT_PREV_NEXT; import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH; import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; public class ForumActivity extends BriarActivity implements EventListener, OnItemClickListener { @@ -356,9 +357,10 @@ public class ForumActivity extends BriarActivity implements EventListener, } public void eventOccurred(Event e) { - if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent m = (MessageValidatedEvent) e; - if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) { + if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent m = (MessageStateChangedEvent) e; + if (m.getState() == DELIVERED && + m.getMessage().getGroupId().equals(groupId)) { LOG.info("Message added, reloading"); loadHeaders(); } diff --git a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java index e6f0f6a26..9c3207983 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java +++ b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java @@ -25,7 +25,7 @@ import org.briarproject.api.event.Event; import org.briarproject.api.event.ForumInvitationReceivedEvent; import org.briarproject.api.event.GroupAddedEvent; import org.briarproject.api.event.GroupRemovedEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPostHeader; @@ -42,6 +42,7 @@ import javax.inject.Inject; import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; public class ForumListFragment extends BaseEventFragment implements View.OnClickListener { @@ -224,14 +225,13 @@ public class ForumListFragment extends BaseEventFragment implements LOG.info("Forum removed, removing from list"); removeForum(g.getGroup().getId()); } - } else if (e instanceof MessageValidatedEvent) { - MessageValidatedEvent m = (MessageValidatedEvent) e; - if (m.isValid()) { - ClientId c = m.getClientId(); - if (c.equals(forumManager.getClientId())) { - LOG.info("Forum post added, reloading"); - loadForumHeaders(m.getMessage().getGroupId()); - } + } else if (e instanceof MessageStateChangedEvent) { + MessageStateChangedEvent m = (MessageStateChangedEvent) e; + ClientId c = m.getClientId(); + if (m.getState() == DELIVERED && + c.equals(forumManager.getClientId())) { + LOG.info("Forum post added, reloading"); + loadForumHeaders(m.getMessage().getGroupId()); } } else if (e instanceof ForumInvitationReceivedEvent) { loadAvailableForums(); diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java index e145d9e84..123d8e4b1 100644 --- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java +++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java @@ -22,6 +22,8 @@ import org.briarproject.api.transport.TransportKeys; import java.util.Collection; import java.util.Map; +import static org.briarproject.api.sync.ValidationManager.State; + /** * Encapsulates the database implementation and exposes high-level operations * to other components. @@ -233,6 +235,24 @@ public interface DatabaseComponent { Collection getMessagesToValidate(Transaction txn, ClientId c) throws DbException; + /** + * Returns the IDs of any messages that need to be delivered to the given + * client. + *

+ * Read-only. + */ + Collection 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. + *

+ * Read-only. + */ + Collection 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 getMessageStatus(Transaction txn, ContactId c, GroupId g) throws DbException; + /** + * Returns the dependencies of the given message. + *

+ * Read-only. + */ + Map getMessageDependencies(Transaction txn, MessageId m) + throws DbException; + + /** + * Returns all IDs of messages that depend on the given message. + *

+ * Read-only. + */ + Map 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 dependencies) throws DbException; + /** * Sets the reordering window for the given contact and transport in the * given rotation period. diff --git a/briar-api/src/org/briarproject/api/event/MessageStateChangedEvent.java b/briar-api/src/org/briarproject/api/event/MessageStateChangedEvent.java new file mode 100644 index 000000000..c2aafa034 --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/MessageStateChangedEvent.java @@ -0,0 +1,42 @@ +package org.briarproject.api.event; + +import org.briarproject.api.sync.ClientId; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.ValidationManager; +import static org.briarproject.api.sync.ValidationManager.State; + +/** + * An event that is broadcast when a message state changed. + */ +public class MessageStateChangedEvent extends Event { + + private final Message message; + private final ClientId clientId; + private final boolean local; + private final State state; + + public MessageStateChangedEvent(Message message, ClientId clientId, + boolean local, State state) { + this.message = message; + this.clientId = clientId; + this.local = local; + this.state = state; + } + + public Message getMessage() { + return message; + } + + public ClientId getClientId() { + return clientId; + } + + public boolean isLocal() { + return local; + } + + public State getState() { + return state; + } + +} diff --git a/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java b/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java deleted file mode 100644 index 26216a873..000000000 --- a/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.briarproject.api.event; - -import org.briarproject.api.db.Metadata; -import org.briarproject.api.sync.ClientId; -import org.briarproject.api.sync.Message; - -/** - * An event that is broadcast when a message has passed or failed validation. - */ -public class MessageValidatedEvent extends Event { - - private final Message message; - private final ClientId clientId; - private final boolean local, valid; - - public MessageValidatedEvent(Message message, ClientId clientId, - boolean local, boolean valid) { - this.message = message; - this.clientId = clientId; - this.local = local; - this.valid = valid; - } - - public Message getMessage() { - return message; - } - - public ClientId getClientId() { - return clientId; - } - - public boolean isLocal() { - return local; - } - - public boolean isValid() { - return valid; - } -} diff --git a/briar-api/src/org/briarproject/api/sync/ValidationManager.java b/briar-api/src/org/briarproject/api/sync/ValidationManager.java index 7304b2e03..e92a6a682 100644 --- a/briar-api/src/org/briarproject/api/sync/ValidationManager.java +++ b/briar-api/src/org/briarproject/api/sync/ValidationManager.java @@ -10,13 +10,13 @@ import org.briarproject.api.db.Transaction; */ public interface ValidationManager { - enum Validity { + enum State { - UNKNOWN(0), INVALID(1), VALID(2); + UNKNOWN(0), INVALID(1), PENDING(2), VALID(3), DELIVERED(4); private final int value; - Validity(int value) { + State(int value) { this.value = value; } @@ -24,8 +24,8 @@ public interface ValidationManager { return value; } - public static Validity fromValue(int value) { - for (Validity s : values()) if (s.value == value) return s; + public static State fromValue(int value) { + for (State s : values()) if (s.value == value) return s; throw new IllegalArgumentException(); } } diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java index 6589fbc81..2fbd55f4a 100644 --- a/briar-core/src/org/briarproject/db/Database.java +++ b/briar-core/src/org/briarproject/db/Database.java @@ -16,7 +16,7 @@ import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageStatus; -import org.briarproject.api.sync.ValidationManager.Validity; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.transport.TransportKeys; import java.util.Collection; @@ -79,7 +79,7 @@ interface Database { /** * 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; /** @@ -326,6 +326,24 @@ interface Database { MessageStatus getMessageStatus(T txn, ContactId c, MessageId m) 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}. + *

+ * Read-only. + */ + Map getMessageDependencies(T txn, MessageId m) + throws DbException; + + /** + * Returns all IDs of messages that depend on the given message. + *

+ * Read-only. + */ + Map getMessageDependents(T txn, MessageId m) + throws DbException; + /** * Returns the IDs of some messages received from the given contact that * need to be acknowledged, up to the given number of messages. @@ -371,6 +389,24 @@ interface Database { Collection getMessagesToValidate(T txn, ClientId c) throws DbException; + /** + * Returns the IDs of any messages that need to be delivered to the given + * client. + *

+ * Read-only. + */ + Collection getMessagesToDeliver(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. + *

+ * Read-only. + */ + Collection 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. @@ -538,7 +574,14 @@ interface Database { /** * 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; + + /* + * Adds a dependency between two MessageIds + */ + void addMessageDependency(T txn, MessageId dependentId, + MessageId dependencyId) throws DbException; /** * Sets the reordering window for the given contact and transport in the diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java index 37c1f0884..4f84a04bd 100644 --- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java +++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java @@ -29,8 +29,8 @@ import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageSharedEvent; import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToRequestEvent; -import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.MessagesAckedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.event.SettingsUpdatedEvent; import org.briarproject.api.identity.Author; @@ -47,7 +47,7 @@ import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageStatus; import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.Request; -import org.briarproject.api.sync.ValidationManager.Validity; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.transport.TransportKeys; import java.util.ArrayList; @@ -64,8 +64,8 @@ import java.util.logging.Logger; import javax.inject.Inject; import static java.util.logging.Level.WARNING; -import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN; -import static org.briarproject.api.sync.ValidationManager.Validity.VALID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES; class DatabaseComponentImpl implements DatabaseComponent { @@ -193,17 +193,18 @@ class DatabaseComponentImpl 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 implements DatabaseComponent { return db.getMessagesToValidate(txn, c); } + public Collection getMessagesToDeliver(Transaction transaction, + ClientId c) throws DbException { + T txn = unbox(transaction); + return db.getMessagesToDeliver(txn, c); + } + + public Collection 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 implements DatabaseComponent { return db.getMessageStatus(txn, c, m); } + public Map 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 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 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 dependencies) + throws DbException { + if (transaction.isReadOnly()) throw new IllegalArgumentException(); + T txn = unbox(transaction); + if (!db.containsMessage(txn, dependent.getId())) + throw new NoSuchMessageException(); + for (MessageId dependencyId : dependencies) { + db.addMessageDependency(txn, dependent.getId(), dependencyId); + } } public void setReorderingWindow(Transaction transaction, ContactId c, diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java index e3e026f22..a52fbe2a1 100644 --- a/briar-core/src/org/briarproject/db/JdbcDatabase.java +++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java @@ -19,7 +19,7 @@ import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageStatus; -import org.briarproject.api.sync.ValidationManager.Validity; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.system.Clock; import org.briarproject.api.transport.IncomingKeys; import org.briarproject.api.transport.OutgoingKeys; @@ -49,9 +49,11 @@ import java.util.logging.Logger; import static java.util.logging.Level.WARNING; import static org.briarproject.api.db.Metadata.REMOVE; -import static org.briarproject.api.sync.ValidationManager.Validity.INVALID; -import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN; -import static org.briarproject.api.sync.ValidationManager.Validity.VALID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.PENDING; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; +import static org.briarproject.api.sync.ValidationManager.State.VALID; import static org.briarproject.db.DatabaseConstants.DB_SETTINGS_NAMESPACE; import static org.briarproject.db.DatabaseConstants.DEVICE_ID_KEY; import static org.briarproject.db.DatabaseConstants.DEVICE_SETTINGS_NAMESPACE; @@ -65,8 +67,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry; */ abstract class JdbcDatabase implements Database { - 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 { + " (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 { + " 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 { 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 { } } - 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 { } } + 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 { } } + private Collection 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 ids = new ArrayList(); + 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 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 { + " 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 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 ids = new HashSet(); while (rs.next()) ids.add(new MessageId(rs.getBytes(1))); @@ -1182,10 +1237,11 @@ abstract class JdbcDatabase implements Database { + " 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 all = new HashMap(); Metadata metadata = null; @@ -1223,23 +1279,38 @@ abstract class JdbcDatabase implements Database { 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 { } } + public Map 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 dependencies = new HashMap(); + 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 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 dependents = new HashMap(); + 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 getMessagesToAck(Connection txn, ContactId c, int maxMessages) throws DbException { PreparedStatement ps = null; @@ -1346,13 +1502,13 @@ abstract class JdbcDatabase implements Database { + " 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 { + " 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 ids = new ArrayList(); @@ -1431,14 +1587,29 @@ abstract class JdbcDatabase implements Database { public Collection getMessagesToValidate(Connection txn, ClientId c) throws DbException { + return getMessagesInState(txn, c, UNKNOWN); + } + + public Collection getMessagesToDeliver(Connection txn, + ClientId c) throws DbException { + return getMessagesInState(txn, c, VALID); + } + + public Collection getPendingMessages(Connection txn, + ClientId c) throws DbException { + return getMessagesInState(txn, c, PENDING); + } + + private Collection 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 ids = new ArrayList(); @@ -1485,13 +1656,13 @@ abstract class JdbcDatabase implements Database { + " 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 ids = new ArrayList(); @@ -2081,13 +2252,13 @@ abstract class JdbcDatabase implements Database { } } - public void setMessageValid(Connection txn, MessageId m, boolean valid) + public void setMessageState(Connection txn, MessageId m, State state) throws DbException { PreparedStatement ps = null; try { - String sql = "UPDATE messages SET valid = ? WHERE messageId = ?"; + String sql = "UPDATE messages SET state = ? WHERE messageId = ?"; ps = txn.prepareStatement(sql); - ps.setInt(1, valid ? VALID.getValue() : INVALID.getValue()); + ps.setInt(1, state.getValue()); ps.setBytes(2, m.getBytes()); int affected = ps.executeUpdate(); if (affected < 0 || affected > 1) throw new DbStateException(); diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java index 1e85803a2..487c31d52 100644 --- a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java +++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java @@ -36,6 +36,8 @@ 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.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.VALID; class ValidationManagerImpl implements ValidationManager, Service, EventListener { @@ -178,7 +180,7 @@ class ValidationManagerImpl implements ValidationManager, Service, try { Metadata meta = result.getMetadata(); db.mergeMessageMetadata(txn, m.getId(), meta); - db.setMessageValid(txn, m, c, true); + db.setMessageState(txn, m.getId(), c, VALID); db.setMessageShared(txn, m, true); IncomingMessageHook hook = hooks.get(c); if (hook != null) @@ -202,7 +204,7 @@ class ValidationManagerImpl implements ValidationManager, Service, try { Transaction txn = db.startTransaction(false); try { - db.setMessageValid(txn, m, c, false); + db.setMessageState(txn, m.getId(), c, INVALID); txn.setComplete(); } finally { db.endTransaction(txn); diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java index ff6f7a918..6a4db39e8 100644 --- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java +++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java @@ -29,7 +29,7 @@ import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageSharedEvent; import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToRequestEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.event.SettingsUpdatedEvent; @@ -53,6 +53,7 @@ import org.jmock.Expectations; import org.jmock.Mockery; import org.junit.Test; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -60,8 +61,9 @@ import java.util.Map; import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; -import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN; -import static org.briarproject.api.sync.ValidationManager.Validity.VALID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; +import static org.briarproject.api.sync.ValidationManager.State.VALID; import static org.briarproject.api.transport.TransportConstants.REORDERING_WINDOW_SIZE; import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES; import static org.junit.Assert.assertEquals; @@ -264,7 +266,7 @@ public class DatabaseComponentImplTest extends BriarTestCase { will(returnValue(true)); oneOf(database).containsMessage(txn, messageId); will(returnValue(false)); - oneOf(database).addMessage(txn, message, VALID, true); + oneOf(database).addMessage(txn, message, DELIVERED, true); oneOf(database).mergeMessageMetadata(txn, messageId, metadata); oneOf(database).getVisibility(txn, groupId); will(returnValue(Collections.singletonList(contactId))); @@ -274,7 +276,7 @@ public class DatabaseComponentImplTest extends BriarTestCase { oneOf(database).commitTransaction(txn); // The message was added, so the listeners should be called oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class))); - oneOf(eventBus).broadcast(with(any(MessageValidatedEvent.class))); + oneOf(eventBus).broadcast(with(any(MessageStateChangedEvent.class))); oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class))); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, @@ -675,11 +677,11 @@ public class DatabaseComponentImplTest extends BriarTestCase { final EventBus eventBus = context.mock(EventBus.class); context.checking(new Expectations() {{ // Check whether the message is in the DB (which it's not) - exactly(8).of(database).startTransaction(); + exactly(10).of(database).startTransaction(); will(returnValue(txn)); - exactly(8).of(database).containsMessage(txn, messageId); + exactly(10).of(database).containsMessage(txn, messageId); will(returnValue(false)); - exactly(8).of(database).abortTransaction(txn); + exactly(10).of(database).abortTransaction(txn); // This is needed for getMessageStatus() to proceed exactly(1).of(database).containsContact(txn, contactId); will(returnValue(true)); @@ -759,7 +761,27 @@ public class DatabaseComponentImplTest extends BriarTestCase { transaction = db.startTransaction(false); try { - db.setMessageValid(transaction, message, clientId, true); + db.setMessageState(transaction, message, clientId, VALID); + fail(); + } catch (NoSuchMessageException expected) { + // Expected + } finally { + db.endTransaction(transaction); + } + + transaction = db.startTransaction(true); + try { + db.getMessageDependencies(transaction, messageId); + fail(); + } catch (NoSuchMessageException expected) { + // Expected + } finally { + db.endTransaction(transaction); + } + + transaction = db.startTransaction(true); + try { + db.getMessageDependents(transaction, messageId); fail(); } catch (NoSuchMessageException expected) { // Expected @@ -1595,4 +1617,79 @@ public class DatabaseComponentImplTest extends BriarTestCase { context.assertIsSatisfied(); } + + @Test + @SuppressWarnings("unchecked") + public void testMessageDependencies() throws Exception { + final int shutdownHandle = 12345; + Mockery context = new Mockery(); + final Database 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 dependencies = new ArrayList<>(2); + dependencies.add(messageId1); + dependencies.add(messageId2); + db.addMessageDependencies(transaction, message, dependencies); + db.getMessageDependencies(transaction, messageId); + db.getMessageDependents(transaction, messageId); + transaction.setComplete(); + } finally { + db.endTransaction(transaction); + } + db.close(); + + context.assertIsSatisfied(); + } } diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java index d967b8159..9d5b1f0b9 100644 --- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java +++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java @@ -19,6 +19,7 @@ import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageStatus; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.transport.IncomingKeys; import org.briarproject.api.transport.OutgoingKeys; import org.briarproject.api.transport.TransportKeys; @@ -46,8 +47,11 @@ import static org.briarproject.api.db.Metadata.REMOVE; import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_LENGTH; -import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN; -import static org.briarproject.api.sync.ValidationManager.Validity.VALID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.PENDING; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; +import static org.briarproject.api.sync.ValidationManager.State.VALID; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -114,7 +118,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addGroup(txn, group); assertTrue(db.containsGroup(txn, groupId)); assertFalse(db.containsMessage(txn, messageId)); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); assertTrue(db.containsMessage(txn, messageId)); db.commitTransaction(txn); db.close(); @@ -152,7 +156,7 @@ public class H2DatabaseTest extends BriarTestCase { // Add a group and a message db.addGroup(txn, group); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); // Removing the group should remove the message assertTrue(db.containsMessage(txn, messageId)); @@ -174,7 +178,7 @@ public class H2DatabaseTest extends BriarTestCase { true)); db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); // The message has no status yet, so it should not be sendable Collection 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 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 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 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 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 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 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 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 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 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 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 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 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 db = open(false); @@ -1049,7 +1381,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addVisibility(txn, contactId, groupId); // Add a message to the group - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, false); // The message should not be sent or seen @@ -1176,7 +1508,7 @@ public class H2DatabaseTest extends BriarTestCase { true)); db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, false); // The message should be visible to the contact diff --git a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java index 1aea2e30b..d55b575c2 100644 --- a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java +++ b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java @@ -28,6 +28,9 @@ import org.junit.Test; import java.util.Arrays; import java.util.concurrent.Executor; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.VALID; + public class ValidationManagerImplTest extends BriarTestCase { private final ClientId clientId = new ClientId(TestUtils.getRandomId()); @@ -88,7 +91,7 @@ public class ValidationManagerImplTest extends BriarTestCase { oneOf(db).startTransaction(false); will(returnValue(txn2)); oneOf(db).mergeMessageMetadata(txn2, messageId, metadata); - oneOf(db).setMessageValid(txn2, message, clientId, true); + oneOf(db).setMessageState(txn2, messageId, clientId, VALID); oneOf(db).setMessageShared(txn2, message, true); // Call the hook for the first message oneOf(hook).incomingMessage(txn2, message, metadata); @@ -107,7 +110,7 @@ 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, messageId1, clientId, INVALID); oneOf(db).endTransaction(txn4); }}); @@ -161,7 +164,7 @@ 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, messageId1, clientId, INVALID); oneOf(db).endTransaction(txn3); }}); @@ -218,7 +221,7 @@ 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, messageId1, clientId, INVALID); oneOf(db).endTransaction(txn3); }}); @@ -256,7 +259,7 @@ public class ValidationManagerImplTest extends BriarTestCase { oneOf(db).startTransaction(false); will(returnValue(txn1)); oneOf(db).mergeMessageMetadata(txn1, messageId, metadata); - oneOf(db).setMessageValid(txn1, message, clientId, true); + oneOf(db).setMessageState(txn1, messageId, clientId, VALID); oneOf(db).setMessageShared(txn1, message, true); // Call the hook oneOf(hook).incomingMessage(txn1, message, metadata); From 5561532c5d615da2e9675ed12e9b250db270540d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 23 May 2016 18:40:40 -0300 Subject: [PATCH 2/5] Implement new message validation logic that handles message dependencies reported from clients. The MessageValidatedEvent has been renamed into a MessageDeliveredEvent since there were no real use cases for the former any more. --- .../ForumSharingIntegrationTest.java | 16 +- .../IntroductionIntegrationTest.java | 10 +- .../sync/ValidationManagerImpl.java | 255 ++++++- .../sync/ValidationManagerImplTest.java | 671 +++++++++++++++++- 4 files changed, 927 insertions(+), 25 deletions(-) diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java index b4a9c78db..79809f444 100644 --- a/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/ForumSharingIntegrationTest.java @@ -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; @@ -61,6 +64,7 @@ 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; @@ -672,8 +676,10 @@ public class ForumSharingIntegrationTest extends BriarTestCase { public void eventOccurred(Event e) { if (e instanceof MessageStateChangedEvent) { MessageStateChangedEvent event = (MessageStateChangedEvent) e; - if (event.getState() == DELIVERED && event.getClientId() - .equals(forumSharingManager0.getClientId()) && + 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 " + event.getMessage().getGroupId().hashCode()); @@ -724,8 +730,10 @@ public class ForumSharingIntegrationTest extends BriarTestCase { public void eventOccurred(Event e) { if (e instanceof MessageStateChangedEvent) { MessageStateChangedEvent event = (MessageStateChangedEvent) e; - if (event.getState() == DELIVERED && event.getClientId() - .equals(forumSharingManager1.getClientId()) && + 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 " + event.getMessage().getGroupId().hashCode()); diff --git a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java index 043fcae7d..52fad50bb 100644 --- a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java @@ -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; @@ -71,6 +74,7 @@ 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; @@ -1050,8 +1054,10 @@ public class IntroductionIntegrationTest extends BriarTestCase { public void eventOccurred(Event e) { if (e instanceof MessageStateChangedEvent) { MessageStateChangedEvent event = (MessageStateChangedEvent) e; - if (event.getState() == DELIVERED && event.getClientId() - .equals(introductionManager0.getClientId()) && + 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 " + diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java index 487c31d52..f6aa74c3a 100644 --- a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java +++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java @@ -23,8 +23,10 @@ import org.briarproject.api.sync.ValidationManager; import org.briarproject.api.sync.MessageContext; import org.briarproject.util.ByteUtils; +import java.util.Collection; import java.util.LinkedList; import java.util.Map; +import java.util.Map.Entry; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; @@ -36,7 +38,9 @@ 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, @@ -66,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 @@ -84,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() { @@ -128,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); @@ -140,6 +148,112 @@ class ValidationManagerImpl implements ValidationManager, Service, }); } + private void deliverOutstandingMessages(final ClientId c) { + dbExecutor.execute(new Runnable() { + @Override + public void run() { + try { + Queue validated = new LinkedList(); + Queue pending = new LinkedList(); + 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 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 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 states = + db.getMessageDependencies(txn, id); + for (Entry 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(); @@ -176,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 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 states = + db.getMessageDependencies(txn, m.getId()); + newState = VALID; + for (Entry 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.setMessageState(txn, m.getId(), c, VALID); - 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 pending = new LinkedList(); + 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 dependents = + db.getMessageDependents(txn, m.getId()); + for (Entry i : dependents + .entrySet()) { + if (i.getValue() != PENDING) continue; + + // check that all dependencies are delivered + Map dependencies = + db.getMessageDependencies(txn, i.getKey()); + for (Entry 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); @@ -202,13 +402,56 @@ class ValidationManagerImpl implements ValidationManager, Service, @Override public void run() { try { + Queue invalid = new LinkedList(); Transaction txn = db.startTransaction(false); try { - db.setMessageState(txn, m.getId(), c, INVALID); + Map 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 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 invalid) { + if (invalid.isEmpty()) return; + dbExecutor.execute(new Runnable() { + @Override + public void run() { + try { + Message m = null; + Group g = null; + Transaction txn = db.startTransaction(true); + try { + MessageId id = invalid.poll(); + byte[] raw = db.getRawMessage(txn, id); + m = parseMessage(id, raw); + g = db.getGroup(txn, m.getGroupId()); + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + if (g != null) markMessageInvalid(m, g.getClientId()); + markNextMessageInvalid(invalid); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); diff --git a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java index d55b575c2..0520b3b1f 100644 --- a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java +++ b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java @@ -16,26 +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); @@ -45,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 dependencies = new ArrayList<>(); + private final MessageContext validResultWithDependencies = + new MessageContext(metadata, dependencies); + private final Map 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 @@ -67,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); @@ -90,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).setMessageState(txn2, messageId, clientId, VALID); - 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)); @@ -110,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).setMessageState(txn4, messageId1, clientId, INVALID); + 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, @@ -123,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 { @@ -137,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); @@ -164,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).setMessageState(txn3, messageId1, clientId, INVALID); + 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, @@ -175,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 @@ -191,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); @@ -221,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).setMessageState(txn3, messageId1, clientId, INVALID); + 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, @@ -232,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 @@ -245,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); @@ -258,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).setMessageState(txn1, messageId, clientId, VALID); - 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, @@ -273,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 @@ -293,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()); + } + } From d3b83a50a64aeb1e4095ab03f783f3e3d3b29872 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 24 May 2016 19:41:09 -0300 Subject: [PATCH 3/5] Let the ForumValidator return parent posts as dependency and add integration tests that make sure that dependencies are handled properly. --- .../org/briarproject/ForumManagerTest.java | 384 ++++++++++++++++-- .../ForumManagerTestComponent.java | 21 + .../forum/ForumPostValidator.java | 15 +- 3 files changed, 380 insertions(+), 40 deletions(-) diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java index 59c88d4d7..b1824191d 100644 --- a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java @@ -2,13 +2,30 @@ package org.briarproject; import junit.framework.Assert; -import org.briarproject.api.db.DatabaseComponent; +import net.jodah.concurrentunit.Waiter; + +import org.briarproject.api.contact.Contact; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.db.DbException; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPost; import org.briarproject.api.forum.ForumPostFactory; import org.briarproject.api.forum.ForumPostHeader; +import org.briarproject.api.forum.ForumSharingManager; +import org.briarproject.api.identity.AuthorFactory; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.lifecycle.LifecycleManager; import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.SyncSession; +import org.briarproject.api.sync.SyncSessionFactory; +import org.briarproject.api.system.Clock; import org.briarproject.contact.ContactModule; import org.briarproject.crypto.CryptoModule; import org.briarproject.forum.ForumModule; @@ -17,55 +34,101 @@ import org.briarproject.properties.PropertiesModule; import org.briarproject.sync.SyncModule; import org.briarproject.transport.TransportModule; import org.briarproject.util.StringUtils; +import org.junit.After; import org.junit.Before; import org.junit.Test; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; import java.util.Collection; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; import javax.inject.Inject; import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.TestCase.assertFalse; +import static org.briarproject.TestPluginsModule.MAX_LATENCY; +import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.PENDING; +import static org.briarproject.api.sync.ValidationManager.State.VALID; import static org.junit.Assert.assertTrue; public class ForumManagerTest { + private LifecycleManager lifecycleManager0, lifecycleManager1; + private SyncSessionFactory sync0, sync1; + private ForumManager forumManager0, forumManager1; + private ContactManager contactManager0, contactManager1; + private ContactId contactId0,contactId1; + private IdentityManager identityManager0, identityManager1; + private LocalAuthor author0, author1; + private Forum forum0; + @Inject - protected ForumManager forumManager; + Clock clock; @Inject - protected ForumPostFactory forumPostFactory; + AuthorFactory authorFactory; @Inject - protected DatabaseComponent db; + ForumPostFactory forumPostFactory; + + // objects accessed from background threads need to be volatile + private volatile ForumSharingManager forumSharingManager0; + private volatile ForumSharingManager forumSharingManager1; + private volatile Waiter validationWaiter; + private volatile Waiter deliveryWaiter; private final File testDir = TestUtils.getTestDirectory(); + private final SecretKey master = TestUtils.getSecretKey(); + private final int TIMEOUT = 15000; + private final String SHARER = "Sharer"; + private final String INVITEE = "Invitee"; + + private static final Logger LOG = + Logger.getLogger(ForumSharingIntegrationTest.class.getName()); + + private ForumManagerTestComponent t0, t1; @Before public void setUp() throws Exception { + ForumManagerTestComponent component = + DaggerForumManagerTestComponent.builder().build(); + component.inject(this); + injectEagerSingletons(component); assertTrue(testDir.mkdirs()); - File tDir = new File(testDir, "db"); + File t0Dir = new File(testDir, SHARER); + t0 = DaggerForumManagerTestComponent.builder() + .testDatabaseModule(new TestDatabaseModule(t0Dir)).build(); + injectEagerSingletons(t0); + File t1Dir = new File(testDir, INVITEE); + t1 = DaggerForumManagerTestComponent.builder() + .testDatabaseModule(new TestDatabaseModule(t1Dir)).build(); + injectEagerSingletons(t1); - ForumManagerTestComponent component = - DaggerForumManagerTestComponent.builder() - .testDatabaseModule(new TestDatabaseModule(tDir)) - .build(); + identityManager0 = t0.getIdentityManager(); + identityManager1 = t1.getIdentityManager(); + contactManager0 = t0.getContactManager(); + contactManager1 = t1.getContactManager(); + forumManager0 = t0.getForumManager(); + forumManager1 = t1.getForumManager(); + forumSharingManager0 = t0.getForumSharingManager(); + forumSharingManager1 = t1.getForumSharingManager(); + sync0 = t0.getSyncSessionFactory(); + sync1 = t1.getSyncSessionFactory(); - component.inject(new LifecycleModule.EagerSingletons()); - component.inject(new ForumModule.EagerSingletons()); - component.inject(new CryptoModule.EagerSingletons()); - component.inject(new ContactModule.EagerSingletons()); - component.inject(new TransportModule.EagerSingletons()); - component.inject(new SyncModule.EagerSingletons()); - component.inject(new PropertiesModule.EagerSingletons()); - component.inject(this); + // initialize waiters fresh for each test + validationWaiter = new Waiter(); + deliveryWaiter = new Waiter(); } - ForumPost createForumPost(GroupId groupId, ForumPost parent, String body, - long ms) - throws Exception { + private ForumPost createForumPost(GroupId groupId, ForumPost parent, + String body, long ms) throws Exception { return forumPostFactory.createAnonymousPost(groupId, ms, parent == null ? null : parent.getMessage().getId(), "text/plain", StringUtils.toUtf8(body)); @@ -73,13 +136,12 @@ public class ForumManagerTest { @Test public void testForumPost() throws Exception { - assertFalse(db.open()); - assertNotNull(forumManager); - Forum forum = forumManager.addForum("TestForum"); - assertEquals(1, forumManager.getForums().size()); - final long ms1 = System.currentTimeMillis() - 1000L; + startLifecycles(); + Forum forum = forumManager0.addForum("TestForum"); + assertEquals(1, forumManager0.getForums().size()); + final long ms1 = clock.currentTimeMillis() - 1000L; final String body1 = "some forum text"; - final long ms2 = System.currentTimeMillis(); + final long ms2 = clock.currentTimeMillis(); final String body2 = "some other forum text"; ForumPost post1 = createForumPost(forum.getGroup().getId(), null, body1, ms1); @@ -87,16 +149,16 @@ public class ForumManagerTest { ForumPost post2 = createForumPost(forum.getGroup().getId(), post1, body2, ms2); assertEquals(ms2, post2.getMessage().getTimestamp()); - forumManager.addLocalPost(post1); - forumManager.setReadFlag(post1.getMessage().getId(), true); - forumManager.addLocalPost(post2); - forumManager.setReadFlag(post2.getMessage().getId(), false); + forumManager0.addLocalPost(post1); + forumManager0.setReadFlag(post1.getMessage().getId(), true); + forumManager0.addLocalPost(post2); + forumManager0.setReadFlag(post2.getMessage().getId(), false); Collection 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 sharers back + contactId0 = contactManager1.addContact(author0, + author1.getId(), master, clock.currentTimeMillis(), true, + true + ); + } + + private void addForum() throws DbException { + forum0 = forumManager0.addForum("Test Forum"); + } + + private void listenToEvents() { + Listener listener0 = new Listener(); + t0.getEventBus().addListener(listener0); + Listener listener1 = new Listener(); + t1.getEventBus().addListener(listener1); + } + + private void sync0To1() throws IOException, TimeoutException { + deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1"); + } + + private void sync1To0() throws IOException, TimeoutException { + deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0"); + } + + private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId, + SyncSessionFactory toSync, ContactId toId, String debug) + throws IOException, TimeoutException { + + if (debug != null) LOG.info("TEST: Sending message from " + debug); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Create an outgoing sync session + SyncSession sessionFrom = + fromSync.createSimplexOutgoingSession(toId, MAX_LATENCY, out); + // Write whatever needs to be written + sessionFrom.run(); + out.close(); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + // Create an incoming sync session + SyncSession sessionTo = toSync.createIncomingSession(fromId, in); + // Read whatever needs to be read + sessionTo.run(); + in.close(); + } + + private void startLifecycles() throws InterruptedException { + // Start the lifecycle manager and wait for it to finish + lifecycleManager0 = t0.getLifecycleManager(); + lifecycleManager1 = t1.getLifecycleManager(); + lifecycleManager0.startServices(); + lifecycleManager1.startServices(); + lifecycleManager0.waitForStartup(); + lifecycleManager1.waitForStartup(); + } + + private void stopLifecycles() throws InterruptedException { + // Clean up + lifecycleManager0.stopServices(); + lifecycleManager1.stopServices(); + lifecycleManager0.waitForShutdown(); + lifecycleManager1.waitForShutdown(); + } + + private void injectEagerSingletons(ForumManagerTestComponent component) { + component.inject(new LifecycleModule.EagerSingletons()); + component.inject(new ForumModule.EagerSingletons()); + component.inject(new CryptoModule.EagerSingletons()); + component.inject(new ContactModule.EagerSingletons()); + component.inject(new TransportModule.EagerSingletons()); + component.inject(new SyncModule.EagerSingletons()); + component.inject(new PropertiesModule.EagerSingletons()); + } + } diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java index cac69fb0d..8a9e5fc6d 100644 --- a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java +++ b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTestComponent.java @@ -1,5 +1,12 @@ package org.briarproject; +import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.forum.ForumManager; +import org.briarproject.api.forum.ForumSharingManager; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.lifecycle.LifecycleManager; +import org.briarproject.api.sync.SyncSessionFactory; import org.briarproject.clients.ClientsModule; import org.briarproject.contact.ContactModule; import org.briarproject.crypto.CryptoModule; @@ -55,4 +62,18 @@ public interface ForumManagerTestComponent { void inject(TransportModule.EagerSingletons init); + LifecycleManager getLifecycleManager(); + + EventBus getEventBus(); + + IdentityManager getIdentityManager(); + + ContactManager getContactManager(); + + ForumSharingManager getForumSharingManager(); + + ForumManager getForumManager(); + + SyncSessionFactory getSyncSessionFactory(); + } diff --git a/briar-core/src/org/briarproject/forum/ForumPostValidator.java b/briar-core/src/org/briarproject/forum/ForumPostValidator.java index 6dbc68e96..e0a86a7b8 100644 --- a/briar-core/src/org/briarproject/forum/ForumPostValidator.java +++ b/briar-core/src/org/briarproject/forum/ForumPostValidator.java @@ -2,8 +2,8 @@ package org.briarproject.forum; import org.briarproject.api.FormatException; import org.briarproject.api.UniqueId; -import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.clients.BdfMessageContext; +import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.KeyParser; import org.briarproject.api.crypto.PublicKey; @@ -16,10 +16,13 @@ import org.briarproject.api.identity.AuthorFactory; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.InvalidMessageException; import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; import org.briarproject.api.system.Clock; import org.briarproject.clients.BdfMessageValidator; import java.security.GeneralSecurityException; +import java.util.Collection; +import java.util.Collections; import static org.briarproject.api.forum.ForumConstants.MAX_CONTENT_TYPE_LENGTH; import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH; @@ -96,10 +99,14 @@ class ForumPostValidator extends BdfMessageValidator { throw new InvalidMessageException("Invalid public key"); } } - // Return the metadata + // Return the metadata and dependencies BdfDictionary meta = new BdfDictionary(); + Collection 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); } } From 7ac7fae30e15fc0df454d1cbd69ff383ccca129a Mon Sep 17 00:00:00 2001 From: str4d Date: Sat, 28 May 2016 00:13:36 +0000 Subject: [PATCH 4/5] Update comments --- .../test/java/org/briarproject/ForumManagerTest.java | 2 +- briar-core/src/org/briarproject/db/Database.java | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java index b1824191d..1714a49ea 100644 --- a/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/ForumManagerTest.java @@ -354,7 +354,7 @@ public class ForumManagerTest { author0.getId(), master, clock.currentTimeMillis(), true, true ); - // invitee adds sharers back + // invitee adds sharer back contactId0 = contactManager1.addContact(author0, author1.getId(), master, clock.currentTimeMillis(), true, true diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java index 2fbd55f4a..779e73722 100644 --- a/briar-core/src/org/briarproject/db/Database.java +++ b/briar-core/src/org/briarproject/db/Database.java @@ -329,7 +329,9 @@ interface Database { /** * Returns the dependencies of the given message. * This method makes sure that dependencies in different groups - * are returned as {@link ValidationManager.State.INVALID}. + * 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. *

* Read-only. */ @@ -468,14 +470,14 @@ interface Database { void lowerRequestedFlag(T txn, ContactId c, Collection 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. */ @@ -577,7 +579,7 @@ interface Database { void setMessageState(T txn, MessageId m, State state) throws DbException; - /* + /** * Adds a dependency between two MessageIds */ void addMessageDependency(T txn, MessageId dependentId, From 19b6afa69ff11609b3e037daca99711bb61436dc Mon Sep 17 00:00:00 2001 From: str4d Date: Sat, 28 May 2016 00:17:17 +0000 Subject: [PATCH 5/5] Move new methods in Database interface to match existing alphabetic sorting --- .../src/org/briarproject/db/Database.java | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java index 779e73722..977921790 100644 --- a/briar-core/src/org/briarproject/db/Database.java +++ b/briar-core/src/org/briarproject/db/Database.java @@ -82,6 +82,12 @@ interface Database { 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 { */ Collection 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. + *

+ * Read-only. + */ + Map getMessageDependencies(T txn, MessageId m) + throws DbException; + + /** + * Returns all IDs of messages that depend on the given message. + *

+ * Read-only. + */ + Map getMessageDependents(T txn, MessageId m) + throws DbException; + /** * Returns the IDs of all messages in the given group. *

@@ -326,26 +352,6 @@ interface Database { MessageStatus getMessageStatus(T txn, ContactId c, MessageId m) 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. - *

- * Read-only. - */ - Map getMessageDependencies(T txn, MessageId m) - throws DbException; - - /** - * Returns all IDs of messages that depend on the given message. - *

- * Read-only. - */ - Map getMessageDependents(T txn, MessageId m) - throws DbException; - /** * Returns the IDs of some messages received from the given contact that * need to be acknowledged, up to the given number of messages. @@ -355,6 +361,15 @@ interface Database { Collection getMessagesToAck(T txn, ContactId c, int maxMessages) throws DbException; + /** + * Returns the IDs of any messages that need to be delivered to the given + * client. + *

+ * Read-only. + */ + Collection 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. @@ -364,15 +379,6 @@ interface Database { Collection getMessagesToOffer(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. - *

- * Read-only. - */ - Collection 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. @@ -382,6 +388,15 @@ interface Database { Collection 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. + *

+ * Read-only. + */ + Collection getMessagesToSend(T txn, ContactId c, int maxLength) + throws DbException; + /** * Returns the IDs of any messages that need to be validated by the given * client. @@ -391,15 +406,6 @@ interface Database { Collection getMessagesToValidate(T txn, ClientId c) throws DbException; - /** - * Returns the IDs of any messages that need to be delivered to the given - * client. - *

- * Read-only. - */ - Collection getMessagesToDeliver(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. @@ -579,12 +585,6 @@ interface Database { void setMessageState(T txn, MessageId m, State state) throws DbException; - /** - * Adds a dependency between two MessageIds - */ - void addMessageDependency(T txn, MessageId dependentId, - MessageId dependencyId) throws DbException; - /** * Sets the reordering window for the given contact and transport in the * given rotation period.