diff --git a/briar-api/src/org/briarproject/api/clients/ClientHelper.java b/briar-api/src/org/briarproject/api/clients/ClientHelper.java index 06b5c7c5cb3bfea4ec8d747e633c883e8cb1c7bc..ea479a7f6e6a89ec07382e49724d9a588b40a1cb 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<MessageId, BdfDictionary> getMessageMetatataAsDictionary(GroupId g) + Map<MessageId, BdfDictionary> getMessageMetadataAsDictionary(GroupId g) throws DbException, FormatException; Map<MessageId, BdfDictionary> getMessageMetadataAsDictionary( Transaction txn, GroupId g) throws DbException, FormatException; + Map<MessageId, BdfDictionary> getMessageMetadataAsDictionary(GroupId g, + BdfDictionary query) throws DbException, FormatException; + + Map<MessageId, BdfDictionary> 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 eca709bc3af17985b32dab4f5c03ac03d80c9889..e145d9e8442fd022e6ee1424429950231a658cf0 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<MessageId, Metadata> 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. + * <p/> + * Read-only. + */ + Map<MessageId, Metadata> getMessageMetadata(Transaction txn, GroupId g, + Metadata query) throws DbException; + /** * Returns the metadata for the given message. * <p/> diff --git a/briar-core/src/org/briarproject/clients/ClientHelperImpl.java b/briar-core/src/org/briarproject/clients/ClientHelperImpl.java index 5c64dc98feba454d705a5703587f315c2d39bae2..61ac38c40dd77cf74194101bcb900f274f0f38c6 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<MessageId, BdfDictionary> getMessageMetatataAsDictionary( + public Map<MessageId, BdfDictionary> getMessageMetadataAsDictionary( GroupId g) throws DbException, FormatException { Map<MessageId, BdfDictionary> map; Transaction txn = db.startTransaction(true); @@ -198,6 +175,34 @@ class ClientHelperImpl implements ClientHelper { return Collections.unmodifiableMap(parsed); } + @Override + public Map<MessageId, BdfDictionary> getMessageMetadataAsDictionary( + GroupId g, BdfDictionary query) throws DbException, + FormatException { + Map<MessageId, BdfDictionary> map; + Transaction txn = db.startTransaction(true); + try { + map = getMessageMetadataAsDictionary(txn, g, query); + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + return map; + } + + @Override + public Map<MessageId, BdfDictionary> getMessageMetadataAsDictionary( + Transaction txn, GroupId g, BdfDictionary query) throws DbException, + FormatException { + Metadata metadata = metadataEncoder.encode(query); + Map<MessageId, Metadata> raw = db.getMessageMetadata(txn, g, metadata); + Map<MessageId, BdfDictionary> parsed = + new HashMap<MessageId, BdfDictionary>(raw.size()); + for (Entry<MessageId, Metadata> 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 981da4f285fd5e97f83926d40fe25fb5ed106d53..6589fbc81477147535e4af61402ae8944b2d8963 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<T> { */ Collection<MessageId> 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. + * <p/> + * Read-only. + */ + Collection<MessageId> getMessageIds(T txn, GroupId g, Metadata query) + throws DbException; + /** * Returns the metadata for all messages in the given group. * <p/> @@ -281,6 +291,16 @@ interface Database<T> { Map<MessageId, Metadata> 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. + * <p/> + * Read-only. + */ + Map<MessageId, Metadata> getMessageMetadata(T txn, GroupId g, + Metadata query) throws DbException; + /** * Returns the metadata for the given message. * <p/> diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java index 45a9c4cbf41c319125aa2530e8e5b7210031f03b..37c1f08849656f65599113db5264d502f1171928 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<T> implements DatabaseComponent { return db.getMessageMetadata(txn, g); } + public Map<MessageId, Metadata> 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 d46221934ba9dc18876bc031e4e9324efa635edf..e3e026f221b757029d6542927878bc8ed52e213c 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<Connection> { } } + public Collection<MessageId> 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<MessageId> 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<String, byte[]> 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<MessageId> ids = new HashSet<MessageId>(); + 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<MessageId, Metadata> getMessageMetadata(Connection txn, GroupId g) throws DbException { PreparedStatement ps = null; @@ -1169,6 +1209,18 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public Map<MessageId, Metadata> getMessageMetadata(Connection txn, + GroupId g, Metadata query) throws DbException { + // Retrieve the matching message IDs + Collection<MessageId> matches = getMessageIds(txn, g, query); + if (matches.isEmpty()) return Collections.emptyMap(); + // Retrieve the metadata for each match + Map<MessageId, Metadata> all = new HashMap<MessageId, Metadata>( + 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 97b08d900f5936118a5ea410019bcbec0a9976a9..ff6f7a918cc75941551be6c222aa64bade8358c5 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<Object> database, EventBus eventBus, ShutdownManager shutdown) { - return new DatabaseComponentImpl<Object>(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 24ffe530807fd9a145c7bfd28ffb59f89e167124..d967b815973b6bb6246dc23d6075b5621522e7b7 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<Group> groups = new ArrayList<Group>(); + List<Group> 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<MessageId> ids = new ArrayList<MessageId>(); + List<MessageId> 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<Connection> 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<MessageId, Metadata> 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<Connection> db = open(false);