From a7ad1e1d94f4f353c775fb92e2e99e721644afb1 Mon Sep 17 00:00:00 2001 From: Joe Schmetzer Date: Sun, 1 Dec 2024 10:44:52 +1100 Subject: [PATCH] Add PropertyAccessor --- .../java/org/hamcrest/beans/HasProperty.java | 3 +- .../hamcrest/beans/HasPropertyWithValue.java | 20 ++-- .../java/org/hamcrest/beans/PropertyUtil.java | 108 +++++++++++------- .../hamcrest/beans/SamePropertyValuesAs.java | 71 +++++------- .../org/hamcrest/beans/PropertyUtilTest.java | 4 +- 5 files changed, 102 insertions(+), 104 deletions(-) diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java index 8494c9a3..7ad45149 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java @@ -30,8 +30,7 @@ public HasProperty(String propertyName) { @Override public boolean matchesSafely(T obj) { try { - return PropertyUtil.getPropertyDescriptor(propertyName, obj) != null || - PropertyUtil.getMethodDescriptor(propertyName, obj) != null; + return PropertyUtil.getPropertyAccessor(propertyName, obj) != null; } catch (IllegalArgumentException e) { return false; } diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java b/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java index 34d8a785..0424df14 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java @@ -4,10 +4,8 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.hamcrest.beans.PropertyUtil.PropertyAccessor; -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; @@ -71,7 +69,7 @@ */ public class HasPropertyWithValue extends TypeSafeDiagnosingMatcher { - private static final Condition.Step WITH_READ_METHOD = withReadMethod(); + private static final Condition.Step WITH_READ_METHOD = withReadMethod(); private final String propertyName; private final Matcher valueMatcher; private final String messageFormat; @@ -113,8 +111,8 @@ public void describeTo(Description description) { .appendDescriptionOf(valueMatcher).appendText(")"); } - private Condition propertyOn(T bean, Description mismatch) { - FeatureDescriptor property = PropertyUtil.getFeatureDescriptor(propertyName, bean); + private Condition propertyOn(T bean, Description mismatch) { + PropertyAccessor property = PropertyUtil.getPropertyAccessor(propertyName, bean); if (property == null) { mismatch.appendText("No property \"" + propertyName + "\""); return notMatched(); @@ -146,13 +144,11 @@ private static Matcher nastyGenericsWorkaround(Matcher valueMatcher) return (Matcher) valueMatcher; } - private static Condition.Step withReadMethod() { - return (property, mismatch) -> { - final Method readMethod = property instanceof PropertyDescriptor ? - ((PropertyDescriptor) property).getReadMethod() : - (((MethodDescriptor) property).getMethod()); + private static Condition.Step withReadMethod() { + return (accessor, mismatch) -> { + final Method readMethod = accessor.readMethod(); if (null == readMethod || readMethod.getReturnType() == void.class) { - mismatch.appendText("property \"" + property.getName() + "\" is not readable"); + mismatch.appendText("property \"" + accessor.propertyName() + "\" is not readable"); return notMatched(); } return matched(readMethod, mismatch); diff --git a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java index d95398d4..875b442c 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java @@ -2,13 +2,17 @@ import java.beans.*; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.Arrays; +import java.util.List; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; /** - * Utility class with static methods for accessing properties on JavaBean objects. + * Utility class with static methods for accessing properties on JavaBean objects, or bean-like + * objects such as records. + *

* See https://docs.oracle.com/javase/8/docs/technotes/guides/beans/index.html for * more information on JavaBeans. * @@ -22,20 +26,25 @@ public class PropertyUtil { private PropertyUtil() { } - public static FeatureDescriptor[] featureDescriptorsFor(T expectedBean) { - FeatureDescriptor[] descriptors = propertyDescriptorsFor(expectedBean, Object.class); + public static List propertyAccessorsFor(T bean) { + PropertyDescriptor[] descriptors = propertyDescriptorsFor(bean, Object.class); if (descriptors != null && descriptors.length > 0) { - return descriptors; + return Arrays.stream(descriptors) + .map(pd -> PropertyAccessor.fromProperty(bean, pd)) + .collect(Collectors.toList()); } - return recordReadAccessorMethodDescriptorsFor(expectedBean); + + MethodDescriptor[] methodDescriptors = fieldReadDescriptorsFor(bean); + return Arrays.stream(methodDescriptors) + .map(md -> PropertyAccessor.fromMethod(bean, md)) + .collect(Collectors.toList()); } - public static FeatureDescriptor getFeatureDescriptor(String propertyName, T bean) { - FeatureDescriptor property = getPropertyDescriptor(propertyName, bean); - if (property != null) { - return property; - } - return getMethodDescriptor(propertyName, bean); + public static PropertyAccessor getPropertyAccessor(String propertyName, T bean) { + return propertyAccessorsFor(bean).stream() + .filter(pa -> propertyName.equals(pa.propertyName())) + .findFirst() + .orElse(null); } /** @@ -73,29 +82,6 @@ public static PropertyDescriptor[] propertyDescriptorsFor(Object fromObj, Class< } } - /** - * Returns the description of the read accessor method with the provided - * name on the provided object's interface. - * This is what you need when you try to find a property from a target object - * when it doesn't follow standard JavaBean specification, a Java Record for example. - * - * @param propertyName the object property name. - * @param fromObj the object to check. - * @return the descriptor of the method, or null if the method does not exist. - * @throws IllegalArgumentException if there's an introspection failure - * @see Java Records - * - */ - public static MethodDescriptor getMethodDescriptor(String propertyName, Object fromObj) throws IllegalArgumentException { - for (MethodDescriptor method : recordReadAccessorMethodDescriptorsFor(fromObj)) { - if (method.getName().equals(propertyName)) { - return method; - } - } - - return null; - } - /** * Returns read accessor method descriptors for the class associated with the given object. * This is useful when you find getter methods for the fields from the object @@ -106,13 +92,13 @@ public static MethodDescriptor getMethodDescriptor(String propertyName, Object f * @return Method descriptors for read accessor methods * @throws IllegalArgumentException if there's an introspection failure */ - public static MethodDescriptor[] recordReadAccessorMethodDescriptorsFor(Object fromObj) throws IllegalArgumentException { + public static MethodDescriptor[] fieldReadDescriptorsFor(Object fromObj) throws IllegalArgumentException { try { Set fieldNames = getFieldNames(fromObj); MethodDescriptor[] methodDescriptors = Introspector.getBeanInfo(fromObj.getClass(), null).getMethodDescriptors(); return Arrays.stream(methodDescriptors) - .filter(IsFieldAccessor.forOneOf(fieldNames)) + .filter(IsPropertyAccessor.forOneOf(fieldNames)) .toArray(MethodDescriptor[]::new); } catch (IntrospectionException e) { throw new IllegalArgumentException("Could not get method descriptors for " + fromObj.getClass(), e); @@ -125,25 +111,61 @@ public static MethodDescriptor[] recordReadAccessorMethodDescriptorsFor(Object f * 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 { - private final Set fieldNames; + private static class IsPropertyAccessor implements Predicate { + private final Set propertyNames; - private IsFieldAccessor(Set fieldNames) { - this.fieldNames = fieldNames; + private IsPropertyAccessor(Set propertyNames) { + this.propertyNames = propertyNames; } - public static IsFieldAccessor forOneOf(Set fieldNames) { - return new IsFieldAccessor(fieldNames); + public static IsPropertyAccessor forOneOf(Set propertyNames) { + return new IsPropertyAccessor(propertyNames); } @Override public boolean test(MethodDescriptor md) { - return fieldNames.contains(md.getDisplayName()) && + return propertyNames.contains(md.getDisplayName()) && md.getMethod().getReturnType() != void.class && md.getMethod().getParameterCount() == 0; } } + public static final class PropertyAccessor { + private final Object theObject; + private final String propertyName; + private final Method readMethod; + + public PropertyAccessor(Object theObject, String propertyName, Method readMethod) { + this.theObject = theObject; + this.propertyName = propertyName; + this.readMethod = readMethod; + } + + public static PropertyAccessor fromProperty(Object theObject, PropertyDescriptor pd) { + return new PropertyAccessor(theObject, pd.getDisplayName(), pd.getReadMethod()); + } + + public static PropertyAccessor fromMethod(Object theObject, MethodDescriptor md) { + return new PropertyAccessor(theObject, md.getDisplayName(), md.getMethod()); + } + + public String propertyName() { + return propertyName; + } + + public Object propertyValue() { + try { + return readMethod.invoke(theObject, NO_ARGUMENTS); + } catch (Exception e) { + throw new IllegalArgumentException("Could not invoke " + readMethod + " on " + theObject, e); + } + } + + public Method readMethod() { + return readMethod; + } + } + /** * Returns the field names of the given object. * It can be the names of the record components of Java Records, for example. diff --git a/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java b/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java index 13583625..f89f125e 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java @@ -3,16 +3,13 @@ import org.hamcrest.Description; import org.hamcrest.DiagnosingMatcher; import org.hamcrest.Matcher; +import org.hamcrest.beans.PropertyUtil.PropertyAccessor; import java.beans.FeatureDescriptor; -import java.beans.MethodDescriptor; -import java.beans.PropertyDescriptor; -import java.lang.reflect.Method; import java.util.*; +import java.util.stream.Collectors; 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.core.IsEqual.equalTo; /** @@ -35,11 +32,11 @@ public class SamePropertyValuesAs extends DiagnosingMatcher { */ @SuppressWarnings("WeakerAccess") public SamePropertyValuesAs(T expectedBean, List ignoredProperties) { - FeatureDescriptor[] descriptors = PropertyUtil.featureDescriptorsFor(expectedBean); + List accessors = PropertyUtil.propertyAccessorsFor(expectedBean); this.expectedBean = expectedBean; this.ignoredFields = ignoredProperties; - this.propertyNames = propertyNamesFrom(descriptors, ignoredProperties); - this.propertyMatchers = propertyMatchersFor(expectedBean, descriptors, ignoredProperties); + this.propertyNames = propertyNamesFrom(accessors, ignoredProperties); + this.propertyMatchers = propertyMatchersFor(expectedBean, accessors, ignoredProperties); } @Override @@ -70,7 +67,8 @@ private boolean isCompatibleType(Object actual, Description mismatchDescription) } private boolean hasNoExtraProperties(Object actual, Description mismatchDescription) { - Set actualPropertyNames = propertyNamesFrom(propertyDescriptorsFor(actual, Object.class), ignoredFields); + List accessors = PropertyUtil.propertyAccessorsFor(actual); + Set actualPropertyNames = propertyNamesFrom(accessors, ignoredFields); actualPropertyNames.removeAll(propertyNames); if (!actualPropertyNames.isEmpty()) { mismatchDescription.appendText("has extra properties called " + actualPropertyNames); @@ -89,24 +87,18 @@ private boolean hasMatchingValues(Object actual, Description mismatchDescription return true; } - private static List propertyMatchersFor(T bean, FeatureDescriptor[] descriptors, List ignoredFields) { - List result = new ArrayList<>(descriptors.length); - for (FeatureDescriptor descriptor : descriptors) { - if (isNotIgnored(ignoredFields, descriptor)) { - result.add(new PropertyMatcher(descriptor, bean)); - } - } - return result; + private static List propertyMatchersFor(T bean, List accessors, List ignoredFields) { + return accessors.stream() + .filter(pa -> !ignoredFields.contains(pa.propertyName())) + .map(pa -> new PropertyMatcher(pa, bean)) + .collect(Collectors.toList()); } - private static Set propertyNamesFrom(FeatureDescriptor[] descriptors, List ignoredFields) { - HashSet result = new HashSet<>(); - for (FeatureDescriptor descriptor : descriptors) { - if (isNotIgnored(ignoredFields, descriptor)) { - result.add(descriptor.getDisplayName()); - } - } - return result; + private static Set propertyNamesFrom(List accessors, List ignoredFields) { + return accessors.stream() + .map(PropertyAccessor::propertyName) + .filter(name -> !ignoredFields.contains(name)) + .collect(Collectors.toSet()); } private static boolean isNotIgnored(List ignoredFields, FeatureDescriptor propertyDescriptor) { @@ -115,23 +107,20 @@ private static boolean isNotIgnored(List ignoredFields, FeatureDescripto @SuppressWarnings("WeakerAccess") private static class PropertyMatcher extends DiagnosingMatcher { - private final Method readMethod; + private final PropertyAccessor expectedAccessor; private final Matcher matcher; - private final String propertyName; - - public PropertyMatcher(FeatureDescriptor descriptor, Object expectedObject) { - this.propertyName = descriptor.getDisplayName(); - this.readMethod = descriptor instanceof PropertyDescriptor ? - ((PropertyDescriptor) descriptor).getReadMethod() : - ((MethodDescriptor) descriptor).getMethod(); - this.matcher = equalTo(readProperty(readMethod, expectedObject)); + + public PropertyMatcher(PropertyAccessor expectedAccessor, Object expectedObject) { + this.expectedAccessor = expectedAccessor; + this.matcher = equalTo(expectedAccessor.propertyValue()); } @Override public boolean matches(Object actual, Description mismatch) { - final Object actualValue = readProperty(readMethod, actual); + PropertyAccessor actualAccessor = PropertyUtil.getPropertyAccessor(expectedAccessor.propertyName(), actual); + Object actualValue = actualAccessor.propertyValue(); if (!matcher.matches(actualValue)) { - mismatch.appendText(propertyName + " "); + mismatch.appendText(expectedAccessor.propertyName() + " "); matcher.describeMismatch(actualValue, mismatch); return false; } @@ -140,15 +129,7 @@ public boolean matches(Object actual, Description mismatch) { @Override public void describeTo(Description description) { - description.appendText(propertyName + ": ").appendDescriptionOf(matcher); - } - } - - private static Object readProperty(Method method, Object target) { - try { - return method.invoke(target, NO_ARGUMENTS); - } catch (Exception e) { - throw new IllegalArgumentException("Could not invoke " + method + " on " + target, e); + description.appendText(expectedAccessor.propertyName() + ": ").appendDescriptionOf(matcher); } } diff --git a/hamcrest/src/test/java/org/hamcrest/beans/PropertyUtilTest.java b/hamcrest/src/test/java/org/hamcrest/beans/PropertyUtilTest.java index e6a01a3f..96f6f655 100644 --- a/hamcrest/src/test/java/org/hamcrest/beans/PropertyUtilTest.java +++ b/hamcrest/src/test/java/org/hamcrest/beans/PropertyUtilTest.java @@ -45,7 +45,7 @@ void testReturnsTheNamesOfAllFieldsFromTargetRecord() { void testReturnsArrayOfMethodDescriptorFromTargetClass() { SamePropertyValuesAsTest.ExampleBean input = new SamePropertyValuesAsTest.ExampleBean("test", 1, null); - MethodDescriptor[] output = PropertyUtil.recordReadAccessorMethodDescriptorsFor(input); + MethodDescriptor[] output = PropertyUtil.fieldReadDescriptorsFor(input); assertThat(output, arrayWithSize(0)); } @@ -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); + MethodDescriptor[] output = PropertyUtil.fieldReadDescriptorsFor(input); assertThat(output, arrayWithSize(5)); assertThat(Arrays.stream(output).map(MethodDescriptor::getDisplayName).collect(Collectors.toList()),