Merge branch '221-group-metadata' into 'master'

Metadata for groups. #221



See merge request !69
This commit is contained in:
akwizgran
2016-01-20 16:54:20 +00:00
7 changed files with 207 additions and 27 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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

View File

@@ -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();

View File

@@ -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();