diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java index 55381c587..6cd47b8ab 100644 --- a/api/net/sf/briar/api/db/DatabaseComponent.java +++ b/api/net/sf/briar/api/db/DatabaseComponent.java @@ -109,6 +109,9 @@ public interface DatabaseComponent { /** Returns the transport details for the given contact. */ Map getTransports(ContactId c) throws DbException; + /** Returns the contacts to which the given group is visible. */ + Collection getVisibility(GroupId g) throws DbException; + /** Processes an acknowledgement from the given contact. */ void receiveAck(ContactId c, Ack a) throws DbException; @@ -138,6 +141,13 @@ public interface DatabaseComponent { /** Records the user's rating for the given author. */ void setRating(AuthorId a, Rating r) throws DbException; + /** + * Makes the given group visible to the given set of contacts and invisible + * to any other contacts. + */ + void setVisibility(GroupId g, Collection visible) + throws DbException; + /** Subscribes to the given group. */ void subscribe(Group g) throws DbException; diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java index fcb55b8f8..d71a8547b 100644 --- a/components/net/sf/briar/db/Database.java +++ b/components/net/sf/briar/db/Database.java @@ -129,6 +129,15 @@ interface Database { */ boolean containsSubscription(T txn, GroupId g) throws DbException; + /** + * Returns true iff the user is subscribed to the given group and the + * group is visible to the given contact. + *

+ * Locking: contacts read, subscriptions read. + */ + boolean containsVisibleSubscription(T txn, GroupId g, ContactId c) + throws DbException; + /** * Returns the IDs of any batches received from the given contact that need * to be acknowledged. @@ -195,7 +204,8 @@ interface Database { /** * Returns the number of children of the message identified by the given - * ID that are present in the database and sendable. + * ID that are present in the database and have sendability scores greater + * than zero. *

* Locking: messages read. */ @@ -269,6 +279,20 @@ interface Database { */ Map getTransports(T txn, ContactId c) throws DbException; + /** + * Returns the contacts to which the given group is visible. + *

+ * Locking: contacts read, subscriptions read. + */ + Collection getVisibility(T txn, GroupId g) throws DbException; + + /** + * Returns the groups to which the user subscribes that are visible to the + * given contact. + */ + Collection getVisibleSubscriptions(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 @@ -380,4 +404,13 @@ interface Database { */ void setTransports(T txn, ContactId c, Map transports, long timestamp) throws DbException; + + /** + * Makes the given group visible to the given set of contacts and invisible + * to any other contacts. + *

+ * Locking: contacts read, subscriptions write. + */ + void setVisibility(T txn, GroupId g, Collection visible) + throws DbException; } diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java index f5bef00e4..bd43de357 100644 --- a/components/net/sf/briar/db/JdbcDatabase.java +++ b/components/net/sf/briar/db/JdbcDatabase.java @@ -36,8 +36,8 @@ import net.sf.briar.util.FileUtils; */ abstract class JdbcDatabase implements Database { - private static final String CREATE_LOCAL_SUBSCRIPTIONS = - "CREATE TABLE localSubscriptions" + private static final String CREATE_SUBSCRIPTIONS = + "CREATE TABLE subscriptions" + " (groupId HASH NOT NULL," + " groupName VARCHAR NOT NULL," + " groupKey BINARY," @@ -54,7 +54,7 @@ abstract class JdbcDatabase implements Database { + " raw BLOB NOT NULL," + " sendability INT NOT NULL," + " PRIMARY KEY (messageId)," - + " FOREIGN KEY (groupId) REFERENCES localSubscriptions (groupId)" + + " FOREIGN KEY (groupId) REFERENCES subscriptions (groupId)" + " ON DELETE CASCADE)"; private static final String INDEX_MESSAGES_BY_PARENT = @@ -76,6 +76,19 @@ abstract class JdbcDatabase implements Database { + " transportsTimestamp TIMESTAMP NOT NULL," + " PRIMARY KEY (contactId))"; + private static final String CREATE_VISIBILITIES = + "CREATE TABLE visibilities" + + " (groupId HASH NOT NULL," + + " contactId INT NOT NULL," + + " PRIMARY KEY (groupId, contactId)," + + " FOREIGN KEY (groupId) REFERENCES subscriptions (groupId)" + + " ON DELETE CASCADE," + + " FOREIGN KEY (contactId) REFERENCES contacts (contactId)" + + " ON DELETE CASCADE)"; + + private static final String INDEX_VISIBILITIES_BY_GROUP = + "CREATE INDEX visibilitiesByGroup ON visibilities (groupId)"; + private static final String CREATE_BATCHES_TO_ACK = "CREATE TABLE batchesToAck" + " (batchId HASH NOT NULL," @@ -216,8 +229,8 @@ abstract class JdbcDatabase implements Database { try { s = txn.createStatement(); if(LOG.isLoggable(Level.FINE)) - LOG.fine("Creating localSubscriptions table"); - s.executeUpdate(insertTypeNames(CREATE_LOCAL_SUBSCRIPTIONS)); + LOG.fine("Creating subscriptions table"); + s.executeUpdate(insertTypeNames(CREATE_SUBSCRIPTIONS)); if(LOG.isLoggable(Level.FINE)) LOG.fine("Creating messages table"); s.executeUpdate(insertTypeNames(CREATE_MESSAGES)); @@ -228,6 +241,10 @@ abstract class JdbcDatabase implements Database { if(LOG.isLoggable(Level.FINE)) LOG.fine("Creating contacts table"); s.executeUpdate(insertTypeNames(CREATE_CONTACTS)); + if(LOG.isLoggable(Level.FINE)) + LOG.fine("Creating visibilities table"); + s.executeUpdate(insertTypeNames(CREATE_VISIBILITIES)); + s.executeUpdate(INDEX_VISIBILITIES_BY_GROUP); if(LOG.isLoggable(Level.FINE)) LOG.fine("Creating batchesToAck table"); s.executeUpdate(insertTypeNames(CREATE_BATCHES_TO_ACK)); @@ -527,7 +544,7 @@ abstract class JdbcDatabase implements Database { public void addSubscription(Connection txn, Group g) throws DbException { PreparedStatement ps = null; try { - String sql = "INSERT INTO localSubscriptions" + String sql = "INSERT INTO subscriptions" + " (groupId, groupName, groupKey)" + " VALUES (?, ?, ?)"; ps = txn.prepareStatement(sql); @@ -603,7 +620,7 @@ abstract class JdbcDatabase implements Database { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT COUNT(groupId) FROM localSubscriptions" + String sql = "SELECT COUNT(groupId) FROM subscriptions" + " WHERE groupId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getBytes()); @@ -625,6 +642,36 @@ abstract class JdbcDatabase implements Database { } } + public boolean containsVisibleSubscription(Connection txn, GroupId g, + ContactId c) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT COUNT(subscriptions.groupId)" + + " FROM subscriptions JOIN visibilities" + + " ON subscriptions.groupId = visibilities.groupId" + + " WHERE subscriptions.groupId = ? AND contactId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + ps.setInt(2, c.getInt()); + rs = ps.executeQuery(); + boolean found = rs.next(); + assert found; + int count = rs.getInt(1); + assert count <= 1; + boolean more = rs.next(); + assert !more; + rs.close(); + ps.close(); + return count > 0; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + public Collection getBatchesToAck(Connection txn, ContactId c) throws DbException { PreparedStatement ps = null; @@ -759,16 +806,20 @@ abstract class JdbcDatabase implements Database { String sql = "SELECT size, raw FROM messages" + " JOIN contactSubscriptions" + " ON messages.groupId = contactSubscriptions.groupId" + + " JOIN visibilities" + + " ON messages.groupId = visibilities.groupId" + " JOIN statuses ON messages.messageId = statuses.messageId" + " WHERE messages.messageId = ?" + " AND contactSubscriptions.contactId = ?" - + " AND statuses.contactId = ? AND status = ?" - + " AND sendability > ZERO()"; + + " AND visibilities.contactId = ?" + + " AND statuses.contactId = ?" + + " AND status = ? AND sendability > ZERO()"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getBytes()); ps.setInt(2, c.getInt()); ps.setInt(3, c.getInt()); - ps.setShort(4, (short) Status.NEW.ordinal()); + ps.setInt(4, c.getInt()); + ps.setShort(5, (short) Status.NEW.ordinal()); rs = ps.executeQuery(); byte[] raw = null; if(rs.next()) { @@ -985,15 +1036,19 @@ abstract class JdbcDatabase implements Database { String sql = "SELECT size, 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 statuses.contactId = ? AND status = ?" - + " AND sendability > ZERO()"; + + " AND visibilities.contactId = ?" + + " AND statuses.contactId = ?" + + " AND status = ? AND sendability > ZERO()"; // FIXME: Investigate the performance impact of "ORDER BY timestamp" ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); ps.setInt(2, c.getInt()); - ps.setShort(3, (short) Status.NEW.ordinal()); + ps.setInt(3, c.getInt()); + ps.setShort(4, (short) Status.NEW.ordinal()); rs = ps.executeQuery(); Collection ids = new ArrayList(); int total = 0; @@ -1025,7 +1080,7 @@ abstract class JdbcDatabase implements Database { ResultSet rs = null; try { String sql = "SELECT groupId, groupName, groupKey" - + " FROM localSubscriptions"; + + " FROM subscriptions"; ps = txn.prepareStatement(sql); rs = ps.executeQuery(); Collection subs = new ArrayList(); @@ -1075,6 +1130,36 @@ abstract class JdbcDatabase implements Database { } } + public Collection getVisibleSubscriptions(Connection txn, + ContactId c) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT subscriptions.groupId, groupName, groupKey" + + " FROM subscriptions JOIN visibilities" + + " ON subscriptions.groupId = visibilities.groupId" + + " WHERE contactId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + rs = ps.executeQuery(); + Collection subs = new ArrayList(); + while(rs.next()) { + GroupId id = new GroupId(rs.getBytes(1)); + String name = rs.getString(2); + byte[] publicKey = rs.getBytes(3); + subs.add(groupFactory.createGroup(id, name, publicKey)); + } + rs.close(); + ps.close(); + return subs; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + public Map getTransports(Connection txn) throws DbException { PreparedStatement ps = null; @@ -1119,9 +1204,30 @@ abstract class JdbcDatabase implements Database { } } + public Collection getVisibility(Connection txn, GroupId g) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT contactId FROM visibilities WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + rs = ps.executeQuery(); + Collection visible = new ArrayList(); + while(rs.next()) visible.add(new ContactId(rs.getInt(1))); + rs.close(); + ps.close(); + return visible; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + public void removeAckedBatch(Connection txn, ContactId c, BatchId b) throws DbException { - // Increment the passover count of all older outstanding batches PreparedStatement ps = null; ResultSet rs = null; try { @@ -1138,6 +1244,7 @@ abstract class JdbcDatabase implements Database { assert !more; rs.close(); ps.close(); + // Increment the passover count of all older outstanding batches sql = "UPDATE outstandingBatches SET passover = passover + ?" + " WHERE contactId = ? AND timestamp < ?"; ps = txn.prepareStatement(sql); @@ -1270,7 +1377,7 @@ abstract class JdbcDatabase implements Database { throws DbException { PreparedStatement ps = null; try { - String sql = "DELETE FROM localSubscriptions WHERE groupId = ?"; + String sql = "DELETE FROM subscriptions WHERE groupId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getBytes()); int rowsAffected = ps.executeUpdate(); @@ -1403,10 +1510,15 @@ abstract class JdbcDatabase implements Database { String sql = "SELECT COUNT(messages.messageId) FROM messages" + " JOIN contactSubscriptions" + " ON messages.groupId = contactSubscriptions.groupId" - + " WHERE messageId = ? AND contactId = ?"; + + " JOIN visibilities" + + " ON messages.groupId = visibilities.groupId" + + " WHERE messageId = ?" + + " AND contactSubscriptions.contactId = ?" + + " AND visibilities.contactId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getBytes()); ps.setInt(2, c.getInt()); + ps.setInt(3, c.getInt()); rs = ps.executeQuery(); boolean found = rs.next(); assert found; @@ -1504,7 +1616,7 @@ abstract class JdbcDatabase implements Database { ps.executeUpdate(); ps.close(); // Store the new transports - if(transports != null) { + if(!transports.isEmpty()) { sql = "INSERT INTO localTransports (key, value)" + " VALUES (?, ?)"; ps = txn.prepareStatement(sql); @@ -1585,4 +1697,36 @@ abstract class JdbcDatabase implements Database { throw new DbException(e); } } + + public void setVisibility(Connection txn, GroupId g, + Collection visible) throws DbException { + PreparedStatement ps = null; + try { + // Delete any existing visibilities + String sql = "DELETE FROM visibilities WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + ps.executeUpdate(); + ps.close(); + if(visible.isEmpty()) return; + // Store the new visibilities + sql = "INSERT INTO visibilities (groupId, contactId)" + + " VALUES (?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + for(ContactId c : visible) { + ps.setInt(2, c.getInt()); + ps.addBatch(); + } + int[] rowsAffectedArray = ps.executeBatch(); + assert rowsAffectedArray.length == visible.size(); + for(int i = 0; i < rowsAffectedArray.length; i++) { + assert rowsAffectedArray[i] == 1; + } + } catch(SQLException e) { + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } } diff --git a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java index 53537c863..48290b18d 100644 --- a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java +++ b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java @@ -448,7 +448,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { try { Txn txn = db.startTransaction(); try { - Collection subs = db.getSubscriptions(txn); + Collection subs = db.getVisibleSubscriptions(txn, c); s.writeSubscriptions(subs); if(LOG.isLoggable(Level.FINE)) LOG.fine("Added " + subs.size() + " subscriptions"); @@ -588,6 +588,28 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } } + public Collection getVisibility(GroupId g) throws DbException { + contactLock.readLock().lock(); + try { + subscriptionLock.readLock().lock(); + try { + Txn txn = db.startTransaction(); + try { + Collection visible = db.getVisibility(txn, g); + db.commitTransaction(txn); + return visible; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.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(); @@ -638,7 +660,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { for(Message m : b.getMessages()) { received++; GroupId g = m.getGroup(); - if(db.containsSubscription(txn, g)) { + if(db.containsVisibleSubscription(txn, g, c)) { if(storeMessage(txn, m, c)) stored++; } } @@ -821,6 +843,34 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } } + public void setVisibility(GroupId g, Collection visible) + throws DbException { + contactLock.readLock().lock(); + try { + subscriptionLock.writeLock().lock(); + try { + Txn txn = db.startTransaction(); + try { + // Remove any ex-contacts from the set + Collection present = + new ArrayList(visible.size()); + for(ContactId c : visible) { + if(db.containsContact(txn, c)) present.add(c); + } + db.setVisibility(txn, g, present); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + } + public void subscribe(Group g) throws DbException { if(LOG.isLoggable(Level.FINE)) LOG.fine("Subscribing to " + g); subscriptionLock.writeLock().lock(); diff --git a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java index 0c3c796af..3140324fe 100644 --- a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java +++ b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java @@ -322,7 +322,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { synchronized(subscriptionLock) { Txn txn = db.startTransaction(); try { - Collection subs = db.getSubscriptions(txn); + Collection subs = db.getVisibleSubscriptions(txn, c); s.writeSubscriptions(subs); if(LOG.isLoggable(Level.FINE)) LOG.fine("Added " + subs.size() + " subscriptions"); @@ -434,6 +434,22 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } + public Collection getVisibility(GroupId g) throws DbException { + synchronized(contactLock) { + synchronized(subscriptionLock) { + Txn txn = db.startTransaction(); + try { + Collection visible = db.getVisibility(txn, g); + db.commitTransaction(txn); + return visible; + } 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) { @@ -471,7 +487,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { for(Message m : b.getMessages()) { received++; GroupId g = m.getGroup(); - if(db.containsSubscription(txn, g)) { + if(db.containsVisibleSubscription(txn, g, c)) { if(storeMessage(txn, m, c)) stored++; } } @@ -604,6 +620,28 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } + public void setVisibility(GroupId g, Collection visible) + throws DbException { + synchronized(contactLock) { + synchronized(subscriptionLock) { + Txn txn = db.startTransaction(); + try { + // Remove any ex-contacts from the set + Collection present = + new ArrayList(visible.size()); + for(ContactId c : visible) { + if(db.containsContact(txn, c)) present.add(c); + } + db.setVisibility(txn, g, present); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + } + public void subscribe(Group g) throws DbException { if(LOG.isLoggable(Level.FINE)) LOG.fine("Subscribing to " + g); synchronized(subscriptionLock) { diff --git a/test/net/sf/briar/db/DatabaseComponentTest.java b/test/net/sf/briar/db/DatabaseComponentTest.java index 87f967537..fe9fd8415 100644 --- a/test/net/sf/briar/db/DatabaseComponentTest.java +++ b/test/net/sf/briar/db/DatabaseComponentTest.java @@ -693,8 +693,8 @@ public abstract class DatabaseComponentTest extends TestCase { allowing(database).commitTransaction(txn); allowing(database).containsContact(txn, contactId); will(returnValue(true)); - // Get the local subscriptions - oneOf(database).getSubscriptions(txn); + // Get the visible subscriptions + oneOf(database).getVisibleSubscriptions(txn, contactId); will(returnValue(Collections.singletonList(group))); // Add the subscriptions to the writer oneOf(subscriptionWriter).writeSubscriptions( @@ -776,10 +776,11 @@ public abstract class DatabaseComponentTest extends TestCase { allowing(database).commitTransaction(txn); allowing(database).containsContact(txn, contactId); will(returnValue(true)); - // Only store messages belonging to subscribed groups + // Only store messages belonging to visible, subscribed groups oneOf(batch).getMessages(); will(returnValue(Collections.singletonList(message))); - oneOf(database).containsSubscription(txn, groupId); + oneOf(database).containsVisibleSubscription(txn, groupId, + contactId); will(returnValue(false)); // The message is not stored but the batch must still be acked oneOf(batch).getId(); @@ -807,10 +808,11 @@ public abstract class DatabaseComponentTest extends TestCase { allowing(database).commitTransaction(txn); allowing(database).containsContact(txn, contactId); will(returnValue(true)); - // Only store messages belonging to subscribed groups + // Only store messages belonging to visible, subscribed groups oneOf(batch).getMessages(); will(returnValue(Collections.singletonList(message))); - oneOf(database).containsSubscription(txn, groupId); + oneOf(database).containsVisibleSubscription(txn, groupId, + contactId); will(returnValue(true)); // The message is stored, but it's a duplicate oneOf(database).addMessage(txn, message); @@ -841,10 +843,11 @@ public abstract class DatabaseComponentTest extends TestCase { allowing(database).commitTransaction(txn); allowing(database).containsContact(txn, contactId); will(returnValue(true)); - // Only store messages belonging to subscribed groups + // Only store messages belonging to visible, subscribed groups oneOf(batch).getMessages(); will(returnValue(Collections.singletonList(message))); - oneOf(database).containsSubscription(txn, groupId); + oneOf(database).containsVisibleSubscription(txn, groupId, + contactId); will(returnValue(true)); // The message is stored, and it's not a duplicate oneOf(database).addMessage(txn, message); @@ -884,10 +887,11 @@ public abstract class DatabaseComponentTest extends TestCase { allowing(database).commitTransaction(txn); allowing(database).containsContact(txn, contactId); will(returnValue(true)); - // Only store messages belonging to subscribed groups + // Only store messages belonging to visible, subscribed groups oneOf(batch).getMessages(); will(returnValue(Collections.singletonList(message))); - oneOf(database).containsSubscription(txn, groupId); + oneOf(database).containsVisibleSubscription(txn, groupId, + contactId); will(returnValue(true)); // The message is stored, and it's not a duplicate oneOf(database).addMessage(txn, message); diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java index 174c025bd..00ebff813 100644 --- a/test/net/sf/briar/db/H2DatabaseTest.java +++ b/test/net/sf/briar/db/H2DatabaseTest.java @@ -203,6 +203,7 @@ public class H2DatabaseTest extends TestCase { // Add a contact, subscribe to a group and store a message assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, group); + db.setVisibility(txn, groupId, Collections.singleton(contactId)); db.setSubscriptions(txn, contactId, Collections.singleton(group), 1); db.addMessage(txn, message); db.setStatus(txn, contactId, messageId, Status.NEW); @@ -237,6 +238,7 @@ public class H2DatabaseTest extends TestCase { // Add a contact, subscribe to a group and store a message assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, group); + db.setVisibility(txn, groupId, Collections.singleton(contactId)); db.setSubscriptions(txn, contactId, Collections.singleton(group), 1); db.addMessage(txn, message); db.setSendability(txn, messageId, 1); @@ -275,6 +277,7 @@ public class H2DatabaseTest extends TestCase { // Add a contact, subscribe to a group and store a message assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, group); + db.setVisibility(txn, groupId, Collections.singleton(contactId)); db.addMessage(txn, message); db.setSendability(txn, messageId, 1); db.setStatus(txn, contactId, messageId, Status.NEW); @@ -308,6 +311,7 @@ public class H2DatabaseTest extends TestCase { // Add a contact, subscribe to a group and store a message assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, group); + db.setVisibility(txn, groupId, Collections.singleton(contactId)); db.setSubscriptions(txn, contactId, Collections.singleton(group), 1); db.addMessage(txn, message); db.setSendability(txn, messageId, 1); @@ -328,6 +332,36 @@ public class H2DatabaseTest extends TestCase { db.close(); } + @Test + public void testSendableMessagesMustBeVisible() throws DbException { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn, null)); + db.addSubscription(txn, group); + db.setSubscriptions(txn, contactId, Collections.singleton(group), 1); + db.addMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The subscription is not visible to the contact, so the message + // should not be sendable + 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)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + @Test public void testBatchesToAck() throws DbException { BatchId batchId1 = new BatchId(TestUtils.getRandomId()); @@ -365,6 +399,7 @@ public class H2DatabaseTest extends TestCase { // Add a contact, subscribe to a group and store a message assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, group); + db.setVisibility(txn, groupId, Collections.singleton(contactId)); db.setSubscriptions(txn, contactId, Collections.singleton(group), 1); db.addMessage(txn, message); db.setSendability(txn, messageId, 1); @@ -401,6 +436,7 @@ public class H2DatabaseTest extends TestCase { // Add a contact, subscribe to a group and store a message assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, group); + db.setVisibility(txn, groupId, Collections.singleton(contactId)); db.setSubscriptions(txn, contactId, Collections.singleton(group), 1); db.addMessage(txn, message); db.setSendability(txn, messageId, 1); @@ -718,13 +754,14 @@ public class H2DatabaseTest extends TestCase { db.setTransports(txn, contactId, transports, 1); assertEquals(transports, db.getTransports(txn, contactId)); // Remove the transport details - db.setTransports(txn, contactId, null, 2); + db.setTransports(txn, contactId, + Collections.emptyMap(), 2); assertEquals(Collections.emptyMap(), db.getTransports(txn, contactId)); // Set the local transport details db.setTransports(txn, transports); assertEquals(transports, db.getTransports(txn)); // Remove the local transport details - db.setTransports(txn, null); + db.setTransports(txn, Collections.emptyMap()); assertEquals(Collections.emptyMap(), db.getTransports(txn)); db.commitTransaction(txn); @@ -880,6 +917,7 @@ public class H2DatabaseTest extends TestCase { // Add a contact, subscribe to a group and store a message assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, group); + db.setVisibility(txn, groupId, Collections.singleton(contactId)); db.setSubscriptions(txn, contactId, Collections.singleton(group), 1); db.addMessage(txn, message); // Set the sendability to > 0 @@ -904,6 +942,7 @@ public class H2DatabaseTest extends TestCase { // Add a contact and subscribe to a group assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, group); + db.setVisibility(txn, groupId, Collections.singleton(contactId)); db.setSubscriptions(txn, contactId, Collections.singleton(group), 1); // The message is not in the database @@ -949,6 +988,26 @@ public class H2DatabaseTest extends TestCase { db.close(); } + @Test + public void testSetStatusSeenIfVisibleRequiresVisibility() + throws DbException { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn, null)); + db.addSubscription(txn, group); + db.addMessage(txn, message); + db.setSubscriptions(txn, contactId, Collections.singleton(group), 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The subscription is not visible + assertFalse(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + @Test public void testSetStatusSeenIfVisibleReturnsTrueIfAlreadySeen() throws DbException { @@ -958,6 +1017,7 @@ public class H2DatabaseTest extends TestCase { // Add a contact, subscribe to a group and store a message assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, group); + db.setVisibility(txn, groupId, Collections.singleton(contactId)); db.setSubscriptions(txn, contactId, Collections.singleton(group), 1); db.addMessage(txn, message); // The message has already been seen by the contact @@ -978,6 +1038,7 @@ public class H2DatabaseTest extends TestCase { // Add a contact, subscribe to a group and store a message assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, group); + db.setVisibility(txn, groupId, Collections.singleton(contactId)); db.setSubscriptions(txn, contactId, Collections.singleton(group), 1); db.addMessage(txn, message); // The message has not been seen by the contact @@ -989,6 +1050,28 @@ public class H2DatabaseTest extends TestCase { db.close(); } + @Test + public void testVisibility() throws DbException { + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and subscribe to a group + assertEquals(contactId, db.addContact(txn, null)); + db.addSubscription(txn, group); + + // The group should not be visible to the contact + assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId)); + + // Make the group visible to the contact + db.setVisibility(txn, groupId, Collections.singleton(contactId)); + assertEquals(Collections.singletonList(contactId), + db.getVisibility(txn, groupId)); + + // Make the group invisible again + db.setVisibility(txn, groupId, Collections.emptySet()); + assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId)); + } + private Database open(boolean resume) throws DbException { Database db = new H2Database(testDir, password, MAX_SIZE, groupFactory); diff --git a/test/net/sf/briar/protocol/FileReadWriteTest.java b/test/net/sf/briar/protocol/FileReadWriteTest.java index 57e29a55d..9f8304687 100644 --- a/test/net/sf/briar/protocol/FileReadWriteTest.java +++ b/test/net/sf/briar/protocol/FileReadWriteTest.java @@ -211,7 +211,7 @@ public class FileReadWriteTest extends TestCase { assertFalse(requested.get(2)); assertTrue(requested.get(3)); // If there are any padding bits, they should all be zero - for(int i = 4; i < requested.size(); i++) assertFalse(requested.get(i)); + assertEquals(2, requested.cardinality()); // Read the subscriptions update assertTrue(reader.hasUserDefined(Tags.SUBSCRIPTIONS));