diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java index 7c6e77d0abfee7f80730f66c9e2caf0324392426..5c9ec6fa53d543770835ec4acffe2abd62eae3bf 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java @@ -8,6 +8,7 @@ import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.PendingContact; import org.briarproject.bramble.api.contact.PendingContactId; import org.briarproject.bramble.api.contact.PendingContactState; +import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent; import org.briarproject.bramble.api.crypto.KeyPair; import org.briarproject.bramble.api.crypto.PublicKey; import org.briarproject.bramble.api.crypto.SecretKey; @@ -15,24 +16,34 @@ import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorInfo; import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionClosedEvent; +import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionOpenedEvent; +import org.briarproject.bramble.api.rendezvous.event.RendezvousFailedEvent; import org.briarproject.bramble.api.transport.KeyManager; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import javax.inject.Inject; +import static org.briarproject.bramble.api.contact.PendingContactState.ADDING_CONTACT; +import static org.briarproject.bramble.api.contact.PendingContactState.FAILED; import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES; @@ -43,24 +54,29 @@ import static org.briarproject.bramble.util.StringUtils.toUtf8; @ThreadSafe @NotNullByDefault -class ContactManagerImpl implements ContactManager { +class ContactManagerImpl implements ContactManager, EventListener { private final DatabaseComponent db; private final KeyManager keyManager; private final IdentityManager identityManager; private final PendingContactFactory pendingContactFactory; + private final EventBus eventBus; - private final List<ContactHook> hooks; + private final List<ContactHook> hooks = new CopyOnWriteArrayList<>(); + private final ConcurrentMap<PendingContactId, PendingContactState> states = + new ConcurrentHashMap<>(); @Inject - ContactManagerImpl(DatabaseComponent db, KeyManager keyManager, + ContactManagerImpl(DatabaseComponent db, + KeyManager keyManager, IdentityManager identityManager, - PendingContactFactory pendingContactFactory) { + PendingContactFactory pendingContactFactory, + EventBus eventBus) { this.db = db; this.keyManager = keyManager; this.identityManager = identityManager; this.pendingContactFactory = pendingContactFactory; - hooks = new CopyOnWriteArrayList<>(); + this.eventBus = eventBus; } @Override @@ -86,6 +102,7 @@ class ContactManagerImpl implements ContactManager { throws DbException, GeneralSecurityException { PendingContact pendingContact = db.getPendingContact(txn, p); db.removePendingContact(txn, p); + states.remove(p); PublicKey theirPublicKey = pendingContact.getPublicKey(); ContactId c = db.addContact(txn, remote, local, theirPublicKey, verified); @@ -139,6 +156,7 @@ class ContactManagerImpl implements ContactManager { } finally { db.endTransaction(txn); } + setState(p.getId(), WAITING_FOR_CONNECTION); return p; } @@ -156,7 +174,9 @@ class ContactManagerImpl implements ContactManager { List<Pair<PendingContact, PendingContactState>> pairs = new ArrayList<>(pendingContacts.size()); for (PendingContact p : pendingContacts) { - pairs.add(new Pair<>(p, WAITING_FOR_CONNECTION)); // TODO + PendingContactState state = states.get(p.getId()); + if (state == null) state = WAITING_FOR_CONNECTION; + pairs.add(new Pair<>(p, state)); } return pairs; } @@ -164,6 +184,7 @@ class ContactManagerImpl implements ContactManager { @Override public void removePendingContact(PendingContactId p) throws DbException { db.transaction(false, txn -> db.removePendingContact(txn, p)); + states.remove(p); } @Override @@ -263,4 +284,48 @@ class ContactManagerImpl implements ContactManager { else return new AuthorInfo(UNVERIFIED, c.getAlias()); } + @Override + public void eventOccurred(Event e) { + if (e instanceof RendezvousConnectionOpenedEvent) { + RendezvousConnectionOpenedEvent r = + (RendezvousConnectionOpenedEvent) e; + setStateConnected(r.getPendingContactId()); + } else if (e instanceof RendezvousConnectionClosedEvent) { + RendezvousConnectionClosedEvent r = + (RendezvousConnectionClosedEvent) e; + // We're only interested in failures - if the rendezvous succeeds + // the pending contact will be removed + if (!r.isSuccess()) setStateDisconnected(r.getPendingContactId()); + } else if (e instanceof RendezvousFailedEvent) { + RendezvousFailedEvent r = (RendezvousFailedEvent) e; + setState(r.getPendingContactId(), FAILED); + } + } + + /** + * Sets the state of the given pending contact and broadcasts an event. + */ + private void setState(PendingContactId p, PendingContactState state) { + states.put(p, state); + eventBus.broadcast(new PendingContactStateChangedEvent(p, state)); + } + + private void setStateConnected(PendingContactId p) { + // Set the state to ADDING_CONTACT if there's no current state or the + // current state is WAITING_FOR_CONNECTION + if (states.putIfAbsent(p, ADDING_CONTACT) == null || + states.replace(p, WAITING_FOR_CONNECTION, ADDING_CONTACT)) { + eventBus.broadcast(new PendingContactStateChangedEvent(p, + ADDING_CONTACT)); + } + } + + private void setStateDisconnected(PendingContactId p) { + // Set the state to WAITING_FOR_CONNECTION if the current state is + // ADDING_CONTACT + if (states.replace(p, ADDING_CONTACT, WAITING_FOR_CONNECTION)) { + eventBus.broadcast(new PendingContactStateChangedEvent(p, + WAITING_FOR_CONNECTION)); + } + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java index f4d6df9f3d0caec5cf3ddfc69b4ace9635de452f..f0b5b6259f072fd2d03fd7f750b2ab44656ab655 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java @@ -3,6 +3,7 @@ package org.briarproject.bramble.contact; import org.briarproject.bramble.api.contact.ContactExchangeManager; import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.HandshakeManager; +import org.briarproject.bramble.api.event.EventBus; import javax.inject.Inject; import javax.inject.Singleton; @@ -20,7 +21,9 @@ public class ContactModule { @Provides @Singleton - ContactManager provideContactManager(ContactManagerImpl contactManager) { + ContactManager provideContactManager(EventBus eventBus, + ContactManagerImpl contactManager) { + eventBus.addListener(contactManager); return contactManager; } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java index 1eab88e887710bd2b5a0fa0c6818880e6ebe937c..b0a50ec161be7868f6faa56f0e9fdcf5addd6cb2 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java @@ -1,25 +1,32 @@ package org.briarproject.bramble.contact; +import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.contact.PendingContact; +import org.briarproject.bramble.api.contact.PendingContactState; +import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent; import org.briarproject.bramble.api.crypto.KeyPair; -import org.briarproject.bramble.api.crypto.PrivateKey; -import org.briarproject.bramble.api.crypto.PublicKey; import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorInfo; import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionClosedEvent; +import org.briarproject.bramble.api.rendezvous.event.RendezvousConnectionOpenedEvent; +import org.briarproject.bramble.api.rendezvous.event.RendezvousFailedEvent; import org.briarproject.bramble.api.transport.KeyManager; import org.briarproject.bramble.test.BrambleMockTestCase; import org.briarproject.bramble.test.DbExpectations; +import org.briarproject.bramble.test.PredicateMatcher; import org.jmock.Expectations; +import org.junit.Before; import org.junit.Test; import java.util.Collection; @@ -28,6 +35,9 @@ import java.util.Random; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.BASE32_LINK_BYTES; +import static org.briarproject.bramble.api.contact.PendingContactState.ADDING_CONTACT; +import static org.briarproject.bramble.api.contact.PendingContactState.FAILED; +import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNKNOWN; @@ -38,6 +48,7 @@ import static org.briarproject.bramble.test.TestUtils.getAgreementPublicKey; import static org.briarproject.bramble.test.TestUtils.getAuthor; import static org.briarproject.bramble.test.TestUtils.getContact; import static org.briarproject.bramble.test.TestUtils.getLocalAuthor; +import static org.briarproject.bramble.test.TestUtils.getPendingContact; import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.test.TestUtils.getSecretKey; import static org.briarproject.bramble.util.StringUtils.getRandomBase32String; @@ -54,24 +65,31 @@ public class ContactManagerImplTest extends BrambleMockTestCase { context.mock(IdentityManager.class); private final PendingContactFactory pendingContactFactory = context.mock(PendingContactFactory.class); - private final ContactManager contactManager; + private final EventBus eventBus = context.mock(EventBus.class); + private final Author remote = getAuthor(); private final LocalAuthor localAuthor = getLocalAuthor(); private final AuthorId local = localAuthor.getId(); private final boolean verified = false, active = true; private final Contact contact = getContact(remote, local, verified); private final ContactId contactId = contact.getId(); + private final KeyPair handshakeKeyPair = + new KeyPair(getAgreementPublicKey(), getAgreementPrivateKey()); + private final PendingContact pendingContact = getPendingContact(); + private final SecretKey rootKey = getSecretKey(); + private final long timestamp = System.currentTimeMillis(); + private final boolean alice = new Random().nextBoolean(); + + private ContactManagerImpl contactManager; - public ContactManagerImplTest() { + @Before + public void setUp() { contactManager = new ContactManagerImpl(db, keyManager, - identityManager, pendingContactFactory); + identityManager, pendingContactFactory, eventBus); } @Test public void testAddContact() throws Exception { - SecretKey rootKey = getSecretKey(); - long timestamp = System.currentTimeMillis(); - boolean alice = new Random().nextBoolean(); Transaction txn = new Transaction(null, false); context.checking(new DbExpectations() {{ @@ -91,6 +109,7 @@ public class ContactManagerImplTest extends BrambleMockTestCase { @Test public void testGetContact() throws Exception { Transaction txn = new Transaction(null, true); + context.checking(new DbExpectations() {{ oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); oneOf(db).getContact(txn, contactId); @@ -104,6 +123,7 @@ public class ContactManagerImplTest extends BrambleMockTestCase { public void testGetContactByAuthor() throws Exception { Transaction txn = new Transaction(null, true); Collection<Contact> contacts = singletonList(contact); + context.checking(new DbExpectations() {{ oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); oneOf(db).getContactsByAuthorId(txn, remote.getId()); @@ -116,6 +136,7 @@ public class ContactManagerImplTest extends BrambleMockTestCase { @Test(expected = NoSuchContactException.class) public void testGetContactByUnknownAuthor() throws Exception { Transaction txn = new Transaction(null, true); + context.checking(new DbExpectations() {{ oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); oneOf(db).getContactsByAuthorId(txn, remote.getId()); @@ -129,6 +150,7 @@ public class ContactManagerImplTest extends BrambleMockTestCase { public void testGetContactByUnknownLocalAuthor() throws Exception { Transaction txn = new Transaction(null, true); Collection<Contact> contacts = singletonList(contact); + context.checking(new DbExpectations() {{ oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); oneOf(db).getContactsByAuthorId(txn, remote.getId()); @@ -142,6 +164,7 @@ public class ContactManagerImplTest extends BrambleMockTestCase { public void testGetContacts() throws Exception { Collection<Contact> contacts = singletonList(contact); Transaction txn = new Transaction(null, true); + context.checking(new DbExpectations() {{ oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); oneOf(db).getContacts(txn); @@ -154,6 +177,7 @@ public class ContactManagerImplTest extends BrambleMockTestCase { @Test public void testRemoveContact() throws Exception { Transaction txn = new Transaction(null, false); + context.checking(new DbExpectations() {{ oneOf(db).transaction(with(false), withDbRunnable(txn)); oneOf(db).getContact(txn, contactId); @@ -187,6 +211,7 @@ public class ContactManagerImplTest extends BrambleMockTestCase { @Test public void testContactExists() throws Exception { Transaction txn = new Transaction(null, true); + context.checking(new DbExpectations() {{ oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); oneOf(db).containsContact(txn, remote.getId(), local); @@ -206,6 +231,7 @@ public class ContactManagerImplTest extends BrambleMockTestCase { oneOf(db).getContactsByAuthorId(txn, remote.getId()); will(returnValue(singletonList(contact))); }}); + AuthorInfo authorInfo = contactManager.getAuthorInfo(txn, remote.getId()); assertEquals(UNVERIFIED, authorInfo.getStatus()); @@ -223,6 +249,7 @@ public class ContactManagerImplTest extends BrambleMockTestCase { oneOf(db).getContactsByAuthorId(txn, remote.getId()); will(returnValue(emptyList())); }}); + AuthorInfo authorInfo = contactManager.getAuthorInfo(txn, remote.getId()); assertEquals(UNKNOWN, authorInfo.getStatus()); @@ -247,6 +274,7 @@ public class ContactManagerImplTest extends BrambleMockTestCase { will(returnValue(localAuthor)); never(db).getContactsByAuthorId(txn, remote.getId()); }}); + authorInfo = contactManager.getAuthorInfo(txn, localAuthor.getId()); assertEquals(OURSELVES, authorInfo.getStatus()); assertNull(authorInfo.getAlias()); @@ -265,19 +293,178 @@ public class ContactManagerImplTest extends BrambleMockTestCase { @Test public void testGetHandshakeLink() throws Exception { Transaction txn = new Transaction(null, true); - PublicKey publicKey = getAgreementPublicKey(); - PrivateKey privateKey = getAgreementPrivateKey(); - KeyPair keyPair = new KeyPair(publicKey, privateKey); String link = "briar://" + getRandomBase32String(BASE32_LINK_BYTES); context.checking(new DbExpectations() {{ oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); oneOf(identityManager).getHandshakeKeys(txn); - will(returnValue(keyPair)); - oneOf(pendingContactFactory).createHandshakeLink(publicKey); + will(returnValue(handshakeKeyPair)); + oneOf(pendingContactFactory).createHandshakeLink( + handshakeKeyPair.getPublic()); will(returnValue(link)); }}); assertEquals(link, contactManager.getHandshakeLink()); } + + @Test + public void testDefaultPendingContactState() throws Exception { + Transaction txn = new Transaction(null, true); + + context.checking(new DbExpectations() {{ + oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); + oneOf(db).getPendingContacts(txn); + will(returnValue(singletonList(pendingContact))); + }}); + + // No events have happened for this pending contact, so the state + // should be WAITING_FOR_CONNECTION + Collection<Pair<PendingContact, PendingContactState>> pairs = + contactManager.getPendingContacts(); + assertEquals(1, pairs.size()); + Pair<PendingContact, PendingContactState> pair = + pairs.iterator().next(); + assertEquals(pendingContact, pair.getFirst()); + assertEquals(WAITING_FOR_CONNECTION, pair.getSecond()); + } + + @Test + public void testPendingContactExpiresBeforeConnection() { + // The pending contact expires - the FAILED state is broadcast + context.checking(new Expectations() {{ + oneOf(eventBus).broadcast(with(new PredicateMatcher<>( + PendingContactStateChangedEvent.class, e -> + e.getPendingContactState() == FAILED))); + }}); + contactManager.eventOccurred(new RendezvousFailedEvent( + pendingContact.getId())); + context.assertIsSatisfied(); + + // A rendezvous connection is opened - no state is broadcast + contactManager.eventOccurred(new RendezvousConnectionOpenedEvent( + pendingContact.getId())); + context.assertIsSatisfied(); + + // The rendezvous connection fails - no state is broadcast + contactManager.eventOccurred(new RendezvousConnectionClosedEvent( + pendingContact.getId(), false)); + } + + @Test + public void testPendingContactExpiresDuringFailedConnection() { + // A rendezvous connection is opened - the ADDING_CONTACT state is + // broadcast + context.checking(new Expectations() {{ + oneOf(eventBus).broadcast(with(new PredicateMatcher<>( + PendingContactStateChangedEvent.class, e -> + e.getPendingContactState() == ADDING_CONTACT))); + }}); + + contactManager.eventOccurred(new RendezvousConnectionOpenedEvent( + pendingContact.getId())); + context.assertIsSatisfied(); + + // The pending contact expires - the FAILED state is broadcast + context.checking(new Expectations() {{ + oneOf(eventBus).broadcast(with(new PredicateMatcher<>( + PendingContactStateChangedEvent.class, e -> + e.getPendingContactState() == FAILED))); + }}); + + contactManager.eventOccurred(new RendezvousFailedEvent( + pendingContact.getId())); + context.assertIsSatisfied(); + + // The rendezvous connection fails - no state is broadcast + contactManager.eventOccurred(new RendezvousConnectionClosedEvent( + pendingContact.getId(), false)); + } + + @Test + public void testPendingContactExpiresDuringSuccessfulConnection() + throws Exception { + Transaction txn = new Transaction(null, false); + + // A rendezvous connection is opened - the ADDING_CONTACT state is + // broadcast + context.checking(new Expectations() {{ + oneOf(eventBus).broadcast(with(new PredicateMatcher<>( + PendingContactStateChangedEvent.class, e -> + e.getPendingContactState() == ADDING_CONTACT))); + }}); + + contactManager.eventOccurred(new RendezvousConnectionOpenedEvent( + pendingContact.getId())); + context.assertIsSatisfied(); + + // The pending contact expires - the FAILED state is broadcast + context.checking(new Expectations() {{ + oneOf(eventBus).broadcast(with(new PredicateMatcher<>( + PendingContactStateChangedEvent.class, e -> + e.getPendingContactState() == FAILED))); + }}); + + contactManager.eventOccurred(new RendezvousFailedEvent( + pendingContact.getId())); + context.assertIsSatisfied(); + + // The pending contact is converted to a contact - no state is broadcast + context.checking(new DbExpectations() {{ + oneOf(db).getPendingContact(txn, pendingContact.getId()); + will(returnValue(pendingContact)); + oneOf(db).removePendingContact(txn, pendingContact.getId()); + oneOf(db).addContact(txn, remote, local, + pendingContact.getPublicKey(), verified); + will(returnValue(contactId)); + oneOf(db).setContactAlias(txn, contactId, + pendingContact.getAlias()); + oneOf(identityManager).getHandshakeKeys(txn); + will(returnValue(handshakeKeyPair)); + oneOf(keyManager).addContact(txn, contactId, + pendingContact.getPublicKey(), handshakeKeyPair); + oneOf(keyManager).addRotationKeys(txn, contactId, rootKey, + timestamp, alice, active); + oneOf(db).getContact(txn, contactId); + will(returnValue(contact)); + }}); + + contactManager.addContact(txn, pendingContact.getId(), remote, + local, rootKey, timestamp, alice, verified, active); + context.assertIsSatisfied(); + + // The rendezvous connection succeeds - no state is broadcast + contactManager.eventOccurred(new RendezvousConnectionClosedEvent( + pendingContact.getId(), true)); + } + + @Test + public void testPendingContactRemovedDuringFailedConnection() + throws Exception { + Transaction txn = new Transaction(null, false); + + // A rendezvous connection is opened - the ADDING_CONTACT state is + // broadcast + context.checking(new Expectations() {{ + oneOf(eventBus).broadcast(with(new PredicateMatcher<>( + PendingContactStateChangedEvent.class, e -> + e.getPendingContactState() == ADDING_CONTACT))); + }}); + + contactManager.eventOccurred(new RendezvousConnectionOpenedEvent( + pendingContact.getId())); + context.assertIsSatisfied(); + + // The pending contact is removed - no state is broadcast + context.checking(new DbExpectations() {{ + oneOf(db).transaction(with(false), withDbRunnable(txn)); + oneOf(db).removePendingContact(txn, pendingContact.getId()); + }}); + + contactManager.removePendingContact(pendingContact.getId()); + context.assertIsSatisfied(); + + // The rendezvous connection fails - no state is broadcast + contactManager.eventOccurred(new RendezvousConnectionClosedEvent( + pendingContact.getId(), false)); + } } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/PredicateMatcher.java b/bramble-core/src/test/java/org/briarproject/bramble/test/PredicateMatcher.java new file mode 100644 index 0000000000000000000000000000000000000000..b08f15b2aa4ed470967a055fdfae26a951637ac4 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/PredicateMatcher.java @@ -0,0 +1,28 @@ +package org.briarproject.bramble.test; + +import org.briarproject.bramble.api.Predicate; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; + +public class PredicateMatcher<T> extends BaseMatcher<T> { + + private final Class<T> matchedClass; + private final Predicate<T> predicate; + + public PredicateMatcher(Class<T> matchedClass, Predicate<T> predicate) { + this.matchedClass = matchedClass; + this.predicate = predicate; + } + + @Override + public boolean matches(Object item) { + if (matchedClass.isInstance(item)) + return predicate.test(matchedClass.cast(item)); + return false; + } + + @Override + public void describeTo(Description description) { + description.appendText("matches an item against a predicate"); + } +}