diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java index 465a886bd7d4b2f8c8189d0bafdeb4068e215b9c..5609844f42a6c8337dbdc0b30d07b894c2b93773 100644 --- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java +++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java @@ -63,6 +63,15 @@ public interface DatabaseComponent { */ void addTransportKeys(ContactId c, TransportKeys k) throws DbException; + /** + * Deletes the message with the given ID. The message ID and any other + * associated data are not deleted. + */ + void deleteMessage(MessageId m) throws DbException; + + /** Deletes any metadata associated with the given message. */ + void deleteMessageMetadata(MessageId m) throws DbException; + /** * Returns an acknowledgement for the given contact, or null if there are * no messages to acknowledge. diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java index a5a22b135cd93aeae3bcd274ee09192af0220e7c..934156308b6b53ef99322206b16f0f642ce41bad 100644 --- a/briar-core/src/org/briarproject/db/Database.java +++ b/briar-core/src/org/briarproject/db/Database.java @@ -215,6 +215,23 @@ interface Database<T> { */ int countOfferedMessages(T txn, ContactId c) throws DbException; + /** + * Deletes the message with the given ID. Unlike + * {@link #removeMessage(Object, MessageId)}, the message ID and any other + * associated data are not deleted, and + * {@link #containsMessage(Object, MessageId)} will continue to return true. + * <p> + * Locking: write. + */ + void deleteMessage(T txn, MessageId m) throws DbException; + + /** + * Deletes any metadata associated with the given message. + * <p> + * Locking: write. + */ + void deleteMessageMetadata(T txn, MessageId m) throws DbException; + /** * Returns the contact with the given ID. * <p> diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java index 9e929d86c83865c158c4c58e3a4cab9f4b191109..0a974ed8edb9095e56ea94fabdf635df4258f617 100644 --- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java +++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java @@ -293,6 +293,40 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } + public void deleteMessage(MessageId m) throws DbException { + lock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if (!db.containsMessage(txn, m)) + throw new NoSuchMessageException(); + db.deleteMessage(txn, m); + } catch (DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + lock.writeLock().unlock(); + } + } + + public void deleteMessageMetadata(MessageId m) throws DbException { + lock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if (!db.containsMessage(txn, m)) + throw new NoSuchMessageException(); + db.deleteMessageMetadata(txn, m); + } catch (DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + lock.writeLock().unlock(); + } + } + public Ack generateAck(ContactId c, int maxMessages) throws DbException { Collection<MessageId> ids; lock.writeLock().lock(); diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java index 79c07da039a7ec53fe8a477c4d99be11dbfa76b9..ac583594bd27844440519e3c9256cab770b06409 100644 --- a/briar-core/src/org/briarproject/db/JdbcDatabase.java +++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java @@ -136,7 +136,7 @@ abstract class JdbcDatabase implements Database<Connection> { + " valid INT NOT NULL," + " shared BOOLEAN NOT NULL," + " length INT NOT NULL," - + " raw BLOB NOT NULL," + + " raw BLOB," // Null if message has been deleted + " PRIMARY KEY (messageId)," + " FOREIGN KEY (groupId)" + " REFERENCES groups (groupId)" @@ -933,6 +933,38 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public void deleteMessage(Connection txn, MessageId m) throws DbException { + PreparedStatement ps = null; + try { + String sql = "UPDATE messages SET raw = NULL WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + int affected = ps.executeUpdate(); + if (affected < 0) throw new DbStateException(); + if (affected > 1) throw new DbStateException(); + ps.close(); + } catch (SQLException e) { + tryToClose(ps); + throw new DbException(e); + } + } + + public void deleteMessageMetadata(Connection txn, MessageId m) + throws DbException { + PreparedStatement ps = null; + try { + String sql = "DELETE FROM messageMetadata WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + int affected = ps.executeUpdate(); + if (affected < 0) throw new DbStateException(); + ps.close(); + } catch (SQLException e) { + tryToClose(ps); + throw new DbException(e); + } + } + public Contact getContact(Connection txn, ContactId c) throws DbException { PreparedStatement ps = null; ResultSet rs = null; @@ -1308,7 +1340,7 @@ abstract class JdbcDatabase implements Database<Connection> { + " ON m.messageId = s.messageId" + " AND gv.contactId = s.contactId" + " WHERE gv.contactId = ?" - + " AND valid = ? AND shared = TRUE" + + " AND valid = ? AND shared = TRUE AND raw IS NOT NULL" + " AND seen = FALSE AND requested = FALSE" + " AND s.expiry < ?" + " ORDER BY timestamp DESC LIMIT ?"; @@ -1367,7 +1399,7 @@ abstract class JdbcDatabase implements Database<Connection> { + " ON m.messageId = s.messageId" + " AND gv.contactId = s.contactId" + " WHERE gv.contactId = ?" - + " AND valid = ? AND shared = TRUE" + + " AND valid = ? AND shared = TRUE AND raw IS NOT NULL" + " AND seen = FALSE" + " AND s.expiry < ?" + " ORDER BY timestamp DESC"; @@ -1401,7 +1433,7 @@ abstract class JdbcDatabase implements Database<Connection> { try { String sql = "SELECT messageId FROM messages AS m" + " JOIN groups AS g ON m.groupId = g.groupId" - + " WHERE valid = ? AND clientId = ?"; + + " WHERE valid = ? AND clientId = ? AND raw IS NOT NULL"; ps = txn.prepareStatement(sql); ps.setInt(1, UNKNOWN.getValue()); ps.setBytes(2, c.getBytes()); @@ -1453,7 +1485,7 @@ abstract class JdbcDatabase implements Database<Connection> { + " ON m.messageId = s.messageId" + " AND gv.contactId = s.contactId" + " WHERE gv.contactId = ?" - + " AND valid = ? AND shared = TRUE" + + " AND valid = ? AND shared = TRUE AND raw IS NOT NULL" + " AND seen = FALSE AND requested = TRUE" + " AND s.expiry < ?" + " ORDER BY timestamp DESC"; diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java index b0930805f3613372bff049863e98c2ac5bf43015..70a9113f23c5de966476e7efbb867f0d94236bc7 100644 --- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java +++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java @@ -543,11 +543,11 @@ public class DatabaseComponentImplTest extends BriarTestCase { final EventBus eventBus = context.mock(EventBus.class); context.checking(new Expectations() {{ // Check whether the message is in the DB (which it's not) - exactly(6).of(database).startTransaction(); + exactly(8).of(database).startTransaction(); will(returnValue(txn)); - exactly(6).of(database).containsMessage(txn, messageId); + exactly(8).of(database).containsMessage(txn, messageId); will(returnValue(false)); - exactly(6).of(database).abortTransaction(txn); + exactly(8).of(database).abortTransaction(txn); // This is needed for getMessageStatus() to proceed exactly(1).of(database).containsContact(txn, contactId); will(returnValue(true)); @@ -555,6 +555,20 @@ public class DatabaseComponentImplTest extends BriarTestCase { DatabaseComponent db = createDatabaseComponent(database, eventBus, shutdown); + try { + db.deleteMessage(messageId); + fail(); + } catch (NoSuchMessageException expected) { + // Expected + } + + try { + db.deleteMessageMetadata(messageId); + fail(); + } catch (NoSuchMessageException expected) { + // Expected + } + try { db.getRawMessage(messageId); fail(); diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java index 2fe48c6c3e4f2240e5d00c09431581b400cc2882..1e880356b2adddadf63a084acb8b6a7223f5c4a8 100644 --- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java +++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java @@ -52,6 +52,8 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -937,6 +939,17 @@ public class H2DatabaseTest extends BriarTestCase { assertTrue(retrieved.containsKey("baz")); assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + // Delete the metadata + db.deleteMessageMetadata(txn, messageId); + + // Retrieve the metadata again + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + + // Retrieve the metadata for the group again + all = db.getMessageMetadata(txn, groupId); + assertTrue(all.isEmpty()); + db.commitTransaction(txn); db.close(); } @@ -1071,6 +1084,51 @@ public class H2DatabaseTest extends BriarTestCase { db.close(); } + @Test + public void testDeleteMessage() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, a group and a message + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId)); + db.addGroup(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addMessage(txn, message, VALID, true); + db.addStatus(txn, contactId, messageId, false, false); + + // The message should be visible to the contact + assertTrue(db.containsVisibleMessage(txn, contactId, messageId)); + + // The message should be sendable + Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, + ONE_MEGABYTE); + assertEquals(Collections.singletonList(messageId), ids); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertEquals(Collections.singletonList(messageId), ids); + + // The raw message should not be null + assertNotNull(db.getRawMessage(txn, messageId)); + + // Delete the message + db.deleteMessage(txn, messageId); + + // The message should be visible to the contact + assertTrue(db.containsVisibleMessage(txn, contactId, messageId)); + + // The message should not be sendable + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // The raw message should be null + assertNull(db.getRawMessage(txn, messageId)); + + db.commitTransaction(txn); + db.close(); + } + @Test public void testExceptionHandling() throws Exception { Database<Connection> db = open(false);