Skip to content

Replace bean FeatureDescriptor in PropertyUtils with PropertyAccessor #429

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public HasProperty(String propertyName) {
@Override
public boolean matchesSafely(T obj) {
try {
return PropertyUtil.getPropertyDescriptor(propertyName, obj) != null ||
PropertyUtil.getMethodDescriptor(propertyName, obj) != null;
PropertyAccessor accessor = new PropertyAccessor(obj);
return accessor.fieldNames().contains(propertyName);
} catch (IllegalArgumentException e) {
return false;
}
Expand Down
29 changes: 11 additions & 18 deletions hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.hamcrest.beans.PropertyAccessor.PropertyReadLens;

import java.beans.FeatureDescriptor;
import java.beans.MethodDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
Expand All @@ -16,7 +14,7 @@

import static org.hamcrest.Condition.matched;
import static org.hamcrest.Condition.notMatched;
import static org.hamcrest.beans.PropertyUtil.NO_ARGUMENTS;
import static org.hamcrest.beans.PropertyAccessor.NO_ARGUMENTS;

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

private static final Condition.Step<FeatureDescriptor, Method> WITH_READ_METHOD = withReadMethod();
private static final Condition.Step<PropertyReadLens, Method> WITH_READ_METHOD = withReadMethod();
private final String propertyName;
private final Matcher<Object> valueMatcher;
private final String messageFormat;
Expand Down Expand Up @@ -113,17 +111,14 @@ public void describeTo(Description description) {
.appendDescriptionOf(valueMatcher).appendText(")");
}

private Condition<FeatureDescriptor> propertyOn(T bean, Description mismatch) {
FeatureDescriptor property = PropertyUtil.getPropertyDescriptor(propertyName, bean);
if (property == null) {
property = PropertyUtil.getMethodDescriptor(propertyName, bean);
}
if (property == null) {
private Condition<PropertyReadLens> propertyOn(T bean, Description mismatch) {
PropertyAccessor accessor = new PropertyAccessor(bean);
if (!accessor.fieldNames().contains(propertyName)) {
mismatch.appendText("No property \"" + propertyName + "\"");
return notMatched();
}

return matched(property, mismatch);
return matched(accessor.readLensFor(propertyName), mismatch);
}

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

private static Condition.Step<FeatureDescriptor, Method> withReadMethod() {
return (property, mismatch) -> {
final Method readMethod = property instanceof PropertyDescriptor ?
((PropertyDescriptor) property).getReadMethod() :
(((MethodDescriptor) property).getMethod());
private static Condition.Step<PropertyReadLens, Method> withReadMethod() {
return (readLens, mismatch) -> {
final Method readMethod = readLens.getReadMethod();
if (null == readMethod || readMethod.getReturnType() == void.class) {
mismatch.appendText("property \"" + property.getName() + "\" is not readable");
mismatch.appendText("property \"" + readLens.getName() + "\" is not readable");
return notMatched();
}
return matched(readMethod, mismatch);
Expand Down
219 changes: 219 additions & 0 deletions hamcrest/src/main/java/org/hamcrest/beans/PropertyAccessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package org.hamcrest.beans;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.MethodDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
* Utility class to help with finding properties in an object.
* <p>
* The properties can be either properties as described by the
* JavaBean specification and APIs, or it will fall back to finding
* fields with corresponding methods, enabling the property matchers
* to work with newer classes like Records.
* <p>
* 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
* more information on JavaBeans.
*/
public class PropertyAccessor {
private final Object beanLikeObject;
private final SortedMap<String, PropertyReadLens> readLenses;

/**
* Constructor.
* @param beanLikeObject the object to search for properties.
*/
public PropertyAccessor(Object beanLikeObject) {
this.beanLikeObject = beanLikeObject;
this.readLenses = new TreeMap<>(makeLensesFor(beanLikeObject));
}

private Map<String, PropertyReadLens> makeLensesFor(Object bean) {
PropertyDescriptor[] properties = propertyDescriptorsFor(bean, Object.class);
if (properties != null && properties.length > 0) {
return makePropertyLensesFrom(properties);
}

return makeFieldMethodLensesFor(bean);
}

private Map<String, PropertyReadLens> makePropertyLensesFrom(PropertyDescriptor[] descriptors) {
return Arrays.stream(descriptors)
.map(pd -> new PropertyReadLens(pd.getDisplayName(), pd.getReadMethod()))
.collect(Collectors.toMap(PropertyReadLens::getName, Function.identity()));
}

private Map<String, PropertyReadLens> makeFieldMethodLensesFor(Object bean) {
try {
Set<String> fieldNames = getFieldNames(bean);
MethodDescriptor[] methodDescriptors = Introspector.getBeanInfo(bean.getClass(), null).getMethodDescriptors();
return Arrays.stream(methodDescriptors)
.filter(IsPropertyAccessor.forOneOf(fieldNames))
.map(md -> new PropertyReadLens(md.getDisplayName(), md.getMethod()))
.collect(Collectors.toMap(PropertyReadLens::getName, Function.identity()));
}
catch (IntrospectionException e) {
throw new IllegalArgumentException("Could not get method descriptors for " + bean.getClass(), e);
}
}

/**
* The names of properties that were found in the object.
* @return a set of field names
*/
public Set<String> fieldNames() {
return readLenses.keySet();
}

/**
* The collection of lenses for all the properties that were found in the
* object.
* @return the collection of lenses
*/
public Collection<PropertyReadLens> readLenses() {
return readLenses.values();
}

/**
* The read lens for the specified property.
* @param propertyName the property to find the lens for.
* @return the read lens for the property
*/
public PropertyReadLens readLensFor(String propertyName) {
return readLenses.get(propertyName);
}

/**
* The value of the specified property.
* @param propertyName the name of the property
* @return the value of the given property name.
*/
public Object fieldValue(String propertyName) {
PropertyReadLens lens = readLenses.get(propertyName);
if (lens == null) {
String message = String.format("Unknown property '%s' for bean '%s'", propertyName, beanLikeObject);
throw new IllegalArgumentException(message);
}
return lens.getValue();
}

/**
* Returns the field names of the given object.
* It can be the names of the record components of Java Records, for example.
*
* @param fromObj the object to check
* @return The field names
* @throws IllegalArgumentException if there's a security issue reading the fields
*/
private static Set<String> getFieldNames(Object fromObj) throws IllegalArgumentException {
try {
return Arrays.stream(fromObj.getClass().getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toSet());
} catch (SecurityException e) {
throw new IllegalArgumentException("Could not get record component names for " + fromObj.getClass(), e);
}
}


/**
* Predicate that checks if a given {@link MethodDescriptor} corresponds to a field.
* <p>
* This predicate assumes a method is a field access if the method name exactly
* matches the field name, takes no parameters and returns a non-void type.
*/
private static class IsPropertyAccessor implements Predicate<MethodDescriptor> {
private final Set<String> propertyNames;

private IsPropertyAccessor(Set<String> propertyNames) {
this.propertyNames = propertyNames;
}

public static IsPropertyAccessor forOneOf(Set<String> propertyNames) {
return new IsPropertyAccessor(propertyNames);
}

@Override
public boolean test(MethodDescriptor md) {
return propertyNames.contains(md.getDisplayName()) &&
md.getMethod().getReturnType() != void.class &&
md.getMethod().getParameterCount() == 0;
}
}

/**
* Encapsulates a property in the parent object.
*/
public class PropertyReadLens {
private final String name;
private final Method readMethod;

/**
* Constructor.
* @param name the name of the property
* @param readMethod the method that can be used to get the value of the property
*/
public PropertyReadLens(String name, Method readMethod) {
this.name = name;
this.readMethod = readMethod;
}

/**
* The name of the property
* @return the name of the property.
*/
public String getName() {
return name;
}

/**
* The read method for the property.
* @return the read method for the property.
*/
public Method getReadMethod() {
return readMethod;
}

/**
* The value of the property.
* @return the value of the property.
*/
public Object getValue() {
Object bean = PropertyAccessor.this.beanLikeObject;
try {
return readMethod.invoke(bean, NO_ARGUMENTS);
} catch (Exception e) {
throw new IllegalArgumentException("Could not invoke " + readMethod + " on " + bean, e);
}
}
}

/**
* Returns all the property descriptors for the class associated with the given object
*
* @param fromObj Use the class of this object
* @param stopClass Don't include any properties from this ancestor class upwards.
* @return Property descriptors
* @throws IllegalArgumentException if there's a introspection failure
*/
public static PropertyDescriptor[] propertyDescriptorsFor(Object fromObj, Class<Object> stopClass) throws IllegalArgumentException {
try {
return Introspector.getBeanInfo(fromObj.getClass(), stopClass).getPropertyDescriptors();
} catch (IntrospectionException e) {
throw new IllegalArgumentException("Could not get property descriptors for " + fromObj.getClass(), e);
}
}

/**
* Empty object array, used for documenting that we are deliberately passing no arguments to a method.
*/
public static final Object[] NO_ARGUMENTS = new Object[0];

}
Loading