diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java b/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java index 04c3404ded64bc0cbbae35918a30ba7f09ed1506..79076e317469c5396f648f46dc67ad1f87fe51d6 100644 --- a/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java +++ b/briar-android/src/net/sf/briar/android/blogs/BlogActivity.java @@ -118,7 +118,7 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { serviceConnection.waitForStartup(); long now = System.currentTimeMillis(); Collection<GroupMessageHeader> headers = - db.getMessageHeaders(groupId); + db.getGroupMessageHeaders(groupId); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Load took " + duration + " ms"); diff --git a/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java b/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java index 5f35cc3e9157f5638b5858104c521e4806c1d475..2beedf041873bbaa322e65980ed95b7cb170bfc3 100644 --- a/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java +++ b/briar-android/src/net/sf/briar/android/blogs/BlogListActivity.java @@ -129,7 +129,7 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { boolean postable = local.contains(g.getId()); try { Collection<GroupMessageHeader> headers = - db.getMessageHeaders(g.getId()); + db.getGroupMessageHeaders(g.getId()); displayHeaders(g, postable, headers); } catch(NoSuchSubscriptionException e) { if(LOG.isLoggable(INFO)) @@ -256,7 +256,7 @@ implements OnClickListener, DatabaseListener, NoBlogsDialog.Listener { serviceConnection.waitForStartup(); long now = System.currentTimeMillis(); Collection<GroupMessageHeader> headers = - db.getMessageHeaders(g.getId()); + db.getGroupMessageHeaders(g.getId()); boolean postable = db.getLocalGroups().contains(g); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) diff --git a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java index 0cdd423055dccde1550d623c11fe7be6d7770ece..ee389d2feba68f65ecf43a16a9b76c5e7b0fef6b 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupActivity.java @@ -116,7 +116,7 @@ OnClickListener, OnItemClickListener { serviceConnection.waitForStartup(); long now = System.currentTimeMillis(); Collection<GroupMessageHeader> headers = - db.getMessageHeaders(groupId); + db.getGroupMessageHeaders(groupId); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Load took " + duration + " ms"); diff --git a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java index 83f3c3edd56c81e9203a18a922fea25daaaa77ec..3deda412db42abff09c6296f61764dccc788d363 100644 --- a/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/GroupListActivity.java @@ -124,7 +124,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { if(g.isRestricted()) continue; try { Collection<GroupMessageHeader> headers = - db.getMessageHeaders(g.getId()); + db.getGroupMessageHeaders(g.getId()); displayHeaders(g, headers); } catch(NoSuchSubscriptionException e) { if(LOG.isLoggable(INFO)) @@ -244,7 +244,7 @@ implements OnClickListener, DatabaseListener, NoGroupsDialog.Listener { serviceConnection.waitForStartup(); long now = System.currentTimeMillis(); Collection<GroupMessageHeader> headers = - db.getMessageHeaders(g.getId()); + db.getGroupMessageHeaders(g.getId()); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Partial load took " + duration + " ms"); diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java index fca12f6e4288b4c6a06a4dbdccb6f59831419d98..e93e9f19fe04bf79f9a2d93b94bd50117a2e2bde 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java +++ b/briar-android/src/net/sf/briar/android/messages/ConversationActivity.java @@ -19,7 +19,6 @@ import net.sf.briar.android.BriarService.BriarServiceConnection; import net.sf.briar.android.widgets.HorizontalBorder; import net.sf.briar.api.AuthorId; import net.sf.briar.api.ContactId; -import net.sf.briar.api.LocalAuthor; import net.sf.briar.api.android.DatabaseUiExecutor; import net.sf.briar.api.db.DatabaseComponent; import net.sf.briar.api.db.DbException; @@ -60,7 +59,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { @Inject @DatabaseUiExecutor private volatile Executor dbUiExecutor; private volatile ContactId contactId = null; private volatile AuthorId localAuthorId = null; - private volatile String localAuthorName = null; @Override public void onCreate(Bundle state) { @@ -82,7 +80,7 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { layout.setOrientation(VERTICAL); layout.setGravity(CENTER_HORIZONTAL); - adapter = new ConversationAdapter(this, contactName); + adapter = new ConversationAdapter(this); list = new ListView(this); // Give me all the width and all the unused height list.setLayoutParams(MATCH_WRAP_1); @@ -118,14 +116,12 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { try { serviceConnection.waitForStartup(); long now = System.currentTimeMillis(); - LocalAuthor localAuthor = db.getLocalAuthor(localAuthorId); - localAuthorName = localAuthor.getName(); Collection<PrivateMessageHeader> headers = db.getPrivateMessageHeaders(contactId); long duration = System.currentTimeMillis() - now; if(LOG.isLoggable(INFO)) LOG.info("Load took " + duration + " ms"); - displayHeaders(localAuthor, headers); + displayHeaders(headers); } catch(NoSuchContactException e) { if(LOG.isLoggable(INFO)) LOG.info("Contact removed"); finishOnUiThread(); @@ -141,11 +137,10 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { }); } - private void displayHeaders(final LocalAuthor localAuthor, + private void displayHeaders( final Collection<PrivateMessageHeader> headers) { runOnUiThread(new Runnable() { public void run() { - adapter.setLocalAuthorName(localAuthor.getName()); adapter.clear(); for(PrivateMessageHeader h : headers) adapter.add(h); adapter.sort(AscendingHeaderComparator.INSTANCE); @@ -228,7 +223,6 @@ implements DatabaseListener, OnClickListener, OnItemClickListener { Intent i = new Intent(this, ReadPrivateMessageActivity.class); i.putExtra("net.sf.briar.CONTACT_ID", contactId.getInt()); i.putExtra("net.sf.briar.CONTACT_NAME", contactName); - i.putExtra("net.sf.briar.LOCAL_AUTHOR_NAME", localAuthorName); i.putExtra("net.sf.briar.MESSAGE_ID", item.getId().getBytes()); i.putExtra("net.sf.briar.CONTENT_TYPE", item.getContentType()); i.putExtra("net.sf.briar.TIMESTAMP", item.getTimestamp()); diff --git a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java b/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java index 7094b77ed16faeb640bdbc89d63d6911ce75a6d9..0e1ffc63a6c05869145456f7188efcbfa8aaa5b0 100644 --- a/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java +++ b/briar-android/src/net/sf/briar/android/messages/ConversationAdapter.java @@ -23,23 +23,13 @@ import android.widget.TextView; class ConversationAdapter extends ArrayAdapter<PrivateMessageHeader> { - private final String contactName; - - private String localAuthorName = null; - - ConversationAdapter(Context ctx, String contactName) { + ConversationAdapter(Context ctx) { super(ctx, android.R.layout.simple_expandable_list_item_1, new ArrayList<PrivateMessageHeader>()); - this.contactName = contactName; - } - - void setLocalAuthorName(String localAuthorName) { - this.localAuthorName = localAuthorName; } @Override public View getView(int position, View convertView, ViewGroup parent) { - if(localAuthorName == null) throw new IllegalStateException(); PrivateMessageHeader item = getItem(position); Context ctx = getContext(); @@ -59,8 +49,7 @@ class ConversationAdapter extends ArrayAdapter<PrivateMessageHeader> { name.setTextSize(18); name.setMaxLines(1); name.setPadding(10, 10, 10, 10); - if(item.isIncoming()) name.setText(contactName); - else name.setText(localAuthorName); + name.setText(item.getAuthor().getName()); innerLayout.addView(name); if(item.getContentType().equals("text/plain")) { diff --git a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java index 722348bcf6310fe1e154755b5710e37cfe0b891d..bcd11a7e67570e47cb9ff8f54cc2504833ef3f36 100644 --- a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java +++ b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java @@ -180,6 +180,10 @@ public interface DatabaseComponent { /** Returns the group with the given ID, if the user subscribes to it. */ Group getGroup(GroupId g) throws DbException; + /** Returns the headers of all messages in the given group. */ + Collection<GroupMessageHeader> getGroupMessageHeaders(GroupId g) + throws DbException; + /** Returns the pseudonym with the given ID. */ LocalAuthor getLocalAuthor(AuthorId a) throws DbException; @@ -199,10 +203,6 @@ public interface DatabaseComponent { /** Returns the body of the message with the given ID. */ byte[] getMessageBody(MessageId m) throws DbException; - /** Returns the headers of all messages in the given group. */ - Collection<GroupMessageHeader> getMessageHeaders(GroupId g) - throws DbException; - /** * Returns the headers of all private messages to or from the given * contact. diff --git a/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java b/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java index 7e6d98cee65318cda02311afb654ec5a7b0e4f8b..df6e4ac240449dced84cb06934ff5bada1260990 100644 --- a/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java +++ b/briar-api/src/net/sf/briar/api/db/GroupMessageHeader.java @@ -8,35 +8,17 @@ import net.sf.briar.api.messaging.Rating; public class GroupMessageHeader extends MessageHeader { private final GroupId groupId; - private final Author author; - private final Rating rating; - public GroupMessageHeader(MessageId id, MessageId parent, + public GroupMessageHeader(MessageId id, MessageId parent, Author author, String contentType, String subject, long timestamp, boolean read, - boolean starred, GroupId groupId, Author author, Rating rating) { - super(id, parent, contentType, subject, timestamp, read, starred); + boolean starred, Rating rating, GroupId groupId) { + super(id, parent, author, contentType, subject, timestamp, read, + starred, rating); this.groupId = groupId; - this.author = author; - this.rating = rating; } /** Returns the ID of the group to which the message belongs. */ public GroupId getGroupId() { return groupId; } - - /** - * Returns the message's author, or null if this is an anonymous message. - */ - public Author getAuthor() { - return author; - } - - /** - * Returns the rating for the message's author, or Rating.UNRATED if this - * is an anonymous message. - */ - public Rating getRating() { - return rating; - } } diff --git a/briar-api/src/net/sf/briar/api/db/MessageHeader.java b/briar-api/src/net/sf/briar/api/db/MessageHeader.java index 95a927d3e7fdf8ae7334c1da297c16570cd8f414..2f48df5b6a639eec5749c8e9d1ea159a305ef56d 100644 --- a/briar-api/src/net/sf/briar/api/db/MessageHeader.java +++ b/briar-api/src/net/sf/briar/api/db/MessageHeader.java @@ -1,23 +1,30 @@ package net.sf.briar.api.db; +import net.sf.briar.api.Author; import net.sf.briar.api.messaging.MessageId; +import net.sf.briar.api.messaging.Rating; public abstract class MessageHeader { private final MessageId id, parent; + private final Author author; private final String contentType, subject; private final long timestamp; private final boolean read, starred; + private final Rating rating; - protected MessageHeader(MessageId id, MessageId parent, String contentType, - String subject, long timestamp, boolean read, boolean starred) { + protected MessageHeader(MessageId id, MessageId parent, Author author, + String contentType, String subject, long timestamp, boolean read, + boolean starred, Rating rating) { this.id = id; this.parent = parent; + this.author = author; this.contentType = contentType; this.subject = subject; this.timestamp = timestamp; this.read = read; this.starred = starred; + this.rating = rating; } /** Returns the message's unique identifier. */ @@ -33,6 +40,13 @@ public abstract class MessageHeader { return parent; } + /** + * Returns the message's author, or null if this is an anonymous message. + */ + public Author getAuthor() { + return author; + } + /** Returns the message's content type. */ public String getContentType() { return contentType; @@ -57,4 +71,12 @@ public abstract class MessageHeader { public boolean isStarred() { return starred; } + + /** + * Returns the rating for the message's author, or Rating.UNRATED if this + * is an anonymous message. + */ + public Rating getRating() { + return rating; + } } diff --git a/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java b/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java index af55211c66fb691f233ce82816525d03fca2ff29..5a9b3c34728f093dfffb139c6f672282343d51b9 100644 --- a/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java +++ b/briar-api/src/net/sf/briar/api/db/PrivateMessageHeader.java @@ -1,17 +1,21 @@ package net.sf.briar.api.db; +import net.sf.briar.api.Author; import net.sf.briar.api.ContactId; import net.sf.briar.api.messaging.MessageId; +import net.sf.briar.api.messaging.Rating; public class PrivateMessageHeader extends MessageHeader { private final ContactId contactId; private final boolean incoming; - public PrivateMessageHeader(MessageId id, MessageId parent, + public PrivateMessageHeader(MessageId id, MessageId parent, Author author, String contentType, String subject, long timestamp, boolean read, - boolean starred, ContactId contactId, boolean incoming) { - super(id, parent, contentType, subject, timestamp, read, starred); + boolean starred, Rating rating, ContactId contactId, + boolean incoming) { + super(id, parent, author, contentType, subject, timestamp, read, + starred, rating); this.contactId = contactId; this.incoming = incoming; } diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java index f00b294a4ecf01b646387aba8878f263970c929e..499dd6c933b17569e8be9bcdf0f9b97b371813e5 100644 --- a/briar-core/src/net/sf/briar/db/Database.java +++ b/briar-core/src/net/sf/briar/db/Database.java @@ -101,7 +101,8 @@ interface Database<T> { * <p> * Locking: message write. */ - boolean addGroupMessage(T txn, Message m) throws DbException; + boolean addGroupMessage(T txn, Message m, boolean incoming) + throws DbException; /** * Stores a pseudonym that the user can use to sign messages. @@ -131,7 +132,8 @@ interface Database<T> { * <p> * Locking: message write. */ - boolean addPrivateMessage(T txn, Message m, ContactId c) throws DbException; + boolean addPrivateMessage(T txn, Message m, ContactId c, boolean incoming) + throws DbException; /** * Stores the given temporary secrets and deletes any secrets that have @@ -272,6 +274,14 @@ interface Database<T> { */ Group getGroup(T txn, GroupId g) throws DbException; + /** + * Returns the headers of all messages in the given group. + * <p> + * Locking: message read, rating read. + */ + Collection<GroupMessageHeader> getGroupMessageHeaders(T txn, GroupId g) + throws DbException; + /** * Returns the parent of the given group message, or null if either the * message has no parent, or the parent is absent from the database, or the @@ -333,19 +343,11 @@ interface Database<T> { */ byte[] getMessageBody(T txn, MessageId m) throws DbException; - /** - * Returns the headers of all messages in the given group. - * <p> - * Locking: message read, rating read. - */ - Collection<GroupMessageHeader> getMessageHeaders(T txn, GroupId g) - throws DbException; - /** * Returns the headers of all private messages to or from the given * contact. * <p> - * Locking: message read. + * Locking: contact read, identity read, message read, rating read. */ Collection<PrivateMessageHeader> getPrivateMessageHeaders(T txn, ContactId c) throws DbException; diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java index fcaff944b87b5d53b20258a5ff8784ec91dca065..b37caf2079dd4809dcf25577d859a5c0372392e0 100644 --- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java +++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java @@ -347,10 +347,9 @@ DatabaseCleaner.Callback { * Locking: contact read, message write, rating read. * @param sender is null for a locally generated message. */ - private boolean storeGroupMessage(T txn, Message m, ContactId sender) - throws DbException { + private boolean storeGroupMessage(T txn, Message m, ContactId sender) throws DbException { if(m.getGroup() == null) throw new IllegalArgumentException(); - boolean stored = db.addGroupMessage(txn, m); + boolean stored = db.addGroupMessage(txn, m, sender != null); if(stored && sender == null) db.setReadFlag(txn, m.getId(), true); // Mark the message as seen by the sender MessageId id = m.getId(); @@ -456,9 +455,9 @@ DatabaseCleaner.Callback { /** * If the given message is already in the database, returns false. - * Otherwise stores the message and marks it as new or seen with respect to - * the given contact, depending on whether the message is outgoing or - * incoming, respectively. + * Otherwise stores the message and marks it as seen or unseen with respect + * to the given contact, depending on whether the message is incoming or + * outgoing, respectively. * <p> * Locking: message write. */ @@ -466,7 +465,7 @@ DatabaseCleaner.Callback { boolean incoming) throws DbException { if(m.getGroup() != null) throw new IllegalArgumentException(); if(m.getAuthor() != null) throw new IllegalArgumentException(); - if(!db.addPrivateMessage(txn, m, c)) { + if(!db.addPrivateMessage(txn, m, c, incoming)) { if(LOG.isLoggable(INFO)) LOG.info("Duplicate private message not stored"); return false; @@ -984,6 +983,37 @@ DatabaseCleaner.Callback { } } + public Collection<GroupMessageHeader> getGroupMessageHeaders(GroupId g) + throws DbException { + messageLock.readLock().lock(); + try { + ratingLock.readLock().lock(); + try { + subscriptionLock.readLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsSubscription(txn, g)) + throw new NoSuchSubscriptionException(); + Collection<GroupMessageHeader> headers = + db.getGroupMessageHeaders(txn, g); + db.commitTransaction(txn); + return headers; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.readLock().unlock(); + } + } finally { + ratingLock.readLock().unlock(); + } + } finally { + messageLock.readLock().unlock(); + } + } + public LocalAuthor getLocalAuthor(AuthorId a) throws DbException { identityLock.readLock().lock(); try { @@ -1093,53 +1123,37 @@ DatabaseCleaner.Callback { } } - public Collection<GroupMessageHeader> getMessageHeaders(GroupId g) - throws DbException { - messageLock.readLock().lock(); + public Collection<PrivateMessageHeader> getPrivateMessageHeaders( + ContactId c) throws DbException { + contactLock.readLock().lock(); try { - ratingLock.readLock().lock(); + identityLock.readLock().lock(); try { - subscriptionLock.readLock().lock(); + messageLock.readLock().lock(); try { - T txn = db.startTransaction(); + ratingLock.readLock().lock(); try { - if(!db.containsSubscription(txn, g)) - throw new NoSuchSubscriptionException(); - Collection<GroupMessageHeader> headers = - db.getMessageHeaders(txn, g); - db.commitTransaction(txn); - return headers; - } catch(DbException e) { - db.abortTransaction(txn); - throw e; + T txn = db.startTransaction(); + try { + Collection<PrivateMessageHeader> headers = + db.getPrivateMessageHeaders(txn, c); + db.commitTransaction(txn); + return headers; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + ratingLock.readLock().unlock(); } } finally { - subscriptionLock.readLock().unlock(); + messageLock.readLock().unlock(); } } finally { - ratingLock.readLock().unlock(); - } - } finally { - messageLock.readLock().unlock(); - } - } - - public Collection<PrivateMessageHeader> getPrivateMessageHeaders( - ContactId c) throws DbException { - messageLock.readLock().lock(); - try { - T txn = db.startTransaction(); - try { - Collection<PrivateMessageHeader> headers = - db.getPrivateMessageHeaders(txn, c); - db.commitTransaction(txn); - return headers; - } catch(DbException e) { - db.abortTransaction(txn); - throw e; + identityLock.readLock().unlock(); } } finally { - messageLock.readLock().unlock(); + contactLock.readLock().unlock(); } } @@ -1510,675 +1524,675 @@ DatabaseCleaner.Callback { * <p> * Locking: contact read, message write, rating read, subscription read. */ - private boolean storeMessage(T txn, ContactId c, Message m) - throws DbException { - long now = clock.currentTimeMillis(); - if(m.getTimestamp() > now + MAX_CLOCK_DIFFERENCE) { - if(LOG.isLoggable(INFO)) - LOG.info("Discarding message with future timestamp"); - return false; - } - Group g = m.getGroup(); - if(g == null) return storePrivateMessage(txn, m, c, true); - if(!db.containsVisibleSubscription(txn, c, g.getId())) { - if(LOG.isLoggable(INFO)) - LOG.info("Discarding message without visible subscription"); - return false; - } - return storeGroupMessage(txn, m, c); - } - - public Request receiveOffer(ContactId c, Offer o) throws DbException { - Collection<MessageId> offered; - BitSet request; - contactLock.readLock().lock(); - try { - messageLock.writeLock().lock(); - try { - subscriptionLock.readLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - offered = o.getMessageIds(); - request = new BitSet(offered.size()); - Iterator<MessageId> it = offered.iterator(); - for(int i = 0; it.hasNext(); i++) { - // If the message is not in the database, or not - // visible to the contact, request it - MessageId m = it.next(); - if(!db.setStatusSeenIfVisible(txn, c, m)) - request.set(i); - } - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.readLock().unlock(); - } - } finally { - messageLock.writeLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - return new Request(request, offered.size()); - } - - public void receiveRetentionAck(ContactId c, RetentionAck a) - throws DbException { - contactLock.readLock().lock(); - try { - retentionLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - db.setRetentionUpdateAcked(txn, c, a.getVersion()); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - retentionLock.writeLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - } - - public void receiveRetentionUpdate(ContactId c, RetentionUpdate u) - throws DbException { - boolean updated; - contactLock.readLock().lock(); - try { - retentionLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - updated = db.setRetentionTime(txn, c, u.getRetentionTime(), - u.getVersion()); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - retentionLock.writeLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - if(updated) callListeners(new RemoteRetentionTimeUpdatedEvent(c)); - } - - public void receiveSubscriptionAck(ContactId c, SubscriptionAck a) - throws DbException { - contactLock.readLock().lock(); - try { - subscriptionLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - db.setSubscriptionUpdateAcked(txn, c, a.getVersion()); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.writeLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - } - - public void receiveSubscriptionUpdate(ContactId c, SubscriptionUpdate u) - throws DbException { - boolean updated; - contactLock.readLock().lock(); - try { - subscriptionLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - Collection<Group> groups = u.getGroups(); - long version = u.getVersion(); - updated = db.setSubscriptions(txn, c, groups, version); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.writeLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - if(updated) callListeners(new RemoteSubscriptionsUpdatedEvent(c)); - } - - public void receiveTransportAck(ContactId c, TransportAck a) - throws DbException { - contactLock.readLock().lock(); - try { - transportLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - TransportId t = a.getId(); - if(!db.containsTransport(txn, t)) - throw new NoSuchTransportException(); - db.setTransportUpdateAcked(txn, c, t, a.getVersion()); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - transportLock.writeLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - } - - public void receiveTransportUpdate(ContactId c, TransportUpdate u) - throws DbException { - boolean updated; - contactLock.readLock().lock(); - try { - transportLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - TransportId t = u.getId(); - TransportProperties p = u.getProperties(); - long version = u.getVersion(); - updated = db.setRemoteProperties(txn, c, t, p, version); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - transportLock.writeLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - if(updated) - callListeners(new RemoteTransportsUpdatedEvent(c, u.getId())); - } - - public void removeContact(ContactId c) throws DbException { - contactLock.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.containsContact(txn, c)) - throw new NoSuchContactException(); - db.removeContact(txn, c); - 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 { - contactLock.writeLock().unlock(); - } - callListeners(new ContactRemovedEvent(c)); - } - - public void removeTransport(TransportId t) throws DbException { - transportLock.writeLock().lock(); - try { - windowLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsTransport(txn, t)) - throw new NoSuchTransportException(); - db.removeTransport(txn, t); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - windowLock.writeLock().unlock(); - } - } finally { - transportLock.writeLock().unlock(); - } - callListeners(new TransportRemovedEvent(t)); - } - - public void setConnectionWindow(ContactId c, TransportId t, long period, - long centre, byte[] bitmap) throws DbException { - contactLock.readLock().lock(); - try { - transportLock.readLock().lock(); - try { - windowLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - if(!db.containsTransport(txn, t)) - throw new NoSuchTransportException(); - db.setConnectionWindow(txn, c, t, period, centre, - bitmap); - db.setLastConnected(txn, c, clock.currentTimeMillis()); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - windowLock.writeLock().unlock(); - } - } finally { - transportLock.readLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - } - - public void setRating(AuthorId a, Rating r) throws DbException { - boolean changed; - messageLock.writeLock().lock(); - try { - ratingLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - Rating old = db.setRating(txn, a, r); - changed = (old != r); - // Update the sendability of the author's messages - if(r == GOOD && old != GOOD) - updateAuthorSendability(txn, a, true); - else if(r != GOOD && old == GOOD) - updateAuthorSendability(txn, a, false); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - ratingLock.writeLock().unlock(); - } - } finally { - messageLock.writeLock().unlock(); - } - if(changed) callListeners(new RatingChangedEvent(a, r)); - } - - public boolean setReadFlag(MessageId m, boolean read) throws DbException { - messageLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsMessage(txn, m)) - throw new NoSuchMessageException(); - boolean wasRead = db.setReadFlag(txn, m, read); - db.commitTransaction(txn); - return wasRead; - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - messageLock.writeLock().unlock(); - } - } - - public void setRemoteProperties(ContactId c, - Map<TransportId, TransportProperties> p) throws DbException { - contactLock.readLock().lock(); - try { - transportLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - db.setRemoteProperties(txn, c, p); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - transportLock.writeLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - } - - public void setSeen(ContactId c, Collection<MessageId> seen) - throws DbException { - contactLock.readLock().lock(); - try { - messageLock.writeLock().lock(); - try { - subscriptionLock.readLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsContact(txn, c)) - throw new NoSuchContactException(); - for(MessageId m : seen) - db.setStatusSeenIfVisible(txn, c, m); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.readLock().unlock(); - } - } finally { - messageLock.writeLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - } - - /** - * Updates the sendability of all messages written by the given author, and - * the ancestors of those messages if necessary. - * <p> - * Locking: message write. - * @param increment true if the user's rating for the author has changed - * from not good to good, or false if it has changed from good to not good. - */ - private void updateAuthorSendability(T txn, AuthorId a, boolean increment) - throws DbException { - for(MessageId id : db.getMessagesByAuthor(txn, a)) { - int sendability = db.getSendability(txn, id); - if(increment) { - db.setSendability(txn, id, sendability + 1); - if(sendability == 0) - updateAncestorSendability(txn, id, true); - } else { - assert sendability > 0; - db.setSendability(txn, id, sendability - 1); - if(sendability == 1) - updateAncestorSendability(txn, id, false); - } - } - } - - public boolean setStarredFlag(MessageId m, boolean starred) - throws DbException { - messageLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsMessage(txn, m)) - throw new NoSuchMessageException(); - boolean wasStarred = db.setStarredFlag(txn, m, starred); - db.commitTransaction(txn); - return wasStarred; - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - messageLock.writeLock().unlock(); - } - } - - public void setVisibility(GroupId g, Collection<ContactId> visible) - throws DbException { - Collection<ContactId> affected = new ArrayList<ContactId>(); - contactLock.readLock().lock(); - try { - subscriptionLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsSubscription(txn, g)) - throw new NoSuchSubscriptionException(); - // Use HashSets for O(1) lookups, O(n) overall running time - HashSet<ContactId> now = new HashSet<ContactId>(visible); - Collection<ContactId> before = db.getVisibility(txn, g); - before = new HashSet<ContactId>(before); - // Set the group's visibility for each current contact - for(ContactId c : db.getContactIds(txn)) { - boolean wasBefore = before.contains(c); - boolean isNow = now.contains(c); - if(!wasBefore && isNow) { - db.addVisibility(txn, c, g); - affected.add(c); - } else if(wasBefore && !isNow) { - db.removeVisibility(txn, c, g); - affected.add(c); - } - } - // Make the group invisible to future contacts - db.setVisibleToAll(txn, g, false); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.writeLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - if(!affected.isEmpty()) - callListeners(new LocalSubscriptionsUpdatedEvent(affected)); - } - - public void setVisibleToAll(GroupId g, boolean visible) throws DbException { - Collection<ContactId> affected = new ArrayList<ContactId>(); - contactLock.readLock().lock(); - try { - subscriptionLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsSubscription(txn, g)) - throw new NoSuchSubscriptionException(); - // Make the group visible or invisible to future contacts - db.setVisibleToAll(txn, g, visible); - if(visible) { - // Make the group visible to all current contacts - Collection<ContactId> before = db.getVisibility(txn, g); - before = new HashSet<ContactId>(before); - for(ContactId c : db.getContactIds(txn)) { - if(!before.contains(c)) { - db.addVisibility(txn, c, g); - affected.add(c); - } - } - } - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.writeLock().unlock(); - } - } finally { - contactLock.readLock().unlock(); - } - if(!affected.isEmpty()) - callListeners(new LocalSubscriptionsUpdatedEvent(affected)); - } - - public boolean subscribe(Group g) throws DbException { - boolean added = false; - subscriptionLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if(!db.containsSubscription(txn, g.getId())) - added = db.addSubscription(txn, g); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.writeLock().unlock(); - } - if(added) callListeners(new SubscriptionAddedEvent(g)); - return added; - } - - public void unsubscribe(Group g) throws DbException { - Collection<ContactId> affected; - messageLock.writeLock().lock(); - try { - subscriptionLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - GroupId id = g.getId(); - if(!db.containsSubscription(txn, id)) - throw new NoSuchSubscriptionException(); - affected = db.getVisibility(txn, id); - db.removeSubscription(txn, id); - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - subscriptionLock.writeLock().unlock(); - } - } finally { - messageLock.writeLock().unlock(); - } - callListeners(new SubscriptionRemovedEvent(g)); - callListeners(new LocalSubscriptionsUpdatedEvent(affected)); - } - - public void checkFreeSpaceAndClean() throws DbException { - long freeSpace = db.getFreeSpace(); - if(LOG.isLoggable(INFO)) LOG.info(freeSpace + " bytes free space"); - while(freeSpace < MIN_FREE_SPACE) { - boolean expired = expireMessages(BYTES_PER_SWEEP); - if(freeSpace < CRITICAL_FREE_SPACE && !expired) { - // FIXME: Work out what to do here - throw new Error("Disk space is critically low"); - } - Thread.yield(); - freeSpace = db.getFreeSpace(); - } - } - - /** - * Removes the oldest messages from the database, with a total size less - * than or equal to the given size, and returns true if any messages were - * removed. - */ - private boolean expireMessages(int size) throws DbException { - Collection<MessageId> expired; - messageLock.writeLock().lock(); - try { - retentionLock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - expired = db.getOldMessages(txn, size); - if(!expired.isEmpty()) { - for(MessageId m : expired) removeMessage(txn, m); - db.incrementRetentionVersions(txn); - if(LOG.isLoggable(INFO)) - LOG.info("Expired " + expired.size() + " messages"); - } - db.commitTransaction(txn); - } catch(DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - retentionLock.writeLock().unlock(); - } - } finally { - messageLock.writeLock().unlock(); - } - if(expired.isEmpty()) return false; - callListeners(new MessageExpiredEvent()); - return true; - } - - /** - * Removes the given message (and all associated state) from the database. - * <p> - * Locking: message write. - */ - private void removeMessage(T txn, MessageId m) throws DbException { - int sendability = db.getSendability(txn, m); - // If the message is sendable, deleting it may affect its ancestors' - // sendability (backward inclusion) - if(sendability > 0) updateAncestorSendability(txn, m, false); - db.removeMessage(txn, m); - } - - public boolean shouldCheckFreeSpace() { - synchronized(spaceLock) { - long now = clock.currentTimeMillis(); - if(bytesStoredSinceLastCheck > MAX_BYTES_BETWEEN_SPACE_CHECKS - || now - timeOfLastCheck > MAX_MS_BETWEEN_SPACE_CHECKS) { - bytesStoredSinceLastCheck = 0; - timeOfLastCheck = now; - return true; - } - } - return false; - } + private boolean storeMessage(T txn, ContactId c, Message m) + throws DbException { + long now = clock.currentTimeMillis(); + if(m.getTimestamp() > now + MAX_CLOCK_DIFFERENCE) { + if(LOG.isLoggable(INFO)) + LOG.info("Discarding message with future timestamp"); + return false; + } + Group g = m.getGroup(); + if(g == null) return storePrivateMessage(txn, m, c, true); + if(!db.containsVisibleSubscription(txn, c, g.getId())) { + if(LOG.isLoggable(INFO)) + LOG.info("Discarding message without visible subscription"); + return false; + } + return storeGroupMessage(txn, m, c); + } + + public Request receiveOffer(ContactId c, Offer o) throws DbException { + Collection<MessageId> offered; + BitSet request; + contactLock.readLock().lock(); + try { + messageLock.writeLock().lock(); + try { + subscriptionLock.readLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + offered = o.getMessageIds(); + request = new BitSet(offered.size()); + Iterator<MessageId> it = offered.iterator(); + for(int i = 0; it.hasNext(); i++) { + // If the message is not in the database, or not + // visible to the contact, request it + MessageId m = it.next(); + if(!db.setStatusSeenIfVisible(txn, c, m)) + request.set(i); + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.readLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + return new Request(request, offered.size()); + } + + public void receiveRetentionAck(ContactId c, RetentionAck a) + throws DbException { + contactLock.readLock().lock(); + try { + retentionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + db.setRetentionUpdateAcked(txn, c, a.getVersion()); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + retentionLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + } + + public void receiveRetentionUpdate(ContactId c, RetentionUpdate u) + throws DbException { + boolean updated; + contactLock.readLock().lock(); + try { + retentionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + updated = db.setRetentionTime(txn, c, u.getRetentionTime(), + u.getVersion()); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + retentionLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + if(updated) callListeners(new RemoteRetentionTimeUpdatedEvent(c)); + } + + public void receiveSubscriptionAck(ContactId c, SubscriptionAck a) + throws DbException { + contactLock.readLock().lock(); + try { + subscriptionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + db.setSubscriptionUpdateAcked(txn, c, a.getVersion()); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + } + + public void receiveSubscriptionUpdate(ContactId c, SubscriptionUpdate u) + throws DbException { + boolean updated; + contactLock.readLock().lock(); + try { + subscriptionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + Collection<Group> groups = u.getGroups(); + long version = u.getVersion(); + updated = db.setSubscriptions(txn, c, groups, version); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + if(updated) callListeners(new RemoteSubscriptionsUpdatedEvent(c)); + } + + public void receiveTransportAck(ContactId c, TransportAck a) + throws DbException { + contactLock.readLock().lock(); + try { + transportLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + TransportId t = a.getId(); + if(!db.containsTransport(txn, t)) + throw new NoSuchTransportException(); + db.setTransportUpdateAcked(txn, c, t, a.getVersion()); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + transportLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + } + + public void receiveTransportUpdate(ContactId c, TransportUpdate u) + throws DbException { + boolean updated; + contactLock.readLock().lock(); + try { + transportLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + TransportId t = u.getId(); + TransportProperties p = u.getProperties(); + long version = u.getVersion(); + updated = db.setRemoteProperties(txn, c, t, p, version); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + transportLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + if(updated) + callListeners(new RemoteTransportsUpdatedEvent(c, u.getId())); + } + + public void removeContact(ContactId c) throws DbException { + contactLock.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.containsContact(txn, c)) + throw new NoSuchContactException(); + db.removeContact(txn, c); + 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 { + contactLock.writeLock().unlock(); + } + callListeners(new ContactRemovedEvent(c)); + } + + public void removeTransport(TransportId t) throws DbException { + transportLock.writeLock().lock(); + try { + windowLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsTransport(txn, t)) + throw new NoSuchTransportException(); + db.removeTransport(txn, t); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + windowLock.writeLock().unlock(); + } + } finally { + transportLock.writeLock().unlock(); + } + callListeners(new TransportRemovedEvent(t)); + } + + public void setConnectionWindow(ContactId c, TransportId t, long period, + long centre, byte[] bitmap) throws DbException { + contactLock.readLock().lock(); + try { + transportLock.readLock().lock(); + try { + windowLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + if(!db.containsTransport(txn, t)) + throw new NoSuchTransportException(); + db.setConnectionWindow(txn, c, t, period, centre, + bitmap); + db.setLastConnected(txn, c, clock.currentTimeMillis()); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + windowLock.writeLock().unlock(); + } + } finally { + transportLock.readLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + } + + public void setRating(AuthorId a, Rating r) throws DbException { + boolean changed; + messageLock.writeLock().lock(); + try { + ratingLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + Rating old = db.setRating(txn, a, r); + changed = (old != r); + // Update the sendability of the author's messages + if(r == GOOD && old != GOOD) + updateAuthorSendability(txn, a, true); + else if(r != GOOD && old == GOOD) + updateAuthorSendability(txn, a, false); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + ratingLock.writeLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + if(changed) callListeners(new RatingChangedEvent(a, r)); + } + + public boolean setReadFlag(MessageId m, boolean read) throws DbException { + messageLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsMessage(txn, m)) + throw new NoSuchMessageException(); + boolean wasRead = db.setReadFlag(txn, m, read); + db.commitTransaction(txn); + return wasRead; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + messageLock.writeLock().unlock(); + } + } + + public void setRemoteProperties(ContactId c, + Map<TransportId, TransportProperties> p) throws DbException { + contactLock.readLock().lock(); + try { + transportLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + db.setRemoteProperties(txn, c, p); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + transportLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + } + + public void setSeen(ContactId c, Collection<MessageId> seen) + throws DbException { + contactLock.readLock().lock(); + try { + messageLock.writeLock().lock(); + try { + subscriptionLock.readLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsContact(txn, c)) + throw new NoSuchContactException(); + for(MessageId m : seen) + db.setStatusSeenIfVisible(txn, c, m); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.readLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + } + + /** + * Updates the sendability of all messages written by the given author, and + * the ancestors of those messages if necessary. + * <p> + * Locking: message write. + * @param increment true if the user's rating for the author has changed + * from not good to good, or false if it has changed from good to not good. + */ + private void updateAuthorSendability(T txn, AuthorId a, boolean increment) + throws DbException { + for(MessageId id : db.getMessagesByAuthor(txn, a)) { + int sendability = db.getSendability(txn, id); + if(increment) { + db.setSendability(txn, id, sendability + 1); + if(sendability == 0) + updateAncestorSendability(txn, id, true); + } else { + assert sendability > 0; + db.setSendability(txn, id, sendability - 1); + if(sendability == 1) + updateAncestorSendability(txn, id, false); + } + } + } + + public boolean setStarredFlag(MessageId m, boolean starred) + throws DbException { + messageLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsMessage(txn, m)) + throw new NoSuchMessageException(); + boolean wasStarred = db.setStarredFlag(txn, m, starred); + db.commitTransaction(txn); + return wasStarred; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + messageLock.writeLock().unlock(); + } + } + + public void setVisibility(GroupId g, Collection<ContactId> visible) + throws DbException { + Collection<ContactId> affected = new ArrayList<ContactId>(); + contactLock.readLock().lock(); + try { + subscriptionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsSubscription(txn, g)) + throw new NoSuchSubscriptionException(); + // Use HashSets for O(1) lookups, O(n) overall running time + HashSet<ContactId> now = new HashSet<ContactId>(visible); + Collection<ContactId> before = db.getVisibility(txn, g); + before = new HashSet<ContactId>(before); + // Set the group's visibility for each current contact + for(ContactId c : db.getContactIds(txn)) { + boolean wasBefore = before.contains(c); + boolean isNow = now.contains(c); + if(!wasBefore && isNow) { + db.addVisibility(txn, c, g); + affected.add(c); + } else if(wasBefore && !isNow) { + db.removeVisibility(txn, c, g); + affected.add(c); + } + } + // Make the group invisible to future contacts + db.setVisibleToAll(txn, g, false); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + if(!affected.isEmpty()) + callListeners(new LocalSubscriptionsUpdatedEvent(affected)); + } + + public void setVisibleToAll(GroupId g, boolean visible) throws DbException { + Collection<ContactId> affected = new ArrayList<ContactId>(); + contactLock.readLock().lock(); + try { + subscriptionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsSubscription(txn, g)) + throw new NoSuchSubscriptionException(); + // Make the group visible or invisible to future contacts + db.setVisibleToAll(txn, g, visible); + if(visible) { + // Make the group visible to all current contacts + Collection<ContactId> before = db.getVisibility(txn, g); + before = new HashSet<ContactId>(before); + for(ContactId c : db.getContactIds(txn)) { + if(!before.contains(c)) { + db.addVisibility(txn, c, g); + affected.add(c); + } + } + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + } finally { + contactLock.readLock().unlock(); + } + if(!affected.isEmpty()) + callListeners(new LocalSubscriptionsUpdatedEvent(affected)); + } + + public boolean subscribe(Group g) throws DbException { + boolean added = false; + subscriptionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if(!db.containsSubscription(txn, g.getId())) + added = db.addSubscription(txn, g); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + if(added) callListeners(new SubscriptionAddedEvent(g)); + return added; + } + + public void unsubscribe(Group g) throws DbException { + Collection<ContactId> affected; + messageLock.writeLock().lock(); + try { + subscriptionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + GroupId id = g.getId(); + if(!db.containsSubscription(txn, id)) + throw new NoSuchSubscriptionException(); + affected = db.getVisibility(txn, id); + db.removeSubscription(txn, id); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + subscriptionLock.writeLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + callListeners(new SubscriptionRemovedEvent(g)); + callListeners(new LocalSubscriptionsUpdatedEvent(affected)); + } + + public void checkFreeSpaceAndClean() throws DbException { + long freeSpace = db.getFreeSpace(); + if(LOG.isLoggable(INFO)) LOG.info(freeSpace + " bytes free space"); + while(freeSpace < MIN_FREE_SPACE) { + boolean expired = expireMessages(BYTES_PER_SWEEP); + if(freeSpace < CRITICAL_FREE_SPACE && !expired) { + // FIXME: Work out what to do here + throw new Error("Disk space is critically low"); + } + Thread.yield(); + freeSpace = db.getFreeSpace(); + } + } + + /** + * Removes the oldest messages from the database, with a total size less + * than or equal to the given size, and returns true if any messages were + * removed. + */ + private boolean expireMessages(int size) throws DbException { + Collection<MessageId> expired; + messageLock.writeLock().lock(); + try { + retentionLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + expired = db.getOldMessages(txn, size); + if(!expired.isEmpty()) { + for(MessageId m : expired) removeMessage(txn, m); + db.incrementRetentionVersions(txn); + if(LOG.isLoggable(INFO)) + LOG.info("Expired " + expired.size() + " messages"); + } + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + retentionLock.writeLock().unlock(); + } + } finally { + messageLock.writeLock().unlock(); + } + if(expired.isEmpty()) return false; + callListeners(new MessageExpiredEvent()); + return true; + } + + /** + * Removes the given message (and all associated state) from the database. + * <p> + * Locking: message write. + */ + private void removeMessage(T txn, MessageId m) throws DbException { + int sendability = db.getSendability(txn, m); + // If the message is sendable, deleting it may affect its ancestors' + // sendability (backward inclusion) + if(sendability > 0) updateAncestorSendability(txn, m, false); + db.removeMessage(txn, m); + } + + public boolean shouldCheckFreeSpace() { + synchronized(spaceLock) { + long now = clock.currentTimeMillis(); + if(bytesStoredSinceLastCheck > MAX_BYTES_BETWEEN_SPACE_CHECKS + || now - timeOfLastCheck > MAX_MS_BETWEEN_SPACE_CHECKS) { + bytesStoredSinceLastCheck = 0; + timeOfLastCheck = now; + return true; + } + } + return false; + } } diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java index 797ea9a56b6a759a88f337241cae256075d6bf57..1578e7d3887583cf823ad6c585691c186858c0f3 100644 --- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java +++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java @@ -161,6 +161,7 @@ abstract class JdbcDatabase implements Database<Connection> { + " bodyStart INT NOT NULL," + " bodyLength INT NOT NULL," + " raw BLOB NOT NULL," + + " incoming BOOLEAN NOT NULL," + " sendability INT UNSIGNED," // Null for private messages + " contactId INT UNSIGNED," // Null for group messages + " read BOOLEAN NOT NULL," @@ -685,7 +686,7 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public boolean addGroupMessage(Connection txn, Message m) + public boolean addGroupMessage(Connection txn, Message m, boolean incoming) throws DbException { if(m.getGroup() == null) throw new IllegalArgumentException(); if(containsMessage(txn, m.getId())) return false; @@ -694,9 +695,9 @@ abstract class JdbcDatabase implements Database<Connection> { String sql = "INSERT INTO messages (messageId, parentId, groupId," + " authorId, authorName, authorKey, contentType, subject," + " timestamp, length, bodyStart, bodyLength, raw," - + " sendability, read, starred)" - + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ZERO()," - + " FALSE, FALSE)"; + + " incoming, sendability, read, starred)" + + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," + + " ZERO(), FALSE, FALSE)"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getId().getBytes()); if(m.getParent() == null) ps.setNull(2, BINARY); @@ -720,6 +721,7 @@ abstract class JdbcDatabase implements Database<Connection> { ps.setInt(11, m.getBodyStart()); ps.setInt(12, m.getBodyLength()); ps.setBytes(13, raw); + ps.setBoolean(14, incoming); int affected = ps.executeUpdate(); if(affected != 1) throw new DbStateException(); ps.close(); @@ -803,16 +805,17 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public boolean addPrivateMessage(Connection txn, Message m, ContactId c) - throws DbException { + public boolean addPrivateMessage(Connection txn, Message m, ContactId c, + boolean incoming) throws DbException { if(m.getGroup() != null) throw new IllegalArgumentException(); + if(m.getAuthor() != null) throw new IllegalArgumentException(); if(containsMessage(txn, m.getId())) return false; PreparedStatement ps = null; try { String sql = "INSERT INTO messages (messageId, parentId," + " contentType, subject, timestamp, length, bodyStart," - + " bodyLength, raw, contactId, read, starred)" - + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE, FALSE)"; + + " bodyLength, raw, incoming, contactId, read, starred)" + + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE, FALSE)"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getId().getBytes()); if(m.getParent() == null) ps.setNull(2, BINARY); @@ -825,7 +828,8 @@ abstract class JdbcDatabase implements Database<Connection> { ps.setInt(7, m.getBodyStart()); ps.setInt(8, m.getBodyLength()); ps.setBytes(9, raw); - ps.setInt(10, c.getInt()); + ps.setBoolean(10, incoming); + ps.setInt(11, c.getInt()); int affected = ps.executeUpdate(); if(affected != 1) throw new DbStateException(); ps.close(); @@ -1333,6 +1337,60 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public Collection<GroupMessageHeader> getGroupMessageHeaders(Connection txn, + GroupId g) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT messageId, parentId, m.authorId, authorName," + + " authorKey, rating, contentType, subject, timestamp," + + " read, starred" + + " FROM messages AS m" + + " LEFT OUTER JOIN ratings AS r" + + " ON m.authorId = r.authorId" + + " WHERE groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + rs = ps.executeQuery(); + List<GroupMessageHeader> headers = + new ArrayList<GroupMessageHeader>(); + while(rs.next()) { + MessageId id = new MessageId(rs.getBytes(1)); + byte[] b = rs.getBytes(2); + MessageId parent = b == null ? null : new MessageId(b); + Author author; + Rating rating; + b = rs.getBytes(3); + if(b == null) { + author = null; + rating = UNRATED; + } else { + AuthorId authorId = new AuthorId(b); + String authorName = rs.getString(4); + byte[] authorKey = rs.getBytes(5); + author = new Author(authorId, authorName, authorKey); + // NULL == 0 == UNRATED + rating = Rating.values()[rs.getByte(6)]; + } + String contentType = rs.getString(7); + String subject = rs.getString(8); + long timestamp = rs.getLong(9); + boolean read = rs.getBoolean(10); + boolean starred = rs.getBoolean(11); + headers.add(new GroupMessageHeader(id, parent, author, + contentType, subject, timestamp, read, starred, rating, + g)); + } + rs.close(); + ps.close(); + return Collections.unmodifiableList(headers); + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public MessageId getGroupMessageParent(Connection txn, MessageId m) throws DbException { PreparedStatement ps = null; @@ -1546,59 +1604,6 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public Collection<GroupMessageHeader> getMessageHeaders(Connection txn, - GroupId g) throws DbException { - PreparedStatement ps = null; - ResultSet rs = null; - try { - String sql = "SELECT messageId, parentId, m.authorId, authorName," - + " authorKey, rating, contentType, subject, timestamp," - + " read, starred" - + " FROM messages AS m" - + " LEFT OUTER JOIN ratings AS r" - + " ON m.authorId = r.authorId" - + " WHERE groupId = ?"; - ps = txn.prepareStatement(sql); - ps.setBytes(1, g.getBytes()); - rs = ps.executeQuery(); - List<GroupMessageHeader> headers = - new ArrayList<GroupMessageHeader>(); - while(rs.next()) { - MessageId id = new MessageId(rs.getBytes(1)); - byte[] b = rs.getBytes(2); - MessageId parent = b == null ? null : new MessageId(b); - Author author; - Rating rating; - b = rs.getBytes(3); - if(b == null) { - author = null; - rating = UNRATED; - } else { - AuthorId authorId = new AuthorId(b); - String authorName = rs.getString(4); - byte[] authorKey = rs.getBytes(5); - author = new Author(authorId, authorName, authorKey); - // NULL == 0 == UNRATED - rating = Rating.values()[rs.getByte(6)]; - } - String contentType = rs.getString(7); - String subject = rs.getString(8); - long timestamp = rs.getLong(9); - boolean read = rs.getBoolean(10); - boolean starred = rs.getBoolean(11); - headers.add(new GroupMessageHeader(id, parent, contentType, - subject, timestamp, read, starred, g, author, rating)); - } - rs.close(); - ps.close(); - return Collections.unmodifiableList(headers); - } catch(SQLException e) { - tryToClose(rs); - tryToClose(ps); - throw new DbException(e); - } - } - public Collection<MessageId> getMessagesByAuthor(Connection txn, AuthorId a) throws DbException { PreparedStatement ps = null; @@ -1767,13 +1772,18 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { + // Get the incoming message headers String sql = "SELECT m.messageId, parentId, contentType, subject," - + " timestamp, read, starred, seen" + + " timestamp, read, starred, c.authorId, name, publicKey," + + " rating" + " FROM messages AS m" - + " JOIN statuses AS s" - + " ON m.messageId = s.messageId" - + " AND m.contactId = s.contactId" - + " WHERE m.contactId = ? AND groupId IS NULL"; + + " JOIN contacts AS c" + + " ON m.contactId = c.contactId" + + " LEFT OUTER JOIN ratings AS r" + + " ON c.authorId = r.authorId" + + " WHERE m.contactId = ?" + + " AND groupId IS NULL" + + " AND incoming = TRUE"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); rs = ps.executeQuery(); @@ -1788,9 +1798,53 @@ abstract class JdbcDatabase implements Database<Connection> { long timestamp = rs.getLong(5); boolean read = rs.getBoolean(6); boolean starred = rs.getBoolean(7); - boolean seen = rs.getBoolean(8); - headers.add(new PrivateMessageHeader(id, parent, contentType, - subject, timestamp, read, starred, c, seen)); + AuthorId authorId = new AuthorId(rs.getBytes(8)); + String authorName = rs.getString(9); + byte[] authorKey = rs.getBytes(10); + Author author = new Author(authorId, authorName, authorKey); + // NULL == 0 == UNRATED + Rating rating = Rating.values()[rs.getByte(11)]; + headers.add(new PrivateMessageHeader(id, parent, author, + contentType, subject, timestamp, read, starred, rating, + c, true)); + } + rs.close(); + ps.close(); + // Get the outgoing message headers + sql = "SELECT m.messageId, parentId, contentType, subject," + + " timestamp, read, starred, a.authorId, a.name," + + " a.publicKey, rating" + + " FROM messages AS m" + + " JOIN contacts AS c" + + " ON m.contactId = c.contactId" + + " JOIN localAuthors AS a" + + " ON c.localAuthorId = a.authorId" + + " LEFT OUTER JOIN ratings AS r" + + " ON c.localAuthorId = r.authorId" + + " WHERE m.contactId = ?" + + " AND groupId IS NULL" + + " AND incoming = FALSE"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + rs = ps.executeQuery(); + while(rs.next()) { + MessageId id = new MessageId(rs.getBytes(1)); + byte[] b = rs.getBytes(2); + MessageId parent = b == null ? null : new MessageId(b); + String contentType = rs.getString(3); + String subject = rs.getString(4); + long timestamp = rs.getLong(5); + boolean read = rs.getBoolean(6); + boolean starred = rs.getBoolean(7); + AuthorId authorId = new AuthorId(rs.getBytes(8)); + String authorName = rs.getString(9); + byte[] authorKey = rs.getBytes(10); + Author author = new Author(authorId, authorName, authorKey); + // NULL == 0 == UNRATED + Rating rating = Rating.values()[rs.getByte(11)]; + headers.add(new PrivateMessageHeader(id, parent, author, + contentType, subject, timestamp, read, starred, rating, + c, false)); } rs.close(); ps.close(); diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java index 84d3a10d6f5eaa125a57f404cf2a68366f4fc4e6..fbb0fdda958dabf8f584f491f6b23a9ec1b272fa 100644 --- a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java +++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java @@ -172,7 +172,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { // getMessageHeaders(groupId) oneOf(database).containsSubscription(txn, groupId); will(returnValue(true)); - oneOf(database).getMessageHeaders(txn, groupId); + oneOf(database).getGroupMessageHeaders(txn, groupId); will(returnValue(Collections.emptyList())); // getSubscriptions() oneOf(database).getSubscriptions(txn); @@ -212,7 +212,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase { db.getRemoteProperties(transportId)); db.subscribe(group); // First time - listeners called db.subscribe(group); // Second time - not called - assertEquals(Collections.emptyList(), db.getMessageHeaders(groupId)); + assertEquals(Collections.emptyList(), + db.getGroupMessageHeaders(groupId)); assertEquals(Arrays.asList(groupId), db.getSubscriptions()); db.unsubscribe(group); db.removeContact(contactId); @@ -367,7 +368,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { will(returnValue(txn)); oneOf(database).containsSubscription(txn, groupId); will(returnValue(true)); - oneOf(database).addGroupMessage(txn, message); + oneOf(database).addGroupMessage(txn, message, false); will(returnValue(false)); oneOf(database).commitTransaction(txn); }}); @@ -392,7 +393,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { will(returnValue(txn)); oneOf(database).containsSubscription(txn, groupId); will(returnValue(true)); - oneOf(database).addGroupMessage(txn, message); + oneOf(database).addGroupMessage(txn, message, false); will(returnValue(true)); oneOf(database).setReadFlag(txn, messageId, true); oneOf(database).getContactIds(txn); @@ -428,7 +429,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { will(returnValue(txn)); oneOf(database).containsSubscription(txn, groupId); will(returnValue(true)); - oneOf(database).addGroupMessage(txn, message); + oneOf(database).addGroupMessage(txn, message, false); will(returnValue(true)); oneOf(database).setReadFlag(txn, messageId, true); oneOf(database).getContactIds(txn); @@ -467,7 +468,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase { oneOf(database).containsContact(txn, contactId); will(returnValue(true)); // addLocalPrivateMessage(privateMessage, contactId) - oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + oneOf(database).addPrivateMessage(txn, privateMessage, contactId, + false); will(returnValue(false)); }}); DatabaseComponent db = createDatabaseComponent(database, cleaner, @@ -492,7 +494,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase { oneOf(database).containsContact(txn, contactId); will(returnValue(true)); // addLocalPrivateMessage(privateMessage, contactId) - oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + oneOf(database).addPrivateMessage(txn, privateMessage, contactId, + false); will(returnValue(true)); oneOf(database).setReadFlag(txn, messageId, true); oneOf(database).addStatus(txn, contactId, messageId, false); @@ -702,7 +705,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { } catch(NoSuchSubscriptionException expected) {} try { - db.getMessageHeaders(groupId); + db.getGroupMessageHeaders(groupId); fail(); } catch(NoSuchSubscriptionException expected) {} @@ -1148,7 +1151,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase { oneOf(database).containsContact(txn, contactId); will(returnValue(true)); // The message is stored - oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + oneOf(database).addPrivateMessage(txn, privateMessage, contactId, + true); will(returnValue(true)); oneOf(database).addStatus(txn, contactId, messageId, true); // The message must be acked @@ -1175,8 +1179,9 @@ public abstract class DatabaseComponentTest extends BriarTestCase { oneOf(database).commitTransaction(txn); oneOf(database).containsContact(txn, contactId); will(returnValue(true)); - // The message is stored, but it's a duplicate - oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + // The message is not stored, it's a duplicate + oneOf(database).addPrivateMessage(txn, privateMessage, contactId, + true); will(returnValue(false)); // The message must still be acked oneOf(database).addMessageToAck(txn, contactId, messageId); @@ -1236,8 +1241,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase { oneOf(database).containsVisibleSubscription(txn, contactId, groupId); will(returnValue(true)); - // The message is stored, but it's a duplicate - oneOf(database).addGroupMessage(txn, message); + // The message is not stored, it's a duplicate + oneOf(database).addGroupMessage(txn, message, true); will(returnValue(false)); oneOf(database).addStatus(txn, contactId, messageId, true); // The message must be acked @@ -1269,7 +1274,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { groupId); will(returnValue(true)); // The message is stored, and it's not a duplicate - oneOf(database).addGroupMessage(txn, message); + oneOf(database).addGroupMessage(txn, message, true); will(returnValue(true)); oneOf(database).addStatus(txn, contactId, messageId, true); // Set the status to seen = true for all other contacts (none) @@ -1311,7 +1316,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { groupId); will(returnValue(true)); // The message is stored, and it's not a duplicate - oneOf(database).addGroupMessage(txn, message); + oneOf(database).addGroupMessage(txn, message, true); will(returnValue(true)); oneOf(database).addStatus(txn, contactId, messageId, true); // Set the status to seen = true for all other contacts (none) @@ -1513,7 +1518,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { will(returnValue(txn)); oneOf(database).containsSubscription(txn, groupId); will(returnValue(true)); - oneOf(database).addGroupMessage(txn, message); + oneOf(database).addGroupMessage(txn, message, false); will(returnValue(true)); oneOf(database).setReadFlag(txn, messageId, true); oneOf(database).getContactIds(txn); @@ -1553,7 +1558,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase { oneOf(database).containsContact(txn, contactId); will(returnValue(true)); // addLocalPrivateMessage(privateMessage, contactId) - oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + oneOf(database).addPrivateMessage(txn, privateMessage, contactId, + false); will(returnValue(true)); oneOf(database).setReadFlag(txn, messageId, true); oneOf(database).addStatus(txn, contactId, messageId, false); @@ -1585,7 +1591,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase { will(returnValue(txn)); oneOf(database).containsSubscription(txn, groupId); will(returnValue(true)); - oneOf(database).addGroupMessage(txn, message); + oneOf(database).addGroupMessage(txn, message, false); will(returnValue(false)); oneOf(database).commitTransaction(txn); // The message was not added, so the listener should not be called @@ -1615,7 +1621,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase { oneOf(database).containsContact(txn, contactId); will(returnValue(true)); // addLocalPrivateMessage(privateMessage, contactId) - oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + oneOf(database).addPrivateMessage(txn, privateMessage, contactId, + false); will(returnValue(false)); // The message was not added, so the listener should not be called }}); diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java index aa0f778a3ddf9b14f9d982cfb8d4f23105a6eb74..b2c8e45be3cd07e25887016b4bf9dfaa5dbd595a 100644 --- a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java +++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java @@ -110,10 +110,10 @@ public class H2DatabaseTest extends BriarTestCase { db.addSubscription(txn, group); assertTrue(db.containsSubscription(txn, groupId)); assertFalse(db.containsMessage(txn, messageId)); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, true); assertTrue(db.containsMessage(txn, messageId)); assertFalse(db.containsMessage(txn, messageId1)); - db.addPrivateMessage(txn, privateMessage, contactId); + db.addPrivateMessage(txn, privateMessage, contactId, true); assertTrue(db.containsMessage(txn, messageId1)); db.commitTransaction(txn); db.close(); @@ -173,7 +173,7 @@ public class H2DatabaseTest extends BriarTestCase { // Subscribe to a group and store a message db.addSubscription(txn, group); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); // Unsubscribing from the group should remove the message assertTrue(db.containsMessage(txn, messageId)); @@ -192,7 +192,7 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact and store a private message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addPrivateMessage(txn, privateMessage, contactId); + db.addPrivateMessage(txn, privateMessage, contactId, false); // Removing the contact should remove the message assertTrue(db.containsMessage(txn, messageId1)); @@ -212,7 +212,7 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact and store a private message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addPrivateMessage(txn, privateMessage, contactId); + db.addPrivateMessage(txn, privateMessage, contactId, false); // The message has no status yet, so it should not be sendable assertFalse(db.hasSendableMessages(txn, contactId)); @@ -241,7 +241,7 @@ public class H2DatabaseTest extends BriarTestCase { // Add a contact and store a private message db.addLocalAuthor(txn, localAuthor); assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addPrivateMessage(txn, privateMessage, contactId); + db.addPrivateMessage(txn, privateMessage, contactId, false); db.addStatus(txn, contactId, messageId1, false); // The message is sendable, but too large to send @@ -273,7 +273,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addSubscription(txn, group); db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); db.addStatus(txn, contactId, messageId, false); // The message should not be sendable @@ -312,7 +312,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addSubscription(txn, group); db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); db.setSendability(txn, messageId, 1); // The message has no status yet, so it should not be sendable @@ -349,7 +349,7 @@ public class H2DatabaseTest extends BriarTestCase { assertEquals(contactId, db.addContact(txn, author, localAuthorId)); db.addSubscription(txn, group); db.addVisibility(txn, contactId, groupId); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); db.setSendability(txn, messageId, 1); db.addStatus(txn, contactId, messageId, false); @@ -388,7 +388,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addSubscription(txn, group); db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); db.setSendability(txn, messageId, 1); db.addStatus(txn, contactId, messageId, false); @@ -419,7 +419,7 @@ public class H2DatabaseTest extends BriarTestCase { assertEquals(contactId, db.addContact(txn, author, localAuthorId)); db.addSubscription(txn, group); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); db.setSendability(txn, messageId, 1); db.addStatus(txn, contactId, messageId, false); @@ -505,7 +505,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addSubscription(txn, group); db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); db.setSendability(txn, messageId, 1); db.addStatus(txn, contactId, messageId, false); @@ -545,8 +545,8 @@ public class H2DatabaseTest extends BriarTestCase { // Subscribe to a group and store two messages db.addSubscription(txn, group); - db.addGroupMessage(txn, message); - db.addGroupMessage(txn, message1); + db.addGroupMessage(txn, message, false); + db.addGroupMessage(txn, message1, false); // Check that each message is retrievable via its author Iterator<MessageId> it = @@ -583,10 +583,10 @@ public class H2DatabaseTest extends BriarTestCase { // Subscribe to the groups and store the messages db.addSubscription(txn, group); db.addSubscription(txn, group1); - db.addGroupMessage(txn, message); - db.addGroupMessage(txn, child1); - db.addGroupMessage(txn, child2); - db.addGroupMessage(txn, child3); + db.addGroupMessage(txn, message, false); + db.addGroupMessage(txn, child1, false); + db.addGroupMessage(txn, child2, false); + db.addGroupMessage(txn, child3, false); // Make all the children sendable db.setSendability(txn, childId1, 1); db.setSendability(txn, childId2, 5); @@ -613,8 +613,8 @@ public class H2DatabaseTest extends BriarTestCase { // Subscribe to a group and store two messages db.addSubscription(txn, group); - db.addGroupMessage(txn, message); - db.addGroupMessage(txn, message1); + db.addGroupMessage(txn, message, false); + db.addGroupMessage(txn, message1, false); // Allowing enough capacity for one message should return the older one Iterator<MessageId> it = db.getOldMessages(txn, size).iterator(); @@ -653,7 +653,7 @@ public class H2DatabaseTest extends BriarTestCase { // Storing a message should reduce the free space Connection txn = db.startTransaction(); db.addSubscription(txn, group); - db.addGroupMessage(txn, message1); + db.addGroupMessage(txn, message1, false); db.commitTransaction(txn); assertTrue(db.getFreeSpace() < free); @@ -894,7 +894,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addSubscription(txn, group); db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); // Set the sendability to > 0 and the status to seen = true db.setSendability(txn, messageId, 1); @@ -919,7 +919,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addSubscription(txn, group); db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); // Set the sendability to 0 and the status to seen = false db.setSendability(txn, messageId, 0); @@ -945,7 +945,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); db.setRetentionTime(txn, contactId, timestamp + 1, 1); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); // Set the sendability to > 0 and the status to seen = false db.setSendability(txn, messageId, 1); @@ -969,7 +969,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addSubscription(txn, group); db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); // Set the sendability to > 0 and the status to seen = false db.setSendability(txn, messageId, 1); @@ -1032,7 +1032,7 @@ public class H2DatabaseTest extends BriarTestCase { assertEquals(contactId, db.addContact(txn, author, localAuthorId)); db.addSubscription(txn, group); db.addVisibility(txn, contactId, groupId); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); db.addStatus(txn, contactId, messageId, false); // There's no contact subscription for the group @@ -1053,7 +1053,7 @@ public class H2DatabaseTest extends BriarTestCase { assertEquals(contactId, db.addContact(txn, author, localAuthorId)); db.addSubscription(txn, group); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); db.addStatus(txn, contactId, messageId, false); // The subscription is not visible @@ -1075,7 +1075,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addSubscription(txn, group); db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); // The message has already been seen by the contact db.addStatus(txn, contactId, messageId, true); @@ -1098,7 +1098,7 @@ public class H2DatabaseTest extends BriarTestCase { db.addSubscription(txn, group); db.addVisibility(txn, contactId, groupId); db.setSubscriptions(txn, contactId, Arrays.asList(group), 1); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); // The message has not been seen by the contact db.addStatus(txn, contactId, messageId, false); @@ -1146,7 +1146,7 @@ public class H2DatabaseTest extends BriarTestCase { MessageId childId = new MessageId(TestUtils.getRandomId()); Message child = new TestMessage(childId, null, group, null, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, child); + db.addGroupMessage(txn, child, false); assertTrue(db.containsMessage(txn, childId)); assertNull(db.getGroupMessageParent(txn, childId)); @@ -1167,7 +1167,7 @@ public class H2DatabaseTest extends BriarTestCase { MessageId parentId = new MessageId(TestUtils.getRandomId()); Message child = new TestMessage(childId, parentId, group, null, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, child); + db.addGroupMessage(txn, child, false); assertTrue(db.containsMessage(txn, childId)); assertFalse(db.containsMessage(txn, parentId)); assertNull(db.getGroupMessageParent(txn, childId)); @@ -1195,8 +1195,8 @@ public class H2DatabaseTest extends BriarTestCase { contentType, subject, timestamp, raw); Message parent = new TestMessage(parentId, null, group1, null, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, child); - db.addGroupMessage(txn, parent); + db.addGroupMessage(txn, child, false); + db.addGroupMessage(txn, parent, false); assertTrue(db.containsMessage(txn, childId)); assertTrue(db.containsMessage(txn, parentId)); assertNull(db.getGroupMessageParent(txn, childId)); @@ -1219,8 +1219,8 @@ public class H2DatabaseTest extends BriarTestCase { MessageId childId = new MessageId(TestUtils.getRandomId()); Message child = new TestMessage(childId, messageId1, group, null, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, child); - db.addPrivateMessage(txn, privateMessage, contactId); + db.addGroupMessage(txn, child, false); + db.addPrivateMessage(txn, privateMessage, contactId, false); assertTrue(db.containsMessage(txn, childId)); assertTrue(db.containsMessage(txn, messageId1)); assertNull(db.getGroupMessageParent(txn, childId)); @@ -1245,8 +1245,8 @@ public class H2DatabaseTest extends BriarTestCase { contentType, subject, timestamp, raw); Message parent = new TestMessage(parentId, null, group, null, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, child); - db.addGroupMessage(txn, parent); + db.addGroupMessage(txn, child, false); + db.addGroupMessage(txn, parent, false); assertTrue(db.containsMessage(txn, childId)); assertTrue(db.containsMessage(txn, parentId)); assertEquals(parentId, db.getGroupMessageParent(txn, childId)); @@ -1271,8 +1271,8 @@ public class H2DatabaseTest extends BriarTestCase { contentType, subject, timestamp, raw, 5, bodyLength); Message privateMessage1 = new TestMessage(messageId1, null, null, null, contentType, subject, timestamp, raw, 10, bodyLength); - db.addGroupMessage(txn, message1); - db.addPrivateMessage(txn, privateMessage1, contactId); + db.addGroupMessage(txn, message1, false); + db.addPrivateMessage(txn, privateMessage1, contactId, false); // Calculate the expected message bodies byte[] expectedBody = new byte[bodyLength]; @@ -1305,19 +1305,19 @@ public class H2DatabaseTest extends BriarTestCase { db.addSubscription(txn, group); // Store a couple of messages - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); MessageId messageId1 = new MessageId(TestUtils.getRandomId()); MessageId parentId = new MessageId(TestUtils.getRandomId()); long timestamp1 = System.currentTimeMillis(); Message message1 = new TestMessage(messageId1, parentId, group, author, contentType, subject, timestamp1, raw); - db.addGroupMessage(txn, message1); + db.addGroupMessage(txn, message1, false); // Mark one of the messages read assertFalse(db.setReadFlag(txn, messageId, true)); // Retrieve the message headers Collection<GroupMessageHeader> headers = - db.getMessageHeaders(txn, groupId); + db.getGroupMessageHeaders(txn, groupId); Iterator<GroupMessageHeader> it = headers.iterator(); boolean messageFound = false, message1Found = false; // First header (order is undefined) @@ -1380,7 +1380,7 @@ public class H2DatabaseTest extends BriarTestCase { // Subscribe to a group and store a message db.addSubscription(txn, group); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); // The message should be unread by default assertFalse(db.getReadFlag(txn, messageId)); @@ -1406,7 +1406,7 @@ public class H2DatabaseTest extends BriarTestCase { // Subscribe to a group and store a message db.addSubscription(txn, group); - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); // The message should be unstarred by default assertFalse(db.getStarredFlag(txn, messageId)); @@ -1437,17 +1437,17 @@ public class H2DatabaseTest extends BriarTestCase { db.addSubscription(txn, group1); // Store two messages in the first group - db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message, false); MessageId messageId1 = new MessageId(TestUtils.getRandomId()); Message message1 = new TestMessage(messageId1, null, group, author, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, message1); + db.addGroupMessage(txn, message1, false); // Store one message in the second group MessageId messageId2 = new MessageId(TestUtils.getRandomId()); Message message2 = new TestMessage(messageId2, null, group1, author, contentType, subject, timestamp, raw); - db.addGroupMessage(txn, message2); + db.addGroupMessage(txn, message2, false); // Mark one of the messages in the first group read assertFalse(db.setReadFlag(txn, messageId, true));