From b03d0a206bc7c6a067bf5152bb9c6c10d52f81f4 Mon Sep 17 00:00:00 2001 From: Torsten Grote <t@grobox.de> Date: Wed, 18 May 2016 18:24:03 -0300 Subject: [PATCH] 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 87cfec0977..b4a9c78db6 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 259be1a9d4..043fcae7d5 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 166c8e84fb..7e3d83c9df 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 d70f7d439c..7b21f28ac2 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 2c8e3b5df4..f91d064405 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 b2069a6f2d..cfa26e8c8a 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 e6f0f6a266..9c32079838 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 e145d9e844..123d8e4b16 100644 --- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java +++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java @@ -22,6 +22,8 @@ import org.briarproject.api.transport.TransportKeys; import java.util.Collection; import java.util.Map; +import static org.briarproject.api.sync.ValidationManager.State; + /** * Encapsulates the database implementation and exposes high-level operations * to other components. @@ -233,6 +235,24 @@ public interface DatabaseComponent { Collection<MessageId> getMessagesToValidate(Transaction txn, ClientId c) throws DbException; + /** + * Returns the IDs of any messages that need to be delivered to the given + * client. + * <p/> + * Read-only. + */ + Collection<MessageId> getMessagesToDeliver(Transaction txn, ClientId c) + throws DbException; + + /** + * Returns the IDs of any messages that are still pending due to + * dependencies to other messages for the given client. + * <p/> + * Read-only. + */ + Collection<MessageId> getPendingMessages(Transaction txn, ClientId c) + throws DbException; + /** * Returns the message with the given ID, in serialised form, or null if * the message has been deleted. @@ -276,6 +296,22 @@ public interface DatabaseComponent { Collection<MessageStatus> getMessageStatus(Transaction txn, ContactId c, GroupId g) throws DbException; + /** + * Returns the dependencies of the given message. + * <p/> + * Read-only. + */ + Map<MessageId, State> getMessageDependencies(Transaction txn, MessageId m) + throws DbException; + + /** + * Returns all IDs of messages that depend on the given message. + * <p/> + * Read-only. + */ + Map<MessageId, State> getMessageDependents(Transaction txn, MessageId m) + throws DbException; + /** * Returns the status of the given message with respect to the given * contact. @@ -391,11 +427,17 @@ public interface DatabaseComponent { throws DbException; /** - * Marks the given message as valid or invalid. + * Sets the state of the message with respect to validation and delivery. */ - void setMessageValid(Transaction txn, Message m, ClientId c, boolean valid) + void setMessageState(Transaction txn, Message m, ClientId c, State valid) throws DbException; + /** + * Adds dependencies for a message + */ + void addMessageDependencies(Transaction txn, Message dependent, + Collection<MessageId> dependencies) throws DbException; + /** * Sets the reordering window for the given contact and transport in the * given rotation period. diff --git a/briar-api/src/org/briarproject/api/event/MessageStateChangedEvent.java b/briar-api/src/org/briarproject/api/event/MessageStateChangedEvent.java new file mode 100644 index 0000000000..c2aafa0347 --- /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 26216a8730..0000000000 --- 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 7304b2e038..e92a6a6827 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 6589fbc814..2fbd55f4ac 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<T> { /** * Stores a message. */ - void addMessage(T txn, Message m, Validity validity, boolean shared) + void addMessage(T txn, Message m, State state, boolean shared) throws DbException; /** @@ -326,6 +326,24 @@ interface Database<T> { 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}. + * <p/> + * Read-only. + */ + Map<MessageId, State> getMessageDependencies(T txn, MessageId m) + throws DbException; + + /** + * Returns all IDs of messages that depend on the given message. + * <p/> + * Read-only. + */ + Map<MessageId, State> getMessageDependents(T txn, MessageId m) + throws DbException; + /** * Returns the IDs of 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<T> { Collection<MessageId> getMessagesToValidate(T txn, ClientId c) throws DbException; + /** + * Returns the IDs of any messages that need to be delivered to the given + * client. + * <p/> + * Read-only. + */ + Collection<MessageId> getMessagesToDeliver(T txn, ClientId c) + throws DbException; + + /** + * Returns the IDs of any messages that are still pending due to + * dependencies to other messages for the given client. + * <p/> + * Read-only. + */ + Collection<MessageId> getPendingMessages(T txn, ClientId c) + throws DbException; + /** * Returns the message with the given ID, in serialised form, or null if * the message has been deleted. @@ -538,7 +574,14 @@ interface Database<T> { /** * Marks the given message as valid or invalid. */ - void setMessageValid(T txn, MessageId m, boolean valid) throws DbException; + void setMessageState(T txn, MessageId m, State state) + throws DbException; + + /* + * 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 37c1f08849..4f84a04bd0 100644 --- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java +++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java @@ -29,8 +29,8 @@ import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageSharedEvent; import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToRequestEvent; -import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.MessagesAckedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.event.SettingsUpdatedEvent; import org.briarproject.api.identity.Author; @@ -47,7 +47,7 @@ import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageStatus; import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.Request; -import org.briarproject.api.sync.ValidationManager.Validity; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.transport.TransportKeys; import java.util.ArrayList; @@ -64,8 +64,8 @@ import java.util.logging.Logger; import javax.inject.Inject; import static java.util.logging.Level.WARNING; -import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN; -import static org.briarproject.api.sync.ValidationManager.Validity.VALID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES; class DatabaseComponentImpl<T> implements DatabaseComponent { @@ -193,17 +193,18 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { if (!db.containsGroup(txn, m.getGroupId())) throw new NoSuchGroupException(); if (!db.containsMessage(txn, m.getId())) { - addMessage(txn, m, VALID, shared); + addMessage(txn, m, DELIVERED, shared); transaction.attach(new MessageAddedEvent(m, null)); - transaction.attach(new MessageValidatedEvent(m, c, true, true)); + transaction.attach(new MessageStateChangedEvent(m, c, true, + DELIVERED)); if (shared) transaction.attach(new MessageSharedEvent(m)); } db.mergeMessageMetadata(txn, m.getId(), meta); } - private void addMessage(T txn, Message m, Validity validity, boolean shared) + private void addMessage(T txn, Message m, State state, boolean shared) throws DbException { - db.addMessage(txn, m, validity, shared); + db.addMessage(txn, m, state, shared); for (ContactId c : db.getVisibility(txn, m.getGroupId())) { boolean offered = db.removeOfferedMessage(txn, c, m.getId()); db.addStatus(txn, c, m.getId(), offered, offered); @@ -411,6 +412,18 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { return db.getMessagesToValidate(txn, c); } + public Collection<MessageId> getMessagesToDeliver(Transaction transaction, + ClientId c) throws DbException { + T txn = unbox(transaction); + return db.getMessagesToDeliver(txn, c); + } + + public Collection<MessageId> getPendingMessages(Transaction transaction, + ClientId c) throws DbException { + T txn = unbox(transaction); + return db.getPendingMessages(txn, c); + } + public byte[] getRawMessage(Transaction transaction, MessageId m) throws DbException { T txn = unbox(transaction); @@ -463,6 +476,22 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { return db.getMessageStatus(txn, c, m); } + public Map<MessageId, State> getMessageDependencies(Transaction transaction, + MessageId m) throws DbException { + T txn = unbox(transaction); + if (!db.containsMessage(txn, m)) + throw new NoSuchMessageException(); + return db.getMessageDependencies(txn, m); + } + + public Map<MessageId, State> getMessageDependents(Transaction transaction, + MessageId m) throws DbException { + T txn = unbox(transaction); + if (!db.containsMessage(txn, m)) + throw new NoSuchMessageException(); + return db.getMessageDependents(txn, m); + } + public Settings getSettings(Transaction transaction, String namespace) throws DbException { T txn = unbox(transaction); @@ -664,14 +693,26 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { if (shared) transaction.attach(new MessageSharedEvent(m)); } - public void setMessageValid(Transaction transaction, Message m, ClientId c, - boolean valid) throws DbException { + public void setMessageState(Transaction transaction, Message m, ClientId c, + State state) throws DbException { if (transaction.isReadOnly()) throw new IllegalArgumentException(); T txn = unbox(transaction); if (!db.containsMessage(txn, m.getId())) throw new NoSuchMessageException(); - db.setMessageValid(txn, m.getId(), valid); - transaction.attach(new MessageValidatedEvent(m, c, false, valid)); + db.setMessageState(txn, m.getId(), state); + transaction.attach(new MessageStateChangedEvent(m, c, false, state)); + } + + public void addMessageDependencies(Transaction transaction, + Message dependent, Collection<MessageId> dependencies) + throws DbException { + if (transaction.isReadOnly()) throw new IllegalArgumentException(); + T txn = unbox(transaction); + if (!db.containsMessage(txn, dependent.getId())) + throw new NoSuchMessageException(); + for (MessageId dependencyId : dependencies) { + db.addMessageDependency(txn, dependent.getId(), dependencyId); + } } public void setReorderingWindow(Transaction transaction, ContactId c, diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java index e3e026f221..a52fbe2a1d 100644 --- a/briar-core/src/org/briarproject/db/JdbcDatabase.java +++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java @@ -19,7 +19,7 @@ import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageStatus; -import org.briarproject.api.sync.ValidationManager.Validity; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.system.Clock; import org.briarproject.api.transport.IncomingKeys; import org.briarproject.api.transport.OutgoingKeys; @@ -49,9 +49,11 @@ import java.util.logging.Logger; import static java.util.logging.Level.WARNING; import static org.briarproject.api.db.Metadata.REMOVE; -import static org.briarproject.api.sync.ValidationManager.Validity.INVALID; -import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN; -import static org.briarproject.api.sync.ValidationManager.Validity.VALID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.PENDING; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; +import static org.briarproject.api.sync.ValidationManager.State.VALID; import static org.briarproject.db.DatabaseConstants.DB_SETTINGS_NAMESPACE; import static org.briarproject.db.DatabaseConstants.DEVICE_ID_KEY; import static org.briarproject.db.DatabaseConstants.DEVICE_SETTINGS_NAMESPACE; @@ -65,8 +67,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry; */ abstract class JdbcDatabase implements Database<Connection> { - private static final int SCHEMA_VERSION = 24; - private static final int MIN_SCHEMA_VERSION = 24; + private static final int SCHEMA_VERSION = 25; + private static final int MIN_SCHEMA_VERSION = 25; private static final String CREATE_SETTINGS = "CREATE TABLE settings" @@ -131,7 +133,7 @@ abstract class JdbcDatabase implements Database<Connection> { + " (messageId HASH NOT NULL," + " groupId HASH NOT NULL," + " timestamp BIGINT NOT NULL," - + " valid INT NOT NULL," + + " state INT NOT NULL," + " shared BOOLEAN NOT NULL," + " length INT NOT NULL," + " raw BLOB," // Null if message has been deleted @@ -150,6 +152,14 @@ abstract class JdbcDatabase implements Database<Connection> { + " REFERENCES messages (messageId)" + " ON DELETE CASCADE)"; + private static final String CREATE_MESSAGE_DEPENDENCIES = + "CREATE TABLE messageDependencies" + + " (messageId HASH NOT NULL," + + " dependencyId HASH NOT NULL," // Not a foreign key + + " FOREIGN KEY (messageId)" + + " REFERENCES messages (messageId)" + + " ON DELETE CASCADE)"; + private static final String CREATE_OFFERS = "CREATE TABLE offers" + " (messageId HASH NOT NULL," // Not a foreign key @@ -320,6 +330,7 @@ abstract class JdbcDatabase implements Database<Connection> { s.executeUpdate(insertTypeNames(CREATE_GROUP_VISIBILITIES)); s.executeUpdate(insertTypeNames(CREATE_MESSAGES)); s.executeUpdate(insertTypeNames(CREATE_MESSAGE_METADATA)); + s.executeUpdate(insertTypeNames(CREATE_MESSAGE_DEPENDENCIES)); s.executeUpdate(insertTypeNames(CREATE_OFFERS)); s.executeUpdate(insertTypeNames(CREATE_STATUSES)); s.executeUpdate(insertTypeNames(CREATE_TRANSPORTS)); @@ -520,18 +531,18 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void addMessage(Connection txn, Message m, Validity validity, + public void addMessage(Connection txn, Message m, State state, boolean shared) throws DbException { PreparedStatement ps = null; try { String sql = "INSERT INTO messages (messageId, groupId, timestamp," - + " valid, shared, length, raw)" + + " state, shared, length, raw)" + " VALUES (?, ?, ?, ?, ?, ?, ?)"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getId().getBytes()); ps.setBytes(2, m.getGroupId().getBytes()); ps.setLong(3, m.getTimestamp()); - ps.setInt(4, validity.getValue()); + ps.setInt(4, state.getValue()); ps.setBoolean(5, shared); byte[] raw = m.getRaw(); ps.setInt(6, raw.length); @@ -596,6 +607,25 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public void addMessageDependency(Connection txn, MessageId dependentId, + MessageId dependencyId) throws DbException { + PreparedStatement ps = null; + try { + String sql = + "INSERT INTO messageDependencies (messageId, dependencyId)" + + " VALUES (?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, dependentId.getBytes()); + ps.setBytes(2, dependencyId.getBytes()); + int affected = ps.executeUpdate(); + if (affected != 1) throw new DbStateException(); + ps.close(); + } catch (SQLException e) { + tryToClose(ps); + throw new DbException(e); + } + } + public void addTransport(Connection txn, TransportId t, int maxLatency) throws DbException { PreparedStatement ps = null; @@ -1135,10 +1165,33 @@ abstract class JdbcDatabase implements Database<Connection> { } } + private Collection<MessageId> getMessageIds(Connection txn, GroupId g, + State state) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT messageId FROM messages" + + " WHERE state = ? AND groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, state.getValue()); + ps.setBytes(2, g.getBytes()); + rs = ps.executeQuery(); + List<MessageId> ids = new ArrayList<MessageId>(); + while (rs.next()) ids.add(new MessageId(rs.getBytes(1))); + rs.close(); + ps.close(); + return Collections.unmodifiableList(ids); + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public Collection<MessageId> getMessageIds(Connection txn, GroupId g, Metadata query) throws DbException { - // If there are no query terms, return all messages - if (query.isEmpty()) return getMessageIds(txn, g); + // If there are no query terms, return all delivered messages + if (query.isEmpty()) return getMessageIds(txn, g, DELIVERED); PreparedStatement ps = null; ResultSet rs = null; try { @@ -1148,12 +1201,14 @@ abstract class JdbcDatabase implements Database<Connection> { + " FROM messages AS m" + " JOIN messageMetadata AS md" + " ON m.messageId = md.messageId" - + " WHERE groupId = ? AND key = ? AND value = ?"; + + " WHERE state = ? AND groupId = ?" + + " AND key = ? AND value = ?"; for (Entry<String, byte[]> e : query.entrySet()) { ps = txn.prepareStatement(sql); - ps.setBytes(1, g.getBytes()); - ps.setString(2, e.getKey()); - ps.setBytes(3, e.getValue()); + ps.setInt(1, DELIVERED.getValue()); + ps.setBytes(2, g.getBytes()); + ps.setString(3, e.getKey()); + ps.setBytes(4, e.getValue()); rs = ps.executeQuery(); Set<MessageId> ids = new HashSet<MessageId>(); while (rs.next()) ids.add(new MessageId(rs.getBytes(1))); @@ -1182,10 +1237,11 @@ abstract class JdbcDatabase implements Database<Connection> { + " FROM messages AS m" + " JOIN messageMetadata AS md" + " ON m.messageId = md.messageId" - + " WHERE groupId = ?" + + " WHERE state = ? AND groupId = ?" + " ORDER BY m.messageId"; ps = txn.prepareStatement(sql); - ps.setBytes(1, g.getBytes()); + ps.setInt(1, DELIVERED.getValue()); + ps.setBytes(2, g.getBytes()); rs = ps.executeQuery(); Map<MessageId, Metadata> all = new HashMap<MessageId, Metadata>(); Metadata metadata = null; @@ -1223,23 +1279,38 @@ abstract class JdbcDatabase implements Database<Connection> { public Metadata getGroupMetadata(Connection txn, GroupId g) throws DbException { - return getMetadata(txn, g.getBytes(), "groupMetadata", "groupId"); + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT key, value FROM groupMetadata" + + " WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + rs = ps.executeQuery(); + Metadata metadata = new Metadata(); + while (rs.next()) metadata.put(rs.getString(1), rs.getBytes(2)); + rs.close(); + ps.close(); + return metadata; + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } } public Metadata getMessageMetadata(Connection txn, MessageId m) throws DbException { - return getMetadata(txn, m.getBytes(), "messageMetadata", "messageId"); - } - - private Metadata getMetadata(Connection txn, byte[] id, String tableName, - String columnName) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT key, value FROM " + tableName - + " WHERE " + columnName + " = ?"; + String sql = "SELECT key, value FROM messageMetadata AS md" + + " JOIN messages AS m" + + " ON m.messageId = md.messageId" + + " WHERE m.state = ? AND md.messageId = ?"; ps = txn.prepareStatement(sql); - ps.setBytes(1, id); + ps.setInt(1, DELIVERED.getValue()); + ps.setBytes(2, m.getBytes()); rs = ps.executeQuery(); Metadata metadata = new Metadata(); while (rs.next()) metadata.put(rs.getString(1), rs.getBytes(2)); @@ -1312,6 +1383,91 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public Map<MessageId, State> getMessageDependencies(Connection txn, + MessageId m) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT d.dependencyId, m.state, m.groupId" + + " FROM messageDependencies AS d" + + " LEFT OUTER JOIN messages AS m" + + " ON d.dependencyId = m.messageId" + + " WHERE d.messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + Map<MessageId, State> dependencies = new HashMap<MessageId, State>(); + while (rs.next()) { + MessageId messageId = new MessageId(rs.getBytes(1)); + State state = State.fromValue(rs.getInt(2)); + if (state != UNKNOWN) { + // set dependency invalid if it is in a different group + if (!hasGroupId(txn, m, rs.getBytes(3))) state = INVALID; + } + dependencies.put(messageId, state); + } + rs.close(); + ps.close(); + return Collections.unmodifiableMap(dependencies); + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + private boolean hasGroupId(Connection txn, MessageId m, byte[] g) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT NULL FROM messages" + + " WHERE messageId = ? AND groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + ps.setBytes(2, g); + rs = ps.executeQuery(); + boolean same = rs.next(); + if (rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + return same; + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + public Map<MessageId, State> getMessageDependents(Connection txn, + MessageId m) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT d.messageId, m.state" + + " FROM messageDependencies AS d" + + " LEFT OUTER JOIN messages AS m" + + " ON d.messageId = m.messageId" + + " WHERE dependencyId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + Map<MessageId, State> dependents = new HashMap<MessageId, State>(); + while (rs.next()) { + MessageId messageId = new MessageId(rs.getBytes(1)); + State state = State.fromValue(rs.getInt(2)); + dependents.put(messageId, state); + } + rs.close(); + ps.close(); + return Collections.unmodifiableMap(dependents); + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public Collection<MessageId> getMessagesToAck(Connection txn, ContactId c, int maxMessages) throws DbException { PreparedStatement ps = null; @@ -1346,13 +1502,13 @@ abstract class JdbcDatabase implements Database<Connection> { + " JOIN statuses AS s" + " ON m.messageId = s.messageId" + " WHERE contactId = ?" - + " AND valid = ? AND shared = TRUE AND raw IS NOT NULL" + + " AND state = ? AND shared = TRUE AND raw IS NOT NULL" + " AND seen = FALSE AND requested = FALSE" + " AND expiry < ?" + " ORDER BY timestamp DESC LIMIT ?"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); - ps.setInt(2, VALID.getValue()); + ps.setInt(2, DELIVERED.getValue()); ps.setLong(3, now); ps.setInt(4, maxMessages); rs = ps.executeQuery(); @@ -1402,13 +1558,13 @@ abstract class JdbcDatabase implements Database<Connection> { + " JOIN statuses AS s" + " ON m.messageId = s.messageId" + " WHERE contactId = ?" - + " AND valid = ? AND shared = TRUE AND raw IS NOT NULL" + + " AND state = ? AND shared = TRUE AND raw IS NOT NULL" + " AND seen = FALSE" + " AND expiry < ?" + " ORDER BY timestamp DESC"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); - ps.setInt(2, VALID.getValue()); + ps.setInt(2, DELIVERED.getValue()); ps.setLong(3, now); rs = ps.executeQuery(); List<MessageId> ids = new ArrayList<MessageId>(); @@ -1431,14 +1587,29 @@ abstract class JdbcDatabase implements Database<Connection> { public Collection<MessageId> getMessagesToValidate(Connection txn, ClientId c) throws DbException { + return getMessagesInState(txn, c, UNKNOWN); + } + + public Collection<MessageId> getMessagesToDeliver(Connection txn, + ClientId c) throws DbException { + return getMessagesInState(txn, c, VALID); + } + + public Collection<MessageId> getPendingMessages(Connection txn, + ClientId c) throws DbException { + return getMessagesInState(txn, c, PENDING); + } + + private Collection<MessageId> getMessagesInState(Connection txn, ClientId c, + State state) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { String sql = "SELECT messageId FROM messages AS m" + " JOIN groups AS g ON m.groupId = g.groupId" - + " WHERE valid = ? AND clientId = ? AND raw IS NOT NULL"; + + " WHERE state = ? AND clientId = ? AND raw IS NOT NULL"; ps = txn.prepareStatement(sql); - ps.setInt(1, UNKNOWN.getValue()); + ps.setInt(1, state.getValue()); ps.setBytes(2, c.getBytes()); rs = ps.executeQuery(); List<MessageId> ids = new ArrayList<MessageId>(); @@ -1485,13 +1656,13 @@ abstract class JdbcDatabase implements Database<Connection> { + " JOIN statuses AS s" + " ON m.messageId = s.messageId" + " WHERE contactId = ?" - + " AND valid = ? AND shared = TRUE AND raw IS NOT NULL" + + " AND state = ? AND shared = TRUE AND raw IS NOT NULL" + " AND seen = FALSE AND requested = TRUE" + " AND expiry < ?" + " ORDER BY timestamp DESC"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); - ps.setInt(2, VALID.getValue()); + ps.setInt(2, DELIVERED.getValue()); ps.setLong(3, now); rs = ps.executeQuery(); List<MessageId> ids = new ArrayList<MessageId>(); @@ -2081,13 +2252,13 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void setMessageValid(Connection txn, MessageId m, boolean valid) + public void setMessageState(Connection txn, MessageId m, State state) throws DbException { PreparedStatement ps = null; try { - String sql = "UPDATE messages SET valid = ? WHERE messageId = ?"; + String sql = "UPDATE messages SET state = ? WHERE messageId = ?"; ps = txn.prepareStatement(sql); - ps.setInt(1, valid ? VALID.getValue() : INVALID.getValue()); + ps.setInt(1, state.getValue()); ps.setBytes(2, m.getBytes()); int affected = ps.executeUpdate(); if (affected < 0 || affected > 1) throw new DbStateException(); diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java index 1e85803a22..487c31d52c 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 ff6f7a918c..6a4db39e81 100644 --- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java +++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java @@ -29,7 +29,7 @@ import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageSharedEvent; import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToRequestEvent; -import org.briarproject.api.event.MessageValidatedEvent; +import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.event.SettingsUpdatedEvent; @@ -53,6 +53,7 @@ import org.jmock.Expectations; import org.jmock.Mockery; import org.junit.Test; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -60,8 +61,9 @@ import java.util.Map; import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; -import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN; -import static org.briarproject.api.sync.ValidationManager.Validity.VALID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; +import static org.briarproject.api.sync.ValidationManager.State.VALID; import static org.briarproject.api.transport.TransportConstants.REORDERING_WINDOW_SIZE; import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES; import static org.junit.Assert.assertEquals; @@ -264,7 +266,7 @@ public class DatabaseComponentImplTest extends BriarTestCase { will(returnValue(true)); oneOf(database).containsMessage(txn, messageId); will(returnValue(false)); - oneOf(database).addMessage(txn, message, VALID, true); + oneOf(database).addMessage(txn, message, DELIVERED, true); oneOf(database).mergeMessageMetadata(txn, messageId, metadata); oneOf(database).getVisibility(txn, groupId); will(returnValue(Collections.singletonList(contactId))); @@ -274,7 +276,7 @@ public class DatabaseComponentImplTest extends BriarTestCase { oneOf(database).commitTransaction(txn); // The message was added, so the listeners should be called oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class))); - oneOf(eventBus).broadcast(with(any(MessageValidatedEvent.class))); + oneOf(eventBus).broadcast(with(any(MessageStateChangedEvent.class))); oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class))); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, @@ -675,11 +677,11 @@ public class DatabaseComponentImplTest extends BriarTestCase { final EventBus eventBus = context.mock(EventBus.class); context.checking(new Expectations() {{ // Check whether the message is in the DB (which it's not) - exactly(8).of(database).startTransaction(); + exactly(10).of(database).startTransaction(); will(returnValue(txn)); - exactly(8).of(database).containsMessage(txn, messageId); + exactly(10).of(database).containsMessage(txn, messageId); will(returnValue(false)); - exactly(8).of(database).abortTransaction(txn); + exactly(10).of(database).abortTransaction(txn); // This is needed for getMessageStatus() to proceed exactly(1).of(database).containsContact(txn, contactId); will(returnValue(true)); @@ -759,7 +761,27 @@ public class DatabaseComponentImplTest extends BriarTestCase { transaction = db.startTransaction(false); try { - db.setMessageValid(transaction, message, clientId, true); + db.setMessageState(transaction, message, clientId, VALID); + fail(); + } catch (NoSuchMessageException expected) { + // Expected + } finally { + db.endTransaction(transaction); + } + + transaction = db.startTransaction(true); + try { + db.getMessageDependencies(transaction, messageId); + fail(); + } catch (NoSuchMessageException expected) { + // Expected + } finally { + db.endTransaction(transaction); + } + + transaction = db.startTransaction(true); + try { + db.getMessageDependents(transaction, messageId); fail(); } catch (NoSuchMessageException expected) { // Expected @@ -1595,4 +1617,79 @@ public class DatabaseComponentImplTest extends BriarTestCase { context.assertIsSatisfied(); } + + @Test + @SuppressWarnings("unchecked") + public void testMessageDependencies() throws Exception { + final int shutdownHandle = 12345; + Mockery context = new Mockery(); + final Database<Object> database = context.mock(Database.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final EventBus eventBus = context.mock(EventBus.class); + final MessageId messageId2 = new MessageId(TestUtils.getRandomId()); + context.checking(new Expectations() {{ + // open() + oneOf(database).open(); + will(returnValue(false)); + oneOf(shutdown).addShutdownHook(with(any(Runnable.class))); + will(returnValue(shutdownHandle)); + // startTransaction() + oneOf(database).startTransaction(); + will(returnValue(txn)); + // addLocalMessage() + oneOf(database).containsGroup(txn, groupId); + will(returnValue(true)); + oneOf(database).containsMessage(txn, messageId); + will(returnValue(false)); + oneOf(database).addMessage(txn, message, DELIVERED, true); + oneOf(database).getVisibility(txn, groupId); + will(returnValue(Collections.singletonList(contactId))); + oneOf(database).mergeMessageMetadata(txn, messageId, metadata); + oneOf(database).removeOfferedMessage(txn, contactId, messageId); + will(returnValue(false)); + oneOf(database).addStatus(txn, contactId, messageId, false, false); + // addMessageDependencies() + oneOf(database).containsMessage(txn, messageId); + will(returnValue(true)); + oneOf(database).addMessageDependency(txn, messageId, messageId1); + oneOf(database).addMessageDependency(txn, messageId, messageId2); + // getMessageDependencies() + oneOf(database).containsMessage(txn, messageId); + will(returnValue(true)); + oneOf(database).getMessageDependencies(txn, messageId); + // getMessageDependents() + oneOf(database).containsMessage(txn, messageId); + will(returnValue(true)); + oneOf(database).getMessageDependents(txn, messageId); + // broadcast for message added event + oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class))); + oneOf(eventBus).broadcast(with(any(MessageStateChangedEvent.class))); + oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class))); + // endTransaction() + oneOf(database).commitTransaction(txn); + // close() + oneOf(shutdown).removeShutdownHook(shutdownHandle); + oneOf(database).close(); + }}); + DatabaseComponent db = createDatabaseComponent(database, eventBus, + shutdown); + + assertFalse(db.open()); + Transaction transaction = db.startTransaction(false); + try { + db.addLocalMessage(transaction, message, clientId, metadata, true); + Collection<MessageId> dependencies = new ArrayList<>(2); + dependencies.add(messageId1); + dependencies.add(messageId2); + db.addMessageDependencies(transaction, message, dependencies); + db.getMessageDependencies(transaction, messageId); + db.getMessageDependents(transaction, messageId); + transaction.setComplete(); + } finally { + db.endTransaction(transaction); + } + db.close(); + + context.assertIsSatisfied(); + } } diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java index d967b81597..9d5b1f0b91 100644 --- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java +++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java @@ -19,6 +19,7 @@ import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageStatus; +import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.transport.IncomingKeys; import org.briarproject.api.transport.OutgoingKeys; import org.briarproject.api.transport.TransportKeys; @@ -46,8 +47,11 @@ import static org.briarproject.api.db.Metadata.REMOVE; import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_LENGTH; -import static org.briarproject.api.sync.ValidationManager.Validity.UNKNOWN; -import static org.briarproject.api.sync.ValidationManager.Validity.VALID; +import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.api.sync.ValidationManager.State.PENDING; +import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN; +import static org.briarproject.api.sync.ValidationManager.State.VALID; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -114,7 +118,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addGroup(txn, group); assertTrue(db.containsGroup(txn, groupId)); assertFalse(db.containsMessage(txn, messageId)); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); assertTrue(db.containsMessage(txn, messageId)); db.commitTransaction(txn); db.close(); @@ -152,7 +156,7 @@ public class H2DatabaseTest extends BriarTestCase { // Add a group and a message db.addGroup(txn, group); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); // Removing the group should remove the message assertTrue(db.containsMessage(txn, messageId)); @@ -174,7 +178,7 @@ public class H2DatabaseTest extends BriarTestCase { true)); db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); // The message has no status yet, so it should not be sendable Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, @@ -202,7 +206,7 @@ public class H2DatabaseTest extends BriarTestCase { } @Test - public void testSendableMessagesMustBeValid() throws Exception { + public void testSendableMessagesMustBeDelivered() throws Exception { Database<Connection> db = open(false); Connection txn = db.startTransaction(); @@ -222,15 +226,31 @@ public class H2DatabaseTest extends BriarTestCase { ids = db.getMessagesToOffer(txn, contactId, 100); assertTrue(ids.isEmpty()); - // Marking the message valid should make it sendable - db.setMessageValid(txn, messageId, true); + // Marking the message delivered should make it sendable + db.setMessageState(txn, messageId, DELIVERED); ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); assertEquals(Collections.singletonList(messageId), ids); ids = db.getMessagesToOffer(txn, contactId, 100); assertEquals(Collections.singletonList(messageId), ids); // Marking the message invalid should make it unsendable - db.setMessageValid(txn, messageId, false); + db.setMessageState(txn, messageId, INVALID); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // Marking the message valid should make it unsendable + // TODO do we maybe want to already send valid messages? If we do, we need also to call db.setMessageShared() earlier. + db.setMessageState(txn, messageId, VALID); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // Marking the message pending should make it unsendable + // TODO do we maybe want to already send pending messages? If we do, we need also to call db.setMessageShared() earlier. + db.setMessageState(txn, messageId, PENDING); ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); assertTrue(ids.isEmpty()); ids = db.getMessagesToOffer(txn, contactId, 100); @@ -251,7 +271,7 @@ public class H2DatabaseTest extends BriarTestCase { true)); db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addMessage(txn, message, VALID, false); + db.addMessage(txn, message, DELIVERED, false); db.addStatus(txn, contactId, messageId, false, false); // The message is not shared, so it should not be sendable @@ -290,7 +310,7 @@ public class H2DatabaseTest extends BriarTestCase { true)); db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, false); // The message is sendable, but too large to send @@ -321,10 +341,10 @@ public class H2DatabaseTest extends BriarTestCase { // Add some messages to ack MessageId messageId1 = new MessageId(TestUtils.getRandomId()); Message message1 = new Message(messageId1, groupId, timestamp, raw); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, true); db.raiseAckFlag(txn, contactId, messageId); - db.addMessage(txn, message1, VALID, true); + db.addMessage(txn, message1, DELIVERED, true); db.addStatus(txn, contactId, messageId1, false, true); db.raiseAckFlag(txn, contactId, messageId1); @@ -354,7 +374,7 @@ public class H2DatabaseTest extends BriarTestCase { true)); db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, false); // Retrieve the message from the database and mark it as sent @@ -396,7 +416,7 @@ public class H2DatabaseTest extends BriarTestCase { // Storing a message should reduce the free space Connection txn = db.startTransaction(); db.addGroup(txn, group); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.commitTransaction(txn); assertTrue(db.getFreeSpace() < free); @@ -555,7 +575,7 @@ public class H2DatabaseTest extends BriarTestCase { assertEquals(contactId, db.addContact(txn, author, localAuthorId, true)); db.addGroup(txn, group); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, false); // The group is not visible @@ -866,7 +886,7 @@ public class H2DatabaseTest extends BriarTestCase { // Add a group and a message db.addGroup(txn, group); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); // Attach some metadata to the message Metadata metadata = new Metadata(); @@ -930,6 +950,67 @@ public class H2DatabaseTest extends BriarTestCase { db.close(); } + @Test + public void testMessageMetadataOnlyForDeliveredMessages() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + + // Attach some metadata to the message + Metadata metadata = new Metadata(); + metadata.put("foo", new byte[]{'b', 'a', 'r'}); + metadata.put("baz", new byte[]{'b', 'a', 'm'}); + db.mergeMessageMetadata(txn, messageId, metadata); + + // Retrieve the metadata for the message + Metadata retrieved = db.getMessageMetadata(txn, messageId); + assertEquals(2, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + Map<MessageId, Metadata> map = db.getMessageMetadata(txn, groupId); + assertEquals(1, map.size()); + assertTrue(map.get(messageId).containsKey("foo")); + assertArrayEquals(metadata.get("foo"), map.get(messageId).get("foo")); + assertTrue(map.get(messageId).containsKey("baz")); + assertArrayEquals(metadata.get("baz"), map.get(messageId).get("baz")); + + // No metadata for unknown messages + db.setMessageState(txn, messageId, UNKNOWN); + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + map = db.getMessageMetadata(txn, groupId); + assertTrue(map.isEmpty()); + + // No metadata for invalid messages + db.setMessageState(txn, messageId, INVALID); + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + map = db.getMessageMetadata(txn, groupId); + assertTrue(map.isEmpty()); + + // No metadata for valid messages + db.setMessageState(txn, messageId, VALID); + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + map = db.getMessageMetadata(txn, groupId); + assertTrue(map.isEmpty()); + + // No metadata for pending messages + db.setMessageState(txn, messageId, PENDING); + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + map = db.getMessageMetadata(txn, groupId); + assertTrue(map.isEmpty()); + + db.commitTransaction(txn); + db.close(); + } + @Test public void testMetadataQueries() throws Exception { MessageId messageId1 = new MessageId(TestUtils.getRandomId()); @@ -940,8 +1021,8 @@ public class H2DatabaseTest extends BriarTestCase { // Add a group and two messages db.addGroup(txn, group); - db.addMessage(txn, message, VALID, true); - db.addMessage(txn, message1, VALID, true); + db.addMessage(txn, message, DELIVERED, true); + db.addMessage(txn, message1, DELIVERED, true); // Attach some metadata to the messages Metadata metadata = new Metadata(); @@ -1034,6 +1115,257 @@ public class H2DatabaseTest extends BriarTestCase { db.close(); } + @Test + public void testMetadataQueriesOnlyForDeliveredMessages() throws Exception { + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new Message(messageId1, groupId, timestamp, raw); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and two messages + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + db.addMessage(txn, message1, DELIVERED, true); + + // Attach some metadata to the messages + Metadata metadata = new Metadata(); + metadata.put("foo", new byte[]{'b', 'a', 'r'}); + metadata.put("baz", new byte[]{'b', 'a', 'm'}); + db.mergeMessageMetadata(txn, messageId, metadata); + Metadata metadata1 = new Metadata(); + metadata1.put("foo", new byte[]{'b', 'a', 'r'}); + db.mergeMessageMetadata(txn, messageId1, metadata1); + + for (int i = 1; i <= 2; i++) { + Metadata query; + if (i == 1) { + // Query the metadata with an empty query + query = new Metadata(); + } else { + // Query for foo + query = new Metadata(); + query.put("foo", metadata.get("foo")); + } + + db.setMessageState(txn, messageId, DELIVERED); + db.setMessageState(txn, messageId1, DELIVERED); + Map<MessageId, Metadata> all = + db.getMessageMetadata(txn, groupId, query); + assertEquals(2, all.size()); + assertEquals(2, all.get(messageId).size()); + assertEquals(1, all.get(messageId1).size()); + + // No metadata for unknown messages + db.setMessageState(txn, messageId, UNKNOWN); + db.setMessageState(txn, messageId1, UNKNOWN); + all = db.getMessageMetadata(txn, groupId, query); + assertTrue(all.isEmpty()); + + // No metadata for invalid messages + db.setMessageState(txn, messageId, INVALID); + db.setMessageState(txn, messageId1, INVALID); + all = db.getMessageMetadata(txn, groupId, query); + assertTrue(all.isEmpty()); + + // No metadata for valid messages + db.setMessageState(txn, messageId, VALID); + db.setMessageState(txn, messageId1, VALID); + all = db.getMessageMetadata(txn, groupId, query); + assertTrue(all.isEmpty()); + + // No metadata for pending messages + db.setMessageState(txn, messageId, PENDING); + db.setMessageState(txn, messageId1, PENDING); + all = db.getMessageMetadata(txn, groupId, query); + assertTrue(all.isEmpty()); + } + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testMessageDependencies() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, VALID, true); + + // Create more messages + MessageId mId1 = new MessageId(TestUtils.getRandomId()); + MessageId mId2 = new MessageId(TestUtils.getRandomId()); + MessageId dId1 = new MessageId(TestUtils.getRandomId()); + MessageId dId2 = new MessageId(TestUtils.getRandomId()); + Message m1 = new Message(mId1, groupId, timestamp, raw); + Message m2 = new Message(mId2, groupId, timestamp, raw); + + // Add new messages + db.addMessage(txn, m1, VALID, true); + db.addMessage(txn, m2, INVALID, true); + + // Add dependencies + db.addMessageDependency(txn, messageId, mId1); + db.addMessageDependency(txn, messageId, mId2); + db.addMessageDependency(txn, mId1, dId1); + db.addMessageDependency(txn, mId2, dId2); + + Map<MessageId, State> dependencies; + + // Retrieve dependencies for root + dependencies = db.getMessageDependencies(txn, messageId); + assertEquals(2, dependencies.size()); + assertEquals(VALID, dependencies.get(mId1)); + assertEquals(INVALID, dependencies.get(mId2)); + + // Retrieve dependencies for m1 + dependencies = db.getMessageDependencies(txn, mId1); + assertEquals(1, dependencies.size()); + assertEquals(UNKNOWN, dependencies.get(dId1)); + + // Retrieve dependencies for m2 + dependencies = db.getMessageDependencies(txn, mId2); + assertEquals(1, dependencies.size()); + assertEquals(UNKNOWN, dependencies.get(dId2)); + + // Make sure d's have no dependencies + dependencies = db.getMessageDependencies(txn, dId1); + assertTrue(dependencies.isEmpty()); + dependencies = db.getMessageDependencies(txn, dId2); + assertTrue(dependencies.isEmpty()); + + Map<MessageId, State> dependents; + + // Root message does not have dependents + dependents = db.getMessageDependents(txn, messageId); + assertTrue(dependents.isEmpty()); + + // The root message depends on both m's + dependents = db.getMessageDependents(txn, mId1); + assertEquals(1, dependents.size()); + assertEquals(VALID, dependents.get(messageId)); + dependents = db.getMessageDependents(txn, mId2); + assertEquals(1, dependents.size()); + assertEquals(VALID, dependents.get(messageId)); + + // Both m's depend on the d's + dependents = db.getMessageDependents(txn, dId1); + assertEquals(1, dependents.size()); + assertEquals(VALID, dependents.get(mId1)); + dependents = db.getMessageDependents(txn, dId2); + assertEquals(1, dependents.size()); + assertEquals(INVALID, dependents.get(mId2)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testMessageDependenciesInSameGroup() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + + // Add a second group + GroupId groupId1 = new GroupId(TestUtils.getRandomId()); + Group group1 = new Group(groupId1, group.getClientId(), + TestUtils.getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH)); + db.addGroup(txn, group1); + + // Add a message to the second group + MessageId mId1 = new MessageId(TestUtils.getRandomId()); + Message m1 = new Message(mId1, groupId1, timestamp, raw); + db.addMessage(txn, m1, DELIVERED, true); + + // Create a fake dependency as well + MessageId mId2 = new MessageId(TestUtils.getRandomId()); + + // Create and add a real and proper dependency + MessageId mId3 = new MessageId(TestUtils.getRandomId()); + Message m3 = new Message(mId3, groupId, timestamp, raw); + db.addMessage(txn, m3, PENDING, true); + + // Add dependencies + db.addMessageDependency(txn, messageId, mId1); + db.addMessageDependency(txn, messageId, mId2); + db.addMessageDependency(txn, messageId, mId3); + + // Return invalid dependencies for delivered message m1 + Map<MessageId, State> dependencies; + dependencies = db.getMessageDependencies(txn, messageId); + assertEquals(INVALID, dependencies.get(mId1)); + assertEquals(UNKNOWN, dependencies.get(mId2)); + assertEquals(PENDING, dependencies.get(mId3)); + + // Return invalid dependencies for valid message m1 + db.setMessageState(txn, mId1, VALID); + dependencies = db.getMessageDependencies(txn, messageId); + assertEquals(INVALID, dependencies.get(mId1)); + assertEquals(UNKNOWN, dependencies.get(mId2)); + assertEquals(PENDING, dependencies.get(mId3)); + + // Return invalid dependencies for pending message m1 + db.setMessageState(txn, mId1, PENDING); + dependencies = db.getMessageDependencies(txn, messageId); + assertEquals(INVALID, dependencies.get(mId1)); + assertEquals(UNKNOWN, dependencies.get(mId2)); + assertEquals(PENDING, dependencies.get(mId3)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessagesForValidationAndDelivery() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, VALID, true); + + // Create more messages + MessageId mId1 = new MessageId(TestUtils.getRandomId()); + MessageId mId2 = new MessageId(TestUtils.getRandomId()); + MessageId mId3 = new MessageId(TestUtils.getRandomId()); + MessageId mId4 = new MessageId(TestUtils.getRandomId()); + Message m1 = new Message(mId1, groupId, timestamp, raw); + Message m2 = new Message(mId2, groupId, timestamp, raw); + Message m3 = new Message(mId3, groupId, timestamp, raw); + Message m4 = new Message(mId4, groupId, timestamp, raw); + + // Add new messages with different states + db.addMessage(txn, m1, UNKNOWN, true); + db.addMessage(txn, m2, INVALID, true); + db.addMessage(txn, m3, PENDING, true); + db.addMessage(txn, m4, DELIVERED, true); + + Collection<MessageId> result; + + // Retrieve messages to be validated + result = db.getMessagesToValidate(txn, group.getClientId()); + assertEquals(1, result.size()); + assertTrue(result.contains(mId1)); + + // Retrieve messages to be delivered + result = db.getMessagesToDeliver(txn, group.getClientId()); + assertEquals(1, result.size()); + assertTrue(result.contains(messageId)); + + // Retrieve pending messages + result = db.getPendingMessages(txn, group.getClientId()); + assertEquals(1, result.size()); + assertTrue(result.contains(mId3)); + + db.commitTransaction(txn); + db.close(); + } + @Test public void testGetMessageStatus() throws Exception { Database<Connection> db = open(false); @@ -1049,7 +1381,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addVisibility(txn, contactId, groupId); // Add a message to the group - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, false); // The message should not be sent or seen @@ -1176,7 +1508,7 @@ public class H2DatabaseTest extends BriarTestCase { true)); db.addGroup(txn, group); db.addVisibility(txn, contactId, groupId); - db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message, DELIVERED, true); db.addStatus(txn, contactId, messageId, false, false); // The message should be visible to the contact diff --git a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java index 1aea2e30b0..d55b575c2d 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); -- GitLab