From c7e496230b16405fa032340ed149b05c7d8be3de Mon Sep 17 00:00:00 2001 From: akwizgran Date: Fri, 2 Feb 2018 16:44:35 +0000 Subject: [PATCH 1/3] Add denormalised columns to statuses table. --- .../org/briarproject/bramble/db/Database.java | 32 +-- .../bramble/db/DatabaseComponentImpl.java | 33 +-- .../briarproject/bramble/db/JdbcDatabase.java | 237 ++++++++++++------ .../bramble/db/DatabaseComponentImplTest.java | 60 +++-- .../bramble/db/H2DatabaseTest.java | 123 ++++----- 5 files changed, 263 insertions(+), 222 deletions(-) diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java index 98a610ffe..0a6e54fbd 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java @@ -97,9 +97,12 @@ interface Database { /** * Stores a message. + * + * @param sender the contact from whom the message was received, or null + * if the message was created locally. */ - void addMessage(T txn, Message m, State state, boolean shared) - throws DbException; + void addMessage(T txn, Message m, State state, boolean shared, + @Nullable ContactId sender) throws DbException; /** * Adds a dependency between two messages in the given group. @@ -112,16 +115,6 @@ interface Database { */ void addOfferedMessage(T txn, ContactId c, MessageId m) throws DbException; - /** - * Initialises the status of the given message with respect to the given - * contact. - * - * @param ack whether the message needs to be acknowledged. - * @param seen whether the contact has seen the message. - */ - void addStatus(T txn, ContactId c, MessageId m, boolean ack, boolean seen) - throws DbException; - /** * Stores a transport. */ @@ -280,7 +273,7 @@ interface Database { *

* Read-only. */ - Collection getGroupVisibility(T txn, GroupId g) + Map getGroupVisibility(T txn, GroupId g) throws DbException; /** @@ -584,13 +577,6 @@ interface Database { */ void removeMessage(T txn, MessageId m) throws DbException; - /** - * Removes an offered message that was offered by the given contact, or - * returns false if there is no such message. - */ - boolean removeOfferedMessage(T txn, ContactId c, MessageId m) - throws DbException; - /** * Removes the given offered messages that were offered by the given * contact. @@ -598,12 +584,6 @@ interface Database { void removeOfferedMessages(T txn, ContactId c, Collection requested) throws DbException; - /** - * Removes the status of the given message with respect to the given - * contact. - */ - void removeStatus(T txn, ContactId c, MessageId m) throws DbException; - /** * Removes a transport (and all associated state) from the database. */ diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java index 80e42c8d7..5809c09ee 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java @@ -215,7 +215,7 @@ class DatabaseComponentImpl implements DatabaseComponent { if (!db.containsGroup(txn, m.getGroupId())) throw new NoSuchGroupException(); if (!db.containsMessage(txn, m.getId())) { - addMessage(txn, m, DELIVERED, shared, null); + db.addMessage(txn, m, DELIVERED, shared, null); transaction.attach(new MessageAddedEvent(m, null)); transaction.attach(new MessageStateChangedEvent(m.getId(), true, DELIVERED)); @@ -224,16 +224,6 @@ class DatabaseComponentImpl implements DatabaseComponent { db.mergeMessageMetadata(txn, m.getId(), meta); } - private void addMessage(T txn, Message m, State state, boolean shared, - @Nullable ContactId sender) throws DbException { - db.addMessage(txn, m, state, shared); - for (ContactId c : db.getGroupVisibility(txn, m.getGroupId())) { - boolean offered = db.removeOfferedMessage(txn, c, m.getId()); - boolean seen = offered || (sender != null && c.equals(sender)); - db.addStatus(txn, c, m.getId(), seen, seen); - } - } - @Override public void addTransport(Transaction transaction, TransportId t, int maxLatency) throws DbException { @@ -682,7 +672,7 @@ class DatabaseComponentImpl implements DatabaseComponent { db.raiseSeenFlag(txn, c, m.getId()); db.raiseAckFlag(txn, c, m.getId()); } else { - addMessage(txn, m, UNKNOWN, false, c); + db.addMessage(txn, m, UNKNOWN, false, c); transaction.attach(new MessageAddedEvent(m, c)); } transaction.attach(new MessageToAckEvent(c)); @@ -750,7 +740,8 @@ class DatabaseComponentImpl implements DatabaseComponent { GroupId id = g.getId(); if (!db.containsGroup(txn, id)) throw new NoSuchGroupException(); - Collection affected = db.getGroupVisibility(txn, id); + Collection affected = + db.getGroupVisibility(txn, id).keySet(); db.removeGroup(txn, id); transaction.attach(new GroupRemovedEvent(g)); transaction.attach(new GroupVisibilityUpdatedEvent(affected)); @@ -820,19 +811,9 @@ class DatabaseComponentImpl implements DatabaseComponent { throw new NoSuchGroupException(); Visibility old = db.getGroupVisibility(txn, c, g); if (old == v) return; - if (old == INVISIBLE) { - db.addGroupVisibility(txn, c, g, v == SHARED); - for (MessageId m : db.getMessageIds(txn, g)) { - boolean seen = db.removeOfferedMessage(txn, c, m); - db.addStatus(txn, c, m, seen, seen); - } - } else if (v == INVISIBLE) { - db.removeGroupVisibility(txn, c, g); - for (MessageId m : db.getMessageIds(txn, g)) - db.removeStatus(txn, c, m); - } else { - db.setGroupVisibility(txn, c, g, v == SHARED); - } + if (old == INVISIBLE) db.addGroupVisibility(txn, c, g, v == SHARED); + else if (v == INVISIBLE) db.removeGroupVisibility(txn, c, g); + else db.setGroupVisibility(txn, c, g, v == SHARED); List affected = Collections.singletonList(c); transaction.attach(new GroupVisibilityUpdatedEvent(affected)); } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java index a397213e1..790f16e91 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java @@ -72,7 +72,7 @@ import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry; abstract class JdbcDatabase implements Database { // Package access for testing - static final int CODE_SCHEMA_VERSION = 31; + static final int CODE_SCHEMA_VERSION = 32; private static final String CREATE_SETTINGS = "CREATE TABLE settings" @@ -188,6 +188,13 @@ abstract class JdbcDatabase implements Database { "CREATE TABLE statuses" + " (messageId HASH NOT NULL," + " contactId INT NOT NULL," + + " groupId HASH NOT NULL," // Denormalised + + " timestamp BIGINT NOT NULL," // Denormalised + + " length INT NOT NULL," // Denormalised + + " state INT NOT NULL," // Denormalised + + " groupShared BOOLEAN NOT NULL," // Denormalised + + " messageShared BOOLEAN NOT NULL," // Denormalised + + " deleted BOOLEAN NOT NULL," // Denormalised + " ack BOOLEAN NOT NULL," + " seen BOOLEAN NOT NULL," + " requested BOOLEAN NOT NULL," @@ -199,6 +206,9 @@ abstract class JdbcDatabase implements Database { + " ON DELETE CASCADE," + " FOREIGN KEY (contactId)" + " REFERENCES contacts (contactId)" + + " ON DELETE CASCADE," + + " FOREIGN KEY (groupId)" + + " REFERENCES groups (groupId)" + " ON DELETE CASCADE)"; private static final String CREATE_TRANSPORTS = @@ -252,6 +262,14 @@ abstract class JdbcDatabase implements Database { "CREATE INDEX IF NOT EXISTS messageMetadataByGroupIdState" + " ON messageMetadata (groupId, state)"; + private static final String INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID = + "CREATE INDEX IF NOT EXISTS statusesByContactIdGroupId" + + " ON statuses (contactId, groupId)"; + + private static final String INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP = + "CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp" + + " ON statuses (contactId, timestamp)"; + private static final Logger LOG = Logger.getLogger(JdbcDatabase.class.getName()); @@ -401,6 +419,8 @@ abstract class JdbcDatabase implements Database { s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID); s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID); s.executeUpdate(INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE); + s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID); + s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP); s.close(); } catch (SQLException e) { tryToClose(s); @@ -578,7 +598,7 @@ abstract class JdbcDatabase implements Database { @Override public void addGroupVisibility(Connection txn, ContactId c, GroupId g, - boolean shared) throws DbException { + boolean groupShared) throws DbException { PreparedStatement ps = null; try { String sql = "INSERT INTO groupVisibilities" @@ -587,16 +607,50 @@ abstract class JdbcDatabase implements Database { ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); ps.setBytes(2, g.getBytes()); - ps.setBoolean(3, shared); + ps.setBoolean(3, groupShared); int affected = ps.executeUpdate(); if (affected != 1) throw new DbStateException(); ps.close(); + // Create a status row for each message in the group + addStatus(txn, c, g, groupShared); } catch (SQLException e) { tryToClose(ps); throw new DbException(e); } } + private void addStatus(Connection txn, ContactId c, GroupId g, + boolean groupShared) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT messageId, timestamp, state, shared," + + " length, raw IS NULL" + + " FROM messages" + + " WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + rs = ps.executeQuery(); + while (rs.next()) { + MessageId id = new MessageId(rs.getBytes(1)); + long timestamp = rs.getLong(2); + State state = State.fromValue(rs.getInt(3)); + boolean messageShared = rs.getBoolean(4); + int length = rs.getInt(5); + boolean deleted = rs.getBoolean(6); + boolean seen = removeOfferedMessage(txn, c, id); + addStatus(txn, id, c, g, timestamp, length, state, groupShared, + messageShared, deleted, seen); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + @Override public void addLocalAuthor(Connection txn, LocalAuthor a) throws DbException { @@ -622,7 +676,8 @@ abstract class JdbcDatabase implements Database { @Override public void addMessage(Connection txn, Message m, State state, - boolean shared) throws DbException { + boolean messageShared, @Nullable ContactId sender) + throws DbException { PreparedStatement ps = null; try { String sql = "INSERT INTO messages (messageId, groupId, timestamp," @@ -633,13 +688,24 @@ abstract class JdbcDatabase implements Database { ps.setBytes(2, m.getGroupId().getBytes()); ps.setLong(3, m.getTimestamp()); ps.setInt(4, state.getValue()); - ps.setBoolean(5, shared); + ps.setBoolean(5, messageShared); byte[] raw = m.getRaw(); ps.setInt(6, raw.length); ps.setBytes(7, raw); int affected = ps.executeUpdate(); if (affected != 1) throw new DbStateException(); ps.close(); + // Create a status row for each contact that can see the group + Map visibility = + getGroupVisibility(txn, m.getGroupId()); + for (Entry e : visibility.entrySet()) { + ContactId c = e.getKey(); + boolean offered = removeOfferedMessage(txn, c, m.getId()); + boolean seen = offered || (sender != null && c.equals(sender)); + addStatus(txn, m.getId(), c, m.getGroupId(), m.getTimestamp(), + m.getLength(), state, e.getValue(), messageShared, + false, seen); + } } catch (SQLException e) { tryToClose(ps); throw new DbException(e); @@ -677,19 +743,28 @@ abstract class JdbcDatabase implements Database { } } - @Override - public void addStatus(Connection txn, ContactId c, MessageId m, boolean ack, - boolean seen) throws DbException { + private void addStatus(Connection txn, MessageId m, ContactId c, GroupId g, + long timestamp, int length, State state, boolean groupShared, + boolean messageShared, boolean deleted, boolean seen) + throws DbException { PreparedStatement ps = null; try { - String sql = "INSERT INTO statuses (messageId, contactId, ack," - + " seen, requested, expiry, txCount)" - + " VALUES (?, ?, ?, ?, FALSE, 0, 0)"; + String sql = "INSERT INTO statuses (messageId, contactId, groupId," + + " timestamp, length, state, groupShared, messageShared," + + " deleted, ack, seen, requested, expiry, txCount)" + + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE, 0, 0)"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getBytes()); ps.setInt(2, c.getInt()); - ps.setBoolean(3, ack); - ps.setBoolean(4, seen); + ps.setBytes(3, g.getBytes()); + ps.setLong(4, timestamp); + ps.setInt(5, length); + ps.setInt(6, state.getValue()); + ps.setBoolean(7, groupShared); + ps.setBoolean(8, messageShared); + ps.setBoolean(9, deleted); + ps.setBoolean(10, seen); + ps.setBoolean(11, seen); int affected = ps.executeUpdate(); if (affected != 1) throw new DbStateException(); ps.close(); @@ -941,12 +1016,9 @@ abstract class JdbcDatabase implements Database { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT NULL FROM messages AS m" - + " JOIN groupVisibilities AS gv" - + " ON m.groupId = gv.groupId" - + " WHERE messageId = ?" - + " AND contactId = ?" - + " AND m.shared = TRUE"; + String sql = "SELECT NULL FROM statuses" + + " WHERE messageId = ? AND contactId = ?" + + " AND messageShared = TRUE"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getBytes()); ps.setInt(2, c.getInt()); @@ -998,6 +1070,13 @@ abstract class JdbcDatabase implements Database { if (affected < 0) throw new DbStateException(); if (affected > 1) throw new DbStateException(); ps.close(); + // Update denormalised column in statuses + sql = "UPDATE statuses SET deleted = TRUE WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + affected = ps.executeUpdate(); + if (affected < 0) throw new DbStateException(); + ps.close(); } catch (SQLException e) { tryToClose(ps); throw new DbException(e); @@ -1220,18 +1299,19 @@ abstract class JdbcDatabase implements Database { } @Override - public Collection getGroupVisibility(Connection txn, GroupId g) + public Map getGroupVisibility(Connection txn, GroupId g) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT contactId FROM groupVisibilities" + String sql = "SELECT contactId, shared FROM groupVisibilities" + " WHERE groupId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getBytes()); rs = ps.executeQuery(); - List visible = new ArrayList<>(); - while (rs.next()) visible.add(new ContactId(rs.getInt(1))); + Map visible = new HashMap<>(); + while (rs.next()) + visible.put(new ContactId(rs.getInt(1)), rs.getBoolean(2)); rs.close(); ps.close(); return visible; @@ -1509,12 +1589,8 @@ abstract class JdbcDatabase implements Database { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT m.messageId, txCount > 0, seen" - + " FROM messages AS m" - + " JOIN statuses AS s" - + " ON m.messageId = s.messageId" - + " WHERE groupId = ?" - + " AND contactId = ?"; + String sql = "SELECT messageId, txCount > 0, seen FROM statuses" + + " WHERE groupId = ? AND contactId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getBytes()); ps.setInt(2, c.getInt()); @@ -1537,15 +1613,13 @@ abstract class JdbcDatabase implements Database { } @Override - public MessageStatus getMessageStatus(Connection txn, - ContactId c, MessageId m) throws DbException { + public MessageStatus getMessageStatus(Connection txn, ContactId c, + MessageId m) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT txCount > 0, seen" - + " FROM statuses" - + " WHERE messageId = ?" - + " AND contactId = ?"; + String sql = "SELECT txCount > 0, seen FROM statuses" + + " WHERE messageId = ? AND contactId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getBytes()); ps.setInt(2, c.getInt()); @@ -1687,14 +1761,10 @@ abstract class JdbcDatabase implements Database { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT m.messageId FROM messages AS m" - + " JOIN groupVisibilities AS gv" - + " ON m.groupId = gv.groupId" - + " JOIN statuses AS s" - + " ON m.messageId = s.messageId" - + " AND gv.contactId = s.contactId" - + " WHERE gv.contactId = ? AND gv.shared = TRUE" - + " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL" + String sql = "SELECT messageId FROM statuses" + + " WHERE contactId = ? AND state = ?" + + " AND groupShared = TRUE AND messageShared = TRUE" + + " AND deleted = FALSE" + " AND seen = FALSE AND requested = FALSE" + " AND expiry < ?" + " ORDER BY timestamp LIMIT ?"; @@ -1748,14 +1818,10 @@ abstract class JdbcDatabase implements Database { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT length, m.messageId FROM messages AS m" - + " JOIN groupVisibilities AS gv" - + " ON m.groupId = gv.groupId" - + " JOIN statuses AS s" - + " ON m.messageId = s.messageId" - + " AND gv.contactId = s.contactId" - + " WHERE gv.contactId = ? AND gv.shared = TRUE" - + " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL" + String sql = "SELECT length, messageId FROM statuses" + + " WHERE contactId = ? AND state = ?" + + " AND groupShared = TRUE AND messageShared = TRUE" + + " AND deleted = FALSE" + " AND seen = FALSE" + " AND expiry < ?" + " ORDER BY timestamp"; @@ -1819,8 +1885,8 @@ abstract class JdbcDatabase implements Database { } @Override - public Collection getMessagesToShare( - Connection txn, ClientId c) throws DbException { + public Collection getMessagesToShare(Connection txn, ClientId c) + throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { @@ -1854,15 +1920,10 @@ abstract class JdbcDatabase implements Database { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT expiry FROM messages AS m" - + " JOIN groupVisibilities AS gv" - + " ON m.groupId = gv.groupId" - + " JOIN statuses AS s" - + " ON m.messageId = s.messageId" - + " AND gv.contactId = s.contactId" - + " WHERE gv.contactId = ? AND gv.shared = TRUE" - + " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL" - + " AND seen = FALSE" + String sql = "SELECT expiry FROM statuses" + + " WHERE contactId = ? AND state = ?" + + " AND groupShared = TRUE AND messageShared = TRUE" + + " AND deleted = FALSE AND seen = FALSE" + " ORDER BY expiry LIMIT 1"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); @@ -1914,14 +1975,10 @@ abstract class JdbcDatabase implements Database { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT length, m.messageId FROM messages AS m" - + " JOIN groupVisibilities AS gv" - + " ON m.groupId = gv.groupId" - + " JOIN statuses AS s" - + " ON m.messageId = s.messageId" - + " AND gv.contactId = s.contactId" - + " WHERE gv.contactId = ? AND gv.shared = TRUE" - + " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL" + String sql = "SELECT length, messageId FROM statuses" + + " WHERE contactId = ? AND state = ?" + + " AND groupShared = TRUE AND messageShared = TRUE" + + " AND deleted = FALSE" + " AND seen = FALSE AND requested = TRUE" + " AND expiry < ?" + " ORDER BY timestamp"; @@ -2397,6 +2454,8 @@ abstract class JdbcDatabase implements Database { int affected = ps.executeUpdate(); if (affected != 1) throw new DbStateException(); ps.close(); + // Remove status rows for the messages in the group + for (MessageId m : getMessageIds(txn, g)) removeStatus(txn, c, m); } catch (SQLException e) { tryToClose(ps); throw new DbException(e); @@ -2436,8 +2495,7 @@ abstract class JdbcDatabase implements Database { } } - @Override - public boolean removeOfferedMessage(Connection txn, ContactId c, + private boolean removeOfferedMessage(Connection txn, ContactId c, MessageId m) throws DbException { PreparedStatement ps = null; try { @@ -2481,16 +2539,15 @@ abstract class JdbcDatabase implements Database { } } - @Override - public void removeStatus(Connection txn, ContactId c, MessageId m) + private void removeStatus(Connection txn, ContactId c, MessageId m) throws DbException { PreparedStatement ps = null; try { String sql = "DELETE FROM statuses" - + " WHERE contactId = ? AND messageId = ?"; + + " WHERE messageId = ? AND contactId = ?"; ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - ps.setBytes(2, m.getBytes()); + ps.setBytes(1, m.getBytes()); + ps.setInt(2, c.getInt()); int affected = ps.executeUpdate(); if (affected != 1) throw new DbStateException(); ps.close(); @@ -2586,6 +2643,16 @@ abstract class JdbcDatabase implements Database { int affected = ps.executeUpdate(); if (affected < 0 || affected > 1) throw new DbStateException(); ps.close(); + // Update denormalised column in statuses + sql = "UPDATE statuses SET groupShared = ?" + + " WHERE contactId = ? AND groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBoolean(1, shared); + ps.setInt(2, c.getInt()); + ps.setBytes(3, g.getBytes()); + affected = ps.executeUpdate(); + if (affected < 0) throw new DbStateException(); + ps.close(); } catch (SQLException e) { tryToClose(ps); throw new DbException(e); @@ -2604,6 +2671,14 @@ abstract class JdbcDatabase implements Database { int affected = ps.executeUpdate(); if (affected < 0 || affected > 1) throw new DbStateException(); ps.close(); + // Update denormalised column in statuses + sql = "UPDATE statuses SET messageShared = TRUE" + + " WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + affected = ps.executeUpdate(); + if (affected < 0) throw new DbStateException(); + ps.close(); } catch (SQLException e) { tryToClose(ps); throw new DbException(e); @@ -2630,6 +2705,14 @@ abstract class JdbcDatabase implements Database { affected = ps.executeUpdate(); if (affected < 0) throw new DbStateException(); ps.close(); + // Update denormalised column in statuses + sql = "UPDATE statuses SET state = ? WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, state.getValue()); + ps.setBytes(2, m.getBytes()); + affected = ps.executeUpdate(); + if (affected < 0) throw new DbStateException(); + ps.close(); } catch (SQLException e) { tryToClose(ps); throw new DbException(e); diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java index 103ff1a7e..06ffdae4d 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java @@ -176,7 +176,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); oneOf(database).getGroupVisibility(txn, groupId); - will(returnValue(Collections.emptyList())); + will(returnValue(Collections.emptyMap())); oneOf(database).removeGroup(txn, groupId); oneOf(eventBus).broadcast(with(any(GroupRemovedEvent.class))); oneOf(eventBus).broadcast(with(any( @@ -255,13 +255,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { will(returnValue(true)); oneOf(database).containsMessage(txn, messageId); will(returnValue(false)); - oneOf(database).addMessage(txn, message, DELIVERED, true); + oneOf(database).addMessage(txn, message, DELIVERED, true, null); oneOf(database).mergeMessageMetadata(txn, messageId, metadata); - oneOf(database).getGroupVisibility(txn, groupId); - will(returnValue(Collections.singletonList(contactId))); - oneOf(database).removeOfferedMessage(txn, contactId, messageId); - will(returnValue(false)); - oneOf(database).addStatus(txn, contactId, messageId, false, false); oneOf(database).commitTransaction(txn); // The message was added, so the listeners should be called oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class))); @@ -1042,12 +1037,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { will(returnValue(VISIBLE)); oneOf(database).containsMessage(txn, messageId); will(returnValue(false)); - oneOf(database).addMessage(txn, message, UNKNOWN, false); - oneOf(database).getGroupVisibility(txn, groupId); - will(returnValue(Collections.singletonList(contactId))); - oneOf(database).removeOfferedMessage(txn, contactId, messageId); - will(returnValue(false)); - oneOf(database).addStatus(txn, contactId, messageId, true, true); + oneOf(database).addMessage(txn, message, UNKNOWN, false, contactId); // Second time oneOf(database).containsContact(txn, contactId); will(returnValue(true)); @@ -1206,7 +1196,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { } @Test - public void testChangingVisibilityCallsListeners() throws Exception { + public void testChangingVisibilityFromInvisibleToVisibleCallsListeners() + throws Exception { context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); @@ -1217,11 +1208,6 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { oneOf(database).getGroupVisibility(txn, contactId, groupId); will(returnValue(INVISIBLE)); // Not yet visible oneOf(database).addGroupVisibility(txn, contactId, groupId, false); - oneOf(database).getMessageIds(txn, groupId); - will(returnValue(Collections.singletonList(messageId))); - oneOf(database).removeOfferedMessage(txn, contactId, messageId); - will(returnValue(false)); - oneOf(database).addStatus(txn, contactId, messageId, false, false); oneOf(database).commitTransaction(txn); oneOf(eventBus).broadcast(with(any( GroupVisibilityUpdatedEvent.class))); @@ -1238,6 +1224,35 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { } } + @Test + public void testChangingVisibilityFromVisibleToInvisibleCallsListeners() + throws Exception { + context.checking(new Expectations() {{ + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsContact(txn, contactId); + will(returnValue(true)); + oneOf(database).containsGroup(txn, groupId); + will(returnValue(true)); + oneOf(database).getGroupVisibility(txn, contactId, groupId); + will(returnValue(VISIBLE)); // Not yet visible + oneOf(database).removeGroupVisibility(txn, contactId, groupId); + oneOf(database).commitTransaction(txn); + oneOf(eventBus).broadcast(with(any( + GroupVisibilityUpdatedEvent.class))); + }}); + DatabaseComponent db = createDatabaseComponent(database, eventBus, + shutdown); + + Transaction transaction = db.startTransaction(false); + try { + db.setGroupVisibility(transaction, contactId, groupId, INVISIBLE); + db.commitTransaction(transaction); + } finally { + db.endTransaction(transaction); + } + } + @Test public void testNotChangingVisibilityDoesNotCallListeners() throws Exception { @@ -1476,13 +1491,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { will(returnValue(true)); oneOf(database).containsMessage(txn, messageId); will(returnValue(false)); - oneOf(database).addMessage(txn, message, DELIVERED, true); - oneOf(database).getGroupVisibility(txn, groupId); - will(returnValue(Collections.singletonList(contactId))); + oneOf(database).addMessage(txn, message, DELIVERED, true, null); oneOf(database).mergeMessageMetadata(txn, messageId, metadata); - oneOf(database).removeOfferedMessage(txn, contactId, messageId); - will(returnValue(false)); - oneOf(database).addStatus(txn, contactId, messageId, false, false); // addMessageDependencies() oneOf(database).containsMessage(txn, messageId); will(returnValue(true)); diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/H2DatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/H2DatabaseTest.java index 84434164c..ceb03cb3c 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/db/H2DatabaseTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/H2DatabaseTest.java @@ -122,7 +122,7 @@ public class H2DatabaseTest extends BrambleTestCase { db.addGroup(txn, group); assertTrue(db.containsGroup(txn, groupId)); assertFalse(db.containsMessage(txn, messageId)); - db.addMessage(txn, message, DELIVERED, true); + db.addMessage(txn, message, DELIVERED, true, null); assertTrue(db.containsMessage(txn, messageId)); db.commitTransaction(txn); db.close(); @@ -160,7 +160,7 @@ public class H2DatabaseTest extends BrambleTestCase { // Add a group and a message db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); + db.addMessage(txn, message, DELIVERED, true, null); // Removing the group should remove the message assertTrue(db.containsMessage(txn, messageId)); @@ -182,18 +182,11 @@ public class H2DatabaseTest extends BrambleTestCase { true, true)); db.addGroup(txn, group); db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, DELIVERED, true); + db.addMessage(txn, message, DELIVERED, true, null); - // The message has no status yet, so it should not be sendable - Collection ids = db.getMessagesToSend(txn, contactId, - ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertTrue(ids.isEmpty()); - - // Adding a status with seen = false should make the message sendable - db.addStatus(txn, contactId, messageId, false, false); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + // The contact has not seen the message, so it should be sendable + Collection ids = + db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); assertEquals(Collections.singletonList(messageId), ids); ids = db.getMessagesToOffer(txn, contactId, 100); assertEquals(Collections.singletonList(messageId), ids); @@ -220,8 +213,7 @@ public class H2DatabaseTest extends BrambleTestCase { true, true)); db.addGroup(txn, group); db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, UNKNOWN, true); - db.addStatus(txn, contactId, messageId, false, false); + db.addMessage(txn, message, UNKNOWN, true, null); // The message has not been validated, so it should not be sendable Collection ids = db.getMessagesToSend(txn, contactId, @@ -265,8 +257,7 @@ public class H2DatabaseTest extends BrambleTestCase { assertEquals(contactId, db.addContact(txn, author, localAuthorId, true, true)); db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, false); + db.addMessage(txn, message, DELIVERED, true, null); // The group is invisible, so the message should not be sendable Collection ids = db.getMessagesToSend(txn, contactId, @@ -318,8 +309,7 @@ public class H2DatabaseTest extends BrambleTestCase { true, true)); db.addGroup(txn, group); db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, DELIVERED, false); - db.addStatus(txn, contactId, messageId, false, false); + db.addMessage(txn, message, DELIVERED, false, null); // The message is not shared, so it should not be sendable Collection ids = db.getMessagesToSend(txn, contactId, @@ -350,8 +340,7 @@ public class H2DatabaseTest extends BrambleTestCase { true, true)); db.addGroup(txn, group); db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, false); + db.addMessage(txn, message, DELIVERED, true, null); // The message is sendable, but too large to send Collection ids = db.getMessagesToSend(txn, contactId, @@ -381,12 +370,8 @@ public class H2DatabaseTest extends BrambleTestCase { // Add some messages to ack MessageId messageId1 = new MessageId(TestUtils.getRandomId()); Message message1 = new Message(messageId1, groupId, timestamp, raw); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, true); - db.raiseAckFlag(txn, contactId, messageId); - db.addMessage(txn, message1, DELIVERED, true); - db.addStatus(txn, contactId, messageId1, false, true); - db.raiseAckFlag(txn, contactId, messageId1); + db.addMessage(txn, message, DELIVERED, true, contactId); + db.addMessage(txn, message1, DELIVERED, true, contactId); // Both message IDs should be returned Collection ids = db.getMessagesToAck(txn, contactId, 1234); @@ -399,6 +384,14 @@ public class H2DatabaseTest extends BrambleTestCase { assertEquals(Collections.emptyList(), db.getMessagesToAck(txn, contactId, 1234)); + // Raise the ack flag again + db.raiseAckFlag(txn, contactId, messageId); + db.raiseAckFlag(txn, contactId, messageId1); + + // Both message IDs should be returned + ids = db.getMessagesToAck(txn, contactId, 1234); + assertEquals(Arrays.asList(messageId, messageId1), ids); + db.commitTransaction(txn); db.close(); } @@ -414,8 +407,7 @@ public class H2DatabaseTest extends BrambleTestCase { true, true)); db.addGroup(txn, group); db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, false); + db.addMessage(txn, message, DELIVERED, true, null); // Retrieve the message from the database and mark it as sent Collection ids = db.getMessagesToSend(txn, contactId, @@ -456,7 +448,7 @@ public class H2DatabaseTest extends BrambleTestCase { // Storing a message should reduce the free space Connection txn = db.startTransaction(); db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); + db.addMessage(txn, message, DELIVERED, true, null); db.commitTransaction(txn); assertTrue(db.getFreeSpace() < free); @@ -604,15 +596,14 @@ public class H2DatabaseTest extends BrambleTestCase { Database db = open(false); Connection txn = db.startTransaction(); - // Add a contact, a group and a message + // Add a contact, an invisible group and a message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId, true, true)); db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, false); + db.addMessage(txn, message, DELIVERED, true, null); - // The group is not visible + // The group is not visible so the message should not be visible assertFalse(db.containsVisibleMessage(txn, contactId, messageId)); db.commitTransaction(txn); @@ -632,31 +623,31 @@ public class H2DatabaseTest extends BrambleTestCase { // The group should not be visible to the contact assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId)); - assertEquals(Collections.emptyList(), + assertEquals(Collections.emptyMap(), db.getGroupVisibility(txn, groupId)); // Make the group visible to the contact db.addGroupVisibility(txn, contactId, groupId, false); assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId)); - assertEquals(Collections.singletonList(contactId), + assertEquals(Collections.singletonMap(contactId, false), db.getGroupVisibility(txn, groupId)); // Share the group with the contact db.setGroupVisibility(txn, contactId, groupId, true); assertEquals(SHARED, db.getGroupVisibility(txn, contactId, groupId)); - assertEquals(Collections.singletonList(contactId), + assertEquals(Collections.singletonMap(contactId, true), db.getGroupVisibility(txn, groupId)); // Unshare the group with the contact db.setGroupVisibility(txn, contactId, groupId, false); assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId)); - assertEquals(Collections.singletonList(contactId), + assertEquals(Collections.singletonMap(contactId, false), db.getGroupVisibility(txn, groupId)); // Make the group invisible again db.removeGroupVisibility(txn, contactId, groupId); assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId)); - assertEquals(Collections.emptyList(), + assertEquals(Collections.emptyMap(), db.getGroupVisibility(txn, groupId)); db.commitTransaction(txn); @@ -876,8 +867,7 @@ public class H2DatabaseTest extends BrambleTestCase { // Remove some of the offered messages and count again List half = ids.subList(0, 5); db.removeOfferedMessages(txn, contactId, half); - assertTrue(db.removeOfferedMessage(txn, contactId, ids.get(5))); - assertEquals(4, db.countOfferedMessages(txn, contactId)); + assertEquals(5, db.countOfferedMessages(txn, contactId)); db.commitTransaction(txn); db.close(); @@ -928,7 +918,7 @@ public class H2DatabaseTest extends BrambleTestCase { // Add a group and a message db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); + db.addMessage(txn, message, DELIVERED, true, null); // Attach some metadata to the message Metadata metadata = new Metadata(); @@ -999,7 +989,7 @@ public class H2DatabaseTest extends BrambleTestCase { // Add a group and a message db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); + db.addMessage(txn, message, DELIVERED, true, null); // Attach some metadata to the message Metadata metadata = new Metadata(); @@ -1060,8 +1050,8 @@ public class H2DatabaseTest extends BrambleTestCase { // Add a group and two messages db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); - db.addMessage(txn, message1, DELIVERED, true); + db.addMessage(txn, message, DELIVERED, true, null); + db.addMessage(txn, message1, DELIVERED, true, null); // Attach some metadata to the messages Metadata metadata = new Metadata(); @@ -1164,8 +1154,8 @@ public class H2DatabaseTest extends BrambleTestCase { // Add a group and two messages db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); - db.addMessage(txn, message1, DELIVERED, true); + db.addMessage(txn, message, DELIVERED, true, null); + db.addMessage(txn, message1, DELIVERED, true, null); // Attach some metadata to the messages Metadata metadata = new Metadata(); @@ -1239,9 +1229,9 @@ public class H2DatabaseTest extends BrambleTestCase { // Add a group and some messages db.addGroup(txn, group); - db.addMessage(txn, message, PENDING, true); - db.addMessage(txn, message1, DELIVERED, true); - db.addMessage(txn, message2, INVALID, true); + db.addMessage(txn, message, PENDING, true, contactId); + db.addMessage(txn, message1, DELIVERED, true, contactId); + db.addMessage(txn, message2, INVALID, true, contactId); // Add dependencies db.addMessageDependency(txn, groupId, messageId, messageId1); @@ -1308,7 +1298,7 @@ public class H2DatabaseTest extends BrambleTestCase { // Add a group and a message db.addGroup(txn, group); - db.addMessage(txn, message, PENDING, true); + db.addMessage(txn, message, PENDING, true, contactId); // Add a second group GroupId groupId1 = new GroupId(TestUtils.getRandomId()); @@ -1319,7 +1309,7 @@ public class H2DatabaseTest extends BrambleTestCase { // Add a message to the second group MessageId messageId1 = new MessageId(TestUtils.getRandomId()); Message message1 = new Message(messageId1, groupId1, timestamp, raw); - db.addMessage(txn, message1, DELIVERED, true); + db.addMessage(txn, message1, DELIVERED, true, contactId); // Create an ID for a missing message MessageId messageId2 = new MessageId(TestUtils.getRandomId()); @@ -1327,7 +1317,7 @@ public class H2DatabaseTest extends BrambleTestCase { // Add another message to the first group MessageId messageId3 = new MessageId(TestUtils.getRandomId()); Message message3 = new Message(messageId3, groupId, timestamp, raw); - db.addMessage(txn, message3, DELIVERED, true); + db.addMessage(txn, message3, DELIVERED, true, contactId); // Add dependencies between the messages db.addMessageDependency(txn, groupId, messageId, messageId1); @@ -1374,10 +1364,10 @@ public class H2DatabaseTest extends BrambleTestCase { // Add a group and some messages with different states db.addGroup(txn, group); - db.addMessage(txn, m1, UNKNOWN, true); - db.addMessage(txn, m2, INVALID, true); - db.addMessage(txn, m3, PENDING, true); - db.addMessage(txn, m4, DELIVERED, true); + db.addMessage(txn, m1, UNKNOWN, true, contactId); + db.addMessage(txn, m2, INVALID, true, contactId); + db.addMessage(txn, m3, PENDING, true, contactId); + db.addMessage(txn, m4, DELIVERED, true, contactId); Collection result; @@ -1411,10 +1401,10 @@ public class H2DatabaseTest extends BrambleTestCase { // Add a group and some messages db.addGroup(txn, group); - db.addMessage(txn, m1, DELIVERED, true); - db.addMessage(txn, m2, DELIVERED, false); - db.addMessage(txn, m3, DELIVERED, false); - db.addMessage(txn, m4, DELIVERED, true); + db.addMessage(txn, m1, DELIVERED, true, contactId); + db.addMessage(txn, m2, DELIVERED, false, contactId); + db.addMessage(txn, m3, DELIVERED, false, contactId); + db.addMessage(txn, m4, DELIVERED, true, contactId); // Introduce dependencies between the messages db.addMessageDependency(txn, groupId, mId1, mId2); @@ -1443,8 +1433,7 @@ public class H2DatabaseTest extends BrambleTestCase { true, true)); db.addGroup(txn, group); db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, false); + db.addMessage(txn, message, DELIVERED, true, null); // The message should not be sent or seen MessageStatus status = db.getMessageStatus(txn, contactId, messageId); @@ -1546,8 +1535,7 @@ public class H2DatabaseTest extends BrambleTestCase { true, true)); db.addGroup(txn, group); db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, false); + db.addMessage(txn, message, DELIVERED, true, null); // The message should be visible to the contact assertTrue(db.containsVisibleMessage(txn, contactId, messageId)); @@ -1621,7 +1609,7 @@ public class H2DatabaseTest extends BrambleTestCase { // Add a group and a message db.addGroup(txn, group); - db.addMessage(txn, message, UNKNOWN, false); + db.addMessage(txn, message, UNKNOWN, false, contactId); // Walk the message through the validation and delivery states assertEquals(UNKNOWN, db.getMessageState(txn, messageId)); @@ -1647,14 +1635,13 @@ public class H2DatabaseTest extends BrambleTestCase { assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(), true, true)); db.addGroup(txn, group); - db.addMessage(txn, message, UNKNOWN, false); + db.addMessage(txn, message, UNKNOWN, false, null); // There should be no messages to send assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId)); // Share the group with the contact - still no messages to send db.addGroupVisibility(txn, contactId, groupId, true); - db.addStatus(txn, contactId, messageId, false, false); assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId)); // Set the message's state to DELIVERED - still no messages to send From 3e28323ab1c86a947ed4dac474231f7f7a405002 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Mon, 19 Feb 2018 16:25:02 +0000 Subject: [PATCH 2/3] Test that visibility change affects expected contacts. --- .../bramble/db/DatabaseComponentImplTest.java | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java index 06ffdae4d..2b1dfb427 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java @@ -48,6 +48,7 @@ import org.briarproject.bramble.api.transport.IncomingKeys; import org.briarproject.bramble.api.transport.OutgoingKeys; import org.briarproject.bramble.api.transport.TransportKeys; import org.briarproject.bramble.test.BrambleMockTestCase; +import org.briarproject.bramble.test.CaptureArgumentAction; import org.briarproject.bramble.test.TestUtils; import org.briarproject.bramble.util.StringUtils; import org.jmock.Expectations; @@ -56,9 +57,12 @@ import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; @@ -160,7 +164,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { ContactStatusChangedEvent.class))); // getContacts() oneOf(database).getContacts(txn); - will(returnValue(Collections.singletonList(contact))); + will(returnValue(singletonList(contact))); // addGroup() oneOf(database).containsGroup(txn, groupId); will(returnValue(false)); @@ -171,12 +175,12 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { will(returnValue(true)); // getGroups() oneOf(database).getGroups(txn, clientId); - will(returnValue(Collections.singletonList(group))); + will(returnValue(singletonList(group))); // removeGroup() oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); oneOf(database).getGroupVisibility(txn, groupId); - will(returnValue(Collections.emptyMap())); + will(returnValue(emptyMap())); oneOf(database).removeGroup(txn, groupId); oneOf(eventBus).broadcast(with(any(GroupRemovedEvent.class))); oneOf(eventBus).broadcast(with(any( @@ -206,11 +210,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { assertEquals(contactId, db.addContact(transaction, author, localAuthorId, true, true)); - assertEquals(Collections.singletonList(contact), + assertEquals(singletonList(contact), db.getContacts(transaction)); db.addGroup(transaction, group); // First time - listeners called db.addGroup(transaction, group); // Second time - not called - assertEquals(Collections.singletonList(group), + assertEquals(singletonList(group), db.getGroups(transaction, clientId)); db.removeGroup(transaction, group); db.removeContact(transaction, contactId); @@ -392,7 +396,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { transaction = db.startTransaction(false); try { - Ack a = new Ack(Collections.singletonList(messageId)); + Ack a = new Ack(singletonList(messageId)); db.receiveAck(transaction, contactId, a); fail(); } catch (NoSuchContactException expected) { @@ -413,7 +417,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { transaction = db.startTransaction(false); try { - Offer o = new Offer(Collections.singletonList(messageId)); + Offer o = new Offer(singletonList(messageId)); db.receiveOffer(transaction, contactId, o); fail(); } catch (NoSuchContactException expected) { @@ -424,7 +428,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { transaction = db.startTransaction(false); try { - Request r = new Request(Collections.singletonList(messageId)); + Request r = new Request(singletonList(messageId)); db.receiveRequest(transaction, contactId, r); fail(); } catch (NoSuchContactException expected) { @@ -1017,7 +1021,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { Transaction transaction = db.startTransaction(false); try { - Ack a = new Ack(Collections.singletonList(messageId)); + Ack a = new Ack(singletonList(messageId)); db.receiveAck(transaction, contactId, a); db.commitTransaction(transaction); } finally { @@ -1187,7 +1191,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { Transaction transaction = db.startTransaction(false); try { - Request r = new Request(Collections.singletonList(messageId)); + Request r = new Request(singletonList(messageId)); db.receiveRequest(transaction, contactId, r); db.commitTransaction(transaction); } finally { @@ -1198,6 +1202,9 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { @Test public void testChangingVisibilityFromInvisibleToVisibleCallsListeners() throws Exception { + AtomicReference event = + new AtomicReference<>(); + context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); @@ -1206,11 +1213,13 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); oneOf(database).getGroupVisibility(txn, contactId, groupId); - will(returnValue(INVISIBLE)); // Not yet visible + will(returnValue(INVISIBLE)); oneOf(database).addGroupVisibility(txn, contactId, groupId, false); oneOf(database).commitTransaction(txn); oneOf(eventBus).broadcast(with(any( GroupVisibilityUpdatedEvent.class))); + will(new CaptureArgumentAction<>(event, + GroupVisibilityUpdatedEvent.class, 0)); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, shutdown); @@ -1222,11 +1231,18 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { } finally { db.endTransaction(transaction); } + + GroupVisibilityUpdatedEvent e = event.get(); + assertNotNull(e); + assertEquals(singletonList(contactId), e.getAffectedContacts()); } @Test public void testChangingVisibilityFromVisibleToInvisibleCallsListeners() throws Exception { + AtomicReference event = + new AtomicReference<>(); + context.checking(new Expectations() {{ oneOf(database).startTransaction(); will(returnValue(txn)); @@ -1235,11 +1251,13 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); oneOf(database).getGroupVisibility(txn, contactId, groupId); - will(returnValue(VISIBLE)); // Not yet visible + will(returnValue(VISIBLE)); oneOf(database).removeGroupVisibility(txn, contactId, groupId); oneOf(database).commitTransaction(txn); oneOf(eventBus).broadcast(with(any( GroupVisibilityUpdatedEvent.class))); + will(new CaptureArgumentAction<>(event, + GroupVisibilityUpdatedEvent.class, 0)); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, shutdown); @@ -1251,6 +1269,10 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { } finally { db.endTransaction(transaction); } + + GroupVisibilityUpdatedEvent e = event.get(); + assertNotNull(e); + assertEquals(singletonList(contactId), e.getAffectedContacts()); } @Test @@ -1282,8 +1304,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase { @Test public void testTransportKeys() throws Exception { TransportKeys transportKeys = createTransportKeys(); - Map keys = Collections.singletonMap( - contactId, transportKeys); + Map keys = + singletonMap(contactId, transportKeys); context.checking(new Expectations() {{ // startTransaction() oneOf(database).startTransaction(); From 9493e242cc20f8368fe1fd0b82d2d26f391fdae9 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Thu, 8 Mar 2018 14:49:22 +0000 Subject: [PATCH 3/3] Add migration to schema version 32. --- .../briarproject/bramble/db/JdbcDatabase.java | 3 +- .../bramble/db/Migration31_32.java | 84 ++++ .../bramble/db/Migration30_31Test.java | 7 +- .../bramble/db/Migration31_32Test.java | 369 ++++++++++++++++++ 4 files changed, 459 insertions(+), 4 deletions(-) create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/db/Migration31_32.java create mode 100644 bramble-core/src/test/java/org/briarproject/bramble/db/Migration31_32Test.java diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java index 790f16e91..006c7c9ec 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java @@ -34,6 +34,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -361,7 +362,7 @@ abstract class JdbcDatabase implements Database { // Package access for testing List> getMigrations() { - return Collections.singletonList(new Migration30_31()); + return Arrays.asList(new Migration30_31(), new Migration31_32()); } private void storeSchemaVersion(Connection txn, int version) diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/Migration31_32.java b/bramble-core/src/main/java/org/briarproject/bramble/db/Migration31_32.java new file mode 100644 index 000000000..361fef4e4 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/Migration31_32.java @@ -0,0 +1,84 @@ +package org.briarproject.bramble.db; + +import org.briarproject.bramble.api.db.DbException; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.logging.Logger; + +import javax.annotation.Nullable; + +import static java.util.logging.Level.WARNING; + +class Migration31_32 implements Migration { + + private static final Logger LOG = + Logger.getLogger(Migration31_32.class.getName()); + + @Override + public int getStartVersion() { + return 31; + } + + @Override + public int getEndVersion() { + return 32; + } + + @Override + public void migrate(Connection txn) throws DbException { + Statement s = null; + try { + s = txn.createStatement(); + // Add denormalised columns + s.execute("ALTER TABLE statuses ADD COLUMN" + + " (groupId BINARY(32)," + + " timestamp BIGINT," + + " length INT," + + " state INT," + + " groupShared BOOLEAN," + + " messageShared BOOLEAN," + + " deleted BOOLEAN)"); + // Populate columns from messages table + s.execute("UPDATE statuses AS s SET (groupId, timestamp, length," + + " state, messageShared, deleted) =" + + " (SELECT groupId, timestamp, length, state, shared," + + " raw IS NULL FROM messages AS m" + + " WHERE s.messageId = m.messageId)"); + // Populate column from groupVisibilities table + s.execute("UPDATE statuses AS s SET groupShared =" + + " (SELECT shared FROM groupVisibilities AS gv" + + " WHERE s.contactId = gv.contactId" + + " AND s.groupId = gv.groupId)"); + // Add not null constraints now columns have been populated + s.execute("ALTER TABLE statuses ALTER COLUMN groupId SET NOT NULL"); + s.execute("ALTER TABLE statuses ALTER COLUMN timestamp" + + " SET NOT NULL"); + s.execute("ALTER TABLE statuses ALTER COLUMN length SET NOT NULL"); + s.execute("ALTER TABLE statuses ALTER COLUMN state SET NOT NULL"); + s.execute("ALTER TABLE statuses ALTER COLUMN groupShared" + + " SET NOT NULL"); + s.execute("ALTER TABLE statuses ALTER COLUMN messageShared" + + " SET NOT NULL"); + s.execute("ALTER TABLE statuses ALTER COLUMN deleted SET NOT NULL"); + // Add foreign key constraint + s.execute("ALTER TABLE statuses" + + " ADD CONSTRAINT statusesForeignKeyGroupId" + + " FOREIGN KEY (groupId)" + + " REFERENCES groups (groupId)" + + " ON DELETE CASCADE"); + } catch (SQLException e) { + tryToClose(s); + throw new DbException(e); + } + } + + private void tryToClose(@Nullable Statement s) { + try { + if (s != null) s.close(); + } catch (SQLException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/Migration30_31Test.java b/bramble-core/src/test/java/org/briarproject/bramble/db/Migration30_31Test.java index ff26a50bc..302e44031 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/db/Migration30_31Test.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/Migration30_31Test.java @@ -24,6 +24,7 @@ import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertTrue; import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED; import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN; +import static org.briarproject.bramble.test.TestUtils.getMessage; import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.util.StringUtils.getRandomString; @@ -34,7 +35,7 @@ public class Migration30_31Test extends BrambleTestCase { private static final String CREATE_GROUPS_STUB = "CREATE TABLE groups" - + " (groupID BINARY(32) NOT NULL," + + " (groupId BINARY(32) NOT NULL," + " PRIMARY KEY (groupId))"; private static final String CREATE_MESSAGES = @@ -66,8 +67,8 @@ public class Migration30_31Test extends BrambleTestCase { private final String url = "jdbc:h2:" + db.getAbsolutePath(); private final GroupId groupId = new GroupId(getRandomId()); private final GroupId groupId1 = new GroupId(getRandomId()); - private final Message message = TestUtils.getMessage(groupId); - private final Message message1 = TestUtils.getMessage(groupId1); + private final Message message = getMessage(groupId); + private final Message message1 = getMessage(groupId1); private final Metadata meta = new Metadata(), meta1 = new Metadata(); private Connection connection = null; diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/Migration31_32Test.java b/bramble-core/src/test/java/org/briarproject/bramble/db/Migration31_32Test.java new file mode 100644 index 000000000..c0335e4d6 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/Migration31_32Test.java @@ -0,0 +1,369 @@ +package org.briarproject.bramble.db; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.sync.ValidationManager.State; +import org.briarproject.bramble.test.BrambleTestCase; +import org.briarproject.bramble.test.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static java.sql.Types.BINARY; +import static junit.framework.Assert.assertFalse; +import static junit.framework.TestCase.assertTrue; +import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN; +import static org.briarproject.bramble.test.TestUtils.getMessage; +import static org.briarproject.bramble.test.TestUtils.getRandomId; +import static org.junit.Assert.assertEquals; + +public class Migration31_32Test extends BrambleTestCase { + + private static final String CREATE_GROUPS_STUB = + "CREATE TABLE groups" + + " (groupId BINARY(32) NOT NULL," + + " PRIMARY KEY (groupId))"; + + private static final String CREATE_CONTACTS_STUB = + "CREATE TABLE contacts" + + " (contactId INT NOT NULL," + + " PRIMARY KEY (contactId))"; + + private static final String CREATE_GROUP_VISIBILITIES_STUB = + "CREATE TABLE groupVisibilities" + + " (contactId INT NOT NULL," + + " groupId BINARY(32) NOT NULL," + + " shared BOOLEAN NOT NULL," + + " PRIMARY KEY (contactId, groupId))"; + + private static final String CREATE_MESSAGES = + "CREATE TABLE messages" + + " (messageId BINARY(32) NOT NULL," + + " groupId BINARY(32) NOT NULL," + + " timestamp BIGINT NOT NULL," + + " state INT NOT NULL," + + " shared BOOLEAN NOT NULL," + + " length INT NOT NULL," + + " raw BLOB," // Null if message has been deleted + + " PRIMARY KEY (messageId)," + + " FOREIGN KEY (groupId)" + + " REFERENCES groups (groupId)" + + " ON DELETE CASCADE)"; + + private static final String CREATE_STATUSES_31 = + "CREATE TABLE statuses" + + " (messageId BINARY(32) NOT NULL," + + " contactId INT NOT NULL," + + " ack BOOLEAN NOT NULL," + + " seen BOOLEAN NOT NULL," + + " requested BOOLEAN NOT NULL," + + " expiry BIGINT NOT NULL," + + " txCount INT NOT NULL," + + " PRIMARY KEY (messageId, contactId)," + + " FOREIGN KEY (messageId)" + + " REFERENCES messages (messageId)" + + " ON DELETE CASCADE," + + " FOREIGN KEY (contactId)" + + " REFERENCES contacts (contactId)" + + " ON DELETE CASCADE)"; + + private final File testDir = TestUtils.getTestDirectory(); + private final File db = new File(testDir, "db"); + private final String url = "jdbc:h2:" + db.getAbsolutePath(); + private final GroupId groupId = new GroupId(getRandomId()); + private final GroupId groupId1 = new GroupId(getRandomId()); + private final ContactId contactId = new ContactId(123); + private final ContactId contactId1 = new ContactId(234); + private final Message message = getMessage(groupId); + private final Message message1 = getMessage(groupId1); + private final Message message2 = getMessage(groupId1); + + private Connection connection = null; + + @Before + public void setUp() throws Exception { + assertTrue(testDir.mkdirs()); + Class.forName("org.h2.Driver"); + connection = DriverManager.getConnection(url); + } + + @After + public void tearDown() throws Exception { + if (connection != null) connection.close(); + TestUtils.deleteTestDirectory(testDir); + } + + @Test + public void testMigration() throws Exception { + try { + Statement s = connection.createStatement(); + s.execute(CREATE_GROUPS_STUB); + s.execute(CREATE_CONTACTS_STUB); + s.execute(CREATE_GROUP_VISIBILITIES_STUB); + s.execute(CREATE_MESSAGES); + s.execute(CREATE_STATUSES_31); + s.close(); + + addGroup(groupId); + addMessage(message, DELIVERED, true, false); + + addGroup(groupId1); + addMessage(message1, UNKNOWN, false, false); + addMessage(message2, DELIVERED, true, true); + + addContact(contactId); + + addGroupVisibility(contactId, groupId, true); + addStatus31(message.getId(), contactId); + + addGroupVisibility(contactId, groupId1, false); + addStatus31(message1.getId(), contactId); + addStatus31(message2.getId(), contactId); + + addContact(contactId1); + + addGroupVisibility(contactId1, groupId1, true); + addStatus31(message1.getId(), contactId1); + addStatus31(message2.getId(), contactId1); + + new Migration31_32().migrate(connection); + + assertTrue(containsStatus(message.getId(), contactId)); + Status32 status = getStatus32(message.getId(), contactId); + assertEquals(groupId, status.groupId); + assertEquals(message.getTimestamp(), status.timestamp); + assertEquals(message.getLength(), status.length); + assertEquals(DELIVERED, status.state); + assertTrue(status.groupShared); + assertTrue(status.messageShared); + assertFalse(status.deleted); + + assertTrue(containsStatus(message1.getId(), contactId)); + status = getStatus32(message1.getId(), contactId); + assertEquals(groupId1, status.groupId); + assertEquals(message1.getTimestamp(), status.timestamp); + assertEquals(message1.getLength(), status.length); + assertEquals(UNKNOWN, status.state); + assertFalse(status.groupShared); + assertFalse(status.messageShared); + assertFalse(status.deleted); + + assertTrue(containsStatus(message2.getId(), contactId)); + status = getStatus32(message2.getId(), contactId); + assertEquals(groupId1, status.groupId); + assertEquals(message2.getTimestamp(), status.timestamp); + assertEquals(message2.getLength(), status.length); + assertEquals(DELIVERED, status.state); + assertFalse(status.groupShared); + assertTrue(status.messageShared); + assertTrue(status.deleted); + + assertFalse(containsStatus(message.getId(), contactId1)); + + assertTrue(containsStatus(message1.getId(), contactId1)); + status = getStatus32(message1.getId(), contactId1); + assertEquals(groupId1, status.groupId); + assertEquals(message1.getTimestamp(), status.timestamp); + assertEquals(message1.getLength(), status.length); + assertEquals(UNKNOWN, status.state); + assertTrue(status.groupShared); + assertFalse(status.messageShared); + assertFalse(status.deleted); + + assertTrue(containsStatus(message2.getId(), contactId1)); + status = getStatus32(message2.getId(), contactId1); + assertEquals(groupId1, status.groupId); + assertEquals(message2.getTimestamp(), status.timestamp); + assertEquals(message2.getLength(), status.length); + assertEquals(DELIVERED, status.state); + assertTrue(status.groupShared); + assertTrue(status.messageShared); + assertTrue(status.deleted); + } catch (SQLException e) { + connection.close(); + throw e; + } + } + + private void addGroup(GroupId g) throws SQLException { + PreparedStatement ps = null; + try { + String sql = "INSERT INTO groups (groupId) VALUES (?)"; + ps = connection.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + int affected = ps.executeUpdate(); + if (affected != 1) throw new DbStateException(); + ps.close(); + } catch (SQLException e) { + if (ps != null) ps.close(); + throw e; + } + } + + private void addContact(ContactId c) throws SQLException { + PreparedStatement ps = null; + try { + String sql = "INSERT INTO contacts (contactId) VALUES (?)"; + ps = connection.prepareStatement(sql); + ps.setInt(1, c.getInt()); + int affected = ps.executeUpdate(); + if (affected != 1) throw new DbStateException(); + ps.close(); + } catch (SQLException e) { + if (ps != null) ps.close(); + throw e; + } + } + + private void addGroupVisibility(ContactId c, GroupId g, boolean shared) + throws SQLException { + PreparedStatement ps = null; + try { + String sql = "INSERT INTO groupVisibilities" + + " (contactId, groupId, shared) VALUES (?, ?, ?)"; + ps = connection.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.setBytes(2, g.getBytes()); + ps.setBoolean(3, shared); + int affected = ps.executeUpdate(); + if (affected != 1) throw new DbStateException(); + ps.close(); + } catch (SQLException e) { + if (ps != null) ps.close(); + throw e; + } + } + + private void addMessage(Message m, State state, boolean shared, + boolean deleted) throws SQLException { + PreparedStatement ps = null; + try { + String sql = "INSERT INTO messages (messageId, groupId, timestamp," + + " state, shared, length, raw)" + + " VALUES (?, ?, ?, ?, ?, ?, ?)"; + ps = connection.prepareStatement(sql); + ps.setBytes(1, m.getId().getBytes()); + ps.setBytes(2, m.getGroupId().getBytes()); + ps.setLong(3, m.getTimestamp()); + ps.setInt(4, state.getValue()); + ps.setBoolean(5, shared); + byte[] raw = m.getRaw(); + ps.setInt(6, raw.length); + if (deleted) ps.setNull(7, BINARY); + else ps.setBytes(7, raw); + int affected = ps.executeUpdate(); + if (affected != 1) throw new DbStateException(); + ps.close(); + } catch (SQLException e) { + if (ps != null) ps.close(); + throw e; + } + } + + private void addStatus31(MessageId m, ContactId c) throws SQLException { + PreparedStatement ps = null; + try { + String sql = "INSERT INTO statuses (messageId, contactId, ack," + + " seen, requested, expiry, txCount)" + + " VALUES (?, ?, FALSE, FALSE, FALSE, 0, 0)"; + ps = connection.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + ps.setInt(2, c.getInt()); + int affected = ps.executeUpdate(); + if (affected != 1) throw new DbStateException(); + ps.close(); + } catch (SQLException e) { + if (ps != null) ps.close(); + throw e; + } + } + + private boolean containsStatus(MessageId m, ContactId c) + throws SQLException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT COUNT (*) FROM statuses" + + " WHERE messageId = ? AND contactId = ?"; + ps = connection.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + ps.setInt(2, c.getInt()); + rs = ps.executeQuery(); + if (!rs.next()) throw new DbStateException(); + int count = rs.getInt(1); + if (count < 0 || count > 1) throw new DbStateException(); + if (rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + return count > 0; + } catch (SQLException e) { + if (rs != null) rs.close(); + if (ps != null) ps.close(); + throw e; + } + } + + private Status32 getStatus32(MessageId m, ContactId c) throws SQLException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT groupId, timestamp, length, state," + + " groupShared, messageShared, deleted" + + " FROM statuses" + + " WHERE messageId = ? AND contactId = ?"; + ps = connection.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + ps.setInt(2, c.getInt()); + rs = ps.executeQuery(); + if (!rs.next()) throw new DbStateException(); + GroupId groupId = new GroupId(rs.getBytes(1)); + long timestamp = rs.getLong(2); + int length = rs.getInt(3); + State state = State.fromValue(rs.getInt(4)); + boolean groupShared = rs.getBoolean(5); + boolean messageShared = rs.getBoolean(6); + boolean deleted = rs.getBoolean(7); + if (rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + return new Status32(groupId, timestamp, length, state, + groupShared, messageShared, deleted); + } catch (SQLException e) { + if (rs != null) rs.close(); + if (ps != null) ps.close(); + throw e; + } + } + + private static class Status32 { + + private final GroupId groupId; + private final long timestamp; + private final int length; + private final State state; + private final boolean groupShared, messageShared, deleted; + + private Status32(GroupId groupId, long timestamp, int length, + State state, boolean groupShared, boolean messageShared, + boolean deleted) { + this.groupId = groupId; + this.timestamp = timestamp; + this.length = length; + this.state = state; + this.groupShared = groupShared; + this.messageShared = messageShared; + this.deleted = deleted; + } + } +}