mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
Merge branch '222-simple-metadata-queries' into 'master'
Simple metadata queries Adds support for retrieving metadata that matches all key/value pairs in a query object. This is a minimal version of #222 that can be extended if we need other query capabilities. See merge request !187
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user