I just had to write to custom matcher for this myself when I could not find a suitable one.
import java.util.function.BiConsumer;
import javax.annotation.Nonnull;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.core.CombinableMatcher;
/**
* Similar to the {@link CombinableMatcher.CombinableEitherMatcher} but only passes if <em>only one</em> of the given
* matchers {@link Matcher#matches(Object)}.
*
* @author bugorskia
*/
public class EitherXorMatcher<T> extends BaseMatcher<T>{
//_ **FIELDS** _//
@Nonnull
private final Matcher< ? super T > aMatcher;
@Nonnull
private final Matcher< ? super T > bMatcher;
//_ **INNER CLASS**_//
/**
* This is just for the builder pattern/fluent interface.
*/
public static final class EitherXorMatcherBuilder<T>{
//_ **FIELDS** _//
@Nonnull
private final Matcher<? super T> aMatcher;
//_ **CONSTRUCTOR** _//
private EitherXorMatcherBuilder( @Nonnull final Matcher<? super T> aMatcher ){
this.aMatcher = aMatcher;
}
//_ **API METHODS** _//
@Nonnull
public Matcher<T> xor( @Nonnull final Matcher<? super T> anotherMatcher ){
return new EitherXorMatcher<>( aMatcher, anotherMatcher );
}
}
//_ **CONSTRUCTOR** _//
private EitherXorMatcher( @Nonnull final Matcher< ? super T > aMatcher, @Nonnull final Matcher< ? super T > bMatcher ){
this.aMatcher = aMatcher;
this.bMatcher = bMatcher;
}
@Nonnull
public static <T> EitherXorMatcherBuilder<T> exclusivelyEither( final Matcher<? super T> aMatcher ){
return new EitherXorMatcherBuilder<>( aMatcher );
}
@Nonnull
public static <T> Matcher<? super T> exclusivelyEither( @Nonnull final Matcher<? super T> aMatcher, @Nonnull final Matcher<? super T> bMatcher ){
return new EitherXorMatcher<>( aMatcher, bMatcher );
}
@Nonnull @Deprecated
public static <T> EitherXorMatcherBuilder<T> either( final Matcher<? super T> aMatcher ){
return exclusivelyEither( aMatcher );
}
//_ **API METHODS** _//
@Override
public boolean matches( final Object item ){
final boolean aMatches = aMatcher.matches( item );
final boolean bMatches = bMatcher.matches( item );
return xor( aMatches, bMatches );
}
@Override
public void describeTo( final Description description ){
description.appendText( "Either { " );
aMatcher.describeTo( description );
description.appendText( " } xor { " );
bMatcher.describeTo( description );
description.appendText( " } " );
}
@Override
public void describeMismatch( final Object item, final Description description ){
final boolean aMatches = aMatcher.matches( item );
final boolean bMatches = bMatcher.matches( item );
assert !xor( aMatches, bMatches ): "Should not have gotten called!";
assert aMatches == bMatches: "This is implied, and more of a developer comment than a runtime check.";
final BiConsumer<Matcher<? super T>,Description> describer;
final String startWord, joinWord;
if( aMatches ){
startWord = "Both";
joinWord = "and";
describer = Matcher::describeTo;
}else{
startWord = "Neither";
joinWord = "nor";
describer = ( m, d ) -> m.describeMismatch( item, description );
}
description.appendText( startWord ).appendText( " { " );
describer.accept( aMatcher, description );
description.appendText( " } " ).appendText( joinWord ).appendText( " { " );
describer.accept( bMatcher, description );
description.appendText( " } " ).appendText( " matched instead of exactly one." );
}
//_ **HELPER METHODS** _//
private static boolean xor( final boolean aMatches, final boolean bMatches ){
// xor :: one or the other but not both
return ( aMatches || bMatches ) && ! ( aMatches && bMatches );
}
}