Added the ability to remove pseudonyms from the database.

This commit is contained in:
akwizgran
2013-12-10 22:23:37 +00:00
parent 667dbfdd4a
commit 47708d489d
10 changed files with 368 additions and 41 deletions

View File

@@ -56,7 +56,7 @@ public interface DatabaseComponent {
/** Stores an endpoint. */
void addEndpoint(Endpoint ep) throws DbException;
/** Stores a pseudonym that the user can use to sign messages. */
/** Stores a local pseudonym. */
void addLocalAuthor(LocalAuthor a) throws DbException;
/** Stores a locally generated group message. */
@@ -179,10 +179,10 @@ public interface DatabaseComponent {
*/
Map<ContactId, Long> getLastConnected() throws DbException;
/** Returns the pseudonym with the given ID. */
/** Returns the local pseudonym with the given ID. */
LocalAuthor getLocalAuthor(AuthorId a) throws DbException;
/** Returns all pseudonyms that the user can use to sign messages. */
/** Returns all local pseudonyms. */
Collection<LocalAuthor> getLocalAuthors() throws DbException;
/** Returns the local transport properties for all transports. */
@@ -295,6 +295,11 @@ public interface DatabaseComponent {
/** Removes a contact (and all associated state) from the database. */
void removeContact(ContactId c) throws DbException;
/**
* Removes a local pseudonym (and all associated state) from the database.
*/
void removeLocalAuthor(AuthorId a) throws DbException;
/**
* Removes a transport (and any associated configuration and local
* properties) from the database.
@@ -302,8 +307,8 @@ public interface DatabaseComponent {
void removeTransport(TransportId t) throws DbException;
/**
* Sets the connection reordering window for the given endoint in the given
* rotation period.
* Sets the connection reordering window for the given endpoint in the
* given rotation period.
*/
void setConnectionWindow(ContactId c, TransportId t, long period,
long centre, byte[] bitmap) throws DbException;

View File

@@ -0,0 +1,10 @@
package net.sf.briar.api.db;
/**
* Thrown when a duplicate pseudonym is added to the database. This exception
* may occur due to concurrent updates and does not indicate a database error.
*/
public class LocalAuthorExistsException extends DbException {
private static final long serialVersionUID = -1483877298070151673L;
}

View File

@@ -0,0 +1,11 @@
package net.sf.briar.api.db;
/**
* Thrown when a database operation is attempted for a pseudonym that is not in
* the database. This exception may occur due to concurrent updates and does
* not indicate a database error.
*/
public class NoSuchLocalAuthorException extends DbException {
private static final long serialVersionUID = 494398665376703860L;
}

View File

@@ -0,0 +1,17 @@
package net.sf.briar.api.db.event;
import net.sf.briar.api.AuthorId;
/** An event that is broadcast when a pseudonym for the user is added. */
public class LocalAuthorAddedEvent extends DatabaseEvent {
private final AuthorId authorId;
public LocalAuthorAddedEvent(AuthorId authorId) {
this.authorId = authorId;
}
public AuthorId getAuthorId() {
return authorId;
}
}

View File

@@ -0,0 +1,17 @@
package net.sf.briar.api.db.event;
import net.sf.briar.api.AuthorId;
/** An event that is broadcast when a pseudonym for the user is removed. */
public class LocalAuthorRemovedEvent extends DatabaseEvent {
private final AuthorId authorId;
public LocalAuthorRemovedEvent(AuthorId authorId) {
this.authorId = authorId;
}
public AuthorId getAuthorId() {
return authorId;
}
}

View File

@@ -49,6 +49,8 @@ import net.sf.briar.api.transport.TemporarySecret;
* <li> transport
* <li> window
* </ul>
* If table A has a foreign key pointing to table B, we get a read lock on A to
* read A, a write lock on A to write A, and write locks on A and B to write B.
*/
interface Database<T> {
@@ -80,8 +82,8 @@ interface Database<T> {
* Stores a contact with the given pseudonym, associated with the given
* local pseudonym, and returns an ID for the contact.
* <p>
* Locking: contact write, retention write, subscription write, transport
* write, window write.
* Locking: contact write, message write, retention write,
* subscription write, transport write, window write.
*/
ContactId addContact(T txn, Author remote, AuthorId local)
throws DbException;
@@ -103,9 +105,10 @@ interface Database<T> {
throws DbException;
/**
* Stores a pseudonym that the user can use to sign messages.
* Stores a local pseudonym.
* <p>
* Locking: contact write, identity write.
* Locking: contact write, identity write, message write, retention write,
* subscription write, transport write, window write.
*/
void addLocalAuthor(T txn, LocalAuthor a) throws DbException;
@@ -181,6 +184,13 @@ interface Database<T> {
*/
boolean containsContact(T txn, ContactId c) throws DbException;
/**
* Returns true if the database contains the given local pseudonym.
* <p>
* Locking: identity read.
*/
boolean containsLocalAuthor(T txn, AuthorId a) throws DbException;
/**
* Returns true if the database contains the given message.
* <p>
@@ -246,6 +256,13 @@ interface Database<T> {
*/
Collection<Contact> getContacts(T txn) throws DbException;
/**
* Returns all contacts associated with the given local pseudonym.
* <p>
* Locking: contact read.
*/
Collection<ContactId> getContacts(T txn, AuthorId a) throws DbException;
/**
* Returns all endpoints.
* <p>
@@ -291,14 +308,14 @@ interface Database<T> {
Map<ContactId, Long> getLastConnected(T txn) throws DbException;
/**
* Returns the pseudonym with the given ID.
* Returns the local pseudonym with the given ID.
* <p>
* Locking: identitiy read.
* Locking: identity read.
*/
LocalAuthor getLocalAuthor(T txn, AuthorId a) throws DbException;
/**
* Returns all pseudonyms that the user can use to sign messages.
* Returns all local pseudonyms.
* <p>
* Locking: identity read.
*/
@@ -566,6 +583,15 @@ interface Database<T> {
*/
void removeContact(T txn, ContactId c) throws DbException;
/**
* Removes the local pseudonym with the given ID (and all associated
* state) from the database.
* <p>
* Locking: contact write, identity write, message write, retention write,
* subscription write, transport write, window write.
*/
void removeLocalAuthor(T txn, AuthorId a) throws DbException;
/**
* Removes a message (and all associated state) from the database.
* <p>

View File

@@ -38,7 +38,9 @@ import net.sf.briar.api.db.ContactExistsException;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.DbException;
import net.sf.briar.api.db.GroupMessageHeader;
import net.sf.briar.api.db.LocalAuthorExistsException;
import net.sf.briar.api.db.NoSuchContactException;
import net.sf.briar.api.db.NoSuchLocalAuthorException;
import net.sf.briar.api.db.NoSuchMessageException;
import net.sf.briar.api.db.NoSuchSubscriptionException;
import net.sf.briar.api.db.NoSuchTransportException;
@@ -48,6 +50,8 @@ import net.sf.briar.api.db.event.ContactRemovedEvent;
import net.sf.briar.api.db.event.DatabaseEvent;
import net.sf.briar.api.db.event.DatabaseListener;
import net.sf.briar.api.db.event.GroupMessageAddedEvent;
import net.sf.briar.api.db.event.LocalAuthorAddedEvent;
import net.sf.briar.api.db.event.LocalAuthorRemovedEvent;
import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent;
import net.sf.briar.api.db.event.LocalTransportsUpdatedEvent;
import net.sf.briar.api.db.event.MessageExpiredEvent;
@@ -186,35 +190,47 @@ DatabaseCleaner.Callback {
ContactId c;
contactLock.writeLock().lock();
try {
retentionLock.writeLock().lock();
identityLock.readLock().lock();
try {
subscriptionLock.writeLock().lock();
messageLock.writeLock().lock();
try {
transportLock.writeLock().lock();
retentionLock.writeLock().lock();
try {
windowLock.writeLock().lock();
subscriptionLock.writeLock().lock();
try {
T txn = db.startTransaction();
transportLock.writeLock().lock();
try {
if(db.containsContact(txn, remote.getId()))
throw new ContactExistsException();
c = db.addContact(txn, remote, local);
db.commitTransaction(txn);
} catch(DbException e) {
db.abortTransaction(txn);
throw e;
windowLock.writeLock().lock();
try {
T txn = db.startTransaction();
try {
if(db.containsContact(txn, remote.getId()))
throw new ContactExistsException();
if(!db.containsLocalAuthor(txn, local))
throw new NoSuchLocalAuthorException();
c = db.addContact(txn, remote, local);
db.commitTransaction(txn);
} catch(DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
windowLock.writeLock().unlock();
}
} finally {
transportLock.writeLock().unlock();
}
} finally {
windowLock.writeLock().unlock();
subscriptionLock.writeLock().unlock();
}
} finally {
transportLock.writeLock().unlock();
retentionLock.writeLock().unlock();
}
} finally {
subscriptionLock.writeLock().unlock();
messageLock.writeLock().unlock();
}
} finally {
retentionLock.writeLock().unlock();
identityLock.readLock().unlock();
}
} finally {
contactLock.writeLock().unlock();
@@ -263,13 +279,40 @@ DatabaseCleaner.Callback {
try {
identityLock.writeLock().lock();
try {
T txn = db.startTransaction();
messageLock.writeLock().lock();
try {
db.addLocalAuthor(txn, a);
db.commitTransaction(txn);
} catch(DbException e) {
db.abortTransaction(txn);
throw e;
retentionLock.writeLock().lock();
try {
subscriptionLock.writeLock().lock();
try {
transportLock.writeLock().lock();
try {
windowLock.writeLock().lock();
try {
T txn = db.startTransaction();
try {
if(db.containsLocalAuthor(txn, a.getId()))
throw new LocalAuthorExistsException();
db.addLocalAuthor(txn, a);
db.commitTransaction(txn);
} catch(DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
windowLock.writeLock().unlock();
}
} finally {
transportLock.writeLock().unlock();
}
} finally {
subscriptionLock.writeLock().unlock();
}
} finally {
retentionLock.writeLock().unlock();
}
} finally {
messageLock.writeLock().unlock();
}
} finally {
identityLock.writeLock().unlock();
@@ -277,6 +320,7 @@ DatabaseCleaner.Callback {
} finally {
contactLock.writeLock().unlock();
}
callListeners(new LocalAuthorAddedEvent(a.getId()));
}
public void addLocalGroupMessage(Message m) throws DbException {
@@ -942,6 +986,8 @@ DatabaseCleaner.Callback {
try {
T txn = db.startTransaction();
try {
if(!db.containsLocalAuthor(txn, a))
throw new NoSuchLocalAuthorException();
LocalAuthor localAuthor = db.getLocalAuthor(txn, a);
db.commitTransaction(txn);
return localAuthor;
@@ -1643,6 +1689,58 @@ DatabaseCleaner.Callback {
callListeners(new ContactRemovedEvent(c));
}
public void removeLocalAuthor(AuthorId a) throws DbException {
Collection<ContactId> affected;
contactLock.writeLock().lock();
try {
identityLock.writeLock().lock();
try {
messageLock.writeLock().lock();
try {
retentionLock.writeLock().lock();
try {
subscriptionLock.writeLock().lock();
try {
transportLock.writeLock().lock();
try {
windowLock.writeLock().lock();
try {
T txn = db.startTransaction();
try {
if(!db.containsLocalAuthor(txn, a))
throw new NoSuchLocalAuthorException();
affected = db.getContacts(txn, a);
db.removeLocalAuthor(txn, a);
db.commitTransaction(txn);
} catch(DbException e) {
db.abortTransaction(txn);
throw e;
}
} finally {
windowLock.writeLock().unlock();
}
} finally {
transportLock.writeLock().unlock();
}
} finally {
subscriptionLock.writeLock().unlock();
}
} finally {
retentionLock.writeLock().unlock();
}
} finally {
messageLock.writeLock().unlock();
}
} finally {
identityLock.writeLock().unlock();
}
} finally {
contactLock.writeLock().unlock();
}
for(ContactId c : affected) callListeners(new ContactRemovedEvent(c));
callListeners(new LocalAuthorRemovedEvent(a));
}
public void removeTransport(TransportId t) throws DbException {
transportLock.writeLock().lock();
try {

View File

@@ -60,6 +60,7 @@ import net.sf.briar.api.transport.TemporarySecret;
abstract class JdbcDatabase implements Database<Connection> {
// Locking: identity
// Dependents: contact, message, retention, subscription, transport, window
private static final String CREATE_LOCAL_AUTHORS =
"CREATE TABLE localAuthors"
+ " (authorId HASH NOT NULL,"
@@ -81,10 +82,7 @@ abstract class JdbcDatabase implements Database<Connection> {
+ " UNIQUE (authorId),"
+ " FOREIGN KEY (localAuthorId)"
+ " REFERENCES localAuthors (authorId)"
+ " ON DELETE RESTRICT)"; // Deletion not allowed
private static final String INDEX_CONTACTS_BY_AUTHOR =
"CREATE INDEX contactsByAuthor ON contacts (authorId)";
+ " ON DELETE CASCADE)";
// Locking: subscription
// Dependents: message
@@ -376,7 +374,6 @@ abstract class JdbcDatabase implements Database<Connection> {
s = txn.createStatement();
s.executeUpdate(insertTypeNames(CREATE_LOCAL_AUTHORS));
s.executeUpdate(insertTypeNames(CREATE_CONTACTS));
s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR);
s.executeUpdate(insertTypeNames(CREATE_GROUPS));
s.executeUpdate(insertTypeNames(CREATE_GROUP_VISIBILITIES));
s.executeUpdate(insertTypeNames(CREATE_CONTACT_GROUPS));
@@ -1000,6 +997,27 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
public boolean containsLocalAuthor(Connection txn, AuthorId a)
throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT NULL FROM localAuthors WHERE authorId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, a.getBytes());
rs = ps.executeQuery();
boolean found = rs.next();
if(rs.next()) throw new DbStateException();
rs.close();
ps.close();
return found;
} catch(SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
public boolean containsMessage(Connection txn, MessageId m)
throws DbException {
PreparedStatement ps = null;
@@ -1228,6 +1246,28 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
public Collection<ContactId> getContacts(Connection txn, AuthorId a)
throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = "SELECT contactId FROM contacts"
+ " WHERE localAuthorId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, a.getBytes());
rs = ps.executeQuery();
List<ContactId> ids = new ArrayList<ContactId>();
while(rs.next()) ids.add(new ContactId(rs.getInt(1)));
rs.close();
ps.close();
return Collections.unmodifiableList(ids);
} catch(SQLException e) {
tryToClose(rs);
tryToClose(ps);
throw new DbException(e);
}
}
public Collection<Endpoint> getEndpoints(Connection txn)
throws DbException {
PreparedStatement ps = null;
@@ -2543,6 +2583,22 @@ abstract class JdbcDatabase implements Database<Connection> {
}
}
public void removeLocalAuthor(Connection txn, AuthorId a)
throws DbException {
PreparedStatement ps = null;
try {
String sql = "DELETE FROM localAuthors WHERE authorId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, a.getBytes());
int affected = ps.executeUpdate();
if(affected != 1) throw new DbStateException();
ps.close();
} catch(SQLException e) {
tryToClose(ps);
throw new DbException(e);
}
}
public void removeMessage(Connection txn, MessageId m) throws DbException {
PreparedStatement ps = null;
try {

View File

@@ -25,12 +25,15 @@ import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.db.AckAndRequest;
import net.sf.briar.api.db.DatabaseComponent;
import net.sf.briar.api.db.NoSuchContactException;
import net.sf.briar.api.db.NoSuchLocalAuthorException;
import net.sf.briar.api.db.NoSuchSubscriptionException;
import net.sf.briar.api.db.NoSuchTransportException;
import net.sf.briar.api.db.event.ContactAddedEvent;
import net.sf.briar.api.db.event.ContactRemovedEvent;
import net.sf.briar.api.db.event.DatabaseListener;
import net.sf.briar.api.db.event.GroupMessageAddedEvent;
import net.sf.briar.api.db.event.LocalAuthorAddedEvent;
import net.sf.briar.api.db.event.LocalAuthorRemovedEvent;
import net.sf.briar.api.db.event.LocalSubscriptionsUpdatedEvent;
import net.sf.briar.api.db.event.PrivateMessageAddedEvent;
import net.sf.briar.api.db.event.SubscriptionAddedEvent;
@@ -122,9 +125,9 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
final ShutdownManager shutdown = context.mock(ShutdownManager.class);
final DatabaseListener listener = context.mock(DatabaseListener.class);
context.checking(new Expectations() {{
exactly(10).of(database).startTransaction();
exactly(11).of(database).startTransaction();
will(returnValue(txn));
exactly(10).of(database).commitTransaction(txn);
exactly(11).of(database).commitTransaction(txn);
// open()
oneOf(database).open();
will(returnValue(false));
@@ -134,10 +137,16 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
oneOf(shutdown).addShutdownHook(with(any(Runnable.class)));
will(returnValue(shutdownHandle));
// addLocalAuthor(localAuthor)
oneOf(database).containsLocalAuthor(txn, localAuthorId);
will(returnValue(false));
oneOf(database).addLocalAuthor(txn, localAuthor);
oneOf(listener).eventOccurred(with(any(
LocalAuthorAddedEvent.class)));
// addContact(author, localAuthorId)
oneOf(database).containsContact(txn, authorId);
will(returnValue(false));
oneOf(database).containsLocalAuthor(txn, localAuthorId);
will(returnValue(true));
oneOf(database).addContact(txn, author, localAuthorId);
will(returnValue(contactId));
oneOf(listener).eventOccurred(with(any(ContactAddedEvent.class)));
@@ -180,6 +189,14 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
will(returnValue(true));
oneOf(database).removeContact(txn, contactId);
oneOf(listener).eventOccurred(with(any(ContactRemovedEvent.class)));
// removeLocalAuthor(localAuthorId)
oneOf(database).containsLocalAuthor(txn, localAuthorId);
will(returnValue(true));
oneOf(database).getContacts(txn, localAuthorId);
will(returnValue(Collections.emptyList()));
oneOf(database).removeLocalAuthor(txn, localAuthorId);
oneOf(listener).eventOccurred(with(any(
LocalAuthorRemovedEvent.class)));
// close()
oneOf(shutdown).removeShutdownHook(shutdownHandle);
oneOf(cleaner).stopCleaning();
@@ -202,6 +219,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
assertEquals(Arrays.asList(groupId), db.getSubscriptions());
db.unsubscribe(group);
db.removeContact(contactId);
db.removeLocalAuthor(localAuthorId);
db.removeListener(listener);
db.close();
@@ -512,6 +530,46 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
context.assertIsSatisfied();
}
@Test
public void testVariousMethodsThrowExceptionIfLocalAuthorIsMissing()
throws Exception {
Mockery context = new Mockery();
@SuppressWarnings("unchecked")
final Database<Object> database = context.mock(Database.class);
final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
final ShutdownManager shutdown = context.mock(ShutdownManager.class);
context.checking(new Expectations() {{
// Check whether the pseudonym is in the DB (which it's not)
exactly(3).of(database).startTransaction();
will(returnValue(txn));
exactly(3).of(database).containsLocalAuthor(txn, localAuthorId);
will(returnValue(false));
exactly(3).of(database).abortTransaction(txn);
// This is needed for addContact() to proceed
exactly(1).of(database).containsContact(txn, authorId);
will(returnValue(false));
}});
DatabaseComponent db = createDatabaseComponent(database, cleaner,
shutdown);
try {
db.addContact(author, localAuthorId);
fail();
} catch(NoSuchLocalAuthorException expected) {}
try {
db.getLocalAuthor(localAuthorId);
fail();
} catch(NoSuchLocalAuthorException expected) {}
try {
db.removeLocalAuthor(localAuthorId);
fail();
} catch(NoSuchLocalAuthorException expected) {}
context.assertIsSatisfied();
}
@Test
public void testVariousMethodsThrowExceptionIfSubscriptionIsMissing()
throws Exception {
@@ -571,6 +629,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
// addLocalAuthor(localAuthor)
oneOf(database).startTransaction();
will(returnValue(txn));
oneOf(database).containsLocalAuthor(txn, localAuthorId);
will(returnValue(false));
oneOf(database).addLocalAuthor(txn, localAuthor);
oneOf(database).commitTransaction(txn);
// addContact(author, localAuthorId)
@@ -578,6 +638,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
will(returnValue(txn));
oneOf(database).containsContact(txn, authorId);
will(returnValue(false));
oneOf(database).containsLocalAuthor(txn, localAuthorId);
will(returnValue(true));
oneOf(database).addContact(txn, author, localAuthorId);
will(returnValue(contactId));
oneOf(database).commitTransaction(txn);

View File

@@ -1715,6 +1715,31 @@ public class H2DatabaseTest extends BriarTestCase {
db.close();
}
@Test
public void testGetContactsByLocalAuthorId() throws Exception {
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add a local author - no contacts should be associated
db.addLocalAuthor(txn, localAuthor);
Collection<ContactId> contacts = db.getContacts(txn, localAuthorId);
assertEquals(Collections.emptyList(), contacts);
// Add a contact associated with the local author
assertEquals(contactId, db.addContact(txn, author, localAuthorId));
contacts = db.getContacts(txn, localAuthorId);
assertEquals(Collections.singletonList(contactId), contacts);
// Remove the local author - the contact should be removed
db.removeLocalAuthor(txn, localAuthorId);
contacts = db.getContacts(txn, localAuthorId);
assertEquals(Collections.emptyList(), contacts);
assertFalse(db.containsContact(txn, contactId));
db.commitTransaction(txn);
db.close();
}
@Test
public void testExceptionHandling() throws Exception {
Database<Connection> db = open(false);