From adee3e121cc7a94e234eba25239f3503d14449f0 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Wed, 27 Jul 2011 20:27:43 +0100 Subject: [PATCH] Added support for registering listeners with the database that are called when new messages are available, and a new method hasSendableMessages(ContactId) that listeners can call to see whether it's worth trying to create a batch. --- .../sf/briar/api/db/DatabaseComponent.java | 9 ++ api/net/sf/briar/api/db/MessageListener.java | 10 ++ components/net/sf/briar/db/Database.java | 21 ++- .../net/sf/briar/db/DatabaseCleaner.java | 2 +- .../sf/briar/db/DatabaseComponentImpl.java | 36 ++++- components/net/sf/briar/db/JdbcDatabase.java | 36 +++++ .../db/ReadWriteLockDatabaseComponent.java | 125 ++++++++++++----- .../db/SynchronizedDatabaseComponent.java | 131 +++++++++++------- .../sf/briar/db/DatabaseComponentTest.java | 78 ++++++++++- test/net/sf/briar/db/H2DatabaseTest.java | 15 +- 10 files changed, 364 insertions(+), 99 deletions(-) create mode 100644 api/net/sf/briar/api/db/MessageListener.java diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java index 6cd47b8ab..5a1352e73 100644 --- a/api/net/sf/briar/api/db/DatabaseComponent.java +++ b/api/net/sf/briar/api/db/DatabaseComponent.java @@ -48,6 +48,12 @@ public interface DatabaseComponent { /** Waits for any open transactions to finish and closes the database. */ void close() throws DbException; + /** Adds a listener to be notified when new messages are available. */ + void addListener(MessageListener m); + + /** Removes a listener. */ + void removeListener(MessageListener m); + /** * Adds a new contact to the database with the given transport details and * returns an ID for the contact. @@ -112,6 +118,9 @@ public interface DatabaseComponent { /** Returns the contacts to which the given group is visible. */ Collection getVisibility(GroupId g) throws DbException; + /** Returns true if any messages are sendable to the given contact. */ + boolean hasSendableMessages(ContactId c) throws DbException; + /** Processes an acknowledgement from the given contact. */ void receiveAck(ContactId c, Ack a) throws DbException; diff --git a/api/net/sf/briar/api/db/MessageListener.java b/api/net/sf/briar/api/db/MessageListener.java new file mode 100644 index 000000000..667d79207 --- /dev/null +++ b/api/net/sf/briar/api/db/MessageListener.java @@ -0,0 +1,10 @@ +package net.sf.briar.api.db; + +/** + * An interface for receiving notifications when the database may have new + * messages available. + */ +public interface MessageListener { + + void messagesAdded(); +} diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java index d71a8547b..35e083bb3 100644 --- a/components/net/sf/briar/db/Database.java +++ b/components/net/sf/briar/db/Database.java @@ -109,28 +109,28 @@ interface Database { void addSubscription(T txn, Group g) throws DbException; /** - * Returns true iff the database contains the given contact. + * Returns true if the database contains the given contact. *

