Skip to content

Commit fc0112c

Browse files
author
smgfreeman
committed
Added FeatureMatcher to allow matching of parts of an object. Applied to implementation of various other matchers.
Factored finding templated type into ReflectiveTypeMatcher More tests.
1 parent 718ba63 commit fc0112c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+500
-186
lines changed

hamcrest-core/src/main/java/org/hamcrest/CustomTypeSafeMatcher.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
* public boolean matchesSafely(String string) {
1010
* return !string.isEmpty();
1111
* }
12+
* public void describeMismatchSafely(String string, Description mismatchDescription) {
13+
* mismatchDescription.appendText("was empty");
14+
* }
1215
* };
1316
* </pre>
1417
* This is a variant of {@link CustomMatcher} that first type checks
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.hamcrest;
2+
3+
import org.hamcrest.internal.ReflectiveTypeFinder;
4+
5+
/**
6+
* Supporting class for matching a feature of an object. Implement <code>valueOfPartIn()</code>
7+
* in a subclass to pull out the feature to be matched against.
8+
*
9+
* @param <T> The type of the object to be matched
10+
* @param <U> The type of the feature to be matched
11+
*/
12+
public abstract class FeatureMatcher<T, U> extends TypeSafeDiagnosingMatcher<T> {
13+
private static final ReflectiveTypeFinder TYPE_FINDER = new ReflectiveTypeFinder("featureValueOf", 1, 0);
14+
private final Matcher<? super U> subMatcher;
15+
private final String featureDescription;
16+
private final String featureName;
17+
18+
/**
19+
* Constructor
20+
* @param subMatcher The matcher to apply to the feature
21+
* @param featureDescription Descriptive text to use in describeTo
22+
* @param featureName Identifying text for mismatch message
23+
*/
24+
public FeatureMatcher(Matcher<? super U> subMatcher, String featureDescription, String featureName) {
25+
super(TYPE_FINDER);
26+
this.subMatcher = subMatcher;
27+
this.featureDescription = featureDescription;
28+
this.featureName = featureName;
29+
}
30+
31+
protected abstract U featureValueOf(T actual);
32+
33+
@Override
34+
protected boolean matchesSafely(T actual, Description mismatchDescription) {
35+
final U featureValue = featureValueOf(actual);
36+
if (!subMatcher.matches(featureValue)) {
37+
mismatchDescription.appendText(featureName).appendText(" was ").appendValue(featureValue);
38+
return false;
39+
}
40+
return true;
41+
};
42+
43+
public final void describeTo(Description description) {
44+
description.appendText(featureDescription).appendText(" ")
45+
.appendDescriptionOf(subMatcher);
46+
}
47+
}
Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.hamcrest;
22

3-
import java.lang.reflect.Method;
3+
import org.hamcrest.internal.ReflectiveTypeFinder;
4+
45

