Skip to content

Commit

Permalink
Rebuild of PropertyAccessor
Browse files Browse the repository at this point in the history
  • Loading branch information
tumbarumba committed Dec 1, 2024
1 parent a7ad1e1 commit 6919275
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 227 deletions.
3 changes: 2 additions & 1 deletion hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public HasProperty(String propertyName) {
@Override
public boolean matchesSafely(T obj) {
try {
return PropertyUtil.getPropertyAccessor(propertyName, obj) != null;
PropertyAccessor accessor = new PropertyAccessor(obj);
return accessor.fieldNames().contains(propertyName);
} catch (IllegalArgumentException e) {
return false;
}
Expand Down
20 changes: 10 additions & 10 deletions hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.hamcrest.beans.PropertyUtil.PropertyAccessor;
import org.hamcrest.beans.PropertyAccessor.PropertyReadLens;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
Expand Down Expand Up @@ -69,7 +69,7 @@
*/
public class HasPropertyWithValue<T> extends TypeSafeDiagnosingMatcher<T> {

private static final Condition.Step<PropertyAccessor, 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 @@ -111,14 +111,14 @@ public void describeTo(Description description) {
.appendDescriptionOf(valueMatcher).appendText(")");
}

private Condition<PropertyAccessor> propertyOn(T bean, Description mismatch) {
PropertyAccessor property = PropertyUtil.getPropertyAccessor(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 @@ -144,11 +144,11 @@ private static Matcher<Object> nastyGenericsWorkaround(Matcher<?> valueMatcher)
return (Matcher<Object>) valueMatcher;
}

private static Condition.Step<PropertyAccessor, Method> withReadMethod() {
return (accessor, mismatch) -> {
final Method readMethod = accessor.readMethod();
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 \"" + accessor.propertyName() + "\" is not readable");
mismatch.appendText("property \"" + readLens.getName() + "\" is not readable");
return notMatched();
}
return matched(readMethod, mismatch);
Expand Down
194 changes: 194 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,194 @@
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.
*/
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 = PropertyUtil.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, PropertyUtil.NO_ARGUMENTS);
} catch (Exception e) {
throw new IllegalArgumentException("Could not invoke " + readMethod + " on " + bean, e);
}
}
}
}
Loading

0 comments on commit 6919275

Please sign in to comment.