From 5cb4075cfd8b3c884744888de307b6876e7f2cc1 Mon Sep 17 00:00:00 2001 From: akwizgran Date: Thu, 4 Aug 2011 13:41:41 +0100 Subject: [PATCH] Added the ability to store transport configuration details in the database - unlike transport properties, these are not shared with contacts. For example, when using email as a transport, the address for sending and receiving emails would be a transport property, while the username and password for the email server would be transport configuration details. Transport plugins can update their configuration details atomically. Also clarified the terminology for transport and subscription updates. --- .../sf/briar/api/db/DatabaseComponent.java | 34 ++- ...criptions.java => SubscriptionUpdate.java} | 4 +- .../{Transports.java => TransportUpdate.java} | 8 +- .../api/protocol/writers/TransportWriter.java | 2 +- components/net/sf/briar/db/Database.java | 27 +- components/net/sf/briar/db/JdbcDatabase.java | 230 ++++++++++-------- .../db/ReadWriteLockDatabaseComponent.java | 70 +++++- .../db/SynchronizedDatabaseComponent.java | 57 ++++- .../briar/protocol/SubscriptionFactory.java | 4 +- .../protocol/SubscriptionFactoryImpl.java | 4 +- .../sf/briar/protocol/SubscriptionReader.java | 10 +- .../sf/briar/protocol/SubscriptionsImpl.java | 4 +- .../sf/briar/protocol/TransportFactory.java | 4 +- .../briar/protocol/TransportFactoryImpl.java | 4 +- .../sf/briar/protocol/TransportReader.java | 26 +- .../net/sf/briar/protocol/TransportsImpl.java | 4 +- .../sf/briar/db/DatabaseComponentTest.java | 126 +++++++--- test/net/sf/briar/db/H2DatabaseTest.java | 34 ++- .../sf/briar/protocol/FileReadWriteTest.java | 16 +- 19 files changed, 454 insertions(+), 214 deletions(-) rename api/net/sf/briar/api/protocol/{Subscriptions.java => SubscriptionUpdate.java} (82%) rename api/net/sf/briar/api/protocol/{Transports.java => TransportUpdate.java} (62%) diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java index 4bbbaec88..f8dda8592 100644 --- a/api/net/sf/briar/api/db/DatabaseComponent.java +++ b/api/net/sf/briar/api/db/DatabaseComponent.java @@ -14,8 +14,8 @@ import net.sf.briar.api.protocol.GroupId; import net.sf.briar.api.protocol.Message; import net.sf.briar.api.protocol.MessageId; import net.sf.briar.api.protocol.Offer; -import net.sf.briar.api.protocol.Subscriptions; -import net.sf.briar.api.protocol.Transports; +import net.sf.briar.api.protocol.SubscriptionUpdate; +import net.sf.briar.api.protocol.TransportUpdate; import net.sf.briar.api.protocol.writers.AckWriter; import net.sf.briar.api.protocol.writers.BatchWriter; import net.sf.briar.api.protocol.writers.OfferWriter; @@ -95,11 +95,11 @@ public interface DatabaseComponent { throws DbException, IOException; /** Generates a subscription update for the given contact. */ - void generateSubscriptions(ContactId c, SubscriptionWriter s) throws + void generateSubscriptionUpdate(ContactId c, SubscriptionWriter s) throws DbException, IOException; /** Generates a transport update for the given contact. */ - void generateTransports(ContactId c, TransportWriter t) throws + void generateTransportUpdate(ContactId c, TransportWriter t) throws DbException, IOException; /** Returns the IDs of all contacts. */ @@ -111,10 +111,13 @@ public interface DatabaseComponent { /** Returns the set of groups to which the user subscribes. */ Collection getSubscriptions() throws DbException; - /** Returns the local transport properties. */ + /** Returns the configuration for the transport with the given name. */ + Map getTransportConfig(String name) throws DbException; + + /** Returns all local transport properties. */ Map> getTransports() throws DbException; - /** Returns the transport properties for the given contact. */ + /** Returns all transport properties for the given contact. */ Map> getTransports(ContactId c) throws DbException; @@ -142,10 +145,12 @@ public interface DatabaseComponent { IOException; /** Processes a subscription update from the given contact. */ - void receiveSubscriptions(ContactId c, Subscriptions s) throws DbException; + void receiveSubscriptionUpdate(ContactId c, SubscriptionUpdate s) + throws DbException; /** Processes a transport update from the given contact. */ - void receiveTransports(ContactId c, Transports t) throws DbException; + void receiveTransportUpdate(ContactId c, TransportUpdate t) + throws DbException; /** Removes a contact (and all associated state) from the database. */ void removeContact(ContactId c) throws DbException; @@ -154,10 +159,17 @@ public interface DatabaseComponent { void setRating(AuthorId a, Rating r) throws DbException; /** - * Sets the local transport properties for the transport with the given - * name, replacing any existing properties for that transport. + * Sets the configuration for the transport with the given name, replacing + * any existing configuration for that transport. */ - void setTransports(String name, Map transports) + void setTransportConfig(String name, Map config) + throws DbException; + + /** + * Sets the transport properties for the transport with the given name, + * replacing any existing properties for that transport. + */ + void setTransportProperties(String name, Map properties) throws DbException; /** diff --git a/api/net/sf/briar/api/protocol/Subscriptions.java b/api/net/sf/briar/api/protocol/SubscriptionUpdate.java similarity index 82% rename from api/net/sf/briar/api/protocol/Subscriptions.java rename to api/net/sf/briar/api/protocol/SubscriptionUpdate.java index 28d670c08..cdbd2fa20 100644 --- a/api/net/sf/briar/api/protocol/Subscriptions.java +++ b/api/net/sf/briar/api/protocol/SubscriptionUpdate.java @@ -3,10 +3,10 @@ package net.sf.briar.api.protocol; import java.util.Collection; /** A packet updating the sender's subscriptions. */ -public interface Subscriptions { +public interface SubscriptionUpdate { /** - * The maximum size of a serialized subscriptions update, excluding + * The maximum size of a serialized subscription update, excluding * encryption and authentication. */ static final int MAX_SIZE = (1024 * 1024) - 100; diff --git a/api/net/sf/briar/api/protocol/Transports.java b/api/net/sf/briar/api/protocol/TransportUpdate.java similarity index 62% rename from api/net/sf/briar/api/protocol/Transports.java rename to api/net/sf/briar/api/protocol/TransportUpdate.java index 8af5d09ac..742ddaad2 100644 --- a/api/net/sf/briar/api/protocol/Transports.java +++ b/api/net/sf/briar/api/protocol/TransportUpdate.java @@ -2,16 +2,16 @@ package net.sf.briar.api.protocol; import java.util.Map; -/** A packet updating the sender's transports. */ -public interface Transports { +/** A packet updating the sender's transport properties. */ +public interface TransportUpdate { /** - * The maximum size of a serialised transports update, excluding + * The maximum size of a serialised transport update, excluding * encryption and authentication. */ static final int MAX_SIZE = (1024 * 1024) - 100; - /** Returns the transports contained in the update. */ + /** Returns the transport properties contained in the update. */ Map> getTransports(); /** diff --git a/api/net/sf/briar/api/protocol/writers/TransportWriter.java b/api/net/sf/briar/api/protocol/writers/TransportWriter.java index f835e0059..a8e7f9f76 100644 --- a/api/net/sf/briar/api/protocol/writers/TransportWriter.java +++ b/api/net/sf/briar/api/protocol/writers/TransportWriter.java @@ -3,7 +3,7 @@ package net.sf.briar.api.protocol.writers; import java.io.IOException; import java.util.Map; -/** An interface for creating a transports update. */ +/** An interface for creating a transport update. */ public interface TransportWriter { /** Writes the contents of the update. */ diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java index f60389303..f3f7a865a 100644 --- a/components/net/sf/briar/db/Database.java +++ b/components/net/sf/briar/db/Database.java @@ -268,14 +268,22 @@ interface Database { Collection getSubscriptions(T txn, ContactId c) throws DbException; /** - * Returns the local transport properties. + * Returns the configuration for the transport with the given name. + *

+ * Locking: transports read. + */ + Map getTransportConfig(T txn, String name) + throws DbException; + + /** + * Returns all local transport properties. *

* Locking: transports read. */ Map> getTransports(T txn) throws DbException; /** - * Returns the transport properties for the given contact. + * Returns all transport properties for the given contact. *

* Locking: contacts read, transports read. */ @@ -398,14 +406,23 @@ interface Database { long timestamp) throws DbException; /** - * Sets the local transport properties for the transport with the given - * name, replacing any existing properties for that transport. + * Sets the configuration for the transport with the given name, replacing + * any existing configuration for that transport. *

* Locking: transports write. */ - void setTransports(T txn, String name, Map transports) + void setTransportConfig(T txn, String name, Map config) throws DbException; + /** + * Sets the transport properties for the transport with the given name, + * replacing any existing properties for that transport. + *

+ * Locking: transports write. + */ + void setTransportProperties(T txn, String name, + Map properties) throws DbException; + /** * Sets the transport properties for the given contact, replacing any * existing properties unless the existing properties have a newer diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java index f31fefffd..6a4df751b 100644 --- a/components/net/sf/briar/db/JdbcDatabase.java +++ b/components/net/sf/briar/db/JdbcDatabase.java @@ -173,6 +173,13 @@ abstract class JdbcDatabase implements Database { + " value VARCHAR NOT NULL," + " PRIMARY KEY (transportName, key))"; + private static final String CREATE_TRANSPORT_CONFIG = + "CREATE TABLE transportConfig" + + " (transportName VARCHAR NOT NULL," + + " key VARCHAR NOT NULL," + + " value VARCHAR NOT NULL," + + " PRIMARY KEY (transportName, key))"; + private static final Logger LOG = Logger.getLogger(JdbcDatabase.class.getName()); @@ -255,6 +262,7 @@ abstract class JdbcDatabase implements Database { s.executeUpdate(INDEX_STATUSES_BY_CONTACT); s.executeUpdate(insertTypeNames(CREATE_CONTACT_TRANSPORTS)); s.executeUpdate(insertTypeNames(CREATE_TRANSPORTS)); + s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_CONFIG)); s.close(); } catch(SQLException e) { tryToClose(s); @@ -1096,6 +1104,115 @@ abstract class JdbcDatabase implements Database { } } + public Map getTransportConfig(Connection txn, + String name) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT key, value FROM transportConfig" + + " WHERE transportName = ?"; + ps = txn.prepareStatement(sql); + ps.setString(1, name); + rs = ps.executeQuery(); + Map config = new TreeMap(); + while(rs.next()) config.put(rs.getString(1), rs.getString(2)); + rs.close(); + ps.close(); + return config; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + public Map> getTransports(Connection txn) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT transportName, key, value" + + " FROM transports" + + " ORDER BY transportName"; + ps = txn.prepareStatement(sql); + rs = ps.executeQuery(); + Map> transports = + new TreeMap>(); + Map properties = null; + String lastName = null; + while(rs.next()) { + String name = rs.getString(1); + if(!name.equals(lastName)) { + properties = new TreeMap(); + transports.put(name, properties); + } + properties.put(rs.getString(2), rs.getString(3)); + } + rs.close(); + ps.close(); + return transports; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + public Map> getTransports(Connection txn, + ContactId c) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT transportName, key, value" + + " FROM contactTransports" + + " WHERE contactId = ?" + + " ORDER BY transportName"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + rs = ps.executeQuery(); + Map> transports = + new TreeMap>(); + Map properties = null; + String lastName = null; + while(rs.next()) { + String name = rs.getString(1); + if(!name.equals(lastName)) { + properties = new TreeMap(); + transports.put(name, properties); + } + properties.put(rs.getString(2), rs.getString(3)); + } + rs.close(); + ps.close(); + return transports; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + public Collection getVisibility(Connection txn, GroupId g) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT contactId FROM visibilities WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + rs = ps.executeQuery(); + Collection visible = new ArrayList(); + while(rs.next()) visible.add(new ContactId(rs.getInt(1))); + rs.close(); + ps.close(); + return visible; + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public Collection getVisibleSubscriptions(Connection txn, ContactId c) throws DbException { PreparedStatement ps = null; @@ -1125,93 +1242,6 @@ abstract class JdbcDatabase implements Database { } } - public Map> getTransports(Connection txn) - throws DbException { - PreparedStatement ps = null; - ResultSet rs = null; - try { - String sql = "SELECT transportName, key, value" - + " FROM transports" - + " ORDER BY transportName"; - ps = txn.prepareStatement(sql); - rs = ps.executeQuery(); - Map> outer = - new TreeMap>(); - Map inner = null; - String lastName = null; - while(rs.next()) { - String name = rs.getString(1); - if(!name.equals(lastName)) { - inner = new TreeMap(); - outer.put(name, inner); - } - inner.put(rs.getString(2), rs.getString(3)); - } - rs.close(); - ps.close(); - return outer; - } catch(SQLException e) { - tryToClose(rs); - tryToClose(ps); - throw new DbException(e); - } - } - - public Map> getTransports(Connection txn, - ContactId c) throws DbException { - PreparedStatement ps = null; - ResultSet rs = null; - try { - String sql = "SELECT transportName, key, value" - + " FROM contactTransports" - + " WHERE contactId = ?" - + " ORDER BY transportName"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - rs = ps.executeQuery(); - Map> outer = - new TreeMap>(); - Map inner = null; - String lastName = null; - while(rs.next()) { - String name = rs.getString(1); - if(!name.equals(lastName)) { - inner = new TreeMap(); - outer.put(name, inner); - } - inner.put(rs.getString(2), rs.getString(3)); - } - rs.close(); - ps.close(); - return outer; - } catch(SQLException e) { - tryToClose(rs); - tryToClose(ps); - throw new DbException(e); - } - } - - public Collection getVisibility(Connection txn, GroupId g) - throws DbException { - PreparedStatement ps = null; - ResultSet rs = null; - try { - String sql = "SELECT contactId FROM visibilities WHERE groupId = ?"; - ps = txn.prepareStatement(sql); - ps.setBytes(1, g.getBytes()); - rs = ps.executeQuery(); - Collection visible = new ArrayList(); - while(rs.next()) visible.add(new ContactId(rs.getInt(1))); - rs.close(); - ps.close(); - return visible; - } catch(SQLException e) { - tryToClose(rs); - tryToClose(ps); - throw new DbException(e); - } - } - public boolean hasSendableMessages(Connection txn, ContactId c) throws DbException { PreparedStatement ps = null; @@ -1609,28 +1639,33 @@ abstract class JdbcDatabase implements Database { } } - public void setTransports(Connection txn, String name, - Map transports) throws DbException { + public void setTransportConfig(Connection txn, String name, + Map config) throws DbException { + setTransportDetails(txn, name, config, "transportConfig"); + } + + private void setTransportDetails(Connection txn, String name, + Map details, String table) throws DbException { PreparedStatement ps = null; try { - // Delete any existing properties for the named transport - String sql = "DELETE FROM transports WHERE transportName = ?"; + // Delete any existing details for the named transport + String sql = "DELETE FROM " + table + " WHERE transportName = ?"; ps = txn.prepareStatement(sql); ps.setString(1, name); ps.executeUpdate(); ps.close(); - // Store the new properties - sql = "INSERT INTO transports (transportName, key, value)" + // Store the new details + sql = "INSERT INTO " + table + " (transportName, key, value)" + " VALUES (?, ?, ?)"; ps = txn.prepareStatement(sql); ps.setString(1, name); - for(Entry e : transports.entrySet()) { + for(Entry e : details.entrySet()) { ps.setString(2, e.getKey()); ps.setString(3, e.getValue()); ps.addBatch(); } int[] batchAffected = ps.executeBatch(); - if(batchAffected.length != transports.size()) + if(batchAffected.length != details.size()) throw new DbStateException(); for(int i = 0; i < batchAffected.length; i++) { if(batchAffected[i] != 1) throw new DbStateException(); @@ -1642,6 +1677,11 @@ abstract class JdbcDatabase implements Database { } } + public void setTransportProperties(Connection txn, String name, + Map properties) throws DbException { + setTransportDetails(txn, name, properties, "transports"); + } + public void setTransports(Connection txn, ContactId c, Map> transports, long timestamp) throws DbException { diff --git a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java index 9d85dbe78..2221390f4 100644 --- a/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java +++ b/components/net/sf/briar/db/ReadWriteLockDatabaseComponent.java @@ -24,8 +24,8 @@ import net.sf.briar.api.protocol.GroupId; import net.sf.briar.api.protocol.Message; import net.sf.briar.api.protocol.MessageId; import net.sf.briar.api.protocol.Offer; -import net.sf.briar.api.protocol.Subscriptions; -import net.sf.briar.api.protocol.Transports; +import net.sf.briar.api.protocol.SubscriptionUpdate; +import net.sf.briar.api.protocol.TransportUpdate; import net.sf.briar.api.protocol.writers.AckWriter; import net.sf.briar.api.protocol.writers.BatchWriter; import net.sf.briar.api.protocol.writers.OfferWriter; @@ -107,7 +107,12 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { try { subscriptionLock.writeLock().lock(); try { - db.close(); + transportLock.writeLock().lock(); + try { + db.close(); + } finally { + transportLock.writeLock().unlock(); + } } finally { subscriptionLock.writeLock().unlock(); } @@ -459,7 +464,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } } - public void generateSubscriptions(ContactId c, SubscriptionWriter s) + public void generateSubscriptionUpdate(ContactId c, SubscriptionWriter s) throws DbException, IOException { contactLock.readLock().lock(); try { @@ -488,7 +493,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } } - public void generateTransports(ContactId c, TransportWriter t) + public void generateTransportUpdate(ContactId c, TransportWriter t) throws DbException, IOException { contactLock.readLock().lock(); try { @@ -569,6 +574,24 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } } + public Map getTransportConfig(String name) + throws DbException { + transportLock.readLock().lock(); + try { + Txn txn = db.startTransaction(); + try { + Map config = db.getTransportConfig(txn, name); + db.commitTransaction(txn); + return config; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + transportLock.readLock().unlock(); + } + } + public Map> getTransports() throws DbException { transportLock.readLock().lock(); try { @@ -793,7 +816,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } } - public void receiveSubscriptions(ContactId c, Subscriptions s) + public void receiveSubscriptionUpdate(ContactId c, SubscriptionUpdate s) throws DbException { // Update the contact's subscriptions contactLock.writeLock().lock(); @@ -820,7 +843,7 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } } - public void receiveTransports(ContactId c, Transports t) + public void receiveTransportUpdate(ContactId c, TransportUpdate t) throws DbException { // Update the contact's transport properties contactLock.writeLock().lock(); @@ -907,15 +930,40 @@ class ReadWriteLockDatabaseComponent extends DatabaseComponentImpl { } } - public void setTransports(String name, Map transports) - throws DbException { + public void setTransportConfig(String name, + Map config) throws DbException { boolean changed = false; transportLock.writeLock().lock(); try { Txn txn = db.startTransaction(); try { - if(!transports.equals(db.getTransports(txn).get(name))) { - db.setTransports(txn, name, transports); + Map old = db.getTransportConfig(txn, name); + if(!config.equals(old)) { + db.setTransportConfig(txn, name, config); + changed = true; + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + transportLock.writeLock().unlock(); + } + // Call the listeners outside the lock + if(changed) callListeners(DatabaseListener.Event.TRANSPORTS_UPDATED); + } + + public void setTransportProperties(String name, + Map properties) throws DbException { + boolean changed = false; + transportLock.writeLock().lock(); + try { + Txn txn = db.startTransaction(); + try { + Map old = db.getTransports(txn).get(name); + if(!properties.equals(old)) { + db.setTransportProperties(txn, name, properties); changed = true; } db.commitTransaction(txn); diff --git a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java index 1d7a48350..381dda60f 100644 --- a/components/net/sf/briar/db/SynchronizedDatabaseComponent.java +++ b/components/net/sf/briar/db/SynchronizedDatabaseComponent.java @@ -23,8 +23,8 @@ import net.sf.briar.api.protocol.GroupId; import net.sf.briar.api.protocol.Message; import net.sf.briar.api.protocol.MessageId; import net.sf.briar.api.protocol.Offer; -import net.sf.briar.api.protocol.Subscriptions; -import net.sf.briar.api.protocol.Transports; +import net.sf.briar.api.protocol.SubscriptionUpdate; +import net.sf.briar.api.protocol.TransportUpdate; import net.sf.briar.api.protocol.writers.AckWriter; import net.sf.briar.api.protocol.writers.BatchWriter; import net.sf.briar.api.protocol.writers.OfferWriter; @@ -336,7 +336,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } - public void generateSubscriptions(ContactId c, SubscriptionWriter s) + public void generateSubscriptionUpdate(ContactId c, SubscriptionWriter s) throws DbException, IOException { synchronized(contactLock) { if(!containsContact(c)) throw new NoSuchContactException(); @@ -359,7 +359,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } - public void generateTransports(ContactId c, TransportWriter t) + public void generateTransportUpdate(ContactId c, TransportWriter t) throws DbException, IOException { synchronized(contactLock) { if(!containsContact(c)) throw new NoSuchContactException(); @@ -425,6 +425,21 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } + public Map getTransportConfig(String name) + throws DbException { + synchronized(transportLock) { + Txn txn = db.startTransaction(); + try { + Map config = db.getTransportConfig(txn, name); + db.commitTransaction(txn); + return config; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + } + public Map> getTransports() throws DbException { synchronized(transportLock) { Txn txn = db.startTransaction(); @@ -589,7 +604,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } - public void receiveSubscriptions(ContactId c, Subscriptions s) + public void receiveSubscriptionUpdate(ContactId c, SubscriptionUpdate s) throws DbException { // Update the contact's subscriptions synchronized(contactLock) { @@ -610,7 +625,7 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } - public void receiveTransports(ContactId c, Transports t) + public void receiveTransportUpdate(ContactId c, TransportUpdate t) throws DbException { // Update the contact's transport properties synchronized(contactLock) { @@ -673,14 +688,36 @@ class SynchronizedDatabaseComponent extends DatabaseComponentImpl { } } - public void setTransports(String name, Map transports) - throws DbException { + public void setTransportConfig(String name, + Map config) throws DbException { boolean changed = false; synchronized(transportLock) { Txn txn = db.startTransaction(); try { - if(!transports.equals(db.getTransports(txn).get(name))) { - db.setTransports(txn, name, transports); + Map old = db.getTransportConfig(txn, name); + if(!config.equals(old)) { + db.setTransportConfig(txn, name, config); + changed = true; + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } + // Call the listeners outside the lock + if(changed) callListeners(DatabaseListener.Event.TRANSPORTS_UPDATED); + } + + public void setTransportProperties(String name, + Map properties) throws DbException { + boolean changed = false; + synchronized(transportLock) { + Txn txn = db.startTransaction(); + try { + Map old = db.getTransports(txn).get(name); + if(!properties.equals(old)) { + db.setTransportProperties(txn, name, properties); changed = true; } db.commitTransaction(txn); diff --git a/components/net/sf/briar/protocol/SubscriptionFactory.java b/components/net/sf/briar/protocol/SubscriptionFactory.java index d6be2b293..136bcffea 100644 --- a/components/net/sf/briar/protocol/SubscriptionFactory.java +++ b/components/net/sf/briar/protocol/SubscriptionFactory.java @@ -3,9 +3,9 @@ package net.sf.briar.protocol; import java.util.Collection; import net.sf.briar.api.protocol.Group; -import net.sf.briar.api.protocol.Subscriptions; +import net.sf.briar.api.protocol.SubscriptionUpdate; interface SubscriptionFactory { - Subscriptions createSubscriptions(Collection subs, long timestamp); + SubscriptionUpdate createSubscriptions(Collection subs, long timestamp); } diff --git a/components/net/sf/briar/protocol/SubscriptionFactoryImpl.java b/components/net/sf/briar/protocol/SubscriptionFactoryImpl.java index 53c21e50c..0d3391ebe 100644 --- a/components/net/sf/briar/protocol/SubscriptionFactoryImpl.java +++ b/components/net/sf/briar/protocol/SubscriptionFactoryImpl.java @@ -3,11 +3,11 @@ package net.sf.briar.protocol; import java.util.Collection; import net.sf.briar.api.protocol.Group; -import net.sf.briar.api.protocol.Subscriptions; +import net.sf.briar.api.protocol.SubscriptionUpdate; class SubscriptionFactoryImpl implements SubscriptionFactory { - public Subscriptions createSubscriptions(Collection subs, + public SubscriptionUpdate createSubscriptions(Collection subs, long timestamp) { return new SubscriptionsImpl(subs, timestamp); } diff --git a/components/net/sf/briar/protocol/SubscriptionReader.java b/components/net/sf/briar/protocol/SubscriptionReader.java index b3c68b9b6..c13125d6a 100644 --- a/components/net/sf/briar/protocol/SubscriptionReader.java +++ b/components/net/sf/briar/protocol/SubscriptionReader.java @@ -4,7 +4,7 @@ import java.io.IOException; import java.util.Collection; import net.sf.briar.api.protocol.Group; -import net.sf.briar.api.protocol.Subscriptions; +import net.sf.briar.api.protocol.SubscriptionUpdate; import net.sf.briar.api.protocol.Tags; import net.sf.briar.api.serial.Consumer; import net.sf.briar.api.serial.ObjectReader; @@ -12,7 +12,7 @@ import net.sf.briar.api.serial.Reader; import com.google.inject.Inject; -class SubscriptionReader implements ObjectReader { +class SubscriptionReader implements ObjectReader { private final ObjectReader groupReader; private final SubscriptionFactory subscriptionFactory; @@ -24,9 +24,9 @@ class SubscriptionReader implements ObjectReader { this.subscriptionFactory = subscriptionFactory; } - public Subscriptions readObject(Reader r) throws IOException { + public SubscriptionUpdate readObject(Reader r) throws IOException { // Initialise the consumer - Consumer counting = new CountingConsumer(Subscriptions.MAX_SIZE); + Consumer counting = new CountingConsumer(SubscriptionUpdate.MAX_SIZE); // Read the data r.addConsumer(counting); r.readUserDefinedTag(Tags.SUBSCRIPTIONS); @@ -35,7 +35,7 @@ class SubscriptionReader implements ObjectReader { r.removeObjectReader(Tags.GROUP); long timestamp = r.readInt64(); r.removeConsumer(counting); - // Build and return the subscriptions update + // Build and return the subscription update return subscriptionFactory.createSubscriptions(subs, timestamp); } } diff --git a/components/net/sf/briar/protocol/SubscriptionsImpl.java b/components/net/sf/briar/protocol/SubscriptionsImpl.java index c3967cfc8..43be09e70 100644 --- a/components/net/sf/briar/protocol/SubscriptionsImpl.java +++ b/components/net/sf/briar/protocol/SubscriptionsImpl.java @@ -3,9 +3,9 @@ package net.sf.briar.protocol; import java.util.Collection; import net.sf.briar.api.protocol.Group; -import net.sf.briar.api.protocol.Subscriptions; +import net.sf.briar.api.protocol.SubscriptionUpdate; -class SubscriptionsImpl implements Subscriptions { +class SubscriptionsImpl implements SubscriptionUpdate { private final Collection subs; private final long timestamp; diff --git a/components/net/sf/briar/protocol/TransportFactory.java b/components/net/sf/briar/protocol/TransportFactory.java index d56c0c582..d784e7aa3 100644 --- a/components/net/sf/briar/protocol/TransportFactory.java +++ b/components/net/sf/briar/protocol/TransportFactory.java @@ -2,10 +2,10 @@ package net.sf.briar.protocol; import java.util.Map; -import net.sf.briar.api.protocol.Transports; +import net.sf.briar.api.protocol.TransportUpdate; interface TransportFactory { - Transports createTransports(Map> transports, + TransportUpdate createTransports(Map> transports, long timestamp); } diff --git a/components/net/sf/briar/protocol/TransportFactoryImpl.java b/components/net/sf/briar/protocol/TransportFactoryImpl.java index 1cabeed06..376653a3a 100644 --- a/components/net/sf/briar/protocol/TransportFactoryImpl.java +++ b/components/net/sf/briar/protocol/TransportFactoryImpl.java @@ -2,11 +2,11 @@ package net.sf.briar.protocol; import java.util.Map; -import net.sf.briar.api.protocol.Transports; +import net.sf.briar.api.protocol.TransportUpdate; class TransportFactoryImpl implements TransportFactory { - public Transports createTransports(Map> transports, + public TransportUpdate createTransports(Map> transports, long timestamp) { return new TransportsImpl(transports, timestamp); } diff --git a/components/net/sf/briar/protocol/TransportReader.java b/components/net/sf/briar/protocol/TransportReader.java index b945c1b65..92a2a4790 100644 --- a/components/net/sf/briar/protocol/TransportReader.java +++ b/components/net/sf/briar/protocol/TransportReader.java @@ -5,14 +5,14 @@ import java.util.Map; import java.util.TreeMap; import net.sf.briar.api.protocol.Tags; -import net.sf.briar.api.protocol.Transports; +import net.sf.briar.api.protocol.TransportUpdate; import net.sf.briar.api.serial.Consumer; import net.sf.briar.api.serial.ObjectReader; import net.sf.briar.api.serial.Reader; import com.google.inject.Inject; -class TransportReader implements ObjectReader { +class TransportReader implements ObjectReader { private final TransportFactory transportFactory; @@ -21,32 +21,32 @@ class TransportReader implements ObjectReader { this.transportFactory = transportFactory; } - public Transports readObject(Reader r) throws IOException { + public TransportUpdate readObject(Reader r) throws IOException { // Initialise the consumer - Consumer counting = new CountingConsumer(Transports.MAX_SIZE); + Consumer counting = new CountingConsumer(TransportUpdate.MAX_SIZE); // Read the data r.addConsumer(counting); r.readUserDefinedTag(Tags.TRANSPORTS); // Transport maps are always written in delimited form - Map> outer = + Map> transports = new TreeMap>(); r.readMapStart(); while(!r.hasMapEnd()) { - String name = r.readString(Transports.MAX_SIZE); - Map inner = new TreeMap(); + String name = r.readString(TransportUpdate.MAX_SIZE); + Map properties = new TreeMap(); r.readMapStart(); while(!r.hasMapEnd()) { - String key = r.readString(Transports.MAX_SIZE); - String value = r.readString(Transports.MAX_SIZE); - inner.put(key, value); + String key = r.readString(TransportUpdate.MAX_SIZE); + String value = r.readString(TransportUpdate.MAX_SIZE); + properties.put(key, value); } r.readMapEnd(); - outer.put(name, inner); + transports.put(name, properties); } r.readMapEnd(); long timestamp = r.readInt64(); r.removeConsumer(counting); - // Build and return the transports update - return transportFactory.createTransports(outer, timestamp); + // Build and return the transport update + return transportFactory.createTransports(transports, timestamp); } } diff --git a/components/net/sf/briar/protocol/TransportsImpl.java b/components/net/sf/briar/protocol/TransportsImpl.java index 157b1d2af..988c071fa 100644 --- a/components/net/sf/briar/protocol/TransportsImpl.java +++ b/components/net/sf/briar/protocol/TransportsImpl.java @@ -2,9 +2,9 @@ package net.sf.briar.protocol; import java.util.Map; -import net.sf.briar.api.protocol.Transports; +import net.sf.briar.api.protocol.TransportUpdate; -class TransportsImpl implements Transports { +class TransportsImpl implements TransportUpdate { private final Map> transports; private final long timestamp; diff --git a/test/net/sf/briar/db/DatabaseComponentTest.java b/test/net/sf/briar/db/DatabaseComponentTest.java index cf8db1128..6ef02cdc9 100644 --- a/test/net/sf/briar/db/DatabaseComponentTest.java +++ b/test/net/sf/briar/db/DatabaseComponentTest.java @@ -24,8 +24,8 @@ import net.sf.briar.api.protocol.GroupId; import net.sf.briar.api.protocol.Message; import net.sf.briar.api.protocol.MessageId; import net.sf.briar.api.protocol.Offer; -import net.sf.briar.api.protocol.Subscriptions; -import net.sf.briar.api.protocol.Transports; +import net.sf.briar.api.protocol.SubscriptionUpdate; +import net.sf.briar.api.protocol.TransportUpdate; import net.sf.briar.api.protocol.writers.AckWriter; import net.sf.briar.api.protocol.writers.BatchWriter; import net.sf.briar.api.protocol.writers.OfferWriter; @@ -456,9 +456,9 @@ public abstract class DatabaseComponentTest extends TestCase { final Batch batch = context.mock(Batch.class); final Offer offer = context.mock(Offer.class); final RequestWriter requestWriter = context.mock(RequestWriter.class); - final Subscriptions subscriptionsUpdate = - context.mock(Subscriptions.class); - final Transports transportsUpdate = context.mock(Transports.class); + final SubscriptionUpdate subscriptionsUpdate = + context.mock(SubscriptionUpdate.class); + final TransportUpdate transportsUpdate = context.mock(TransportUpdate.class); context.checking(new Expectations() {{ // Check whether the contact is still in the DB - which it's not exactly(12).of(database).startTransaction(); @@ -491,12 +491,12 @@ public abstract class DatabaseComponentTest extends TestCase { } catch(NoSuchContactException expected) {} try { - db.generateSubscriptions(contactId, subscriptionWriter); + db.generateSubscriptionUpdate(contactId, subscriptionWriter); fail(); } catch(NoSuchContactException expected) {} try { - db.generateTransports(contactId, transportWriter); + db.generateTransportUpdate(contactId, transportWriter); fail(); } catch(NoSuchContactException expected) {} @@ -521,12 +521,12 @@ public abstract class DatabaseComponentTest extends TestCase { } catch(NoSuchContactException expected) {} try { - db.receiveSubscriptions(contactId, subscriptionsUpdate); + db.receiveSubscriptionUpdate(contactId, subscriptionsUpdate); fail(); } catch(NoSuchContactException expected) {} try { - db.receiveTransports(contactId, transportsUpdate); + db.receiveTransportUpdate(contactId, transportsUpdate); fail(); } catch(NoSuchContactException expected) {} @@ -696,7 +696,7 @@ public abstract class DatabaseComponentTest extends TestCase { } @Test - public void testGenerateSubscriptions() throws Exception { + public void testGenerateSubscriptionUpdate() throws Exception { final MessageId messageId1 = new MessageId(TestUtils.getRandomId()); final Collection sendable = new ArrayList(); sendable.add(messageId); @@ -722,13 +722,13 @@ public abstract class DatabaseComponentTest extends TestCase { }}); DatabaseComponent db = createDatabaseComponent(database, cleaner); - db.generateSubscriptions(contactId, subscriptionWriter); + db.generateSubscriptionUpdate(contactId, subscriptionWriter); context.assertIsSatisfied(); } @Test - public void testGenerateTransports() throws Exception { + public void testGenerateTransportUpdate() throws Exception { final MessageId messageId1 = new MessageId(TestUtils.getRandomId()); final Collection sendable = new ArrayList(); sendable.add(messageId); @@ -753,7 +753,7 @@ public abstract class DatabaseComponentTest extends TestCase { }}); DatabaseComponent db = createDatabaseComponent(database, cleaner); - db.generateTransports(contactId, transportWriter); + db.generateTransportUpdate(contactId, transportWriter); context.assertIsSatisfied(); } @@ -982,14 +982,14 @@ public abstract class DatabaseComponentTest extends TestCase { } @Test - public void testReceiveSubscriptions() throws Exception { + public void testReceiveSubscriptionUpdate() throws Exception { final long timestamp = 1234L; Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); - final Subscriptions subscriptionsUpdate = - context.mock(Subscriptions.class); + final SubscriptionUpdate subscriptionUpdate = + context.mock(SubscriptionUpdate.class); context.checking(new Expectations() {{ allowing(database).startTransaction(); will(returnValue(txn)); @@ -997,28 +997,29 @@ public abstract class DatabaseComponentTest extends TestCase { allowing(database).containsContact(txn, contactId); will(returnValue(true)); // Get the contents of the update - oneOf(subscriptionsUpdate).getSubscriptions(); + oneOf(subscriptionUpdate).getSubscriptions(); will(returnValue(Collections.singletonList(group))); - oneOf(subscriptionsUpdate).getTimestamp(); + oneOf(subscriptionUpdate).getTimestamp(); will(returnValue(timestamp)); oneOf(database).setSubscriptions(txn, contactId, Collections.singletonList(group), timestamp); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner); - db.receiveSubscriptions(contactId, subscriptionsUpdate); + db.receiveSubscriptionUpdate(contactId, subscriptionUpdate); context.assertIsSatisfied(); } @Test - public void testReceiveTransports() throws Exception { + public void testReceiveTransportUpdate() throws Exception { final long timestamp = 1234L; Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); - final Transports transportsUpdate = context.mock(Transports.class); + final TransportUpdate transportUpdate = + context.mock(TransportUpdate.class); context.checking(new Expectations() {{ allowing(database).startTransaction(); will(returnValue(txn)); @@ -1026,16 +1027,16 @@ public abstract class DatabaseComponentTest extends TestCase { allowing(database).containsContact(txn, contactId); will(returnValue(true)); // Get the contents of the update - oneOf(transportsUpdate).getTransports(); + oneOf(transportUpdate).getTransports(); will(returnValue(transports)); - oneOf(transportsUpdate).getTimestamp(); + oneOf(transportUpdate).getTimestamp(); will(returnValue(timestamp)); oneOf(database).setTransports(txn, contactId, transports, timestamp); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner); - db.receiveTransports(contactId, transportsUpdate); + db.receiveTransportUpdate(contactId, transportUpdate); context.assertIsSatisfied(); } @@ -1103,7 +1104,12 @@ public abstract class DatabaseComponentTest extends TestCase { } @Test - public void testTransportsChangedCallsListeners() throws Exception { + public void testTransportPropertiesChangedCallsListeners() + throws Exception { + final Map properties = + Collections.singletonMap("bar", "baz"); + final Map properties1 = + Collections.singletonMap("baz", "bam"); Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); @@ -1113,9 +1119,8 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).startTransaction(); will(returnValue(txn)); oneOf(database).getTransports(txn); - will(returnValue(transports)); - oneOf(database).setTransports(txn, "bar", - Collections.singletonMap("baz", "bam")); + will(returnValue(Collections.singletonMap("foo", properties))); + oneOf(database).setTransportProperties(txn, "foo", properties1); oneOf(database).commitTransaction(txn); oneOf(listener).eventOccurred( DatabaseListener.Event.TRANSPORTS_UPDATED); @@ -1123,13 +1128,16 @@ public abstract class DatabaseComponentTest extends TestCase { DatabaseComponent db = createDatabaseComponent(database, cleaner); db.addListener(listener); - db.setTransports("bar", Collections.singletonMap("baz", "bam")); + db.setTransportProperties("foo", properties1); context.assertIsSatisfied(); } @Test - public void testTransportsUnchangedDoesNotCallListeners() throws Exception { + public void testTransportPropertiesUnchangedDoesNotCallListeners() + throws Exception { + final Map properties = + Collections.singletonMap("bar", "baz"); Mockery context = new Mockery(); @SuppressWarnings("unchecked") final Database database = context.mock(Database.class); @@ -1139,13 +1147,67 @@ public abstract class DatabaseComponentTest extends TestCase { oneOf(database).startTransaction(); will(returnValue(txn)); oneOf(database).getTransports(txn); - will(returnValue(transports)); + will(returnValue(Collections.singletonMap("foo", properties))); oneOf(database).commitTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner); db.addListener(listener); - db.setTransports("foo", transports.get("foo")); + db.setTransportProperties("foo", properties); + + context.assertIsSatisfied(); + } + + @Test + public void testTransportConfigChangedCallsListeners() throws Exception { + final Map config = + Collections.singletonMap("bar", "baz"); + final Map config1 = + Collections.singletonMap("baz", "bam"); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getTransportConfig(txn, "foo"); + will(returnValue(config)); + oneOf(database).setTransportConfig(txn, "foo", config1); + oneOf(database).commitTransaction(txn); + oneOf(listener).eventOccurred( + DatabaseListener.Event.TRANSPORTS_UPDATED); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner); + + db.addListener(listener); + db.setTransportConfig("foo", config1); + + context.assertIsSatisfied(); + } + + @Test + public void testTransportConfigUnchangedDoesNotCallListeners() + throws Exception { + final Map config = + Collections.singletonMap("bar", "baz"); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getTransportConfig(txn, "foo"); + will(returnValue(config)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner); + + db.addListener(listener); + db.setTransportConfig("foo", config); context.assertIsSatisfied(); } diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java index 7de9d7953..ef82cfc49 100644 --- a/test/net/sf/briar/db/H2DatabaseTest.java +++ b/test/net/sf/briar/db/H2DatabaseTest.java @@ -780,7 +780,7 @@ public class H2DatabaseTest extends TestCase { } @Test - public void testUpdateTransports() throws DbException { + public void testUpdateTransportPropertiess() throws DbException { Database db = open(false); Connection txn = db.startTransaction(); @@ -799,13 +799,14 @@ public class H2DatabaseTest extends TestCase { Collections.>emptyMap(), 2); assertEquals(Collections.emptyMap(), db.getTransports(txn, contactId)); // Set the local transport properties - for(String s : transports.keySet()) { - db.setTransports(txn, s, transports.get(s)); + for(String name : transports.keySet()) { + db.setTransportProperties(txn, name, transports.get(name)); } assertEquals(transports, db.getTransports(txn)); // Remove the local transport properties - for(String s : transports.keySet()) { - db.setTransports(txn, s, Collections.emptyMap()); + for(String name : transports.keySet()) { + db.setTransportProperties(txn, name, + Collections.emptyMap()); } assertEquals(Collections.emptyMap(), db.getTransports(txn)); @@ -813,6 +814,29 @@ public class H2DatabaseTest extends TestCase { db.close(); } + + @Test + public void testUpdateTransportConfig() throws DbException { + Map config = Collections.singletonMap("bar", "baz"); + Map config1 = Collections.singletonMap("baz", "bam"); + Database db = open(false); + Connection txn = db.startTransaction(); + + // Set the transport config + db.setTransportConfig(txn, "foo", config); + assertEquals(config, db.getTransportConfig(txn, "foo")); + // Update the transport config + db.setTransportConfig(txn, "foo", config1); + assertEquals(config1, db.getTransportConfig(txn, "foo")); + // Remove the transport config + db.setTransportConfig(txn, "foo", + Collections.emptyMap()); + assertEquals(Collections.emptyMap(), db.getTransportConfig(txn, "foo")); + + db.commitTransaction(txn); + db.close(); + } + @Test public void testTransportsNotUpdatedIfTimestampIsOld() throws DbException { Database db = open(false); diff --git a/test/net/sf/briar/protocol/FileReadWriteTest.java b/test/net/sf/briar/protocol/FileReadWriteTest.java index 52516a380..808a630e3 100644 --- a/test/net/sf/briar/protocol/FileReadWriteTest.java +++ b/test/net/sf/briar/protocol/FileReadWriteTest.java @@ -27,9 +27,9 @@ import net.sf.briar.api.protocol.MessageEncoder; import net.sf.briar.api.protocol.MessageId; import net.sf.briar.api.protocol.Offer; import net.sf.briar.api.protocol.Request; -import net.sf.briar.api.protocol.Subscriptions; +import net.sf.briar.api.protocol.SubscriptionUpdate; import net.sf.briar.api.protocol.Tags; -import net.sf.briar.api.protocol.Transports; +import net.sf.briar.api.protocol.TransportUpdate; import net.sf.briar.api.protocol.UniqueId; import net.sf.briar.api.protocol.writers.AckWriter; import net.sf.briar.api.protocol.writers.BatchWriter; @@ -217,10 +217,10 @@ public class FileReadWriteTest extends TestCase { // If there are any padding bits, they should all be zero assertEquals(2, requested.cardinality()); - // Read the subscriptions update + // Read the subscription update assertTrue(reader.hasUserDefined(Tags.SUBSCRIPTIONS)); - Subscriptions s = reader.readUserDefined(Tags.SUBSCRIPTIONS, - Subscriptions.class); + SubscriptionUpdate s = reader.readUserDefined(Tags.SUBSCRIPTIONS, + SubscriptionUpdate.class); Collection subs = s.getSubscriptions(); assertEquals(2, subs.size()); Iterator it2 = subs.iterator(); @@ -229,10 +229,10 @@ public class FileReadWriteTest extends TestCase { assertTrue(s.getTimestamp() > start); assertTrue(s.getTimestamp() <= System.currentTimeMillis()); - // Read the transports update + // Read the transport update assertTrue(reader.hasUserDefined(Tags.TRANSPORTS)); - Transports t = reader.readUserDefined(Tags.TRANSPORTS, - Transports.class); + TransportUpdate t = reader.readUserDefined(Tags.TRANSPORTS, + TransportUpdate.class); assertEquals(transports, t.getTransports()); assertTrue(t.getTimestamp() > start); assertTrue(t.getTimestamp() <= System.currentTimeMillis());