56
/**
67
* Convenient base class for Matchers that require a non-null value of a specific type
@@ -11,19 +12,42 @@
1112
* @param <T>
1213
* @author Neil Dunn
1314
* @author Nat Pryce
15+
* @author Steve Freeman
1416
*/
1517
public abstract class TypeSafeDiagnosingMatcher<T> extends BaseMatcher<T> {
18+
private static final ReflectiveTypeFinder TYPE_FINDER = new ReflectiveTypeFinder("matchesSafely", 2, 0);
1619
private final Class<?> expectedType;
1720

18-
protected TypeSafeDiagnosingMatcher() {
19-
this.expectedType = findExpectedType(getClass());
20-
}
21-
2221
/**
2322
* Subclasses should implement this. The item will already have been checked
2423
* for the specific type and will never be null.
2524
*/
26-
public abstract boolean matchesSafely(T item, Description mismatchDescription);
25+
protected abstract boolean matchesSafely(T item, Description mismatchDescription);
26+
27+
/**
28+
* Use this constructor if the subclass that implements <code>matchesSafely</code>
29+
* is <em>not</em> the class that binds &lt;T&gt; to a type.
30+
* @param expectedType The expectedType of the actual value.
31+
*/
32+
protected TypeSafeDiagnosingMatcher(Class<?> expectedType) {
33+
this.expectedType = expectedType;
34+
}
35+
36+
/**
37+
* Use this constructor if the subclass that implements <code>matchesSafely</code>
38+
* is <em>not</em> the class that binds &lt;T&gt; to a type.
39+
* @param typeFinder A type finder to extract the type
40+
*/
41+
protected TypeSafeDiagnosingMatcher(ReflectiveTypeFinder typeFinder) {
42+
this.expectedType = typeFinder.findExpectedType(getClass());
43+
}
44+
45+
/**
46+
* The default constructor for simple sub types
47+
*/
48+
protected TypeSafeDiagnosingMatcher() {
49+
this(TYPE_FINDER);
50+
}
2751

2852
@SuppressWarnings("unchecked")
2953
public final boolean matches(Object item) {
@@ -32,31 +56,13 @@ public final boolean matches(Object item) {
3256
&& matchesSafely((T) item, new Description.NullDescription());
3357
}
3458

35-
@Override
3659
@SuppressWarnings("unchecked")
60+
@Override
3761
public final void describeMismatch(Object item, Description mismatchDescription) {
62+
if (item == null || !expectedType.isInstance(item)) {
63+
super.describeMismatch(item, mismatchDescription);
64+
} else {
3865
matchesSafely((T) item, mismatchDescription);
39-
}
40-
41-
private Class<?> findExpectedType(Class<?> fromClass) {
42-
for (Class<?> c = fromClass; c != Object.class; c = c.getSuperclass()) {
43-
for (Method method : c.getDeclaredMethods()) {
44-
if (canObtainExpectedTypeFrom(method)) {
45-
return obtainExpectedTypeFrom(method);
46-
}
47-
}
48-
}
49-
50-
throw new Error("Cannot determine correct type for matchesSafely() method.");
51-
}
52-
53-
protected boolean canObtainExpectedTypeFrom(Method method) {
54-
return method.getName().equals("matchesSafely")
55-
&& method.getParameterTypes().length == 2
56-
&& !method.isSynthetic();
57-
}
58-
59-
protected Class<?> obtainExpectedTypeFrom(Method method) {
60-
return method.getParameterTypes()[0];
66+
}
6167
}
6268
}
Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package org.hamcrest;
22

3-
import java.lang.reflect.Method;
3+
import org.hamcrest.internal.ReflectiveTypeFinder;
44

55
/**
66
* Convenient base class for Matchers that require a non-null value of a specific type.
@@ -9,42 +9,45 @@
99
* @author Joe Walnes
1010
*/
1111
public abstract class TypeSafeMatcher<T> extends BaseMatcher<T> {
12-
private Class<?> expectedType;
12+
private static final ReflectiveTypeFinder TYPE_FINDER = new ReflectiveTypeFinder("matchesSafely", 1, 0);
13+
final private Class<?> expectedType;
1314

1415
/**
15-
* Subclasses should implement this. The item will already have been checked for
16+
* Subclasses should implement these. The item will already have been checked for
1617
* the specific type and will never be null.
1718
*/
18-
public abstract boolean matchesSafely(T item);
19-
20-
protected TypeSafeMatcher() {
21-
expectedType = findExpectedType(getClass());
22-
}
23-
24-
private static Class<?> findExpectedType(Class<?> fromClass) {
25-
for (Class<?> c = fromClass; c != Object.class; c = c.getSuperclass()) {
26-
for (Method method : c.getDeclaredMethods()) {
27-
if (isMatchesSafelyMethod(method)) {
28-
return method.getParameterTypes()[0];
29-
}
30-
}
31-
}
32-
33-
throw new Error("Cannot determine correct type for matchesSafely() method.");
34-
}
19+
protected abstract boolean matchesSafely(T item);
20+
protected abstract void describeMismatchSafely(T item, Description mismatchDescription);
3521

36-
private static boolean isMatchesSafelyMethod(Method method) {
37-
return method.getName().equals("matchesSafely")
38-
&& method.getParameterTypes().length == 1
39-
&& !method.isSynthetic();
22+
/**
23+
* The default constructor for simple sub types
24+
*/
25+
protected TypeSafeMatcher() {
26+
this(TYPE_FINDER);
4027
}
4128

42-
protected TypeSafeMatcher(Class<T> expectedType) {
29+
30+
/**
31+
* Use this constructor if the subclass that implements <code>matchesSafely</code>
32+
* is <em>not</em> the class that binds &lt;T&gt; to a type.
33+
* @param expectedType The expectedType of the actual value.
34+
*/
35+
protected TypeSafeMatcher(Class<?> expectedType) {
4336
this.expectedType = expectedType;
4437
}
4538

39+
40+
/**
41+
* Use this constructor if the subclass that implements <code>matchesSafely</code>
42+
* is <em>not</em> the class that binds &lt;T&gt; to a type.
43+
* @param typeFinder A type finder to extract the type
44+
*/
45+
protected TypeSafeMatcher(ReflectiveTypeFinder typeFinder) {
46+
this.expectedType = typeFinder.findExpectedType(getClass());
47+
}
48+
4649
/**
47-
* Method made final to prevent accidental override.
50+
* Methods made final to prevent accidental override.
4851
* If you need to override this, there's no point on extending TypeSafeMatcher.
4952
* Instead, extend the {@link BaseMatcher}.
5053
*/
@@ -54,4 +57,14 @@ public final boolean matches(Object item) {
5457
&& expectedType.isInstance(item)
5558
&& matchesSafely((T) item);
5659
}
60+
61+
@SuppressWarnings("unchecked")
62+
@Override
63+
final public void describeMismatch(Object item, Description description) {
64+
if (item == null || ! expectedType.isInstance(item)) {
65+
super.describeMismatch(item, description);
66+
} else {
67+
describeMismatchSafely((T)item, description);
68+
}
69+
}
5770
}

hamcrest-core/src/main/java/org/hamcrest/core/IsCollectionContaining.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ public boolean matchesSafely(Iterable<? super T> collection) {
2828
return false;
2929
}
3030

31+
@Override
32+
public void describeMismatchSafely(Iterable<? super T> item, Description mismatchDescription) {
33+
mismatchDescription.appendValue(item);
34+
}
35+
3136
public void describeTo(Description description) {
3237
description
3338
.appendText("a collection containing ")

hamcrest-core/src/main/java/org/hamcrest/core/SubstringMatcher.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ protected SubstringMatcher(final String substring) {
1818
public boolean matchesSafely(String item) {
1919
return evalSubstringOf(item);
2020
}
21-
21+
@Override
22+
public void describeMismatchSafely(String item, Description mismatchDescription) {
23+
mismatchDescription.appendText("was \"").appendText(item).appendText("\"");
24+
}
25+
2226
public void describeTo(Description description) {
2327
description.appendText("a string ")
2428
.appendText(relationship())
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* The TypeSafe classes, and their descendants, need a mechanism to find out what type has been used as a parameter
3+
* for the concrete matcher. Unfortunately, this type is lost during type erasure so we need to use reflection
4+
* to get it back, by picking out the type of a known parameter to a known method.
5+
* The catch is that, with bridging methods, this type is only visible in the class that actually implements
6+
* the expected method, so the ReflectiveTypeFinder needs to be applied to that class or a subtype.
7+
*
8+
* For example, the abstract <code>TypeSafeDiagnosingMatcher&lt;T&gt;</code> defines an abstract method
9+
* <pre>protected abstract boolean matchesSafely(T item, Description mismatchDescription);</pre>
10+
* By default it uses <code>new ReflectiveTypeFinder("matchesSafely", 2, 0); </code> to find the
11+
* parameterised type. If we create a <code>TypeSafeDiagnosingMatcher&lt;String&gt;</code>, the type
12+
* finder will return <code>String.class</code>.
13+
*
14+
* A <code>FeatureMatcher</code> is an abstract subclass of <code>TypeSafeDiagnosingMatcher</code>.
15+
* Although it has a templated implementation of <code>matchesSafely(&lt;T&gt;, Decription);</code>, the
16+
* actualy run-time signature of this is <code>matchesSafely(Object, Description);</code>. Instead,
17+
* we must find the type by reflecting on the concrete implementation of
18+
* <pre>protected abstract U featureValueOf(T actual);</pre>
19+
* a method which is declared in <code>FeatureMatcher</code>.
20+
*
21+
* In short, use this to extract a type from a method in the leaf class of a templated class hierarchy.
22+
*
23+
* @author Steve Freeman
24+
* @author Nat Pryce
25+
*/
26+
package org.hamcrest.internal;
27+
28+
import java.lang.reflect.Method;
29+
30+
public class ReflectiveTypeFinder {
31+
private final String methodName;
32+
private final int expectedNumberOfParameters;
33+
private final int typedParameter;
34+
35+
public ReflectiveTypeFinder(String methodName, int expectedNumberOfParameters, int typedParameter) {
36+
this.methodName = methodName;
37+
this.expectedNumberOfParameters = expectedNumberOfParameters;
38+
this.typedParameter = typedParameter;
39+
}
40+
41+
public Class<?> findExpectedType(Class<?> fromClass) {
42+
for (Class<?> c = fromClass; c != Object.class; c = c.getSuperclass()) {
43+
for (Method method : c.getDeclaredMethods()) {
44+
if (canObtainExpectedTypeFrom(method)) {
45+
return expectedTypeFrom(method);
46+
}
47+
}
48+
}
49+
throw new Error("Cannot determine correct type for " + methodName + "() method.");
50+
}
51+
52+
/**
53+
* @param method The method to examine.
54+
* @return true if this method references the relevant type
55+
*/
56+
protected boolean canObtainExpectedTypeFrom(Method method) {
57+
return method.getName().equals(methodName)
58+
&& method.getParameterTypes().length == expectedNumberOfParameters
59+
&& !method.isSynthetic();
60+
}
61+
62+
63+
/**
64+
* @param method The method from which to extract
65+
* @return The type we're looking for
66+
*/
67+
protected Class<?> expectedTypeFrom(Method method) {
68+
return method.getParameterTypes()[typedParameter];
69+
}
70+
}

hamcrest-examples/src/main/java/org/hamcrest/examples/junit4/ExampleWithAssertThat.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ public boolean matchesSafely(ComplicatedClass item) {
6262
// TODO Auto-generated method stub
6363
return false;
6464
}
65+
@Override
66+
public void describeMismatchSafely(ComplicatedClass item, Description mismatchDescription) {
67+
// TODO Auto-generated method stub
68+
69+
}
6570
};
6671
}
6772

hamcrest-library/src/main/java/org/hamcrest/beans/HasProperty.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public HasProperty(String propertyName) {
2525
}
2626

2727
@Override
28-
public boolean matchesSafely(T obj) {
28+
public boolean matchesSafely(T obj) {
2929
// TODO(ngd): this is not type safe.
3030
try {
3131
return PropertyUtil.getPropertyDescriptor(propertyName, obj) != null;
@@ -34,6 +34,11 @@ public boolean matchesSafely(T obj) {
3434
return false;
3535
}
3636
}
37+
38+
@Override
39+
public void describeMismatchSafely(T item, Description mismatchDescription) {
40+
mismatchDescription.appendText("no ").appendValue(propertyName).appendText(" in ").appendValue(item);
41+
};
3742

3843
public void describeTo(Description description) {
3944
description.appendText("hasProperty(").appendValue(propertyName).appendText(")");

0 commit comments

Comments
 (0)