Skip to content

Commit 6919275

Browse files
committed
Rebuild of PropertyAccessor
1 parent a7ad1e1 commit 6919275

File tree

8 files changed

+279
-227
lines changed

8 files changed

+279
-227
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public HasProperty(String propertyName) {
3030
@Override
3131
public boolean matchesSafely(T obj) {
3232
try {
33-
return PropertyUtil.getPropertyAccessor(propertyName, obj) != null;
33+
PropertyAccessor accessor = new PropertyAccessor(obj);
34+
return accessor.fieldNames().contains(propertyName);
3435
} catch (IllegalArgumentException e) {
3536
return false;
3637
}

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

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

99
import java.lang.reflect.InvocationTargetException;
1010
import java.lang.reflect.Method;
@@ -69,7 +69,7 @@
6969
*/
7070
public class HasPropertyWithValue<T> extends TypeSafeDiagnosingMatcher<T> {
7171

72-
private static final Condition.Step<PropertyAccessor, Method> WITH_READ_METHOD = withReadMethod();
72+
private static final Condition.Step<PropertyReadLens, Method> WITH_READ_METHOD = withReadMethod();
7373
private final String propertyName;
7474
private final Matcher<Object> valueMatcher;
7575
private final String messageFormat;
@@ -111,14 +111,14 @@ public void describeTo(Description description) {
111111
.appendDescriptionOf(valueMatcher).appendText(")");
112112
}
113113

114-
private Condition<PropertyAccessor> propertyOn(T bean, Description mismatch) {
115-
PropertyAccessor property = PropertyUtil.getPropertyAccessor(propertyName, bean);
116-
if (property == null) {
114+
private Condition<PropertyReadLens> propertyOn(T bean, Description mismatch) {
115+
PropertyAccessor accessor = new PropertyAccessor(bean);
116+
if (!accessor.fieldNames().contains(propertyName)) {
117117
mismatch.appendText("No property \"" + propertyName + "\"");
118118
return notMatched();
119119
}
120120

121-
return matched(property, mismatch);
121+
return matched(accessor.readLensFor(propertyName), mismatch);
122122
}
123123

124124
private Condition.Step<Method, Object> withPropertyValue(final T bean) {
@@ -144,11 +144,11 @@ private static Matcher<Object> nastyGenericsWorkaround(Matcher<?> valueMatcher)
144144
return (Matcher<Object>) valueMatcher;
145145
}
146146

147-
private static Condition.Step<PropertyAccessor, Method> withReadMethod() {
148-
return (accessor, mismatch) -> {
149-
final Method readMethod = accessor.readMethod();
147+
private static Condition.Step<PropertyReadLens, Method> withReadMethod() {
148+
return (readLens, mismatch) -> {
149+
final Method readMethod = readLens.getReadMethod();
150150
if (null == readMethod || readMethod.getReturnType() == void.class) {
151-
mismatch.appendText("property \"" + accessor.propertyName() + "\" is not readable");
151+
mismatch.appendText("property \"" + readLens.getName() + "\" is not readable");
152152
return notMatched();
153153
}
154154
return matched(readMethod, mismatch);
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
*/
22+
public class PropertyAccessor {
23+
private final Object beanLikeObject;
24+
private final SortedMap<String, PropertyReadLens> readLenses;
25+
26+
/**
27+
* Constructor.
28+
* @param beanLikeObject the object to search for properties.
29+
*/
30+
public PropertyAccessor(Object beanLikeObject) {
31+
this.beanLikeObject = beanLikeObject;
32+
this.readLenses = new TreeMap<>(makeLensesFor(beanLikeObject));
33+
}
34+
35+
private Map<String, PropertyReadLens> makeLensesFor(Object bean) {
36+
PropertyDescriptor[] properties = PropertyUtil.propertyDescriptorsFor(bean, Object.class);
37+
if (properties != null && properties.length > 0) {
38+
return makePropertyLensesFrom(properties);
39+
}
40+
41+
return makeFieldMethodLensesFor(bean);
42+
}
43+
44+
private Map<String, PropertyReadLens> makePropertyLensesFrom(PropertyDescriptor[] descriptors) {
45+
return Arrays.stream(descriptors)
46+
.map(pd -> new PropertyReadLens(pd.getDisplayName(), pd.getReadMethod()))
47+
.collect(Collectors.toMap(PropertyReadLens::getName, Function.identity()));
48+
}
49+
50+
private Map<String, PropertyReadLens> makeFieldMethodLensesFor(Object bean) {
51+
try {
52+
Set<String> fieldNames = getFieldNames(bean);
53+
MethodDescriptor[] methodDescriptors = Introspector.getBeanInfo(bean.getClass(), null).getMethodDescriptors();
54+
return Arrays.stream(methodDescriptors)
55+
.filter(IsPropertyAccessor.forOneOf(fieldNames))
56+
.map(md -> new PropertyReadLens(md.getDisplayName(), md.getMethod()))
57+
.collect(Collectors.toMap(PropertyReadLens::getName, Function.identity()));
58+
}
59+
catch (IntrospectionException e) {
60+
throw new IllegalArgumentException("Could not get method descriptors for " + bean.getClass(), e);
61+
}
62+
}
63+
64+
/**
65+
* The names of properties that were found in the object.
66+
* @return a set of field names
67+
*/
68+
public Set<String> fieldNames() {
69+
return readLenses.keySet();
70+
}
71+
72+
/**
73+
* The collection of lenses for all the properties that were found in the
74+
* object.
75+
* @return the collection of lenses
76+
*/
77+
public Collection<PropertyReadLens> readLenses() {
78+
return readLenses.values();
79+
}
80+
81+
/**
82+
* The read lens for the specified property.
83+
* @param propertyName the property to find the lens for.
84+
* @return the read lens for the property
85+
*/
86+
public PropertyReadLens readLensFor(String propertyName) {
87+
return readLenses.get(propertyName);
88+
}
89+
90+
/**
91+
* The value of the specified property.
92+
* @param propertyName the name of the property
93+
* @return the value of the given property name.
94+
*/
95+
public Object fieldValue(String propertyName) {
96+
PropertyReadLens lens = readLenses.get(propertyName);
97+
if (lens == null) {
98+
String message = String.format("Unknown property '%s' for bean '%s'", propertyName, beanLikeObject);
99+
throw new IllegalArgumentException(message);
100+
}
101+
return lens.getValue();
102+
}
103+
104+
/**
105+
* Returns the field names of the given object.
106+
* It can be the names of the record components of Java Records, for example.
107+
*
108+
* @param fromObj the object to check
109+
* @return The field names
110+
* @throws IllegalArgumentException if there's a security issue reading the fields
111+
*/
112+
private static Set<String> getFieldNames(Object fromObj) throws IllegalArgumentException {
113+
try {
114+
return Arrays.stream(fromObj.getClass().getDeclaredFields())
115+
.map(Field::getName)
116+
.collect(Collectors.toSet());
117+
} catch (SecurityException e) {
118+
throw new IllegalArgumentException("Could not get record component names for " + fromObj.getClass(), e);
119+
}
120+
}
121+
122+
123+
/**
124+
* Predicate that checks if a given {@link MethodDescriptor} corresponds to a field.
125+
* <p>
126+
* This predicate assumes a method is a field access if the method name exactly
127+
* matches the field name, takes no parameters and returns a non-void type.
128+
*/
129+
private static class IsPropertyAccessor implements Predicate<MethodDescriptor> {
130+
private final Set<String> propertyNames;
131+
132+
private IsPropertyAccessor(Set<String> propertyNames) {
133+
this.propertyNames = propertyNames;
134+
}
135+
136+
public static IsPropertyAccessor forOneOf(Set<String> propertyNames) {
137+
return new IsPropertyAccessor(propertyNames);
138+
}
139+
140+
@Override
141+
public boolean test(MethodDescriptor md) {
142+
return propertyNames.contains(md.getDisplayName()) &&
143+
md.getMethod().getReturnType() != void.class &&
144+
md.getMethod().getParameterCount() == 0;
145+
}
146+
}
147+
148+
/**
149+
* Encapsulates a property in the parent object.
150+
*/
151+
public class PropertyReadLens {
152+
private final String name;
153+
private final Method readMethod;
154+
155+
/**
156+
* Constructor.
157+
* @param name the name of the property
158+
* @param readMethod the method that can be used to get the value of the property
159+
*/
160+
public PropertyReadLens(String name, Method readMethod) {
161+
this.name = name;
162+
this.readMethod = readMethod;
163+
}
164+
165+
/**
166+
* The name of the property
167+
* @return the name of the property.
168+
*/
169+
public String getName() {
170+
return name;
171+
}
172+
173+
/**
174+
* The read method for the property.
175+
* @return the read method for the property.
176+
*/
177+
public Method getReadMethod() {
178+
return readMethod;
179+
}
180+
181+
/**
182+
* The value of the property.
183+
* @return the value of the property.
184+
*/
185+
public Object getValue() {
186+
Object bean = PropertyAccessor.this.beanLikeObject;
187+
try {
188+
return readMethod.invoke(bean, PropertyUtil.NO_ARGUMENTS);
189+
} catch (Exception e) {
190+
throw new IllegalArgumentException("Could not invoke " + readMethod + " on " + bean, e);
191+
}
192+
}
193+
}
194+
}

0 commit comments

Comments
 (0)