* Locking: contacts read. */ boolean containsContact(T txn, ContactId c) throws DbException; /** - * Returns true iff the database contains the given message. + * Returns true if the database contains the given message. *

* Locking: messages read. */ boolean containsMessage(T txn, MessageId m) throws DbException; /** - * Returns true iff the user is subscribed to the given group. + * Returns true if the user is subscribed to the given group. *

* Locking: subscriptions read. */ boolean containsSubscription(T txn, GroupId g) throws DbException; /** - * Returns true iff the user is subscribed to the given group and the + * Returns true if the user is subscribed to the given group and the * group is visible to the given contact. *

* Locking: contacts read, subscriptions read. @@ -189,7 +189,8 @@ interface Database { * if the message is not present in the database or is not sendable to the * given contact. *

- * Locking: contacts read, messages read, messageStatuses read. + * Locking: contacts read, messages read, messageStatuses read, + * subscriptions read. */ byte[] getMessageIfSendable(T txn, ContactId c, MessageId m) throws DbException; @@ -246,7 +247,8 @@ interface Database { * Returns the IDs of some messages that are eligible to be sent to the * given contact, with a total size less than or equal to the given size. *

- * Locking: contacts read, messages read, messageStatuses read. + * Locking: contacts read, messages read, messageStatuses read, + * subscriptions read. */ Collection getSendableMessages(T txn, ContactId c, int size) throws DbException; @@ -293,6 +295,13 @@ interface Database { Collection getVisibleSubscriptions(T txn, ContactId c) throws DbException; + /** + * Returns true if any messages are sendable to the given contact. + *

+ * Locking: contacts read, messages read, messageStatuses read. + */ + boolean hasSendableMessages(T txn, ContactId c) throws DbException; + /** * Removes an outstanding batch that has been acknowledged. Any messages in * the batch that are still considered outstanding (Status.SENT) with diff --git a/components/net/sf/briar/db/DatabaseCleaner.java b/components/net/sf/briar/db/DatabaseCleaner.java index 19615e68c..635bc047f 100644 --- a/components/net/sf/briar/db/DatabaseCleaner.java +++ b/components/net/sf/briar/db/DatabaseCleaner.java @@ -25,7 +25,7 @@ interface DatabaseCleaner { void checkFreeSpaceAndClean() throws DbException; /** - * Returns true iff the amount of free storage space available to the + * Returns true if the amount of free storage space available to the * database should be checked. */ boolean shouldCheckFreeSpace(); diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java index 3d98dcc3a..a73c08bed 100644 --- a/components/net/sf/briar/db/DatabaseComponentImpl.java +++ b/components/net/sf/briar/db/DatabaseComponentImpl.java @@ -1,5 +1,8 @@ package net.sf.briar.db; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -7,6 +10,7 @@ import net.sf.briar.api.ContactId; import net.sf.briar.api.Rating; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.MessageListener; import net.sf.briar.api.db.Status; import net.sf.briar.api.protocol.AuthorId; import net.sf.briar.api.protocol.Message; @@ -25,6 +29,8 @@ DatabaseCleaner.Callback { protected final Database db; protected final DatabaseCleaner cleaner; + private final List listeners = + new ArrayList(); // Locking: self private final Object spaceLock = new Object(); private final Object writeLock = new Object(); private long bytesStoredSinceLastCheck = 0L; // Locking: spaceLock @@ -41,6 +47,18 @@ DatabaseCleaner.Callback { cleaner.startCleaning(); } + public void addListener(MessageListener m) { + synchronized(listeners) { + listeners.add(m); + } + } + + public void removeListener(MessageListener m) { + synchronized(listeners) { + listeners.remove(m); + } + } + /** * Removes the oldest messages from the database, with a total size less * than or equal to the given size. @@ -61,6 +79,18 @@ DatabaseCleaner.Callback { return sendability; } + /** Notifies all MessageListeners that new messages may be available. */ + protected void callMessageListeners() { + synchronized(listeners) { + if(!listeners.isEmpty()) { + // Shuffle the listeners so we don't always send new messages + // to contacts in the same order + Collections.shuffle(listeners); + for(MessageListener m : listeners) m.messagesAdded(); + } + } + } + public void checkFreeSpaceAndClean() throws DbException { long freeSpace = db.getFreeSpace(); while(freeSpace < MIN_FREE_SPACE) { @@ -85,7 +115,7 @@ DatabaseCleaner.Callback { } /** - * Returns true iff the database contains the given contact. + * Returns true if the database contains the given contact. *

* Locking: contacts read. */ @@ -121,7 +151,7 @@ DatabaseCleaner.Callback { if(bytesStoredSinceLastCheck > MAX_BYTES_BETWEEN_SPACE_CHECKS) { if(LOG.isLoggable(Level.FINE)) LOG.fine(bytesStoredSinceLastCheck - + " bytes stored since last check"); + + " bytes stored since last check"); bytesStoredSinceLastCheck = 0L; timeOfLastCheck = now; return true; @@ -234,7 +264,7 @@ DatabaseCleaner.Callback { } if(LOG.isLoggable(Level.FINE)) LOG.fine(direct + " messages affected directly, " - + indirect + " indirectly"); + + indirect + " indirectly"); } /** diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java index bd43de357..c39df44f6 100644 --- a/components/net/sf/briar/db/JdbcDatabase.java +++ b/components/net/sf/briar/db/JdbcDatabase.java @@ -1226,6 +1226,42 @@ abstract class JdbcDatabase implements Database { } } + public boolean hasSendableMessages(Connection txn, ContactId c) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT messages.messageId FROM messages" + + " JOIN contactSubscriptions" + + " ON messages.groupId = contactSubscriptions.groupId" + + " JOIN visibilities" + + " ON messages.groupId = visibilities.groupId" + + " JOIN statuses ON messages.messageId = statuses.messageId" + + " WHERE contactSubscriptions.contactId = ?" + + " AND visibilities.contactId = ?" + + " AND statuses.contactId = ?" + + " AND status = ? AND sendability > ZERO()" + + " LIMIT ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.setInt(2, c.getInt()); + ps.setInt(3, c.getInt()); + ps.setShort(4, (short) Status.NEW.ordinal()); + ps.setInt(5, 1); + rs = ps.executeQuery(); + boolean found = rs.next(); + assert !rs.next(); + rs.close(); + ps.close(); + return found; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + public void removeAckedBatch(Connection txn, ContactId c, BatchId b) throws DbException { PreparedStatement ps = null; diff --git a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java index 48290b18d..a4cd24cac 100644 --- a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java +++ b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java @@ -151,6 +151,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } public void addLocallyGeneratedMessage(Message m) throws DbException { + boolean added = false; waitForPermissionToWrite(); contactLock.readLock().lock(); try { @@ -165,7 +166,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { // Don't store the message if the user has // unsubscribed from the group if(db.containsSubscription(txn, m.getGroup())) { - boolean added = storeMessage(txn, m, null); + added = storeMessage(txn, m, null); if(!added) { if(LOG.isLoggable(Level.FINE)) LOG.fine("Duplicate local message"); @@ -191,6 +192,8 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } finally { contactLock.readLock().unlock(); } + // Call the listeners outside the lock + if(added) callMessageListeners(); } public void findLostBatches(ContactId c) throws DbException { @@ -293,26 +296,32 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { int bytesSent = 0; messageStatusLock.readLock().lock(); try { - Txn txn = db.startTransaction(); + subscriptionLock.readLock().lock(); try { - int capacity = b.getCapacity(); - Iterator it = - db.getSendableMessages(txn, c, capacity).iterator(); - sent = new ArrayList(); - while(it.hasNext()) { - MessageId m = it.next(); - byte[] message = db.getMessage(txn, m); - if(!b.writeMessage(message)) break; - bytesSent += message.length; - sent.add(m); + Txn txn = db.startTransaction(); + try { + int capacity = b.getCapacity(); + Collection sendable = + db.getSendableMessages(txn, c, capacity); + Iterator it = sendable.iterator(); + sent = new ArrayList(); + while(it.hasNext()) { + MessageId m = it.next(); + byte[] raw = db.getMessage(txn, m); + if(!b.writeMessage(raw)) break; + bytesSent += raw.length; + sent.add(m); + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } catch(IOException e) { + db.abortTransaction(txn); + throw e; } - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } catch(IOException e) { - db.abortTransaction(txn); - throw e; + } finally { + subscriptionLock.readLock().unlock(); } } finally { messageStatusLock.readLock().unlock(); @@ -351,24 +360,29 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { Collection sent; messageStatusLock.readLock().lock(); try{ - Txn txn = db.startTransaction(); + subscriptionLock.readLock().lock(); try { - sent = new ArrayList(); - int bytesSent = 0; - for(MessageId m : requested) { - byte[] message = db.getMessageIfSendable(txn, c, m); - if(message == null) continue; - if(!b.writeMessage(message)) break; - bytesSent += message.length; - sent.add(m); + Txn txn = db.startTransaction(); + try { + sent = new ArrayList(); + int bytesSent = 0; + for(MessageId m : requested) { + byte[] raw = db.getMessageIfSendable(txn, c, m); + if(raw == null) continue; + if(!b.writeMessage(raw)) break; + bytesSent += raw.length; + sent.add(m); + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } catch(IOException e) { + db.abortTransaction(txn); + throw e; } - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } catch(IOException e) { - db.abortTransaction(txn); - throw e; + } finally { + subscriptionLock.readLock().unlock(); } } finally { messageStatusLock.readLock().unlock(); @@ -610,6 +624,39 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } } + public boolean hasSendableMessages(ContactId c) throws DbException { + contactLock.readLock().lock(); + try { + if(!containsContact(c)) throw new NoSuchContactException(); + messageLock.readLock().lock(); + try { + messageStatusLock.readLock().lock(); + try { + subscriptionLock.readLock().lock(); + try { + Txn txn = db.startTransaction(); + try { + boolean has = db.hasSendableMessages(txn, c); + db.commitTransaction(txn); + return has; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.readLock().unlock(); + } + } finally { + messageStatusLock.readLock().unlock(); + } + } finally { + messageLock.readLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + } + public void receiveAck(ContactId c, Ack a) throws DbException { // Mark all messages in acked batches as seen contactLock.readLock().lock(); @@ -644,6 +691,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } public void receiveBatch(ContactId c, Batch b) throws DbException { + boolean anyAdded = false; waitForPermissionToWrite(); contactLock.readLock().lock(); try { @@ -661,7 +709,10 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { received++; GroupId g = m.getGroup(); if(db.containsVisibleSubscription(txn, g, c)) { - if(storeMessage(txn, m, c)) stored++; + if(storeMessage(txn, m, c)) { + anyAdded = true; + stored++; + } } } if(LOG.isLoggable(Level.FINE)) @@ -685,6 +736,8 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } finally { contactLock.readLock().unlock(); } + // Call the listeners outside the lock + if(anyAdded) callMessageListeners(); } public void receiveOffer(ContactId c, Offer o, RequestWriter r) diff --git a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java index 3140324fe..af8f67cd0 100644 --- a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java +++ b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java @@ -116,6 +116,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } public void addLocallyGeneratedMessage(Message m) throws DbException { + boolean added = false; waitForPermissionToWrite(); synchronized(contactLock) { synchronized(messageLock) { @@ -126,7 +127,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { // Don't store the message if the user has // unsubscribed from the group if(db.containsSubscription(txn, m.getGroup())) { - boolean added = storeMessage(txn, m, null); + added = storeMessage(txn, m, null); if(!added) { if(LOG.isLoggable(Level.FINE)) LOG.fine("Duplicate local message"); @@ -144,6 +145,8 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } } + // Call the listeners outside the lock + if(added) callMessageListeners(); } public void findLostBatches(ContactId c) throws DbException { @@ -217,31 +220,34 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { if(!containsContact(c)) throw new NoSuchContactException(); synchronized(messageLock) { synchronized(messageStatusLock) { - Txn txn = db.startTransaction(); - try { - int capacity = b.getCapacity(); - Iterator it = - db.getSendableMessages(txn, c, capacity).iterator(); - Collection sent = new ArrayList(); - int bytesSent = 0; - while(it.hasNext()) { - MessageId m = it.next(); - byte[] message = db.getMessage(txn, m); - if(!b.writeMessage(message)) break; - bytesSent += message.length; - sent.add(m); + synchronized(subscriptionLock) { + Txn txn = db.startTransaction(); + try { + int capacity = b.getCapacity(); + Collection sendable = + db.getSendableMessages(txn, c, capacity); + Iterator it = sendable.iterator(); + Collection sent = + new ArrayList(); + int bytesSent = 0; + while(it.hasNext()) { + MessageId m = it.next(); + byte[] raw = db.getMessage(txn, m); + if(!b.writeMessage(raw)) break; + bytesSent += raw.length; + sent.add(m); + } + BatchId id = b.finish(); + if(!sent.isEmpty()) + db.addOutstandingBatch(txn, c, id, sent); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } catch(IOException e) { + db.abortTransaction(txn); + throw e; } - BatchId id = b.finish(); - // Record the contents of the batch, unless it's empty - if(!sent.isEmpty()) - db.addOutstandingBatch(txn, c, id, sent); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } catch(IOException e) { - db.abortTransaction(txn); - throw e; } } } @@ -254,29 +260,31 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { if(!containsContact(c)) throw new NoSuchContactException(); synchronized(messageLock) { synchronized(messageStatusLock) { - Txn txn = db.startTransaction(); - try { - Collection sent = new ArrayList(); - int bytesSent = 0; - for(MessageId m : requested) { - byte[] message = db.getMessageIfSendable(txn, c, m); - if(message == null) continue; - if(!b.writeMessage(message)) break; - bytesSent += message.length; - sent.add(m); + synchronized(subscriptionLock) { + Txn txn = db.startTransaction(); + try { + Collection sent = + new ArrayList(); + int bytesSent = 0; + for(MessageId m : requested) { + byte[] raw = db.getMessageIfSendable(txn, c, m); + if(raw == null) continue; + if(!b.writeMessage(raw)) break; + bytesSent += raw.length; + sent.add(m); + } + BatchId id = b.finish(); + if(!sent.isEmpty()) + db.addOutstandingBatch(txn, c, id, sent); + db.commitTransaction(txn); + return sent; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } catch(IOException e) { + db.abortTransaction(txn); + throw e; } - BatchId id = b.finish(); - // Record the contents of the batch, unless it's empty - if(!sent.isEmpty()) - db.addOutstandingBatch(txn, c, id, sent); - db.commitTransaction(txn); - return sent; - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } catch(IOException e) { - db.abortTransaction(txn); - throw e; } } } @@ -450,6 +458,27 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } + public boolean hasSendableMessages(ContactId c) throws DbException { + synchronized(contactLock) { + if(!containsContact(c)) throw new NoSuchContactException(); + synchronized(messageLock) { + synchronized(messageStatusLock) { + synchronized(subscriptionLock) { + Txn txn = db.startTransaction(); + try { + boolean has = db.hasSendableMessages(txn, c); + db.commitTransaction(txn); + return has; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + } + } + } + public void receiveAck(ContactId c, Ack a) throws DbException { // Mark all messages in acked batches as seen synchronized(contactLock) { @@ -475,6 +504,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } public void receiveBatch(ContactId c, Batch b) throws DbException { + boolean anyAdded = false; waitForPermissionToWrite(); synchronized(contactLock) { if(!containsContact(c)) throw new NoSuchContactException(); @@ -488,7 +518,10 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { received++; GroupId g = m.getGroup(); if(db.containsVisibleSubscription(txn, g, c)) { - if(storeMessage(txn, m, c)) stored++; + if(storeMessage(txn, m, c)) { + anyAdded = true; + stored++; + } } } if(LOG.isLoggable(Level.FINE)) @@ -504,6 +537,8 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } } + // Call the listeners outside the lock + if(anyAdded) callMessageListeners(); } public void receiveOffer(ContactId c, Offer o, RequestWriter r) diff --git a/test/net/sf/briar/db/DatabaseComponentTest.java b/test/net/sf/briar/db/DatabaseComponentTest.java index fe9fd8415..9489fa74d 100644 --- a/test/net/sf/briar/db/DatabaseComponentTest.java +++ b/test/net/sf/briar/db/DatabaseComponentTest.java @@ -12,6 +12,7 @@ import net.sf.briar.api.ContactId; import net.sf.briar.api.Rating; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.MessageListener; import net.sf.briar.api.db.NoSuchContactException; import net.sf.briar.api.db.Status; import net.sf.briar.api.protocol.Ack; @@ -80,6 +81,7 @@ public abstract class DatabaseComponentTest extends TestCase { final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); final Group group = context.mock(Group.class); + final MessageListener listener = context.mock(MessageListener.class); context.checking(new Expectations() {{ allowing(database).startTransaction(); will(returnValue(txn)); @@ -117,6 +119,7 @@ public abstract class DatabaseComponentTest extends TestCase { DatabaseComponent db = createDatabaseComponent(database, cleaner); db.open(false); + db.addListener(listener); assertEquals(Rating.UNRATED, db.getRating(authorId)); assertEquals(contactId, db.addContact(transports)); assertEquals(Collections.singletonList(contactId), db.getContacts()); @@ -125,6 +128,7 @@ public abstract class DatabaseComponentTest extends TestCase { assertEquals(Collections.singletonList(groupId), db.getSubscriptions()); db.unsubscribe(groupId); db.removeContact(contactId); + db.removeListener(listener); db.close(); context.assertIsSatisfied(); @@ -388,7 +392,7 @@ public abstract class DatabaseComponentTest extends TestCase { } @Test - public void testAddingASendableMessageTriggersBackwardInclusion() + public void testAddingSendableMessageTriggersBackwardInclusion() throws DbException { Mockery context = new Mockery(); @SuppressWarnings("unchecked") @@ -446,11 +450,11 @@ public abstract class DatabaseComponentTest extends TestCase { final Transports transportsUpdate = context.mock(Transports.class); context.checking(new Expectations() {{ // Check whether the contact is still in the DB - which it's not - exactly(11).of(database).startTransaction(); + exactly(12).of(database).startTransaction(); will(returnValue(txn)); - exactly(11).of(database).containsContact(txn, contactId); + exactly(12).of(database).containsContact(txn, contactId); will(returnValue(false)); - exactly(11).of(database).commitTransaction(txn); + exactly(12).of(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner); @@ -485,6 +489,11 @@ public abstract class DatabaseComponentTest extends TestCase { assertTrue(false); } catch(NoSuchContactException expected) {} + try { + db.hasSendableMessages(contactId); + assertTrue(false); + } catch(NoSuchContactException expected) {} + try { db.receiveAck(contactId, ack); assertTrue(false); @@ -1019,4 +1028,65 @@ public abstract class DatabaseComponentTest extends TestCase { context.assertIsSatisfied(); } + + @Test + public void testAddingMessageCallsListeners() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final MessageListener listener = context.mock(MessageListener.class); + context.checking(new Expectations() {{ + // addLocallyGeneratedMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId); + will(returnValue(true)); + oneOf(database).addMessage(txn, message); + will(returnValue(true)); + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.UNRATED)); + oneOf(database).getNumberOfSendableChildren(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 0); + oneOf(database).commitTransaction(txn); + // The message was added, so the listener should be called + oneOf(listener).messagesAdded(); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner); + + db.addListener(listener); + db.addLocallyGeneratedMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testDuplicateMessageDoesNotCallListeners() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final MessageListener listener = context.mock(MessageListener.class); + context.checking(new Expectations() {{ + // addLocallyGeneratedMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId); + will(returnValue(true)); + oneOf(database).addMessage(txn, message); + will(returnValue(false)); + oneOf(database).commitTransaction(txn); + // The message was not added, so the listener should not be called + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner); + + db.addListener(listener); + db.addLocallyGeneratedMessage(message); + + context.assertIsSatisfied(); + } } diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java index 00ebff813..c66258ad9 100644 --- a/test/net/sf/briar/db/H2DatabaseTest.java +++ b/test/net/sf/briar/db/H2DatabaseTest.java @@ -210,12 +210,14 @@ public class H2DatabaseTest extends TestCase { // The message should not be sendable assertEquals(0, db.getSendability(txn, messageId)); + assertFalse(db.hasSendableMessages(txn, contactId)); Iterator it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertFalse(it.hasNext()); // Changing the sendability to > 0 should make the message sendable db.setSendability(txn, messageId, 1); + assertTrue(db.hasSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertTrue(it.hasNext()); assertEquals(messageId, it.next()); @@ -223,6 +225,7 @@ public class H2DatabaseTest extends TestCase { // Changing the sendability to 0 should make the message unsendable db.setSendability(txn, messageId, 0); + assertFalse(db.hasSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertFalse(it.hasNext()); @@ -244,12 +247,14 @@ public class H2DatabaseTest extends TestCase { db.setSendability(txn, messageId, 1); // The message has no status yet, so it should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); Iterator it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertFalse(it.hasNext()); // Changing the status to Status.NEW should make the message sendable db.setStatus(txn, contactId, messageId, Status.NEW); + assertTrue(db.hasSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertTrue(it.hasNext()); assertEquals(messageId, it.next()); @@ -257,6 +262,7 @@ public class H2DatabaseTest extends TestCase { // Changing the status to SENT should make the message unsendable db.setStatus(txn, contactId, messageId, Status.SENT); + assertFalse(db.hasSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertFalse(it.hasNext()); @@ -283,12 +289,14 @@ public class H2DatabaseTest extends TestCase { db.setStatus(txn, contactId, messageId, Status.NEW); // The contact is not subscribed, so the message should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); Iterator it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertFalse(it.hasNext()); // The contact subscribing should make the message sendable db.setSubscriptions(txn, contactId, Collections.singleton(group), 1); + assertTrue(db.hasSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertTrue(it.hasNext()); assertEquals(messageId, it.next()); @@ -296,6 +304,7 @@ public class H2DatabaseTest extends TestCase { // The contact unsubscribing should make the message unsendable db.setSubscriptions(txn, contactId, Collections.emptySet(), 2); + assertFalse(db.hasSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertFalse(it.hasNext()); @@ -317,12 +326,14 @@ public class H2DatabaseTest extends TestCase { db.setSendability(txn, messageId, 1); db.setStatus(txn, contactId, messageId, Status.NEW); - // The message is too large to send + // The message is sendable, but too large to send + assertTrue(db.hasSendableMessages(txn, contactId)); Iterator it = db.getSendableMessages(txn, contactId, size - 1).iterator(); assertFalse(it.hasNext()); // The message is just the right size to send + assertTrue(db.hasSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, size).iterator(); assertTrue(it.hasNext()); assertEquals(messageId, it.next()); @@ -347,12 +358,14 @@ public class H2DatabaseTest extends TestCase { // The subscription is not visible to the contact, so the message // should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); Iterator it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertFalse(it.hasNext()); // Making the subscription visible should make the message sendable db.setVisibility(txn, groupId, Collections.singleton(contactId)); + assertTrue(db.hasSendableMessages(txn, contactId)); it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); assertTrue(it.hasNext()); assertEquals(messageId, it.next());