diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java index 977df1292b67fd7d38e7cc88c3fd197d03a6e205..b6a980b67b83635f9eef97eca80a5ef3d67a6232 100644 --- a/components/net/sf/briar/db/Database.java +++ b/components/net/sf/briar/db/Database.java @@ -32,6 +32,7 @@ import net.sf.briar.api.transport.ConnectionWindow; * <ul> * <li> contact * <li> message + * <li> messageFlag * <li> messageStatus * <li> rating * <li> subscription @@ -304,6 +305,13 @@ interface Database<T> { */ Rating getRating(T txn, AuthorId a) throws DbException; + /** + * Returns true if the given message has been read. + * <p> + * Locking: message read, messageFlag read. + */ + boolean getRead(T txn, MessageId m) throws DbException; + /** * Returns all remote properties for the given transport. * <p> @@ -346,6 +354,13 @@ interface Database<T> { */ byte[] getSharedSecret(T txn, ContactId c) throws DbException; + /** + * Returns true if the given message has been starred. + * <p> + * Locking: message read, messageFlag read. + */ + boolean getStarred(T txn, MessageId m) throws DbException; + /** * Returns the groups to which the user subscribes. * <p> @@ -433,8 +448,8 @@ interface Database<T> { /** * Removes a contact (and all associated state) from the database. * <p> - * Locking: contact write, message write, messageStatus write, - * subscription write, transport write. + * Locking: contact write, message write, messageFlag write, + * messageStatus write, subscription write, transport write. */ void removeContact(T txn, ContactId c) throws DbException; @@ -450,7 +465,8 @@ interface Database<T> { /** * Removes a message (and all associated state) from the database. * <p> - * Locking: contact read, message write, messageStatus write. + * Locking: contact read, message write, messageFlag write, + * messageStatus write. */ void removeMessage(T txn, MessageId m) throws DbException; @@ -458,8 +474,8 @@ interface Database<T> { * Unsubscribes from the given group. Any messages belonging to the group * are deleted from the database. * <p> - * Locking: contact read, message write, messageStatus write, - * subscription write. + * Locking: contact read, message write, messageFlag write, + * messageStatus write, subscription write. */ void removeSubscription(T txn, GroupId g) throws DbException; @@ -497,6 +513,14 @@ interface Database<T> { */ Rating setRating(T txn, AuthorId a, Rating r) throws DbException; + /** + * Marks the given message read or unread and returns true if it was + * previously read. + * <p> + * Locking: message read, messageFlag write. + */ + boolean setRead(T txn, MessageId m, boolean read) throws DbException; + /** * Sets the sendability score of the given message. * <p> @@ -504,6 +528,14 @@ interface Database<T> { */ void setSendability(T txn, MessageId m, int sendability) throws DbException; + /** + * Marks the given message starred or unstarred and returns true if it was + * previously starred. + * <p> + * Locking: message read, messageFlag write. + */ + boolean setStarred(T txn, MessageId m, boolean starred) throws DbException; + /** * Sets the status of the given message with respect to the given contact. * <p> diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java index 90523a273abea6575755d44d343332bf05609464..36c88cce8d1a7110e9a1641528bc76a7df605d82 100644 --- a/components/net/sf/briar/db/DatabaseComponentImpl.java +++ b/components/net/sf/briar/db/DatabaseComponentImpl.java @@ -81,6 +81,8 @@ DatabaseCleaner.Callback { new ReentrantReadWriteLock(true); private final ReentrantReadWriteLock messageLock = new ReentrantReadWriteLock(true); + private final ReentrantReadWriteLock messageFlagLock = + new ReentrantReadWriteLock(true); private final ReentrantReadWriteLock messageStatusLock = new ReentrantReadWriteLock(true); private final ReentrantReadWriteLock ratingLock = @@ -1137,28 +1139,33 @@ DatabaseCleaner.Callback { try { messageLock.writeLock().lock(); try { - messageStatusLock.writeLock().lock(); + messageFlagLock.writeLock().lock(); try { - subscriptionLock.writeLock().lock(); + messageStatusLock.writeLock().lock(); try { - transportLock.writeLock().lock(); + subscriptionLock.writeLock().lock(); try { - T txn = db.startTransaction(); + transportLock.writeLock().lock(); try { - db.removeContact(txn, c); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; + T txn = db.startTransaction(); + try { + db.removeContact(txn, c); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + transportLock.writeLock().unlock(); } } finally { - transportLock.writeLock().unlock(); + subscriptionLock.writeLock().unlock(); } } finally { - subscriptionLock.writeLock().unlock(); + messageStatusLock.writeLock().unlock(); } } finally { - messageStatusLock.writeLock().unlock(); + messageFlagLock.writeLock().unlock(); } } finally { messageLock.writeLock().unlock(); @@ -1399,26 +1406,31 @@ DatabaseCleaner.Callback { try { messageLock.writeLock().lock(); try { - messageStatusLock.writeLock().lock(); + messageFlagLock.writeLock().lock(); try { - subscriptionLock.writeLock().lock(); + messageStatusLock.writeLock().lock(); try { - T txn = db.startTransaction(); + subscriptionLock.writeLock().lock(); try { - if(db.containsSubscription(txn, g)) { - affected = db.getVisibility(txn, g); - db.removeSubscription(txn, g); + T txn = db.startTransaction(); + try { + if(db.containsSubscription(txn, g)) { + affected = db.getVisibility(txn, g); + db.removeSubscription(txn, g); + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; } - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; + } finally { + subscriptionLock.writeLock().unlock(); } } finally { - subscriptionLock.writeLock().unlock(); + messageStatusLock.writeLock().unlock(); } } finally { - messageStatusLock.writeLock().unlock(); + messageFlagLock.writeLock().unlock(); } } finally { messageLock.writeLock().unlock(); @@ -1457,19 +1469,24 @@ DatabaseCleaner.Callback { try { messageLock.writeLock().lock(); try { - messageStatusLock.writeLock().lock(); + messageFlagLock.writeLock().lock(); try { - T txn = db.startTransaction(); + messageStatusLock.writeLock().lock(); try { - old = db.getOldMessages(txn, size); - for(MessageId m : old) removeMessage(txn, m); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; + T txn = db.startTransaction(); + try { + old = db.getOldMessages(txn, size); + for(MessageId m : old) removeMessage(txn, m); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + messageStatusLock.writeLock().unlock(); } } finally { - messageStatusLock.writeLock().unlock(); + messageFlagLock.writeLock().unlock(); } } finally { messageLock.writeLock().unlock(); diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java index 16799e6bc3093e0d77f09331bedc724ab606da4a..f167aa121cc8c27e9e2d8a3899f7e4b925a21884 100644 --- a/components/net/sf/briar/db/JdbcDatabase.java +++ b/components/net/sf/briar/db/JdbcDatabase.java @@ -225,6 +225,15 @@ abstract class JdbcDatabase implements Database<Connection> { + " FOREIGN KEY (contactId) REFERENCES contacts (contactId)" + " ON DELETE CASCADE)"; + private static final String CREATE_FLAGS = + "CREATE TABLE flags" + + " (messageId HASH NOT NULL," + + " read BOOLEAN NOT NULL," + + " starred BOOLEAN NOT NULL," + + " PRIMARY KEY (messageId)," + + " FOREIGN KEY (messageId) REFERENCES messages (messageId)" + + " ON DELETE CASCADE)"; + private static final Logger LOG = Logger.getLogger(JdbcDatabase.class.getName()); @@ -233,6 +242,7 @@ abstract class JdbcDatabase implements Database<Connection> { private final ConnectionWindowFactory connectionWindowFactory; private final GroupFactory groupFactory; private final MessageHeaderFactory messageHeaderFactory; + private final LinkedList<Connection> connections = new LinkedList<Connection>(); // Locking: self @@ -316,6 +326,7 @@ abstract class JdbcDatabase implements Database<Connection> { s.executeUpdate(insertTypeNames(CREATE_CONNECTION_WINDOWS)); s.executeUpdate(insertTypeNames(CREATE_SUBSCRIPTION_TIMESTAMPS)); s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_TIMESTAMPS)); + s.executeUpdate(insertTypeNames(CREATE_FLAGS)); s.close(); } catch(SQLException e) { tryToClose(s); @@ -1319,6 +1330,27 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public boolean getRead(Connection txn, MessageId m) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT read FROM flags WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + boolean read = false; + if(rs.next()) read = rs.getBoolean(1); + if(rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + return read; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public Map<ContactId, TransportProperties> getRemoteProperties( Connection txn, TransportId t) throws DbException { PreparedStatement ps = null; @@ -1513,6 +1545,27 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public boolean getStarred(Connection txn, MessageId m) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT starred FROM flags WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + boolean starred = false; + if(rs.next()) starred = rs.getBoolean(1); + if(rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + return starred; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public Collection<Group> getSubscriptions(Connection txn) throws DbException { PreparedStatement ps = null; @@ -2068,7 +2121,7 @@ abstract class JdbcDatabase implements Database<Connection> { rs = ps.executeQuery(); Rating old; if(rs.next()) { - // A rating row exists - update it + // A rating row exists - update it if necessary old = Rating.values()[rs.getByte(1)]; if(rs.next()) throw new DbStateException(); rs.close(); @@ -2083,17 +2136,70 @@ abstract class JdbcDatabase implements Database<Connection> { ps.close(); } } else { - // No rating row exists - create one + // No rating row exists - create one if necessary rs.close(); ps.close(); old = Rating.UNRATED; - sql = "INSERT INTO ratings (authorId, rating) VALUES (?, ?)"; - ps = txn.prepareStatement(sql); - ps.setBytes(1, a.getBytes()); - ps.setShort(2, (short) r.ordinal()); - int affected = ps.executeUpdate(); - if(affected != 1) throw new DbStateException(); + if(!old.equals(r)) { + sql = "INSERT INTO ratings (authorId, rating)" + + " VALUES (?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, a.getBytes()); + ps.setShort(2, (short) r.ordinal()); + int affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); + ps.close(); + } + } + return old; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + public boolean setRead(Connection txn, MessageId m, boolean read) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT read FROM flags WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + boolean old; + if(rs.next()) { + // A flag row exists - update it if necessary + old = rs.getBoolean(1); + if(rs.next()) throw new DbStateException(); + rs.close(); ps.close(); + if(old != read) { + sql = "UPDATE flags SET read = ? WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBoolean(1, read); + ps.setBytes(2, m.getBytes()); + int affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); + ps.close(); + } + } else { + // No flag row exists - create one if necessary + ps.close(); + rs.close(); + old = false; + if(old != read) { + sql = "INSERT INTO flags (messageId, read, starred)" + + " VALUES (?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + ps.setBoolean(2, read); + ps.setBoolean(3, false); + int affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); + ps.close(); + } } return old; } catch(SQLException e) { @@ -2121,6 +2227,56 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public boolean setStarred(Connection txn, MessageId m, boolean starred) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT starred FROM flags WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + rs = ps.executeQuery(); + boolean old; + if(rs.next()) { + // A flag row exists - update it if necessary + old = rs.getBoolean(1); + if(rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + if(old != starred) { + sql = "UPDATE flags SET starred = ? WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setBoolean(1, starred); + ps.setBytes(2, m.getBytes()); + int affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); + ps.close(); + } + } else { + // No flag row exists - create one if necessary + ps.close(); + rs.close(); + old = false; + if(old != starred) { + sql = "INSERT INTO flags (messageId, read, starred)" + + " VALUES (?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + ps.setBoolean(2, false); + ps.setBoolean(3, starred); + int affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); + ps.close(); + } + } + return old; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public void setStatus(Connection txn, ContactId c, MessageId m, Status s) throws DbException { PreparedStatement ps = null; @@ -2133,7 +2289,7 @@ abstract class JdbcDatabase implements Database<Connection> { ps.setInt(2, c.getInt()); rs = ps.executeQuery(); if(rs.next()) { - // A status row exists - update it + // A status row exists - update it if necessary Status old = Status.values()[rs.getByte(1)]; if(rs.next()) throw new DbStateException(); rs.close(); diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java index edc59a800e2d42f7d4621c2691107e03ec2829f2..b2f5b16c8705a712cc96b911ebc6198fa388c8e0 100644 --- a/test/net/sf/briar/db/H2DatabaseTest.java +++ b/test/net/sf/briar/db/H2DatabaseTest.java @@ -1705,6 +1705,58 @@ public class H2DatabaseTest extends TestCase { db.close(); } + @Test + public void testReadFlag() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group and store a message + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + + // The message should be unread by default + assertFalse(db.getRead(txn, messageId)); + // Marking the message read should return the old value + assertFalse(db.setRead(txn, messageId, true)); + assertTrue(db.setRead(txn, messageId, true)); + // The message should be read + assertTrue(db.getRead(txn, messageId)); + // Marking the message unread should return the old value + assertTrue(db.setRead(txn, messageId, false)); + assertFalse(db.setRead(txn, messageId, false)); + // Unsubscribe from the group + db.removeSubscription(txn, groupId); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testStarredFlag() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group and store a message + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + + // The message should be unstarred by default + assertFalse(db.getStarred(txn, messageId)); + // Starring the message should return the old value + assertFalse(db.setStarred(txn, messageId, true)); + assertTrue(db.setStarred(txn, messageId, true)); + // The message should be starred + assertTrue(db.getStarred(txn, messageId)); + // Unstarring the message should return the old value + assertTrue(db.setStarred(txn, messageId, false)); + assertFalse(db.setStarred(txn, messageId, false)); + // Unsubscribe from the group + db.removeSubscription(txn, groupId); + + db.commitTransaction(txn); + db.close(); + } + private void assertHeadersAreEqual(MessageHeader h1, MessageHeader h2) { assertEquals(h1.getId(), h2.getId()); if(h1.getParent() == null) assertNull(h2.getParent());