mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-13 03:09:04 +01:00
Metadata for groups. #221
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user