diff --git a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java index 4f26123167ae3643d7cadf26c3f48388488dd84c..67883406cc03e26aedf83ed1bf654008c4db3136 100644 --- a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java +++ b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java @@ -344,11 +344,18 @@ public interface DatabaseComponent { /** * Makes the given group visible to the given set of contacts and invisible - * to any other contacts. + * to any other current or future contacts. */ void setVisibility(GroupId g, Collection<ContactId> visible) throws DbException; + /** + * Makes the given group visible or invisible to future contacts by default. + * If <tt>visible</tt> is true, the group is also made visible to all + * current contacts. + */ + void setVisibleToAll(GroupId g, boolean visible) throws DbException; + /** * Subscribes to the given group, or returns false if the user already has * the maximum number of subscriptions. diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java index 9fdeae416b6748bc1c98be24aa36dd79cc90d414..05dce999c77cba3b27b7ff14910ca1ee02dec1ad 100644 --- a/briar-core/src/net/sf/briar/db/Database.java +++ b/briar-core/src/net/sf/briar/db/Database.java @@ -791,6 +791,13 @@ interface Database<T> { void setTransportUpdateAcked(T txn, ContactId c, TransportId t, long version) throws DbException; + /** + * Makes the given group visible or invisible to future contacts by default. + * <p> + * Locking: subscription write. + */ + void setVisibleToAll(T txn, GroupId g, boolean visible) throws DbException; + /** * Updates the expiry times of the given messages with respect to the given * contact, using the given transmission counts and the latency of the diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java index 4736d6cc8535fc30938d9e3a6138b7a772e4d0c5..f830fe233b8f68b861ff6f934d5b5270f6c83c3c 100644 --- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java +++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java @@ -1982,22 +1982,61 @@ DatabaseCleaner.Callback { if(!db.containsSubscription(txn, g)) throw new NoSuchSubscriptionException(); // Use HashSets for O(1) lookups, O(n) overall running time - HashSet<ContactId> newVisible = - new HashSet<ContactId>(visible); - HashSet<ContactId> oldVisible = - new HashSet<ContactId>(db.getVisibility(txn, g)); + HashSet<ContactId> now = new HashSet<ContactId>(visible); + Collection<ContactId> before = db.getVisibility(txn, g); + before = new HashSet<ContactId>(before); // Set the group's visibility for each current contact for(ContactId c : db.getContactIds(txn)) { - boolean then = oldVisible.contains(c); - boolean now = newVisible.contains(c); - if(!then && now) { + boolean wasBefore = before.contains(c); + boolean isNow = now.contains(c); + if(!wasBefore && isNow) { db.addVisibility(txn, c, g); affected.add(c); - } else if(then && !now) { + } else if(wasBefore && !isNow) { db.removeVisibility(txn, c, g); affected.add(c); } } + // Make the group invisible to future contacts + db.setVisibleToAll(txn, g, false); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + if(!affected.isEmpty()) + callListeners(new LocalSubscriptionsUpdatedEvent(affected)); + } + + public void setVisibleToAll(GroupId g, boolean visible) throws DbException { + Collection<ContactId> affected = new ArrayList<ContactId>(); + contactLock.readLock().lock(); + try { + subscriptionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsSubscription(txn, g)) + throw new NoSuchSubscriptionException(); + // Make the group visible or invisible to future contacts + db.setVisibleToAll(txn, g, visible); + if(visible) { + // Make the group visible to all current contacts + Collection<ContactId> before = db.getVisibility(txn, g); + before = new HashSet<ContactId>(before); + for(ContactId c : db.getContactIds(txn)) { + if(!before.contains(c)) { + db.addVisibility(txn, c, g); + affected.add(c); + } + } + } db.commitTransaction(txn); } catch(DbException e) { db.abortTransaction(txn); diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java index 580d4e8a37ba396304aa23092b352fbed8f96d99..8e3033e88c61a3daaf5dc7799399a8a4baa27591 100644 --- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java +++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java @@ -103,6 +103,7 @@ abstract class JdbcDatabase implements Database<Connection> { + " (groupId HASH NOT NULL," + " name VARCHAR NOT NULL," + " publicKey BINARY," // Null for unrestricted groups + + " visibleToAll BOOLEAN NOT NULL," + " PRIMARY KEY (groupId))"; // Locking: subscription @@ -573,6 +574,27 @@ abstract class JdbcDatabase implements Database<Connection> { if(rs.next()) throw new DbStateException(); rs.close(); ps.close(); + // Make groups that are visible to everyone visible to this contact + sql = "SELECT groupId FROM groups WHERE visibleToAll = TRUE"; + ps = txn.prepareStatement(sql); + rs = ps.executeQuery(); + Collection<byte[]> ids = new ArrayList<byte[]>(); + while(rs.next()) ids.add(rs.getBytes(1)); + rs.close(); + ps.close(); + if(!ids.isEmpty()) { + sql = "INSERT INTO groupVisibilities (contactId, groupId)" + + " VALUES (?, ?)"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + for(byte[] id : ids) { + ps.setBytes(2, id); + ps.addBatch(); + } + affected = ps.executeUpdate(); + if(affected != ids.size()) throw new DbStateException(); + ps.close(); + } // Create a connection time row sql = "INSERT INTO connectionTimes (contactId, lastConnected)" + " VALUES (?, ?)"; @@ -893,8 +915,8 @@ abstract class JdbcDatabase implements Database<Connection> { ps.close(); if(count > MAX_SUBSCRIPTIONS) throw new DbStateException(); if(count == MAX_SUBSCRIPTIONS) return false; - sql = "INSERT INTO groups (groupId, name, publicKey)" - + " VALUES (?, ?, ?)"; + sql = "INSERT INTO groups (groupId, name, publicKey, visibleToAll)" + + " VALUES (?, ?, ?, FALSE)"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getId().getBytes()); ps.setString(2, g.getName()); @@ -3376,6 +3398,23 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public void setVisibleToAll(Connection txn, GroupId g, boolean visible) + throws DbException { + PreparedStatement ps = null; + try { + String sql = "UPDATE groups SET visibleToAll = ? WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBoolean(1, visible); + ps.setBytes(2, g.getBytes()); + int affected = ps.executeUpdate(); + if(affected > 1) throw new DbStateException(); + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + throw new DbException(e); + } + } + public void updateExpiryTimes(Connection txn, ContactId c, Map<MessageId, Integer> sent, long maxLatency) throws DbException { long now = clock.currentTimeMillis(); diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java index 349e48bdf8f193e79dbd1d0927a49f32e0839851..84d3a10d6f5eaa125a57f404cf2a68366f4fc4e6 100644 --- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java +++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java @@ -1729,6 +1729,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { oneOf(database).getContactIds(txn); will(returnValue(both)); oneOf(database).removeVisibility(txn, contactId1, groupId); + oneOf(database).setVisibleToAll(txn, groupId, false); oneOf(database).commitTransaction(txn); oneOf(listener).eventOccurred(with(any( LocalSubscriptionsUpdatedEvent.class))); @@ -1745,7 +1746,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { @Test public void testNotChangingVisibilityDoesNotCallListeners() throws Exception { - final ContactId contactId1 = new ContactId(234); + final ContactId contactId1 = new ContactId(123); final Collection<ContactId> both = Arrays.asList(contactId, contactId1); Mockery context = new Mockery(); @SuppressWarnings("unchecked") @@ -1762,6 +1763,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { will(returnValue(both)); oneOf(database).getContactIds(txn); will(returnValue(both)); + oneOf(database).setVisibleToAll(txn, groupId, false); oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, @@ -1773,6 +1775,57 @@ public abstract class DatabaseComponentTest extends BriarTestCase { context.assertIsSatisfied(); } + @Test + public void testSettingVisibleToAllTrueAffectsCurrentContacts() + throws Exception { + final ContactId contactId1 = new ContactId(123); + final Collection<ContactId> both = Arrays.asList(contactId, contactId1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + // setVisibility() + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId); + will(returnValue(true)); + oneOf(database).getVisibility(txn, groupId); + will(returnValue(Collections.emptyList())); + oneOf(database).getContactIds(txn); + will(returnValue(both)); + oneOf(database).addVisibility(txn, contactId, groupId); + oneOf(database).setVisibleToAll(txn, groupId, false); + oneOf(database).commitTransaction(txn); + oneOf(listener).eventOccurred(with(any( + LocalSubscriptionsUpdatedEvent.class))); + // setVisibleToAll() + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId); + will(returnValue(true)); + oneOf(database).setVisibleToAll(txn, groupId, true); + oneOf(database).getVisibility(txn, groupId); + will(returnValue(Arrays.asList(contactId))); + oneOf(database).getContactIds(txn); + will(returnValue(both)); + oneOf(database).addVisibility(txn, contactId1, groupId); + oneOf(database).commitTransaction(txn); + oneOf(listener).eventOccurred(with(any( + LocalSubscriptionsUpdatedEvent.class))); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown); + + db.addListener(listener); + db.setVisibility(groupId, Arrays.asList(contactId)); + db.setVisibleToAll(groupId, true); + + context.assertIsSatisfied(); + } + @Test public void testTemporarySecrets() throws Exception { Mockery context = new Mockery(); diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java index 87ec46004a0be0ea1a3aa400be0910ef44b6af91..b461ad1e3a9869607a26bcbe9b0d2e9891377c3d 100644 --- a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java +++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java @@ -873,6 +873,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); // The message is not in the database @@ -891,6 +892,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); db.addGroupMessage(txn, message); @@ -915,6 +917,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); db.addGroupMessage(txn, message); @@ -1028,6 +1031,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); db.addGroupMessage(txn, message); db.addStatus(txn, contactId, messageId, false); @@ -1048,8 +1052,8 @@ public class H2DatabaseTest extends BriarTestCase { db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); db.addSubscription(txn, group); - db.addGroupMessage(txn, message); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); + db.addGroupMessage(txn, message); db.addStatus(txn, contactId, messageId, false); // The subscription is not visible