diff --git a/briar-api/src/org/briarproject/api/clients/ClientHelper.java b/briar-api/src/org/briarproject/api/clients/ClientHelper.java index 06b5c7c5c..ea479a7f6 100644 --- a/briar-api/src/org/briarproject/api/clients/ClientHelper.java +++ b/briar-api/src/org/briarproject/api/clients/ClientHelper.java @@ -27,12 +27,6 @@ public interface ClientHelper { Message createMessage(GroupId g, long timestamp, BdfList body) throws FormatException; - BdfDictionary getMessageAsDictionary(MessageId m) throws DbException, - FormatException; - - BdfDictionary getMessageAsDictionary(Transaction txn, MessageId m) - throws DbException, FormatException; - BdfList getMessageAsList(MessageId m) throws DbException, FormatException; BdfList getMessageAsList(Transaction txn, MessageId m) throws DbException, @@ -50,12 +44,19 @@ public interface ClientHelper { BdfDictionary getMessageMetadataAsDictionary(Transaction txn, MessageId m) throws DbException, FormatException; - Map getMessageMetatataAsDictionary(GroupId g) + Map getMessageMetadataAsDictionary(GroupId g) throws DbException, FormatException; Map getMessageMetadataAsDictionary( Transaction txn, GroupId g) throws DbException, FormatException; + Map getMessageMetadataAsDictionary(GroupId g, + BdfDictionary query) throws DbException, FormatException; + + Map getMessageMetadataAsDictionary( + Transaction txn, GroupId g, BdfDictionary query) throws DbException, + FormatException; + void mergeGroupMetadata(GroupId g, BdfDictionary metadata) throws DbException, FormatException; diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java index eca709bc3..e145d9e84 100644 --- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java +++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java @@ -249,6 +249,16 @@ public interface DatabaseComponent { Map getMessageMetadata(Transaction txn, GroupId g) throws DbException; + /** + * Returns the metadata for any messages in the given group with metadata + * that matches all entries in the given query. If the query is empty, the + * metadata for all messages is returned. + *

+ * Read-only. + */ + Map getMessageMetadata(Transaction txn, GroupId g, + Metadata query) throws DbException; + /** * Returns the metadata for the given message. *

diff --git a/briar-core/src/org/briarproject/clients/ClientHelperImpl.java b/briar-core/src/org/briarproject/clients/ClientHelperImpl.java index 5c64dc98f..61ac38c40 100644 --- a/briar-core/src/org/briarproject/clients/ClientHelperImpl.java +++ b/briar-core/src/org/briarproject/clients/ClientHelperImpl.java @@ -85,29 +85,6 @@ class ClientHelperImpl implements ClientHelper { return messageFactory.createMessage(g, timestamp, toByteArray(body)); } - @Override - public BdfDictionary getMessageAsDictionary(MessageId m) throws DbException, - FormatException { - BdfDictionary dictionary; - Transaction txn = db.startTransaction(true); - try { - dictionary = getMessageAsDictionary(txn, m); - txn.setComplete(); - } finally { - db.endTransaction(txn); - } - return dictionary; - } - - @Override - public BdfDictionary getMessageAsDictionary(Transaction txn, MessageId m) - throws DbException, FormatException { - byte[] raw = db.getRawMessage(txn, m); - if (raw == null) return null; - return toDictionary(raw, MESSAGE_HEADER_LENGTH, - raw.length - MESSAGE_HEADER_LENGTH); - } - @Override public BdfList getMessageAsList(MessageId m) throws DbException, FormatException { @@ -174,7 +151,7 @@ class ClientHelperImpl implements ClientHelper { } @Override - public Map getMessageMetatataAsDictionary( + public Map getMessageMetadataAsDictionary( GroupId g) throws DbException, FormatException { Map map; Transaction txn = db.startTransaction(true); @@ -198,6 +175,34 @@ class ClientHelperImpl implements ClientHelper { return Collections.unmodifiableMap(parsed); } + @Override + public Map getMessageMetadataAsDictionary( + GroupId g, BdfDictionary query) throws DbException, + FormatException { + Map map; + Transaction txn = db.startTransaction(true); + try { + map = getMessageMetadataAsDictionary(txn, g, query); + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + return map; + } + + @Override + public Map getMessageMetadataAsDictionary( + Transaction txn, GroupId g, BdfDictionary query) throws DbException, + FormatException { + Metadata metadata = metadataEncoder.encode(query); + Map raw = db.getMessageMetadata(txn, g, metadata); + Map parsed = + new HashMap(raw.size()); + for (Entry e : raw.entrySet()) + parsed.put(e.getKey(), metadataParser.parse(e.getValue())); + return Collections.unmodifiableMap(parsed); + } + @Override public void mergeGroupMetadata(GroupId g, BdfDictionary metadata) throws DbException, FormatException { diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java index 981da4f28..6589fbc81 100644 --- a/briar-core/src/org/briarproject/db/Database.java +++ b/briar-core/src/org/briarproject/db/Database.java @@ -273,6 +273,16 @@ interface Database { */ Collection getMessageIds(T txn, GroupId g) throws DbException; + /** + * Returns the IDs of any messages in the given group with metadata + * matching all entries in the given query. If the query is empty, the IDs + * of all messages are returned. + *

+ * Read-only. + */ + Collection getMessageIds(T txn, GroupId g, Metadata query) + throws DbException; + /** * Returns the metadata for all messages in the given group. *

@@ -281,6 +291,16 @@ interface Database { Map getMessageMetadata(T txn, GroupId g) throws DbException; + /** + * Returns the metadata for any messages in the given group with metadata + * matching all entries in the given query. If the query is empty, the + * metadata for all messages is returned. + *

+ * Read-only. + */ + Map getMessageMetadata(T txn, GroupId g, + Metadata query) throws DbException; + /** * Returns the metadata for the given message. *

diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java index 45a9c4cbf..37c1f0884 100644 --- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java +++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java @@ -427,6 +427,14 @@ class DatabaseComponentImpl implements DatabaseComponent { return db.getMessageMetadata(txn, g); } + public Map getMessageMetadata(Transaction transaction, + GroupId g, Metadata query) throws DbException { + T txn = unbox(transaction); + if (!db.containsGroup(txn, g)) + throw new NoSuchGroupException(); + return db.getMessageMetadata(txn, g, query); + } + public Metadata getMessageMetadata(Transaction transaction, MessageId m) throws DbException { T txn = unbox(transaction); diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java index d46221934..e3e026f22 100644 --- a/briar-core/src/org/briarproject/db/JdbcDatabase.java +++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java @@ -36,10 +36,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -1133,6 +1135,44 @@ abstract class JdbcDatabase implements Database { } } + public Collection getMessageIds(Connection txn, GroupId g, + Metadata query) throws DbException { + // If there are no query terms, return all messages + if (query.isEmpty()) return getMessageIds(txn, g); + PreparedStatement ps = null; + ResultSet rs = null; + try { + // Retrieve the message IDs for each query term and intersect + Set intersection = null; + String sql = "SELECT m.messageId" + + " FROM messages AS m" + + " JOIN messageMetadata AS md" + + " ON m.messageId = md.messageId" + + " WHERE groupId = ? AND key = ? AND value = ?"; + for (Entry e : query.entrySet()) { + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + ps.setString(2, e.getKey()); + ps.setBytes(3, e.getValue()); + rs = ps.executeQuery(); + Set ids = new HashSet(); + while (rs.next()) ids.add(new MessageId(rs.getBytes(1))); + rs.close(); + ps.close(); + if (intersection == null) intersection = ids; + else intersection.retainAll(ids); + // Return early if there are no matches + if (intersection.isEmpty()) return Collections.emptySet(); + } + if (intersection == null) throw new IllegalStateException(); + return Collections.unmodifiableSet(intersection); + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public Map getMessageMetadata(Connection txn, GroupId g) throws DbException { PreparedStatement ps = null; @@ -1169,6 +1209,18 @@ abstract class JdbcDatabase implements Database { } } + public Map getMessageMetadata(Connection txn, + GroupId g, Metadata query) throws DbException { + // Retrieve the matching message IDs + Collection matches = getMessageIds(txn, g, query); + if (matches.isEmpty()) return Collections.emptyMap(); + // Retrieve the metadata for each match + Map all = new HashMap( + matches.size()); + for (MessageId m : matches) all.put(m, getMessageMetadata(txn, m)); + return Collections.unmodifiableMap(all); + } + public Metadata getGroupMetadata(Connection txn, GroupId g) throws DbException { return getMetadata(txn, g.getBytes(), "groupMetadata", "groupId"); diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java index 97b08d900..ff6f7a918 100644 --- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java +++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java @@ -115,8 +115,8 @@ public class DatabaseComponentImplTest extends BriarTestCase { private DatabaseComponent createDatabaseComponent(Database database, EventBus eventBus, ShutdownManager shutdown) { - return new DatabaseComponentImpl(database, Object.class, - eventBus, shutdown); + return new DatabaseComponentImpl<>(database, Object.class, eventBus, + shutdown); } @Test @@ -559,11 +559,11 @@ public class DatabaseComponentImplTest extends BriarTestCase { final EventBus eventBus = context.mock(EventBus.class); context.checking(new Expectations() {{ // Check whether the group is in the DB (which it's not) - exactly(7).of(database).startTransaction(); + exactly(9).of(database).startTransaction(); will(returnValue(txn)); - exactly(7).of(database).containsGroup(txn, groupId); + exactly(9).of(database).containsGroup(txn, groupId); will(returnValue(false)); - exactly(7).of(database).abortTransaction(txn); + exactly(9).of(database).abortTransaction(txn); // This is needed for getMessageStatus(), isVisibleToContact(), and // setVisibleToContact() to proceed exactly(3).of(database).containsContact(txn, contactId); @@ -592,6 +592,26 @@ public class DatabaseComponentImplTest extends BriarTestCase { db.endTransaction(transaction); } + transaction = db.startTransaction(false); + try { + db.getMessageMetadata(transaction, groupId); + fail(); + } catch (NoSuchGroupException expected) { + // Expected + } finally { + db.endTransaction(transaction); + } + + transaction = db.startTransaction(false); + try { + db.getMessageMetadata(transaction, groupId, new Metadata()); + fail(); + } catch (NoSuchGroupException expected) { + // Expected + } finally { + db.endTransaction(transaction); + } + transaction = db.startTransaction(false); try { db.getMessageStatus(transaction, contactId, groupId); diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java index 24ffe5308..d967b8159 100644 --- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java +++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java @@ -595,7 +595,7 @@ public class H2DatabaseTest extends BriarTestCase { @Test public void testMultipleGroupChanges() throws Exception { // Create some groups - List groups = new ArrayList(); + List groups = new ArrayList<>(); for (int i = 0; i < 100; i++) { GroupId id = new GroupId(TestUtils.getRandomId()); ClientId clientId = new ClientId(TestUtils.getRandomId()); @@ -803,7 +803,7 @@ public class H2DatabaseTest extends BriarTestCase { assertEquals(0, db.countOfferedMessages(txn, contactId)); // Add some offered messages and count them - List ids = new ArrayList(); + List ids = new ArrayList<>(); for (int i = 0; i < 10; i++) { MessageId m = new MessageId(TestUtils.getRandomId()); db.addOfferedMessage(txn, contactId, m); @@ -930,6 +930,110 @@ public class H2DatabaseTest extends BriarTestCase { db.close(); } + @Test + public void testMetadataQueries() throws Exception { + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new Message(messageId1, groupId, timestamp, raw); + + Database db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and two messages + db.addGroup(txn, group); + db.addMessage(txn, message, VALID, true); + db.addMessage(txn, message1, VALID, true); + + // Attach some metadata to the messages + 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); + Metadata metadata1 = new Metadata(); + metadata1.put("foo", new byte[]{'q', 'u', 'x'}); + db.mergeMessageMetadata(txn, messageId1, metadata1); + + // Retrieve all the metadata for the group + Map all = db.getMessageMetadata(txn, groupId); + assertEquals(2, all.size()); + assertTrue(all.containsKey(messageId)); + assertTrue(all.containsKey(messageId1)); + Metadata retrieved = all.get(messageId); + 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")); + retrieved = all.get(messageId1); + assertEquals(1, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata1.get("foo"), retrieved.get("foo")); + + // Query the metadata with an empty query + Metadata query = new Metadata(); + all = db.getMessageMetadata(txn, groupId, query); + assertEquals(2, all.size()); + assertTrue(all.containsKey(messageId)); + assertTrue(all.containsKey(messageId1)); + retrieved = all.get(messageId); + 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")); + retrieved = all.get(messageId1); + assertEquals(1, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata1.get("foo"), retrieved.get("foo")); + + // Use a single-term query that matches the first message + query = new Metadata(); + query.put("foo", metadata.get("foo")); + all = db.getMessageMetadata(txn, groupId, query); + assertEquals(1, all.size()); + assertTrue(all.containsKey(messageId)); + retrieved = all.get(messageId); + 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")); + + // Use a single-term query that matches the second message + query = new Metadata(); + query.put("foo", metadata1.get("foo")); + all = db.getMessageMetadata(txn, groupId, query); + assertEquals(1, all.size()); + assertTrue(all.containsKey(messageId1)); + retrieved = all.get(messageId1); + assertEquals(1, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata1.get("foo"), retrieved.get("foo")); + + // Use a multi-term query that matches the first message + query = new Metadata(); + query.put("foo", metadata.get("foo")); + query.put("baz", metadata.get("baz")); + all = db.getMessageMetadata(txn, groupId, query); + assertEquals(1, all.size()); + assertTrue(all.containsKey(messageId)); + retrieved = all.get(messageId); + 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")); + + // Use a multi-term query that doesn't match any messages + query = new Metadata(); + query.put("foo", metadata1.get("foo")); + query.put("baz", metadata.get("baz")); + all = db.getMessageMetadata(txn, groupId, query); + assertTrue(all.isEmpty()); + + db.commitTransaction(txn); + db.close(); + } + @Test public void testGetMessageStatus() throws Exception { Database db = open(false);