Skip to content

Commit 1471527

Browse files
jbrownbrownian-motion
authored andcommitted
Added matchers to check the visibility (public/private) of various reflective elements.
This is helpful, for example, when enforcing the scope of a public-facing API with a test, and provides stronger documentation for the future than mere comments.
1 parent 8522353 commit 1471527

File tree

9 files changed

+754
-0
lines changed

9 files changed

+754
-0
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.hamcrest.reflection;
2+
3+
import java.lang.reflect.Member;
4+
import java.lang.reflect.Modifier;
5+
import java.util.Objects;
6+
7+
/**
8+
* Represents the 4 states of visibility.
9+
*
10+
* @author JJ Brown
11+
*/
12+
enum Visibility {
13+
PUBLIC("public"),
14+
PROTECTED("protected"),
15+
PACKAGE_PROTECTED("package-protected (no modifiers)"),
16+
PRIVATE("private");
17+
18+
public String getDescription() {
19+
return description;
20+
}
21+
22+
private final String description;
23+
24+
Visibility(String description) {
25+
this.description = description;
26+
}
27+
28+
static Visibility of(Class<?> clazz) {
29+
Objects.requireNonNull(clazz, "Cannot determine the visibility of a null-valued reflective Class object");
30+
31+
if (Modifier.isPublic(clazz.getModifiers())) {
32+
return Visibility.PUBLIC;
33+
}
34+
if (Modifier.isProtected(clazz.getModifiers())) {
35+
return Visibility.PROTECTED;
36+
}
37+
if (Modifier.isPrivate(clazz.getModifiers())) {
38+
return Visibility.PRIVATE;
39+
}
40+
return Visibility.PACKAGE_PROTECTED;
41+
}
42+
43+
static Visibility of(Member member) {
44+
Objects.requireNonNull(member, "Cannot determine the visibility of a null-valued reflective member object");
45+
46+
if (Modifier.isPublic(member.getModifiers())) {
47+
return Visibility.PUBLIC;
48+
}
49+
if (Modifier.isProtected(member.getModifiers())) {
50+
return Visibility.PROTECTED;
51+
}
52+
if (Modifier.isPrivate(member.getModifiers())) {
53+
return Visibility.PRIVATE;
54+
}
55+
return Visibility.PACKAGE_PROTECTED;
56+
}
57+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.hamcrest.reflection;
2+
3+
import org.hamcrest.BaseMatcher;
4+
import org.hamcrest.Description;
5+
6+
import java.lang.reflect.Member;
7+
8+
/**
9+
* Matches the visibility of a reflective element, like a {@link Class} or a {@link java.lang.reflect.Method},
10+
* to make assertions about the scope of a module's API.
11+
* <p>
12+
* This class is intentionally not exposed to the public API, to help keep implementation details hidden (and easy to change).
13+
* Please use {@link VisibilityMatchers} to instantiate instances of this class.
14+
*
15+
* @param <T> the type of the element being matched; could be anything
16+
* @author JJ Brown
17+
* @see VisibilityMatchers
18+
*/
19+
class VisibilityMatcher<T> extends BaseMatcher<T> {
20+
private final Visibility expectedVisibility;
21+
22+
VisibilityMatcher(Visibility expectedVisibility) {
23+
this.expectedVisibility = expectedVisibility;
24+
}
25+
26+
@Override
27+
public boolean matches(Object actual) {
28+
if (actual == null) {
29+
return false;
30+
}
31+
if (actual instanceof Class) {
32+
return expectedVisibility == Visibility.of((Class<?>) actual);
33+
}
34+
if (actual instanceof Member) {
35+
return expectedVisibility == Visibility.of((Member) actual);
36+
}
37+
return false;
38+
}
39+
40+
@Override
41+
public void describeTo(Description description) {
42+
description.appendText("is ").appendText(expectedVisibility.getDescription());
43+
}
44+
45+
@Override
46+
public void describeMismatch(Object item, Description description) {
47+
if (item == null) {
48+
description.appendText("was null");
49+
} else if (item instanceof Class) {
50+
description.appendText("was a ")
51+
.appendText(Visibility.of((Class<?>) item).getDescription())
52+
.appendText(" class");
53+
} else if (item instanceof Member) {
54+
description.appendText("was a ")
55+
.appendText(Visibility.of((Member) item).getDescription())
56+
.appendText(" ")
57+
.appendText(item.getClass().getName());
58+
} else {
59+
description.appendText("was " + item.getClass().getName() + " instead of a reflective element like a Class<T>, Constructor<T>, or Method");
60+
}
61+
}
62+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package org.hamcrest.reflection;
2+
3+
import org.hamcrest.Matcher;
4+
5+
/**
6+
* Defines matchers that check the visibility of reflective objects like {@link java.lang.Class} or {@link java.lang.reflect.Method}.
7+
* {@code null} values never match, nor do normal objects; these simply do not match, without raising an Exception.
8+
*
9+
* @author JJ Brown
10+
*/
11+
public class VisibilityMatchers {
12+
// Each matcher is stateless and can match any type, so the individual instances are only made once and stored here for re-use.
13+
private static final VisibilityMatcher<?> PUBLIC = new VisibilityMatcher<>(Visibility.PUBLIC);
14+
private static final VisibilityMatcher<?> PROTECTED = new VisibilityMatcher<>(Visibility.PROTECTED);
15+
private static final VisibilityMatcher<?> PACKAGE_PROTECTED = new VisibilityMatcher<>(Visibility.PACKAGE_PROTECTED);
16+
private static final VisibilityMatcher<?> PRIVATE = new VisibilityMatcher<>(Visibility.PRIVATE);
17+
18+
/**
19+
* Matchers reflective elements that have public visibility.
20+
* Specifically, this matcher only matches elements marked with the keyword {@code public}.
21+
* <br>
22+
* This method matches {@link Class} objects or other {@link java.lang.reflect.Member reflective objects}
23+
* like {@link java.lang.reflect.Field} or {@link java.lang.reflect.Method} used in reflection.
24+
* Any other kind of object, or {@code null} values, do not match (but will not cause an Exception).
25+
*
26+
* @param <T> the type of the object being matched
27+
* @return a matcher that matches reflective elements with exactly the given level of visibility
28+
*/
29+
@SuppressWarnings("unchecked")
30+
public static <T> Matcher<T> isPublic() {
31+
// Each matcher is stateless and can match any type (the generic <T> is for type safety at the use site),
32+
// so it's fine to cast the non-reifiable generic type here at runtime and re-use the same instance.
33+
return (Matcher<T>) PUBLIC;
34+
}
35+
36+
/**
37+
* Matchers reflective elements that have protected visibility.
38+
* Specifically, this matcher only matches elements marked with the keyword {@code protected}; it does NOT match public or private elements.
39+
* <br>
40+
* This method matches {@link Class} objects or other {@link java.lang.reflect.Member reflective objects}
41+
* like {@link java.lang.reflect.Field} or {@link java.lang.reflect.Method} used in reflection.
42+
* Any other kind of object, or {@code null} values, do not match (but will not cause an Exception).
43+
*
44+
* @param <T> the type of the object being matched
45+
* @return a matcher that matches reflective elements with exactly the given level of visibility
46+
*/
47+
@SuppressWarnings("unchecked")
48+
public static <T> Matcher<T> isProtected() {
49+
// Each matcher is stateless and can match any type (the generic <T> is for type safety at the use site),
50+
// so it's fine to cast the non-reifiable generic type here at runtime and re-use the same instance.
51+
return (Matcher<T>) PROTECTED;
52+
}
53+
54+
/**
55+
* Matchers reflective elements that have package-protected visibility.
56+
* Specifically, this matcher only matches elements not marked with any of the visibility keywords {@code public}, {@code protected}, or {@code private}.
57+
* <br>
58+
* This method matches {@link Class} objects or other {@link java.lang.reflect.Member reflective objects}
59+
* like {@link java.lang.reflect.Field} or {@link java.lang.reflect.Method} used in reflection.
60+
* Any other kind of object, or {@code null} values, do not match (but will not cause an Exception).
61+
*
62+
* @param <T> the type of the object being matched
63+
* @return a matcher that matches reflective elements with exactly the given level of visibility
64+
*/
65+
@SuppressWarnings("unchecked")
66+
public static <T> Matcher<T> isPackageProtected() {
67+
// Each matcher is stateless and can match any type (the generic <T> is for type safety at the use site),
68+
// so it's fine to cast the non-reifiable generic type here at runtime and re-use the same instance.
69+
return (Matcher<T>) PACKAGE_PROTECTED;
70+
}
71+
72+
/**
73+
* Matchers reflective elements that have private visibility.
74+
* Specifically, this matcher only matches elements marked with the keyword {@code private}.
75+
* <br>
76+
* This method matches {@link Class} objects or other {@link java.lang.reflect.Member reflective objects}
77+
* like {@link java.lang.reflect.Field} or {@link java.lang.reflect.Method} used in reflection.
78+
* Any other kind of object, or {@code null} values, do not match (but will not cause an Exception).
79+
*
80+
* @param <T> the type of the object being matched
81+
* @return a matcher that matches reflective elements with exactly the given level of visibility
82+
*/
83+
@SuppressWarnings("unchecked")
84+
public static <T> Matcher<T> isPrivate() {
85+
// Each matcher is stateless and can match any type (the generic <T> is for type safety at the use site),
86+
// so it's fine to cast the non-reifiable generic type here at runtime and re-use the same instance.
87+
return (Matcher<T>) PRIVATE;
88+
}
89+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
</head>
6+
<body>
7+
<p>Matchers that perform checks on reflective elements, such as Class&lt;?&gt; and Method&lt;?&gt; objects.</p>
8+
<p>This provides tools to enforce boundaries about the scope of visible items in a module,
9+
and to explicitly ensure that items are available to reflection when they may only be loaded at runtime.</p>
10+
</body>
11+
</html>
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package org.hamcrest.reflection;
2+
3+
4+
import org.hamcrest.AbstractMatcherTest;
5+
import org.hamcrest.Matcher;
6+
import org.junit.Test;
7+
8+
import java.lang.reflect.Field;
9+
import java.lang.reflect.Method;
10+
11+
import static org.hamcrest.reflection.VisibilityMatchers.isPackageProtected;
12+
import static org.hamcrest.reflection.VisibilityMatchers.isPublic;
13+
14+
@SuppressWarnings("unused")
15+
public class IsPackageProtectedTest extends AbstractMatcherTest {
16+
@Override
17+
protected Matcher<?> createMatcher() {
18+
return isPackageProtected();
19+
}
20+
21+
@Test
22+
public void test_packageExposesPublicFactoryMethod() throws NoSuchMethodException {
23+
assertMatches(isPublic(), VisibilityMatchers.class.getMethod("isPackageProtected"));
24+
}
25+
26+
@Test
27+
public void test_isPackageProtected_matchesOnlyPackageProtectedClasses() {
28+
assertDoesNotMatch(isPackageProtected(), PublicClass.class);
29+
assertDoesNotMatch(isPackageProtected(), ProtectedClass.class);
30+
assertMatches(isPackageProtected(), PackageProtectedClass.class);
31+
assertDoesNotMatch(isPackageProtected(), PrivateClass.class);
32+
33+
assertDescription("is package-protected (no modifiers)", isPackageProtected());
34+
35+
assertMismatchDescription("was a public class", isPackageProtected(), PublicClass.class);
36+
assertMismatchDescription("was a protected class", isPackageProtected(), ProtectedClass.class);
37+
assertMismatchDescription("was a private class", isPackageProtected(), PrivateClass.class);
38+
}
39+
40+
41+
@Test
42+
public void test_isPackageProtected_matchesOnlyPackageProtectedFields() throws NoSuchFieldException {
43+
Field publicField = ExampleFields.class.getDeclaredField("publicField");
44+
Field protectedField = ExampleFields.class.getDeclaredField("protectedField");
45+
Field packageProtectedField = ExampleFields.class.getDeclaredField("packageProtectedField");
46+
Field privateField = ExampleFields.class.getDeclaredField("privateField");
47+
48+
assertDoesNotMatch(isPackageProtected(), publicField);
49+
assertDoesNotMatch(isPackageProtected(), protectedField);
50+
assertMatches(isPackageProtected(), packageProtectedField);
51+
assertDoesNotMatch(isPackageProtected(), privateField);
52+
53+
assertDescription("is package-protected (no modifiers)", isPackageProtected());
54+
55+
assertMismatchDescription("was a public java.lang.reflect.Field", isPackageProtected(), publicField);
56+
assertMismatchDescription("was a protected java.lang.reflect.Field", isPackageProtected(), protectedField);
57+
assertMismatchDescription("was a private java.lang.reflect.Field", isPackageProtected(), privateField);
58+
}
59+
60+
61+
@Test
62+
public void test_isPackageProtected_matchesOnlyPackageProtectedMethods() throws NoSuchMethodException {
63+
Method publicMethod = ExampleMethods.class.getDeclaredMethod("publicMethod");
64+
Method protectedMethod = ExampleMethods.class.getDeclaredMethod("protectedMethod");
65+
Method packageProtectedMethod = ExampleMethods.class.getDeclaredMethod("packageProtectedMethod");
66+
Method privateMethod = ExampleMethods.class.getDeclaredMethod("privateMethod");
67+
68+
assertDoesNotMatch(isPackageProtected(), publicMethod);
69+
assertDoesNotMatch(isPackageProtected(), protectedMethod);
70+
assertMatches(isPackageProtected(), packageProtectedMethod);
71+
assertDoesNotMatch(isPackageProtected(), privateMethod);
72+
73+
assertDescription("is package-protected (no modifiers)", isPackageProtected());
74+
75+
assertMismatchDescription("was a public java.lang.reflect.Method", isPackageProtected(), publicMethod);
76+
assertMismatchDescription("was a protected java.lang.reflect.Method", isPackageProtected(), protectedMethod);
77+
assertMismatchDescription("was a private java.lang.reflect.Method", isPackageProtected(), privateMethod);
78+
}
79+
80+
@Test
81+
public void test_isPackageProtected_doesNotMatchNull() {
82+
assertDoesNotMatch(isPackageProtected(), null);
83+
84+
assertMismatchDescription("was null", isPackageProtected(), null);
85+
}
86+
87+
@Test
88+
public void test_isPackageProtected_doesNotMatchNonReflectiveElement() {
89+
assertDoesNotMatch(isPackageProtected(), new Object());
90+
91+
assertMismatchDescription("was java.lang.Object instead of a reflective element like a Class<T>, Constructor<T>, or Method", isPackageProtected(), new Object());
92+
}
93+
94+
public static class PublicClass {
95+
}
96+
97+
protected static class ProtectedClass {
98+
}
99+
100+
static class PackageProtectedClass {
101+
}
102+
103+
private static class PrivateClass {
104+
}
105+
106+
private static class ExampleFields {
107+
public Void publicField;
108+
protected Void protectedField;
109+
Void packageProtectedField;
110+
private Void privateField;
111+
}
112+
113+
private static class ExampleMethods {
114+
public void publicMethod() {
115+
}
116+
117+
protected void protectedMethod() {
118+
}
119+
120+
void packageProtectedMethod() {
121+
}
122+
123+
private void privateMethod() {
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)