diff --git a/api/net/sf/briar/api/db/ContactId.java b/api/net/sf/briar/api/ContactId.java similarity index 94% rename from api/net/sf/briar/api/db/ContactId.java rename to api/net/sf/briar/api/ContactId.java index 39d362a3a97eb2f843be2aac283fadc87638e3b3..0dbdc140fbd7b8d98a0c056704b5853db7898660 100644 --- a/api/net/sf/briar/api/db/ContactId.java +++ b/api/net/sf/briar/api/ContactId.java @@ -1,4 +1,4 @@ -package net.sf.briar.api.db; +package net.sf.briar.api; /** Type-safe wrapper for an integer that uniquely identifies a contact. */ public class ContactId { diff --git a/api/net/sf/briar/api/db/Rating.java b/api/net/sf/briar/api/Rating.java similarity index 80% rename from api/net/sf/briar/api/db/Rating.java rename to api/net/sf/briar/api/Rating.java index 2a61dbfde26d10ecf3143b69ea2253978c6dfd4d..a3ddb57c4d5595216a0cefaccc7f51cf2cec3e42 100644 --- a/api/net/sf/briar/api/db/Rating.java +++ b/api/net/sf/briar/api/Rating.java @@ -1,4 +1,4 @@ -package net.sf.briar.api.db; +package net.sf.briar.api; /** The ratings that may be applied to an author in peer moderation. */ public enum Rating { diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java index ca142d39df885c9c6a97f15e9a3e87a4010626c3..9d3d75b08bfde09dc56daaf6836278a77d0a4c29 100644 --- a/api/net/sf/briar/api/db/DatabaseComponent.java +++ b/api/net/sf/briar/api/db/DatabaseComponent.java @@ -1,7 +1,10 @@ package net.sf.briar.api.db; +import java.util.Map; import java.util.Set; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.Rating; import net.sf.briar.api.protocol.AuthorId; import net.sf.briar.api.protocol.Bundle; import net.sf.briar.api.protocol.GroupId; @@ -32,8 +35,11 @@ public interface DatabaseComponent { /** Waits for any open transactions to finish and closes the database. */ void close() throws DbException; - /** Adds a new contact to the database and returns an ID for the contact. */ - ContactId addContact() throws DbException; + /** + * Adds a new contact to the database with the given transport details and + * returns an ID for the contact. + */ + ContactId addContact(Map<String, String> transports) throws DbException; /** Adds a locally generated message to the database. */ void addLocallyGeneratedMessage(Message m) throws DbException; @@ -53,6 +59,9 @@ public interface DatabaseComponent { /** Returns the set of groups to which the user subscribes. */ Set<GroupId> getSubscriptions() throws DbException; + /** Returns the transport details for the given contact. */ + Map<String, String> getTransports(ContactId c) throws DbException; + /** * Processes a bundle of acknowledgements, subscriptions, and batches of * messages received from the given contact. Some or all of the messages @@ -66,6 +75,12 @@ public interface DatabaseComponent { /** Records the user's rating for the given author. */ void setRating(AuthorId a, Rating r) throws DbException; + /** + * Records the transport details for the given contact, replacing any + * existing transport details. + */ + void setTransports(ContactId c, Map<String, String> transports) throws DbException; + /** Subscribes to the given group. */ void subscribe(GroupId g) throws DbException; diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java index a0e611a8ef3a784c18e46c4d09f6069910b6460b..ca2cdb0b7b8d9dd6905ac716d89b317968f1e307 100644 --- a/components/net/sf/briar/db/Database.java +++ b/components/net/sf/briar/db/Database.java @@ -1,10 +1,11 @@ package net.sf.briar.db; +import java.util.Map; import java.util.Set; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.Rating; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.ContactId; -import net.sf.briar.api.db.Rating; import net.sf.briar.api.db.Status; import net.sf.briar.api.protocol.AuthorId; import net.sf.briar.api.protocol.BatchId; @@ -28,6 +29,7 @@ import net.sf.briar.api.protocol.MessageId; * <li> messageStatuses * <li> ratings * <li> subscriptions + * <li> transports * </ul> */ interface Database<T> { @@ -49,7 +51,11 @@ interface Database<T> { */ void open(boolean resume) throws DbException; - /** Waits for all open transactions to finish and closes the database. */ + /** + * Waits for all open transactions to finish and closes the database. + * <p> + * Locking: all locks write. + */ void close() throws DbException; /** Starts a new transaction and returns an object representing it. */ @@ -75,11 +81,12 @@ interface Database<T> { void addBatchToAck(T txn, ContactId c, BatchId b) throws DbException; /** - * Adds a new contact to the database and returns an ID for the contact. + * Adds a new contact to the database with the given transport details and + * returns an ID for the contact. * <p> - * Locking: contacts write, messageStatuses write. + * Locking: contacts write, transports write. */ - ContactId addContact(T txn) throws DbException; + ContactId addContact(T txn, Map<String, String> transports) throws DbException; /** * Returns false if the given message is already in the database. Otherwise @@ -115,14 +122,14 @@ interface Database<T> { /** * Records a contact's subscription to a group. * <p> - * Locking: contacts read, messageStatuses write. + * Locking: contacts read, subscriptions write. */ void addSubscription(T txn, ContactId c, GroupId g) throws DbException; /** * Removes all recorded subscriptions for the given contact. * <p> - * Locking: contacts read, messageStatuses write. + * Locking: contacts read, subscriptions write. */ void clearSubscriptions(T txn, ContactId c) throws DbException; @@ -150,7 +157,7 @@ interface Database<T> { /** * Returns the IDs of all contacts. * <p> - * Locking: contacts read, messageStatuses read. + * Locking: contacts read. */ Set<ContactId> getContacts(T txn) throws DbException; @@ -238,6 +245,13 @@ interface Database<T> { */ Set<GroupId> getSubscriptions(T txn) throws DbException; + /** + * Returns the transport details for the given contact. + * <p> + * Locking: contacts read, transports read. + */ + Map<String, String> getTransports(T txn, ContactId c) throws DbException; + /** * Removes an outstanding batch that has been acknowledged. Any messages in * the batch that are still considered outstanding (Status.SENT) with @@ -258,7 +272,8 @@ interface Database<T> { /** * Removes a contact (and all associated state) from the database. * <p> - * Locking: contacts write, messageStatuses write. + * Locking: contacts write, messageStatuses write, subscriptions write, + * transports write. */ void removeContact(T txn, ContactId c) throws DbException; @@ -282,20 +297,20 @@ interface Database<T> { * Unsubscribes from the given group. Any messages belonging to the group * are deleted from the database. * <p> - * Locking: contacts read, subscriptions write, messages write, - * messageStatuses write. + * Locking: contacts read, messages write, messageStatuses write, + * subscriptions write. */ void removeSubscription(T txn, GroupId g) throws DbException; /** - * Records the user's rating for the given author. + * Sets the user's rating for the given author. * <p> * Locking: ratings write. */ Rating setRating(T txn, AuthorId a, Rating r) throws DbException; /** - * Records the sendability score of the given message. + * Sets the sendability score of the given message. * <p> * Locking: messages write. */ @@ -307,4 +322,12 @@ interface Database<T> { * Locking: contacts read, messages read, messageStatuses write. */ void setStatus(T txn, ContactId c, MessageId m, Status s) throws DbException; + + /** + * Sets the transport details for the given contact, replacing any existing + * transport details. + * <p> + * Locking: contacts read, transports write. + */ + void setTransports(T txn, ContactId c, Map<String, String> transports) throws DbException; } diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java index edd4fe55661c895eb4fac16f12f51a7e30870f71..b2cfa0bdaab4eb6f257da12e37216e90c4032e53 100644 --- a/components/net/sf/briar/db/DatabaseComponentImpl.java +++ b/components/net/sf/briar/db/DatabaseComponentImpl.java @@ -3,10 +3,10 @@ package net.sf.briar.db; import java.util.logging.Level; import java.util.logging.Logger; -import net.sf.briar.api.db.ContactId; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.Rating; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.Rating; import net.sf.briar.api.db.Status; import net.sf.briar.api.protocol.AuthorId; import net.sf.briar.api.protocol.Batch; diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java index e0440e5ce168334869546ed8170123cc828f4382..dc6b73259f2399d2dfc1cd9d51688fb012ad592e 100644 --- a/components/net/sf/briar/db/JdbcDatabase.java +++ b/components/net/sf/briar/db/JdbcDatabase.java @@ -13,13 +13,16 @@ import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; -import net.sf.briar.api.db.ContactId; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.Rating; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.Rating; import net.sf.briar.api.db.Status; import net.sf.briar.api.protocol.AuthorId; import net.sf.briar.api.protocol.BatchId; @@ -147,6 +150,15 @@ abstract class JdbcDatabase implements Database<Connection> { private static final String INDEX_STATUSES_BY_CONTACT = "CREATE INDEX statusesByContact ON statuses (contactId)"; + private static final String CREATE_TRANSPORTS = + "CREATE TABLE transports" + + " (contactId INT NOT NULL," + + " detailKey VARCHAR NOT NULL," + + " detailValue VARCHAR NOT NULL," + + " PRIMARY KEY (contactId, detailKey)," + + " FOREIGN KEY (contactId) REFERENCES contacts (contactId)" + + " ON DELETE CASCADE)"; + private static final Logger LOG = Logger.getLogger(JdbcDatabase.class.getName()); @@ -236,6 +248,9 @@ abstract class JdbcDatabase implements Database<Connection> { s.executeUpdate(insertHashType(CREATE_STATUSES)); s.executeUpdate(INDEX_STATUSES_BY_MESSAGE); s.executeUpdate(INDEX_STATUSES_BY_CONTACT); + if(LOG.isLoggable(Level.FINE)) + LOG.fine("Creating transports table"); + s.executeUpdate(insertHashType(CREATE_TRANSPORTS)); s.close(); } catch(SQLException e) { tryToClose(s); @@ -363,7 +378,8 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public ContactId addContact(Connection txn) throws DbException { + public ContactId addContact(Connection txn, Map<String, String> transports) + throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { @@ -400,6 +416,25 @@ abstract class JdbcDatabase implements Database<Connection> { rowsAffected = ps.executeUpdate(); assert rowsAffected == 1; ps.close(); + // Store the contact's transport details + if(transports != null) { + sql = "INSERT INTO transports" + + " (contactId, detailKey, detailValue)" + + " VALUES (?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + for(Entry<String, String> e : transports.entrySet()) { + ps.setString(2, e.getKey()); + ps.setString(3, e.getValue()); + ps.addBatch(); + } + int[] rowsAffectedArray = ps.executeBatch(); + assert rowsAffectedArray.length == transports.size(); + for(int i = 0; i < rowsAffectedArray.length; i++) { + assert rowsAffectedArray[i] == 1; + } + ps.close(); + } return c; } catch(SQLException e) { tryToClose(ps); @@ -476,10 +511,10 @@ abstract class JdbcDatabase implements Database<Connection> { ps.setBytes(3, m.getBytes()); ps.addBatch(); } - int[] rowsAffected1 = ps.executeBatch(); - assert rowsAffected1.length == sent.size(); - for(int i = 0; i < rowsAffected1.length; i++) { - assert rowsAffected1[i] == 1; + int[] rowsAffectedArray = ps.executeBatch(); + assert rowsAffectedArray.length == sent.size(); + for(int i = 0; i < rowsAffectedArray.length; i++) { + assert rowsAffectedArray[i] == 1; } ps.close(); // Set the status of each message in the batch to SENT @@ -493,10 +528,10 @@ abstract class JdbcDatabase implements Database<Connection> { ps.setBytes(2, m.getBytes()); ps.addBatch(); } - rowsAffected1 = ps.executeBatch(); - assert rowsAffected1.length == sent.size(); - for(int i = 0; i < rowsAffected1.length; i++) { - assert rowsAffected1[i] <= 1; + rowsAffectedArray = ps.executeBatch(); + assert rowsAffectedArray.length == sent.size(); + for(int i = 0; i < rowsAffectedArray.length; i++) { + assert rowsAffectedArray[i] <= 1; } ps.close(); } catch(SQLException e) { @@ -1067,6 +1102,29 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public Map<String, String> getTransports(Connection txn, ContactId c) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT detailKey, detailValue FROM transports" + + " WHERE contactId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + rs = ps.executeQuery(); + Map<String, String> transports = new TreeMap<String, String>(); + while(rs.next()) transports.put(rs.getString(1), rs.getString(2)); + rs.close(); + ps.close(); + return transports; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } + public void removeAckedBatch(Connection txn, ContactId c, BatchId b) throws DbException { removeBatch(txn, c, b, Status.SEEN); @@ -1097,18 +1155,18 @@ abstract class JdbcDatabase implements Database<Connection> { } rs.close(); ps.close(); - int[] rowsAffected = ps1.executeBatch(); - assert rowsAffected.length == messages; - for(int i = 0; i < rowsAffected.length; i++) { - assert rowsAffected[i] <= 1; + int[] rowsAffectedArray = ps1.executeBatch(); + assert rowsAffectedArray.length == messages; + for(int i = 0; i < rowsAffectedArray.length; i++) { + assert rowsAffectedArray[i] <= 1; } ps1.close(); // Cascade on delete sql = "DELETE FROM outstandingBatches WHERE batchId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, b.getBytes()); - int rowsAffected1 = ps.executeUpdate(); - assert rowsAffected1 <= 1; + int rowsAffected = ps.executeUpdate(); + assert rowsAffected <= 1; ps.close(); } catch(SQLException e) { tryToClose(rs); @@ -1314,4 +1372,40 @@ abstract class JdbcDatabase implements Database<Connection> { throw new DbException(e); } } + + public void setTransports(Connection txn, ContactId c, + Map<String, String> transports) throws DbException { + PreparedStatement ps = null; + try { + // Delete any existing transports + String sql = "DELETE FROM transports WHERE contactId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.executeUpdate(); + ps.close(); + // Store the new transports + if(transports != null) { + sql = "INSERT INTO transports" + + " (contactId, detailKey, detailValue)" + + " VALUES (?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + for(Entry<String, String> e : transports.entrySet()) { + ps.setString(2, e.getKey()); + ps.setString(3, e.getValue()); + ps.addBatch(); + } + int[] rowsAffectedArray = ps.executeBatch(); + assert rowsAffectedArray.length == transports.size(); + for(int i = 0; i < rowsAffectedArray.length; i++) { + assert rowsAffectedArray[i] == 1; + } + ps.close(); + } + } catch(SQLException e) { + tryToClose(ps); + tryToClose(txn); + throw new DbException(e); + } + } } diff --git a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java index eb984b59aac4be2a58367a27ca0f9ebe9ef731af..93c24ab6d46496e5557cebd59279e952aa08414f 100644 --- a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java +++ b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java @@ -2,15 +2,16 @@ package net.sf.briar.db; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; -import net.sf.briar.api.db.ContactId; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.Rating; import net.sf.briar.api.db.DbException; import net.sf.briar.api.db.NoSuchContactException; -import net.sf.briar.api.db.Rating; import net.sf.briar.api.protocol.AuthorId; import net.sf.briar.api.protocol.Batch; import net.sf.briar.api.protocol.BatchId; @@ -46,6 +47,8 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { new ReentrantReadWriteLock(true); private final ReentrantReadWriteLock subscriptionLock = new ReentrantReadWriteLock(true); + private final ReentrantReadWriteLock transportLock = + new ReentrantReadWriteLock(true); @Inject ReadWriteLockDatabaseComponent(Database<Txn> db, DatabaseCleaner cleaner, @@ -83,15 +86,16 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { } } - public ContactId addContact() throws DbException { + public ContactId addContact(Map<String, String> transports) + throws DbException { if(LOG.isLoggable(Level.FINE)) LOG.fine("Adding contact"); contactLock.writeLock().lock(); try { - messageStatusLock.writeLock().lock(); + transportLock.writeLock().lock(); try { Txn txn = db.startTransaction(); try { - ContactId c = db.addContact(txn); + ContactId c = db.addContact(txn, transports); db.commitTransaction(txn); if(LOG.isLoggable(Level.FINE)) LOG.fine("Added contact " + c); @@ -101,7 +105,7 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { throw e; } } finally { - messageStatusLock.writeLock().unlock(); + transportLock.writeLock().unlock(); } } finally { contactLock.writeLock().unlock(); @@ -313,19 +317,14 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { public Set<ContactId> getContacts() throws DbException { contactLock.readLock().lock(); try { - messageStatusLock.readLock().lock(); + Txn txn = db.startTransaction(); try { - Txn txn = db.startTransaction(); - try { - Set<ContactId> contacts = db.getContacts(txn); - db.commitTransaction(txn); - return contacts; - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - messageStatusLock.readLock().unlock(); + Set<ContactId> contacts = db.getContacts(txn); + db.commitTransaction(txn); + return contacts; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; } } finally { contactLock.readLock().unlock(); @@ -366,6 +365,29 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { } } + public Map<String, String> getTransports(ContactId c) throws DbException { + contactLock.readLock().lock(); + try { + if(!containsContact(c)) throw new NoSuchContactException(); + transportLock.readLock().lock(); + try { + Txn txn = db.startTransaction(); + try { + Map<String, String> transports = db.getTransports(txn, c); + db.commitTransaction(txn); + return transports; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + transportLock.readLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + } + public void receiveBundle(ContactId c, Bundle b) throws DbException { if(LOG.isLoggable(Level.FINE)) LOG.fine("Received bundle from " + c + ", " @@ -405,7 +427,7 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { contactLock.readLock().lock(); try { if(!containsContact(c)) throw new NoSuchContactException(); - messageStatusLock.writeLock().lock(); + subscriptionLock.writeLock().lock(); try { Txn txn = db.startTransaction(); try { @@ -423,7 +445,7 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { throw e; } } finally { - messageStatusLock.writeLock().unlock(); + subscriptionLock.writeLock().unlock(); } } finally { contactLock.readLock().lock(); @@ -539,13 +561,23 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { try { messageStatusLock.writeLock().lock(); try { - Txn txn = db.startTransaction(); + subscriptionLock.writeLock().lock(); try { - db.removeContact(txn, c); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; + transportLock.writeLock().lock(); + try { + Txn txn = db.startTransaction(); + try { + db.removeContact(txn, c); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + transportLock.writeLock().unlock(); + } + } finally { + subscriptionLock.writeLock().unlock(); } } finally { messageStatusLock.writeLock().unlock(); @@ -581,6 +613,29 @@ class ReadWriteLockDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { } } + public void setTransports(ContactId c, Map<String, String> transports) + throws DbException { + contactLock.readLock().lock(); + try { + if(!containsContact(c)) throw new NoSuchContactException(); + transportLock.writeLock().lock(); + try { + Txn txn = db.startTransaction(); + try { + db.setTransports(txn, c, transports); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + transportLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + } + public void subscribe(GroupId g) throws DbException { if(LOG.isLoggable(Level.FINE)) LOG.fine("Subscribing to " + g); subscriptionLock.writeLock().lock(); diff --git a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java index ba57834fa2e4a65074517c096e93e5af87d8fc56..55900ec225093245cd461fba4cf77532a672fcbf 100644 --- a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java +++ b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java @@ -2,14 +2,15 @@ package net.sf.briar.db; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.Rating; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.ContactId; import net.sf.briar.api.db.NoSuchContactException; -import net.sf.briar.api.db.Rating; import net.sf.briar.api.protocol.AuthorId; import net.sf.briar.api.protocol.Batch; import net.sf.briar.api.protocol.BatchId; @@ -40,6 +41,7 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { private final Object messageStatusLock = new Object(); private final Object ratingLock = new Object(); private final Object subscriptionLock = new Object(); + private final Object transportLock = new Object(); @Inject SynchronizedDatabaseComponent(Database<Txn> db, DatabaseCleaner cleaner, @@ -54,7 +56,9 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { synchronized(messageStatusLock) { synchronized(ratingLock) { synchronized(subscriptionLock) { - db.close(); + synchronized(transportLock) { + db.close(); + } } } } @@ -62,13 +66,14 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { } } - public ContactId addContact() throws DbException { + public ContactId addContact(Map<String, String> transports) + throws DbException { if(LOG.isLoggable(Level.FINE)) LOG.fine("Adding contact"); synchronized(contactLock) { - synchronized(messageStatusLock) { + synchronized(transportLock) { Txn txn = db.startTransaction(); try { - ContactId c = db.addContact(txn); + ContactId c = db.addContact(txn, transports); db.commitTransaction(txn); if(LOG.isLoggable(Level.FINE)) LOG.fine("Added contact " + c); @@ -229,16 +234,14 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { public Set<ContactId> getContacts() throws DbException { synchronized(contactLock) { - synchronized(messageStatusLock) { - Txn txn = db.startTransaction(); - try { - Set<ContactId> contacts = db.getContacts(txn); - db.commitTransaction(txn); - return contacts; - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } + Txn txn = db.startTransaction(); + try { + Set<ContactId> contacts = db.getContacts(txn); + db.commitTransaction(txn); + return contacts; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; } } } @@ -271,6 +274,23 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { } } + public Map<String, String> getTransports(ContactId c) throws DbException { + synchronized(contactLock) { + if(!containsContact(c)) throw new NoSuchContactException(); + synchronized(transportLock) { + Txn txn = db.startTransaction(); + try { + Map<String, String> transports = db.getTransports(txn, c); + db.commitTransaction(txn); + return transports; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + } + public void receiveBundle(ContactId c, Bundle b) throws DbException { if(LOG.isLoggable(Level.FINE)) LOG.fine("Received bundle from " + c + ", " @@ -300,7 +320,7 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { // Update the contact's subscriptions synchronized(contactLock) { if(!containsContact(c)) throw new NoSuchContactException(); - synchronized(messageStatusLock) { + synchronized(subscriptionLock) { Txn txn = db.startTransaction(); try { db.clearSubscriptions(txn, c); @@ -397,13 +417,17 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { if(LOG.isLoggable(Level.FINE)) LOG.fine("Removing contact " + c); synchronized(contactLock) { synchronized(messageStatusLock) { - Txn txn = db.startTransaction(); - try { - db.removeContact(txn, c); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; + synchronized(subscriptionLock) { + synchronized(transportLock) { + Txn txn = db.startTransaction(); + try { + db.removeContact(txn, c); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } } } } @@ -429,6 +453,23 @@ class SynchronizedDatabaseComponent<Txn> extends DatabaseComponentImpl<Txn> { } } + public void setTransports(ContactId c, Map<String, String> transports) + throws DbException { + synchronized(contactLock) { + if(!containsContact(c)) throw new NoSuchContactException(); + synchronized(transportLock) { + Txn txn = db.startTransaction(); + try { + db.setTransports(txn, c, transports); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + } + public void subscribe(GroupId g) throws DbException { if(LOG.isLoggable(Level.FINE)) LOG.fine("Subscribing to " + g); synchronized(subscriptionLock) { diff --git a/test/net/sf/briar/db/DatabaseComponentTest.java b/test/net/sf/briar/db/DatabaseComponentTest.java index dbab966d3f0013cd023fd57a6f0e433ad2a51561..ba6c808e13c48544cc5b28885ff1e3e2984f650e 100644 --- a/test/net/sf/briar/db/DatabaseComponentTest.java +++ b/test/net/sf/briar/db/DatabaseComponentTest.java @@ -1,15 +1,16 @@ package net.sf.briar.db; import java.util.Collections; +import java.util.Map; import java.util.Set; import junit.framework.TestCase; import net.sf.briar.TestUtils; -import net.sf.briar.api.db.ContactId; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.Rating; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; import net.sf.briar.api.db.NoSuchContactException; -import net.sf.briar.api.db.Rating; import net.sf.briar.api.db.Status; import net.sf.briar.api.protocol.AuthorId; import net.sf.briar.api.protocol.Batch; @@ -65,6 +66,10 @@ public abstract class DatabaseComponentTest extends TestCase { @Test public void testSimpleCalls() throws DbException { + final Map<String, String> transports = + Collections.singletonMap("foo", "bar"); + final Map<String, String> transports1 = + Collections.singletonMap("foo", "bar baz"); final Set<GroupId> subs = Collections.singleton(groupId); Mockery context = new Mockery(); @SuppressWarnings("unchecked") @@ -82,12 +87,21 @@ public abstract class DatabaseComponentTest extends TestCase { // getRating(authorId) oneOf(database).getRating(txn, authorId); will(returnValue(Rating.UNRATED)); - // addContact(contactId) - oneOf(database).addContact(txn); + // addContact(transports) + oneOf(database).addContact(txn, transports); will(returnValue(contactId)); // getContacts() oneOf(database).getContacts(txn); will(returnValue(Collections.singleton(contactId))); + // getTransports(contactId) + oneOf(database).containsContact(txn, contactId); + will(returnValue(true)); + oneOf(database).getTransports(txn, contactId); + will(returnValue(transports)); + // setTransports(contactId, transports1) + oneOf(database).containsContact(txn, contactId); + will(returnValue(true)); + oneOf(database).setTransports(txn, contactId, transports1); // subscribe(groupId) oneOf(database).addSubscription(txn, groupId); // getSubscriptions() @@ -106,8 +120,10 @@ public abstract class DatabaseComponentTest extends TestCase { db.open(false); assertEquals(Rating.UNRATED, db.getRating(authorId)); - assertEquals(contactId, db.addContact()); + assertEquals(contactId, db.addContact(transports)); assertEquals(Collections.singleton(contactId), db.getContacts()); + assertEquals(transports, db.getTransports(contactId)); + db.setTransports(contactId, transports1); db.subscribe(groupId); assertEquals(Collections.singleton(groupId), db.getSubscriptions()); db.unsubscribe(groupId); diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java index 7827da8524f1964b883ab48e38e542d91ff68a68..7e499e1ada5ac8fbcf0683126b38eba6c0acac45 100644 --- a/test/net/sf/briar/db/H2DatabaseTest.java +++ b/test/net/sf/briar/db/H2DatabaseTest.java @@ -6,16 +6,18 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.TreeMap; import java.util.concurrent.atomic.AtomicBoolean; import junit.framework.TestCase; import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.Rating; import net.sf.briar.api.crypto.Password; -import net.sf.briar.api.db.ContactId; import net.sf.briar.api.db.DbException; -import net.sf.briar.api.db.Rating; import net.sf.briar.api.db.Status; import net.sf.briar.api.protocol.AuthorId; import net.sf.briar.api.protocol.BatchId; @@ -80,7 +82,8 @@ public class H2DatabaseTest extends TestCase { // Store some records Connection txn = db.startTransaction(); assertFalse(db.containsContact(txn, contactId)); - assertEquals(contactId, db.addContact(txn)); + Map<String, String> transports = Collections.singletonMap("foo", "bar"); + assertEquals(contactId, db.addContact(txn, transports)); assertTrue(db.containsContact(txn, contactId)); assertFalse(db.containsSubscription(txn, groupId)); db.addSubscription(txn, groupId); @@ -96,6 +99,8 @@ public class H2DatabaseTest extends TestCase { // Check that the records are still there txn = db.startTransaction(); assertTrue(db.containsContact(txn, contactId)); + transports = db.getTransports(txn, contactId); + assertEquals(Collections.singletonMap("foo", "bar"), transports); assertTrue(db.containsSubscription(txn, groupId)); assertTrue(db.containsMessage(txn, messageId)); Message m1 = db.getMessage(txn, messageId); @@ -118,6 +123,7 @@ public class H2DatabaseTest extends TestCase { // Check that the records are gone txn = db.startTransaction(); assertFalse(db.containsContact(txn, contactId)); + assertEquals(Collections.emptyMap(), db.getTransports(txn, contactId)); assertFalse(db.containsSubscription(txn, groupId)); assertFalse(db.containsMessage(txn, messageId)); db.commitTransaction(txn); @@ -130,28 +136,25 @@ public class H2DatabaseTest extends TestCase { ContactId contactId2 = new ContactId(3); ContactId contactId3 = new ContactId(4); MessageFactory messageFactory = new TestMessageFactory(); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Create three contacts Connection txn = db.startTransaction(); assertFalse(db.containsContact(txn, contactId)); - assertEquals(contactId, db.addContact(txn)); + assertEquals(contactId, db.addContact(txn, null)); assertTrue(db.containsContact(txn, contactId)); assertFalse(db.containsContact(txn, contactId1)); - assertEquals(contactId1, db.addContact(txn)); + assertEquals(contactId1, db.addContact(txn, null)); assertTrue(db.containsContact(txn, contactId1)); assertFalse(db.containsContact(txn, contactId2)); - assertEquals(contactId2, db.addContact(txn)); + assertEquals(contactId2, db.addContact(txn, null)); assertTrue(db.containsContact(txn, contactId2)); - /* // Delete one of the contacts db.removeContact(txn, contactId1); assertFalse(db.containsContact(txn, contactId1)); - */ // Add another contact - a new ID should be created assertFalse(db.containsContact(txn, contactId3)); - assertEquals(contactId3, db.addContact(txn)); + assertEquals(contactId3, db.addContact(txn, null)); assertTrue(db.containsContact(txn, contactId3)); db.commitTransaction(txn); db.close(); @@ -180,9 +183,8 @@ public class H2DatabaseTest extends TestCase { public void testUnsubscribingRemovesMessage() throws DbException { Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Subscribe to a group and store a message Connection txn = db.startTransaction(); db.addSubscription(txn, groupId); @@ -204,12 +206,11 @@ public class H2DatabaseTest extends TestCase { public void testSendableMessagesMustBeSendable() throws DbException { Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Add a contact, subscribe to a group and store a message Connection txn = db.startTransaction(); - assertEquals(contactId, db.addContact(txn)); + assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, groupId); db.addSubscription(txn, contactId, groupId); db.addMessage(txn, message); @@ -247,12 +248,11 @@ public class H2DatabaseTest extends TestCase { public void testSendableMessagesMustBeNew() throws DbException { Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Add a contact, subscribe to a group and store a message Connection txn = db.startTransaction(); - assertEquals(contactId, db.addContact(txn)); + assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, groupId); db.addSubscription(txn, contactId, groupId); db.addMessage(txn, message); @@ -296,12 +296,11 @@ public class H2DatabaseTest extends TestCase { public void testSendableMessagesMustBeSubscribed() throws DbException { Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Add a contact, subscribe to a group and store a message Connection txn = db.startTransaction(); - assertEquals(contactId, db.addContact(txn)); + assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, groupId); db.addMessage(txn, message); db.setSendability(txn, messageId, 1); @@ -338,12 +337,11 @@ public class H2DatabaseTest extends TestCase { public void testSendableMessagesMustFitCapacity() throws DbException { Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Add a contact, subscribe to a group and store a message Connection txn = db.startTransaction(); - assertEquals(contactId, db.addContact(txn)); + assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, groupId); db.addSubscription(txn, contactId, groupId); db.addMessage(txn, message); @@ -374,12 +372,11 @@ public class H2DatabaseTest extends TestCase { BatchId batchId1 = new BatchId(TestUtils.getRandomId()); Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Add a contact and some batches to ack Connection txn = db.startTransaction(); - assertEquals(contactId, db.addContact(txn)); + assertEquals(contactId, db.addContact(txn, null)); db.addBatchToAck(txn, contactId, batchId); db.addBatchToAck(txn, contactId, batchId1); db.commitTransaction(txn); @@ -406,12 +403,11 @@ public class H2DatabaseTest extends TestCase { public void testRemoveAckedBatch() throws DbException { Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Add a contact, subscribe to a group and store a message Connection txn = db.startTransaction(); - assertEquals(contactId, db.addContact(txn)); + assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, groupId); db.addSubscription(txn, contactId, groupId); db.addMessage(txn, message); @@ -450,12 +446,11 @@ public class H2DatabaseTest extends TestCase { public void testRemoveLostBatch() throws DbException { Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Add a contact, subscribe to a group and store a message Connection txn = db.startTransaction(); - assertEquals(contactId, db.addContact(txn)); + assertEquals(contactId, db.addContact(txn, null)); db.addSubscription(txn, groupId); db.addSubscription(txn, contactId, groupId); db.addMessage(txn, message); @@ -504,12 +499,11 @@ public class H2DatabaseTest extends TestCase { Set<MessageId> empty = Collections.emptySet(); Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Add a contact Connection txn = db.startTransaction(); - assertEquals(contactId, db.addContact(txn)); + assertEquals(contactId, db.addContact(txn, null)); // Add an oustanding batch (associated with BundleId.NONE) db.addOutstandingBatch(txn, contactId, batchId, empty); // Receive a bundle @@ -535,6 +529,7 @@ public class H2DatabaseTest extends TestCase { lost = db.addReceivedBundle(txn, contactId, bundleId4); assertTrue(lost.isEmpty()); db.commitTransaction(txn); + db.close(); context.assertIsSatisfied(); } @@ -547,9 +542,8 @@ public class H2DatabaseTest extends TestCase { authorId1, timestamp, body); Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Subscribe to a group and store two messages Connection txn = db.startTransaction(); db.addSubscription(txn, groupId); @@ -589,9 +583,8 @@ public class H2DatabaseTest extends TestCase { authorId, timestamp, body); Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Subscribe to the groups and store the messages Connection txn = db.startTransaction(); db.addSubscription(txn, groupId); @@ -626,9 +619,8 @@ public class H2DatabaseTest extends TestCase { authorId, timestamp + 1000, body); Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Subscribe to a group and store two messages Connection txn = db.startTransaction(); db.addSubscription(txn, groupId); @@ -665,9 +657,8 @@ public class H2DatabaseTest extends TestCase { authorId, timestamp, largeBody); Mockery context = new Mockery(); MessageFactory messageFactory = context.mock(MessageFactory.class); - - // Create a new database Database<Connection> db = open(false, messageFactory); + // Sanity check: there should be enough space on disk for this test assertTrue(testDir.getFreeSpace() > MAX_SIZE); // The free space should not be more than the allowed maximum size @@ -692,9 +683,8 @@ public class H2DatabaseTest extends TestCase { final AtomicBoolean transactionFinished = new AtomicBoolean(false); final AtomicBoolean closed = new AtomicBoolean(false); final AtomicBoolean error = new AtomicBoolean(false); - - // Create a new database final Database<Connection> db = open(false, messageFactory); + // Start a transaction Connection txn = db.startTransaction(); // In another thread, close the database @@ -733,9 +723,8 @@ public class H2DatabaseTest extends TestCase { final AtomicBoolean transactionFinished = new AtomicBoolean(false); final AtomicBoolean closed = new AtomicBoolean(false); final AtomicBoolean error = new AtomicBoolean(false); - - // Create a new database final Database<Connection> db = open(false, messageFactory); + // Start a transaction Connection txn = db.startTransaction(); // In another thread, close the database @@ -767,6 +756,32 @@ public class H2DatabaseTest extends TestCase { assertFalse(error.get()); } + @Test + public void testUpdateTransports() throws DbException { + Mockery context = new Mockery(); + MessageFactory messageFactory = context.mock(MessageFactory.class); + Database<Connection> db = open(false, messageFactory); + + // Add a contact with some transport details + Connection txn = db.startTransaction(); + Map<String, String> transports = Collections.singletonMap("foo", "bar"); + assertEquals(contactId, db.addContact(txn, transports)); + assertEquals(transports, db.getTransports(txn, contactId)); + // Replace the transport details + transports = new TreeMap<String, String>(); + transports.put("foo", "bar baz"); + transports.put("bar", "baz quux"); + db.setTransports(txn, contactId, transports); + assertEquals(transports, db.getTransports(txn, contactId)); + // Remove the transport details + db.setTransports(txn, contactId, null); + assertEquals(Collections.emptyMap(), db.getTransports(txn, contactId)); + db.commitTransaction(txn); + + db.close(); + context.assertIsSatisfied(); + } + private Database<Connection> open(boolean resume, MessageFactory messageFactory) throws DbException { final char[] passwordArray = passwordString.toCharArray();