diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java index e67f0cb912690bdcd529dbdaa5575caa935d425b..c1dfc0072419a3b80b1ead10f4222926de3fb0d4 100644 --- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java +++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java @@ -158,6 +158,9 @@ public interface DatabaseComponent { /** Returns the group with the given ID, if the user subscribes to it. */ Group getGroup(GroupId g) throws DbException; + /** Returns the metadata for the given group. */ + Metadata getGroupMetadata(GroupId g) throws DbException; + /** * Returns all groups belonging to the given client to which the user * subscribes. @@ -234,6 +237,12 @@ public interface DatabaseComponent { void incrementStreamCounter(ContactId c, TransportId t, long rotationPeriod) throws DbException; + /** + * Merges the given metadata with the existing metadata for the given + * group. + */ + void mergeGroupMetadata(GroupId g, Metadata meta) throws DbException; + /** * Merges the given properties with the existing local properties for the * given transport. diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java index 30e1754530c28d6a8e622dcdf0968b9c661b1d33..01a7b54bfcfcd1399e6798a4e4afdb15d865b5cb 100644 --- a/briar-core/src/org/briarproject/db/Database.java +++ b/briar-core/src/org/briarproject/db/Database.java @@ -27,8 +27,6 @@ import java.io.IOException; import java.util.Collection; import java.util.Map; -// FIXME: Document the preconditions for calling each method - /** * A low-level interface to the database (DatabaseComponent provides a * high-level interface). Most operations take a transaction argument, which is @@ -275,6 +273,13 @@ interface Database<T> { */ Group getGroup(T txn, GroupId g) throws DbException; + /** + * Returns the metadata for the given group. + * <p> + * Locking: read. + */ + Metadata getGroupMetadata(T txn, GroupId g) throws DbException; + /** * Returns all groups belonging to the given client to which the user * subscribes. @@ -515,6 +520,15 @@ interface Database<T> { void lowerRequestedFlag(T txn, ContactId c, Collection<MessageId> requested) throws DbException; + /* + * Merges the given metadata with the existing metadata for the given + * group. + * <p> + * Locking: write. + */ + void mergeGroupMetadata(T txn, GroupId g, Metadata meta) + throws DbException; + /** * Merges the given properties with the existing local properties for the * given transport. diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java index 07aaee5ccd879fae76b1ef43c77c030e044df8b9..85ff594e6c38893fa66e2702dc09411e5d91f0f4 100644 --- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java +++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java @@ -607,6 +607,25 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } + public Metadata getGroupMetadata(GroupId g) throws DbException { + lock.readLock().lock(); + try { + T txn = db.startTransaction(); + try { + if (!db.containsGroup(txn, g)) + throw new NoSuchSubscriptionException(); + Metadata metadata = db.getGroupMetadata(txn, g); + db.commitTransaction(txn); + return metadata; + } catch (DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + lock.readLock().unlock(); + } + } + public Collection<Group> getGroups(ClientId c) throws DbException { lock.readLock().lock(); try { @@ -954,6 +973,25 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } + public void mergeGroupMetadata(GroupId g, Metadata meta) + throws DbException { + lock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if (!db.containsGroup(txn, g)) + throw new NoSuchSubscriptionException(); + db.mergeGroupMetadata(txn, g, meta); + db.commitTransaction(txn); + } catch (DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + lock.writeLock().unlock(); + } + } + public void mergeLocalProperties(TransportId t, TransportProperties p) throws DbException { boolean changed = false; diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java index c2ea25a9ce72e173f14d1022589fbd63b91a9f1f..69ed999735fafb8987e616884538afca71765a2c 100644 --- a/briar-core/src/org/briarproject/db/JdbcDatabase.java +++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java @@ -65,8 +65,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry; */ abstract class JdbcDatabase implements Database<Connection> { - private static final int SCHEMA_VERSION = 16; - private static final int MIN_SCHEMA_VERSION = 16; + private static final int SCHEMA_VERSION = 17; + private static final int MIN_SCHEMA_VERSION = 17; private static final String CREATE_SETTINGS = "CREATE TABLE settings" @@ -107,6 +107,16 @@ abstract class JdbcDatabase implements Database<Connection> { + " visibleToAll BOOLEAN NOT NULL," + " PRIMARY KEY (groupId))"; + private static final String CREATE_GROUP_METADATA = + "CREATE TABLE groupMetadata" + + " (groupId HASH NOT NULL," + + " key VARCHAR NOT NULL," + + " value BINARY NOT NULL," + + " PRIMARY KEY (groupId, key)," + + " FOREIGN KEY (groupId)" + + " REFERENCES groups (groupId)" + + " ON DELETE CASCADE)"; + private static final String CREATE_GROUP_VISIBILITIES = "CREATE TABLE groupVisibilities" + " (contactId INT NOT NULL," @@ -386,6 +396,7 @@ abstract class JdbcDatabase implements Database<Connection> { s.executeUpdate(insertTypeNames(CREATE_LOCAL_AUTHORS)); s.executeUpdate(insertTypeNames(CREATE_CONTACTS)); s.executeUpdate(insertTypeNames(CREATE_GROUPS)); + s.executeUpdate(insertTypeNames(CREATE_GROUP_METADATA)); s.executeUpdate(insertTypeNames(CREATE_GROUP_VISIBILITIES)); s.executeUpdate(insertTypeNames(CREATE_CONTACT_GROUPS)); s.executeUpdate(insertTypeNames(CREATE_GROUP_VERSIONS)); @@ -1496,16 +1507,25 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public Metadata getGroupMetadata(Connection txn, GroupId g) + throws DbException { + return getMetadata(txn, g.getBytes(), "groupMetadata", "groupId"); + } + 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 messageMetadata" - + " WHERE messageId = ?"; + String sql = "SELECT key, value FROM " + tableName + + " WHERE " + columnName + " = ?"; ps = txn.prepareStatement(sql); - ps.setBytes(1, m.getBytes()); + ps.setBytes(1, id); rs = ps.executeQuery(); Metadata metadata = new Metadata(); while (rs.next()) metadata.put(rs.getString(1), rs.getBytes(2)); @@ -2329,8 +2349,18 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public void mergeGroupMetadata(Connection txn, GroupId g, Metadata meta) + throws DbException { + mergeMetadata(txn, g.getBytes(), meta, "groupMetadata", "groupId"); + } + public void mergeMessageMetadata(Connection txn, MessageId m, Metadata meta) throws DbException { + mergeMetadata(txn, m.getBytes(), meta, "messageMetadata", "messageId"); + } + + private void mergeMetadata(Connection txn, byte[] id, Metadata meta, + String tableName, String columnName) throws DbException { PreparedStatement ps = null; try { // Determine which keys are being removed @@ -2342,10 +2372,10 @@ abstract class JdbcDatabase implements Database<Connection> { } // Delete any keys that are being removed if (!removed.isEmpty()) { - String sql = "DELETE FROM messageMetadata" - + " WHERE messageId = ? AND key = ?"; + String sql = "DELETE FROM " + tableName + + " WHERE " + columnName + " = ? AND key = ?"; ps = txn.prepareStatement(sql); - ps.setBytes(1, m.getBytes()); + ps.setBytes(1, id); for (String key : removed) { ps.setString(2, key); ps.addBatch(); @@ -2361,10 +2391,10 @@ abstract class JdbcDatabase implements Database<Connection> { } if (retained.isEmpty()) return; // Update any keys that already exist - String sql = "UPDATE messageMetadata SET value = ?" - + " WHERE messageId = ? AND key = ?"; + String sql = "UPDATE " + tableName + " SET value = ?" + + " WHERE " + columnName + " = ? AND key = ?"; ps = txn.prepareStatement(sql); - ps.setBytes(2, m.getBytes()); + ps.setBytes(2, id); for (Entry<String, byte[]> e : retained.entrySet()) { ps.setBytes(1, e.getValue()); ps.setString(3, e.getKey()); @@ -2378,10 +2408,11 @@ abstract class JdbcDatabase implements Database<Connection> { if (batchAffected[i] > 1) throw new DbStateException(); } // Insert any keys that don't already exist - sql = "INSERT INTO messageMetadata (messageId, key, value)" + sql = "INSERT INTO " + tableName + + " (" + columnName + ", key, value)" + " VALUES (?, ?, ?)"; ps = txn.prepareStatement(sql); - ps.setBytes(1, m.getBytes()); + ps.setBytes(1, id); int updateIndex = 0, inserted = 0; for (Entry<String, byte[]> e : retained.entrySet()) { if (batchAffected[updateIndex] == 0) { diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java index 49502aa043ee0be44d7448565d415023fc2e4089..49c4a357c813ffccb24f2ffcbc42a9832577cb82 100644 --- a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java +++ b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java @@ -83,8 +83,14 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook, db.addGroup(g); db.addContactGroup(c, g); db.setVisibility(g.getId(), Collections.singletonList(c)); + // Attach the contact ID to the group + BdfDictionary d = new BdfDictionary(); + d.put("contactId", c.getInt()); + db.mergeGroupMetadata(g.getId(), metadataEncoder.encode(d)); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } catch (FormatException e) { + throw new RuntimeException(e); } } @@ -141,18 +147,20 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook, Metadata meta = metadataEncoder.encode(d); db.addLocalMessage(m.getMessage(), CLIENT_ID, meta); } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + throw new RuntimeException(e); } } @Override public ContactId getContactId(GroupId g) throws DbException { - // TODO: Use metadata to attach the contact ID to the group - for (Contact c : db.getContacts()) { - Group conversation = getConversationGroup(c); - if (conversation.getId().equals(g)) return c.getId(); + try { + BdfDictionary d = metadataParser.parse(db.getGroupMetadata(g)); + long id = d.getInteger("contactId"); + return new ContactId((int) id); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + throw new NoSuchContactException(); } - throw new NoSuchContactException(); } @Override diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java index b4ff178a566555b8deee33b651d16d7233c7a507..4aa204bdd731af30a7529631b4a790bd235402bf 100644 --- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java +++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java @@ -533,11 +533,11 @@ public class DatabaseComponentImplTest extends BriarTestCase { final EventBus eventBus = context.mock(EventBus.class); context.checking(new Expectations() {{ // Check whether the subscription is in the DB (which it's not) - exactly(5).of(database).startTransaction(); + exactly(7).of(database).startTransaction(); will(returnValue(txn)); - exactly(5).of(database).containsGroup(txn, groupId); + exactly(7).of(database).containsGroup(txn, groupId); will(returnValue(false)); - exactly(5).of(database).abortTransaction(txn); + exactly(7).of(database).abortTransaction(txn); // This is needed for getMessageStatus() to proceed exactly(1).of(database).containsContact(txn, contactId); will(returnValue(true)); @@ -552,6 +552,13 @@ public class DatabaseComponentImplTest extends BriarTestCase { // Expected } + try { + db.getGroupMetadata(groupId); + fail(); + } catch (NoSuchSubscriptionException expected) { + // Expected + } + try { db.getMessageStatus(contactId, groupId); fail(); @@ -566,6 +573,13 @@ public class DatabaseComponentImplTest extends BriarTestCase { // Expected } + try { + db.mergeGroupMetadata(groupId, metadata); + fail(); + } catch (NoSuchSubscriptionException expected) { + // Expected + } + try { db.removeGroup(group); fail(); diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java index c0cde9e40dab1358eef222487e6327dd743ae689..d6ad4240f83c2cded1a3d866497525c569c2696e 100644 --- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java +++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java @@ -43,6 +43,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import static java.util.concurrent.TimeUnit.SECONDS; +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; @@ -1031,6 +1032,44 @@ public class H2DatabaseTest extends BriarTestCase { db.close(); } + @Test + public void testGroupMetadata() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group + db.addGroup(txn, group); + + // Attach some metadata to the group + Metadata metadata = new Metadata(); + metadata.put("foo", new byte[]{'b', 'a', 'r'}); + metadata.put("baz", new byte[]{'b', 'a', 'm'}); + db.mergeGroupMetadata(txn, groupId, metadata); + + // Retrieve the metadata for the group + Metadata retrieved = db.getGroupMetadata(txn, groupId); + 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")); + + // Update the metadata + metadata.put("foo", REMOVE); + metadata.put("baz", new byte[] {'q', 'u', 'x'}); + db.mergeGroupMetadata(txn, groupId, metadata); + + // Retrieve the metadata again + retrieved = db.getGroupMetadata(txn, groupId); + assertEquals(1, retrieved.size()); + assertFalse(retrieved.containsKey("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + + db.commitTransaction(txn); + db.close(); + } + @Test public void testMessageMetadata() throws Exception { Database<Connection> db = open(false); @@ -1043,22 +1082,49 @@ public class H2DatabaseTest extends BriarTestCase { // 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(1, retrieved.size()); + 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")); // Retrieve the metadata for the group Map<MessageId, Metadata> all = db.getMessageMetadata(txn, groupId); assertEquals(1, all.size()); assertTrue(all.containsKey(messageId)); retrieved = all.get(messageId); - assertEquals(1, retrieved.size()); + 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")); + + // Update the metadata + metadata.put("foo", REMOVE); + metadata.put("baz", new byte[] {'q', 'u', 'x'}); + db.mergeMessageMetadata(txn, messageId, metadata); + + // Retrieve the metadata again + retrieved = db.getMessageMetadata(txn, messageId); + assertEquals(1, retrieved.size()); + assertFalse(retrieved.containsKey("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + + // Retrieve the metadata for the group again + all = db.getMessageMetadata(txn, groupId); + assertEquals(1, all.size()); + assertTrue(all.containsKey(messageId)); + retrieved = all.get(messageId); + assertEquals(1, retrieved.size()); + assertFalse(retrieved.containsKey("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); db.commitTransaction(txn); db.close();