Added message flags: read/unread and starred/unstarred.

This commit is contained in:
akwizgran
2011-10-26 16:32:30 +01:00
parent b70b579fd6
commit 0f6b0e88c1
4 changed files with 304 additions and 47 deletions

View File

@@ -32,6 +32,7 @@ import net.sf.briar.api.transport.ConnectionWindow;
* <ul> * <ul>
* <li> contact * <li> contact
* <li> message * <li> message
* <li> messageFlag
* <li> messageStatus * <li> messageStatus
* <li> rating * <li> rating
* <li> subscription * <li> subscription
@@ -304,6 +305,13 @@ interface Database<T> {
*/ */
Rating getRating(T txn, AuthorId a) throws DbException; 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. * Returns all remote properties for the given transport.
* <p> * <p>
@@ -346,6 +354,13 @@ interface Database<T> {
*/ */
byte[] getSharedSecret(T txn, ContactId c) throws DbException; 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. * Returns the groups to which the user subscribes.
* <p> * <p>
@@ -433,8 +448,8 @@ interface Database<T> {
/** /**
* Removes a contact (and all associated state) from the database. * Removes a contact (and all associated state) from the database.
* <p> * <p>
* Locking: contact write, message write, messageStatus write, * Locking: contact write, message write, messageFlag write,
* subscription write, transport write. * messageStatus write, subscription write, transport write.
*/ */
void removeContact(T txn, ContactId c) throws DbException; 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. * Removes a message (and all associated state) from the database.
* <p> * <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; 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 * Unsubscribes from the given group. Any messages belonging to the group
* are deleted from the database. * are deleted from the database.
* <p> * <p>
* Locking: contact read, message write, messageStatus write, * Locking: contact read, message write, messageFlag write,
* subscription write. * messageStatus write, subscription write.
*/ */
void removeSubscription(T txn, GroupId g) throws DbException; 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; 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. * Sets the sendability score of the given message.
* <p> * <p>
@@ -504,6 +528,14 @@ interface Database<T> {
*/ */
void setSendability(T txn, MessageId m, int sendability) throws DbException; 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. * Sets the status of the given message with respect to the given contact.
* <p> * <p>

View File

@@ -81,6 +81,8 @@ DatabaseCleaner.Callback {
new ReentrantReadWriteLock(true); new ReentrantReadWriteLock(true);
private final ReentrantReadWriteLock messageLock = private final ReentrantReadWriteLock messageLock =
new ReentrantReadWriteLock(true); new ReentrantReadWriteLock(true);
private final ReentrantReadWriteLock messageFlagLock =
new ReentrantReadWriteLock(true);
private final ReentrantReadWriteLock messageStatusLock = private final ReentrantReadWriteLock messageStatusLock =
new ReentrantReadWriteLock(true); new ReentrantReadWriteLock(true);
private final ReentrantReadWriteLock ratingLock = private final ReentrantReadWriteLock ratingLock =
@@ -1137,28 +1139,33 @@ DatabaseCleaner.Callback {
try { try {
messageLock.writeLock().lock(); messageLock.writeLock().lock();
try { try {
messageStatusLock.writeLock().lock(); messageFlagLock.writeLock().lock();
try { try {
subscriptionLock.writeLock().lock(); messageStatusLock.writeLock().lock();
try { try {
transportLock.writeLock().lock(); subscriptionLock.writeLock().lock();
try { try {
T txn = db.startTransaction(); transportLock.writeLock().lock();
try { try {
db.removeContact(txn, c); T txn = db.startTransaction();
db.commitTransaction(txn); try {
} catch(DbException e) { db.removeContact(txn, c);
db.abortTransaction(txn); db.commitTransaction(txn);
throw e; } catch(DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
transportLock.writeLock().unlock();
} }
} finally { } finally {
transportLock.writeLock().unlock(); subscriptionLock.writeLock().unlock();
} }
} finally { } finally {
subscriptionLock.writeLock().unlock(); messageStatusLock.writeLock().unlock();
} }
} finally { } finally {
messageStatusLock.writeLock().unlock(); messageFlagLock.writeLock().unlock();
} }
} finally { } finally {
messageLock.writeLock().unlock(); messageLock.writeLock().unlock();
@@ -1399,26 +1406,31 @@ DatabaseCleaner.Callback {
try { try {
messageLock.writeLock().lock(); messageLock.writeLock().lock();
try { try {
messageStatusLock.writeLock().lock(); messageFlagLock.writeLock().lock();
try { try {
subscriptionLock.writeLock().lock(); messageStatusLock.writeLock().lock();
try { try {
T txn = db.startTransaction(); subscriptionLock.writeLock().lock();
try { try {
if(db.containsSubscription(txn, g)) { T txn = db.startTransaction();
affected = db.getVisibility(txn, g); try {
db.removeSubscription(txn, g); 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); } finally {
} catch(DbException e) { subscriptionLock.writeLock().unlock();
db.abortTransaction(txn);
throw e;
} }
} finally { } finally {
subscriptionLock.writeLock().unlock(); messageStatusLock.writeLock().unlock();
} }
} finally { } finally {
messageStatusLock.writeLock().unlock(); messageFlagLock.writeLock().unlock();
} }
} finally { } finally {
messageLock.writeLock().unlock(); messageLock.writeLock().unlock();
@@ -1457,19 +1469,24 @@ DatabaseCleaner.Callback {
try { try {
messageLock.writeLock().lock(); messageLock.writeLock().lock();
try { try {
messageStatusLock.writeLock().lock(); messageFlagLock.writeLock().lock();
try { try {
T txn = db.startTransaction(); messageStatusLock.writeLock().lock();
try { try {
old = db.getOldMessages(txn, size); T txn = db.startTransaction();
for(MessageId m : old) removeMessage(txn, m); try {
db.commitTransaction(txn); old = db.getOldMessages(txn, size);
} catch(DbException e) { for(MessageId m : old) removeMessage(txn, m);
db.abortTransaction(txn); db.commitTransaction(txn);
throw e; } catch(DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
messageStatusLock.writeLock().unlock();
} }
} finally { } finally {
messageStatusLock.writeLock().unlock(); messageFlagLock.writeLock().unlock();
} }
} finally { } finally {
messageLock.writeLock().unlock(); messageLock.writeLock().unlock();

View File

@@ -225,6 +225,15 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)" + " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+ " ON DELETE CASCADE)"; + " 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 = private static final Logger LOG =
Logger.getLogger(JdbcDatabase.class.getName()); Logger.getLogger(JdbcDatabase.class.getName());
@@ -233,6 +242,7 @@ abstract class JdbcDatabase implements Database<Connection> {
private final ConnectionWindowFactory connectionWindowFactory; private final ConnectionWindowFactory connectionWindowFactory;
private final GroupFactory groupFactory; private final GroupFactory groupFactory;
private final MessageHeaderFactory messageHeaderFactory; private final MessageHeaderFactory messageHeaderFactory;
private final LinkedList<Connection> connections = private final LinkedList<Connection> connections =
new LinkedList<Connection>(); // Locking: self 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_CONNECTION_WINDOWS));
s.executeUpdate(insertTypeNames(CREATE_SUBSCRIPTION_TIMESTAMPS)); s.executeUpdate(insertTypeNames(CREATE_SUBSCRIPTION_TIMESTAMPS));
s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_TIMESTAMPS)); s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_TIMESTAMPS));
s.executeUpdate(insertTypeNames(CREATE_FLAGS));
s.close(); s.close();
} catch(SQLException e) { } catch(SQLException e) {
tryToClose(s); 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( public Map<ContactId, TransportProperties> getRemoteProperties(
Connection txn, TransportId t) throws DbException { Connection txn, TransportId t) throws DbException {
PreparedStatement ps = null; 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) public Collection<Group> getSubscriptions(Connection txn)
throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
@@ -2068,7 +2121,7 @@ abstract class JdbcDatabase implements Database<Connection> {
rs = ps.executeQuery(); rs = ps.executeQuery();
Rating old; Rating old;
if(rs.next()) { if(rs.next()) {
// A rating row exists - update it // A rating row exists - update it if necessary
old = Rating.values()[rs.getByte(1)]; old = Rating.values()[rs.getByte(1)];
if(rs.next()) throw new DbStateException(); if(rs.next()) throw new DbStateException();
rs.close(); rs.close();
@@ -2083,17 +2136,70 @@ abstract class JdbcDatabase implements Database<Connection> {
ps.close(); ps.close();
} }
} else { } else {
// No rating row exists - create one // No rating row exists - create one if necessary
rs.close(); rs.close();
ps.close(); ps.close();
old = Rating.UNRATED; old = Rating.UNRATED;
sql = "INSERT INTO ratings (authorId, rating) VALUES (?, ?)"; if(!old.equals(r)) {
ps = txn.prepareStatement(sql); sql = "INSERT INTO ratings (authorId, rating)"
ps.setBytes(1, a.getBytes()); + " VALUES (?, ?)";
ps.setShort(2, (short) r.ordinal()); ps = txn.prepareStatement(sql);
int affected = ps.executeUpdate(); ps.setBytes(1, a.getBytes());
if(affected != 1) throw new DbStateException(); 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(); 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; return old;
} catch(SQLException e) { } 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) public void setStatus(Connection txn, ContactId c, MessageId m, Status s)
throws DbException { throws DbException {
PreparedStatement ps = null; PreparedStatement ps = null;
@@ -2133,7 +2289,7 @@ abstract class JdbcDatabase implements Database<Connection> {
ps.setInt(2, c.getInt()); ps.setInt(2, c.getInt());
rs = ps.executeQuery(); rs = ps.executeQuery();
if(rs.next()) { if(rs.next()) {
// A status row exists - update it // A status row exists - update it if necessary
Status old = Status.values()[rs.getByte(1)]; Status old = Status.values()[rs.getByte(1)];
if(rs.next()) throw new DbStateException(); if(rs.next()) throw new DbStateException();
rs.close(); rs.close();

View File

@@ -1705,6 +1705,58 @@ public class H2DatabaseTest extends TestCase {
db.close(); 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) { private void assertHeadersAreEqual(MessageHeader h1, MessageHeader h2) {
assertEquals(h1.getId(), h2.getId()); assertEquals(h1.getId(), h2.getId());
if(h1.getParent() == null) assertNull(h2.getParent()); if(h1.getParent() == null) assertNull(h2.getParent());