Skip to content

Commit

Permalink
Refactor to feature descriptor in PropertyUtil
Browse files Browse the repository at this point in the history
  • Loading branch information
tumbarumba committed Nov 30, 2024
1 parent 3d58e99 commit d023d54
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,7 @@ public void describeTo(Description description) {
}

private Condition<FeatureDescriptor> propertyOn(T bean, Description mismatch) {
FeatureDescriptor property = PropertyUtil.getPropertyDescriptor(propertyName, bean);
if (property == null) {
property = PropertyUtil.getMethodDescriptor(propertyName, bean);
}
FeatureDescriptor property = PropertyUtil.getFeatureDescriptor(propertyName, bean);
if (property == null) {
mismatch.appendText("No property \"" + propertyName + "\"");
return notMatched();
Expand Down
66 changes: 50 additions & 16 deletions hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package org.hamcrest.beans;

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

/**
Expand All @@ -24,14 +22,28 @@ public class PropertyUtil {
private PropertyUtil() {
}

public static <T> FeatureDescriptor[] featureDescriptorsFor(T expectedBean) {
FeatureDescriptor[] descriptors = propertyDescriptorsFor(expectedBean, Object.class);
if (descriptors != null && descriptors.length > 0) {
return descriptors;
}
return recordReadAccessorMethodDescriptorsFor(expectedBean);
}

public static <T> FeatureDescriptor getFeatureDescriptor(String propertyName, T bean) {
FeatureDescriptor property = getPropertyDescriptor(propertyName, bean);
if (property != null) {
return property;
}
return getMethodDescriptor(propertyName, bean);
}

/**
* Returns the description of the property with the provided
* name on the provided object's interface.
*
* @param propertyName
* the bean property name.
* @param fromObj
* the object to check.
* @param propertyName the bean property name.
* @param fromObj the object to check.
* @return the descriptor of the property, or null if the property does not exist.
* @throws IllegalArgumentException if there's an introspection failure
*/
Expand Down Expand Up @@ -75,7 +87,7 @@ public static PropertyDescriptor[] propertyDescriptorsFor(Object fromObj, Class<
*
*/
public static MethodDescriptor getMethodDescriptor(String propertyName, Object fromObj) throws IllegalArgumentException {
for (MethodDescriptor method : recordReadAccessorMethodDescriptorsFor(fromObj, null)) {
for (MethodDescriptor method : recordReadAccessorMethodDescriptorsFor(fromObj)) {
if (method.getName().equals(propertyName)) {
return method;
}
Expand All @@ -91,25 +103,47 @@ public static MethodDescriptor getMethodDescriptor(String propertyName, Object f
* Be careful as this doesn't return standard JavaBean getter methods, like a method starting with {@code get-}.
*
* @param fromObj Use the class of this object
* @param stopClass Don't include any properties from this ancestor class upwards.
* @return Method descriptors for read accessor methods
* @throws IllegalArgumentException if there's an introspection failure
*/
public static MethodDescriptor[] recordReadAccessorMethodDescriptorsFor(Object fromObj, Class<Object> stopClass) throws IllegalArgumentException {
public static MethodDescriptor[] recordReadAccessorMethodDescriptorsFor(Object fromObj) throws IllegalArgumentException {
try {
Set<String> recordComponentNames = getFieldNames(fromObj);
MethodDescriptor[] methodDescriptors = Introspector.getBeanInfo(fromObj.getClass(), stopClass).getMethodDescriptors();
Set<String> fieldNames = getFieldNames(fromObj);
MethodDescriptor[] methodDescriptors = Introspector.getBeanInfo(fromObj.getClass(), null).getMethodDescriptors();

return Arrays.stream(methodDescriptors)
.filter(x -> recordComponentNames.contains(x.getDisplayName()))
.filter(x -> x.getMethod().getReturnType() != void.class)
.filter(x -> x.getMethod().getParameterCount() == 0)
.filter(IsFieldAccessor.forOneOf(fieldNames))
.toArray(MethodDescriptor[]::new);
} catch (IntrospectionException e) {
throw new IllegalArgumentException("Could not get method descriptors 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 IsFieldAccessor implements Predicate<MethodDescriptor> {
private final Set<String> fieldNames;

private IsFieldAccessor(Set<String> fieldNames) {
this.fieldNames = fieldNames;
}

public static IsFieldAccessor forOneOf(Set<String> fieldNames) {
return new IsFieldAccessor(fieldNames);
}

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

/**
* Returns the field names of the given object.
* It can be the names of the record components of Java Records, for example.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import static java.util.Arrays.asList;
import static org.hamcrest.beans.PropertyUtil.NO_ARGUMENTS;
import static org.hamcrest.beans.PropertyUtil.propertyDescriptorsFor;
import static org.hamcrest.beans.PropertyUtil.recordReadAccessorMethodDescriptorsFor;
import static org.hamcrest.core.IsEqual.equalTo;

/**
Expand All @@ -36,11 +35,7 @@ public class SamePropertyValuesAs<T> extends DiagnosingMatcher<T> {
*/
@SuppressWarnings("WeakerAccess")
public SamePropertyValuesAs(T expectedBean, List<String> ignoredProperties) {
FeatureDescriptor[] descriptors = propertyDescriptorsFor(expectedBean, Object.class);
if (descriptors == null || descriptors.length == 0) {
descriptors = recordReadAccessorMethodDescriptorsFor(expectedBean, Object.class);
}

FeatureDescriptor[] descriptors = PropertyUtil.featureDescriptorsFor(expectedBean);
this.expectedBean = expectedBean;
this.ignoredFields = ignoredProperties;
this.propertyNames = propertyNamesFrom(descriptors, ignoredProperties);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ void testReturnsTheNamesOfAllFieldsFromTargetRecord() {
void testReturnsArrayOfMethodDescriptorFromTargetClass() {
SamePropertyValuesAsTest.ExampleBean input = new SamePropertyValuesAsTest.ExampleBean("test", 1, null);

MethodDescriptor[] output = PropertyUtil.recordReadAccessorMethodDescriptorsFor(input, Object.class);
MethodDescriptor[] output = PropertyUtil.recordReadAccessorMethodDescriptorsFor(input);

assertThat(output, arrayWithSize(0));
}
Expand All @@ -56,7 +56,7 @@ void testReturnsArrayOfMethodDescriptorFromTargetRecord() {
RecordLikeClass.SmallClass smallClass2 = new RecordLikeClass.SmallClass("small", 3, BigDecimal.ONE, LocalDateTime.of(2024, 1, 2, 3, 4, 5));
RecordLikeClass input = new RecordLikeClass("uno", 22, true, new Long[] {1L, 2L, 3L}, new ArrayList<>(Arrays.asList(smallClass1, smallClass2)));

MethodDescriptor[] output = PropertyUtil.recordReadAccessorMethodDescriptorsFor(input, Object.class);
MethodDescriptor[] output = PropertyUtil.recordReadAccessorMethodDescriptorsFor(input);

assertThat(output, arrayWithSize(5));
assertThat(Arrays.stream(output).map(MethodDescriptor::getDisplayName).collect(Collectors.toList()),
Expand Down

0 comments on commit d023d54

Please sign in to comment.