Skip to content

Commit b8c1bf9

Browse files
authored
Merge pull request #3 from tumbarumba/property-util-tweaks
Property util tweaks
2 parents 10fc9dc + 6205e14 commit b8c1bf9

File tree

8 files changed

+301
-199
lines changed

8 files changed

+301
-199
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ public HasProperty(String propertyName) {
3030
@Override
3131
public boolean matchesSafely(T obj) {
3232
try {
33-
return PropertyUtil.getPropertyDescriptor(propertyName, obj) != null ||
34-
PropertyUtil.getMethodDescriptor(propertyName, obj) != null;
33+
PropertyAccessor accessor = new PropertyAccessor(obj);
34+
return accessor.fieldNames().contains(propertyName);
3535
} catch (IllegalArgumentException e) {
3636
return false;
3737
}

hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
import org.hamcrest.Description;
55
import org.hamcrest.Matcher;
66
import org.hamcrest.TypeSafeDiagnosingMatcher;
7+
import org.hamcrest.beans.PropertyAccessor.PropertyReadLens;
78

8-
import java.beans.FeatureDescriptor;
9-
import java.beans.MethodDescriptor;
10-
import java.beans.PropertyDescriptor;
119
import java.lang.reflect.InvocationTargetException;
1210
import java.lang.reflect.Method;
1311
import java.util.Arrays;
@@ -16,7 +14,7 @@
1614

1715
import static org.hamcrest.Condition.matched;
1816
import static org.hamcrest.Condition.notMatched;
19-
import static org.hamcrest.beans.PropertyUtil.NO_ARGUMENTS;
17+
import static org.hamcrest.beans.PropertyAccessor.NO_ARGUMENTS;
2018

2119
/**
2220
* <p>A matcher that checks if an object has a JavaBean property with the
@@ -71,7 +69,7 @@
7169
*/
7270
public class HasPropertyWithValue<T> extends TypeSafeDiagnosingMatcher<T> {
7371

74-
private static final Condition.Step<FeatureDescriptor, Method> WITH_READ_METHOD = withReadMethod();
72+
private static final Condition.Step<PropertyReadLens, Method> WITH_READ_METHOD = withReadMethod();
7573
private final String propertyName;
7674
private final Matcher<Object> valueMatcher;
7775
private final String messageFormat;
@@ -113,17 +111,14 @@ public void describeTo(Description description) {
113111
.appendDescriptionOf(valueMatcher).appendText(")");
114112
}
115113

116-
private Condition<FeatureDescriptor> propertyOn(T bean, Description mismatch) {
117-
FeatureDescriptor property = PropertyUtil.getPropertyDescriptor(propertyName, bean);
118-
if (property == null) {
119-
property = PropertyUtil.getMethodDescriptor(propertyName, bean);
120-
}
121-
if (property == null) {
114+
private Condition<PropertyReadLens> propertyOn(T bean, Description mismatch) {
115+
PropertyAccessor accessor = new PropertyAccessor(bean);
116+
if (!accessor.fieldNames().contains(propertyName)) {
122117
mismatch.appendText("No property \"" + propertyName + "\"");
123118
return notMatched();
124119
}
125120

126-
return matched(property, mismatch);
121+
return matched(accessor.readLensFor(propertyName), mismatch);
127122
}
128123

129124
private Condition.Step<Method, Object> withPropertyValue(final T bean) {
@@ -149,13 +144,11 @@ private static Matcher<Object> nastyGenericsWorkaround(Matcher<?> valueMatcher)
149144
return (Matcher<Object>) valueMatcher;
150145
}
151146

152-
private static Condition.Step<FeatureDescriptor, Method> withReadMethod() {
153-
return (property, mismatch) -> {
154-
final Method readMethod = property instanceof PropertyDescriptor ?
155-
((PropertyDescriptor) property).getReadMethod() :
156-
(((MethodDescriptor) property).getMethod());
147+
private static Condition.Step<PropertyReadLens, Method> withReadMethod() {
148+
return (readLens, mismatch) -> {
149+
final Method readMethod = readLens.getReadMethod();
157150
if (null == readMethod || readMethod.getReturnType() == void.class) {
158-
mismatch.appendText("property \"" + property.getName() + "\" is not readable");
151+
mismatch.appendText("property \"" + readLens.getName() + "\" is not readable");
159152
return notMatched();
160153
}
161154
return matched(readMethod, mismatch);
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package org.hamcrest.beans;
2+
3+
import java.beans.IntrospectionException;
4+
import java.beans.Introspector;
5+
import java.beans.MethodDescriptor;
6+
import java.beans.PropertyDescriptor;
7+
import java.lang.reflect.Field;
8+
import java.lang.reflect.Method;
9+
import java.util.*;
10+
import java.util.function.Function;
11+
import java.util.function.Predicate;
12+
import java.util.stream.Collectors;
13+
14+
/**
15+
* Utility class to help with finding properties in an object.
16+
* <p>
17+
* The properties can be either properties as described by the
18+
* JavaBean specification and APIs, or it will fall back to finding
19+
* fields with corresponding methods, enabling the property matchers
20+
* to work with newer classes like Records.
21+
* <p>
22+
* See <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/beans/index.html">https://docs.oracle.com/javase/8/docs/technotes/guides/beans/index.html</a> for
23+
* more information on JavaBeans.
24+
*/
25+
public class PropertyAccessor {
26+
private final Object beanLikeObject;
27+
private final SortedMap<String, PropertyReadLens> readLenses;
28+
29+
/**
30+
* Constructor.
31+
* @param beanLikeObject the object to search for properties.
32+
*/
33+
public PropertyAccessor(Object beanLikeObject) {
34+
this.beanLikeObject = beanLikeObject;
35+
this.readLenses = new TreeMap<>(makeLensesFor(beanLikeObject));
36+
}
37+
38+
private Map<String, PropertyReadLens> makeLensesFor(Object bean) {
39+
PropertyDescriptor[] properties = propertyDescriptorsFor(bean, Object.class);
40+
if (properties != null && properties.length > 0) {
41+
return makePropertyLensesFrom(properties);
42+
}
43+
44+
return makeFieldMethodLensesFor(bean);
45+
}
46+
47+
private Map<String, PropertyReadLens> makePropertyLensesFrom(PropertyDescriptor[] descriptors) {
48+
return Arrays.stream(descriptors)
49+
.map(pd -> new PropertyReadLens(pd.getDisplayName(), pd.getReadMethod()))
50+
.collect(Collectors.toMap(PropertyReadLens::getName, Function.identity()));
51+
}
52+
53+
private Map<String, PropertyReadLens> makeFieldMethodLensesFor(Object bean) {
54+
try {
55+
Set<String> fieldNames = getFieldNames(bean);
56+
MethodDescriptor[] methodDescriptors = Introspector.getBeanInfo(bean.getClass(), null).getMethodDescriptors();
57+
return Arrays.stream(methodDescriptors)
58+
.filter(IsPropertyAccessor.forOneOf(fieldNames))
59+
.map(md -> new PropertyReadLens(md.getDisplayName(), md.getMethod()))
60+
.collect(Collectors.toMap(PropertyReadLens::getName, Function.identity()));
61+
}
62+
catch (IntrospectionException e) {
63+
throw new IllegalArgumentException("Could not get method descriptors for " + bean.getClass(), e);
64+
}
65+
}
66+
67+
/**
68+
* The names of properties that were found in the object.
69+
* @return a set of field names
70+
*/
71+
public Set<String> fieldNames() {
72+
return readLenses.keySet();
73+
}
74+
75+
/**
76+
* The collection of lenses for all the properties that were found in the
77+
* object.
78+
* @return the collection of lenses
79+
*/
80+
public Collection<PropertyReadLens> readLenses() {
81+
return readLenses.values();
82+
}
83+
84+
/**
85+
* The read lens for the specified property.
86+
* @param propertyName the property to find the lens for.
87+
* @return the read lens for the property
88+
*/
89+
public PropertyReadLens readLensFor(String propertyName) {
90+
return readLenses.get(propertyName);
91+
}
92+
93+
/**
94+
* The value of the specified property.
95+
* @param propertyName the name of the property
96+
* @return the value of the given property name.
97+
*/
98+
public Object fieldValue(String propertyName) {
99+
PropertyReadLens lens = readLenses.get(propertyName);
100+
if (lens == null) {
101+
String message = String.format("Unknown property '%s' for bean '%s'", propertyName, beanLikeObject);
102+
throw new IllegalArgumentException(message);
103+
}
104+
return lens.getValue();
105+
}
106+
107+
/**
108+
* Returns the field names of the given object.
109+
* It can be the names of the record components of Java Records, for example.
110+
*
111+
* @param fromObj the object to check
112+
* @return The field names
113+
* @throws IllegalArgumentException if there's a security issue reading the fields
114+
*/
115+
private static Set<String> getFieldNames(Object fromObj) throws IllegalArgumentException {
116+
try {
117+
return Arrays.stream(fromObj.getClass().getDeclaredFields())
118+
.map(Field::getName)
119+
.collect(Collectors.toSet());
120+
} catch (SecurityException e) {
121+
throw new IllegalArgumentException("Could not get record component names for " + fromObj.getClass(), e);
122+
}
123+
}
124+
125+
126+
/**
127+
* Predicate that checks if a given {@link MethodDescriptor} corresponds to a field.
128+
* <p>
129+
* This predicate assumes a method is a field access if the method name exactly
130+
* matches the field name, takes no parameters and returns a non-void type.
131+
*/
132+
private static class IsPropertyAccessor implements Predicate<MethodDescriptor> {
133+
private final Set<String> propertyNames;
134+
135+
private IsPropertyAccessor(Set<String> propertyNames) {
136+
this.propertyNames = propertyNames;
137+
}
138+
139+
public static IsPropertyAccessor forOneOf(Set<String> propertyNames) {
140+
return new IsPropertyAccessor(propertyNames);
141+
}
142+
143+
@Override
144+
public boolean test(MethodDescriptor md) {
145+
return propertyNames.contains(md.getDisplayName()) &&
146+
md.getMethod().getReturnType() != void.class &&
147+
md.getMethod().getParameterCount() == 0;
148+
}
149+
}
150+
151+
/**
152+
* Encapsulates a property in the parent object.
153+
*/
154+
public class PropertyReadLens {
155+
private final String name;
156+
private final Method readMethod;
157+
158+
/**
159+
* Constructor.
160+
* @param name the name of the property
161+
* @param readMethod the method that can be used to get the value of the property
162+
*/
163+
public PropertyReadLens(String name, Method readMethod) {
164+
this.name = name;
165+
this.readMethod = readMethod;
166+
}
167+
168+
/**
169+
* The name of the property
170+
* @return the name of the property.
171+
*/
172+
public String getName() {
173+
return name;
174+
}
175+
176+
/**
177+
* The read method for the property.
178+
* @return the read method for the property.
179+
*/
180+
public Method getReadMethod() {
181+
return readMethod;
182+
}
183+
184+
/**
185+
* The value of the property.
186+
* @return the value of the property.
187+
*/
188+
public Object getValue() {
189+
Object bean = PropertyAccessor.this.beanLikeObject;
190+
try {
191+
return readMethod.invoke(bean, NO_ARGUMENTS);
192+
} catch (Exception e) {
193+
throw new IllegalArgumentException("Could not invoke " + readMethod + " on " + bean, e);
194+
}
195+
}
196+
}
197+
198+
/**
199+
* Returns all the property descriptors for the class associated with the given object
200+
*
201+
* @param fromObj Use the class of this object
202+
* @param stopClass Don't include any properties from this ancestor class upwards.
203+
* @return Property descriptors
204+
* @throws IllegalArgumentException if there's a introspection failure
205+
*/
206+
public static PropertyDescriptor[] propertyDescriptorsFor(Object fromObj, Class<Object> stopClass) throws IllegalArgumentException {
207+
try {
208+
return Introspector.getBeanInfo(fromObj.getClass(), stopClass).getPropertyDescriptors();
209+
} catch (IntrospectionException e) {
210+
throw new IllegalArgumentException("Could not get property descriptors for " + fromObj.getClass(), e);
211+
}
212+
}
213+
214+
/**
215+
* Empty object array, used for documenting that we are deliberately passing no arguments to a method.
216+
*/
217+
public static final Object[] NO_ARGUMENTS = new Object[0];
218+
219+
}

0 commit comments

Comments
 (0)