From 6919275bf3814c536128372a835c832aed5493cf Mon Sep 17 00:00:00 2001 From: Joe Schmetzer Date: Sun, 1 Dec 2024 14:18:48 +1100 Subject: [PATCH] Rebuild of PropertyAccessor --- .../java/org/hamcrest/beans/HasProperty.java | 3 +- .../hamcrest/beans/HasPropertyWithValue.java | 20 +- .../org/hamcrest/beans/PropertyAccessor.java | 194 ++++++++++++++++++ .../java/org/hamcrest/beans/PropertyUtil.java | 149 +------------- .../hamcrest/beans/SamePropertyValuesAs.java | 47 +++-- .../beans/HasPropertyWithValueTest.java | 15 +- ...tilTest.java => PropertyAccessorTest.java} | 74 +++---- .../beans/SamePropertyValuesAsTest.java | 4 +- 8 files changed, 279 insertions(+), 227 deletions(-) create mode 100644 hamcrest/src/main/java/org/hamcrest/beans/PropertyAccessor.java rename hamcrest/src/test/java/org/hamcrest/beans/{PropertyUtilTest.java => PropertyAccessorTest.java} (60%) diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java index 7ad45149..59f351b1 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java @@ -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; } diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java b/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java index 0424df14..006dda7d 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java @@ -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; @@ -69,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; @@ -111,14 +111,14 @@ public void describeTo(Description description) { .appendDescriptionOf(valueMatcher).appendText(")"); } - private Condition propertyOn(T bean, Description mismatch) { - PropertyAccessor property = PropertyUtil.getPropertyAccessor(propertyName, bean); - if (property == null) { + private Condition 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 withPropertyValue(final T bean) { @@ -144,11 +144,11 @@ private static Matcher nastyGenericsWorkaround(Matcher valueMatcher) return (Matcher) valueMatcher; } - private static Condition.Step withReadMethod() { - return (accessor, mismatch) -> { - final Method readMethod = accessor.readMethod(); + private static Condition.Step 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); diff --git a/hamcrest/src/main/java/org/hamcrest/beans/PropertyAccessor.java b/hamcrest/src/main/java/org/hamcrest/beans/PropertyAccessor.java new file mode 100644 index 00000000..6726d787 --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/beans/PropertyAccessor.java @@ -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. + *

+ * 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 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 makeLensesFor(Object bean) { + PropertyDescriptor[] properties = PropertyUtil.propertyDescriptorsFor(bean, Object.class); + if (properties != null && properties.length > 0) { + return makePropertyLensesFrom(properties); + } + + return makeFieldMethodLensesFor(bean); + } + + private Map makePropertyLensesFrom(PropertyDescriptor[] descriptors) { + return Arrays.stream(descriptors) + .map(pd -> new PropertyReadLens(pd.getDisplayName(), pd.getReadMethod())) + .collect(Collectors.toMap(PropertyReadLens::getName, Function.identity())); + } + + private Map makeFieldMethodLensesFor(Object bean) { + try { + Set 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 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 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 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. + *

+ * 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 { + private final Set propertyNames; + + private IsPropertyAccessor(Set propertyNames) { + this.propertyNames = propertyNames; + } + + public static IsPropertyAccessor forOneOf(Set 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); + } + } + } +} diff --git a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java index 875b442c..71b7dcea 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java @@ -1,24 +1,16 @@ package org.hamcrest.beans; -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; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; /** - * Utility class with static methods for accessing properties on JavaBean objects, or bean-like - * objects such as records. - *

+ * Utility class with static methods for accessing properties on JavaBean objects. * See https://docs.oracle.com/javase/8/docs/technotes/guides/beans/index.html for * more information on JavaBeans. * * @author Iain McGinniss * @author Steve Freeman - * @author Uno Kim * @since 1.1.0 */ public class PropertyUtil { @@ -26,35 +18,16 @@ public class PropertyUtil { private PropertyUtil() { } - public static List propertyAccessorsFor(T bean) { - PropertyDescriptor[] descriptors = propertyDescriptorsFor(bean, Object.class); - if (descriptors != null && descriptors.length > 0) { - return Arrays.stream(descriptors) - .map(pd -> PropertyAccessor.fromProperty(bean, pd)) - .collect(Collectors.toList()); - } - - MethodDescriptor[] methodDescriptors = fieldReadDescriptorsFor(bean); - return Arrays.stream(methodDescriptors) - .map(md -> PropertyAccessor.fromMethod(bean, md)) - .collect(Collectors.toList()); - } - - public static PropertyAccessor getPropertyAccessor(String propertyName, T bean) { - return propertyAccessorsFor(bean).stream() - .filter(pa -> propertyName.equals(pa.propertyName())) - .findFirst() - .orElse(null); - } - /** * 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 + * @throws IllegalArgumentException if there's a introspection failure */ public static PropertyDescriptor getPropertyDescriptor(String propertyName, Object fromObj) throws IllegalArgumentException { for (PropertyDescriptor property : propertyDescriptorsFor(fromObj, null)) { @@ -72,7 +45,7 @@ public static PropertyDescriptor getPropertyDescriptor(String propertyName, Obje * @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 an introspection failure + * @throws IllegalArgumentException if there's a introspection failure */ public static PropertyDescriptor[] propertyDescriptorsFor(Object fromObj, Class stopClass) throws IllegalArgumentException { try { @@ -82,108 +55,6 @@ public static PropertyDescriptor[] propertyDescriptorsFor(Object fromObj, Class< } } - /** - * 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 - * when it doesn't follow standard JavaBean specification, a Java Record for example. - * 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 - * @return Method descriptors for read accessor methods - * @throws IllegalArgumentException if there's an introspection failure - */ - 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(IsPropertyAccessor.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. - *

- * 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 { - private final Set propertyNames; - - private IsPropertyAccessor(Set propertyNames) { - this.propertyNames = propertyNames; - } - - public static IsPropertyAccessor forOneOf(Set 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; - } - } - - 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. - * - * @param fromObj the object to check - * @return The field names - * @throws IllegalArgumentException if there's a security issue reading the fields - */ - public static Set 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); - } - } - /** * Empty object array, used for documenting that we are deliberately passing no arguments to a method. */ diff --git a/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java b/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java index f89f125e..58425eee 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java @@ -3,7 +3,7 @@ import org.hamcrest.Description; import org.hamcrest.DiagnosingMatcher; import org.hamcrest.Matcher; -import org.hamcrest.beans.PropertyUtil.PropertyAccessor; +import org.hamcrest.beans.PropertyAccessor.PropertyReadLens; import java.beans.FeatureDescriptor; import java.util.*; @@ -32,11 +32,11 @@ public class SamePropertyValuesAs extends DiagnosingMatcher { */ @SuppressWarnings("WeakerAccess") public SamePropertyValuesAs(T expectedBean, List ignoredProperties) { - List accessors = PropertyUtil.propertyAccessorsFor(expectedBean); + PropertyAccessor accessor = new PropertyAccessor(expectedBean); this.expectedBean = expectedBean; this.ignoredFields = ignoredProperties; - this.propertyNames = propertyNamesFrom(accessors, ignoredProperties); - this.propertyMatchers = propertyMatchersFor(expectedBean, accessors, ignoredProperties); + this.propertyNames = propertyNamesFrom(accessor, ignoredProperties); + this.propertyMatchers = propertyMatchersFor(expectedBean, accessor, ignoredProperties); } @Override @@ -67,9 +67,9 @@ private boolean isCompatibleType(Object actual, Description mismatchDescription) } private boolean hasNoExtraProperties(Object actual, Description mismatchDescription) { - List accessors = PropertyUtil.propertyAccessorsFor(actual); - Set actualPropertyNames = propertyNamesFrom(accessors, ignoredFields); - actualPropertyNames.removeAll(propertyNames); + PropertyAccessor accessor = new PropertyAccessor(actual); + Set actualPropertyNames = propertyNamesFrom(accessor, ignoredFields); + propertyNames.forEach(actualPropertyNames::remove); if (!actualPropertyNames.isEmpty()) { mismatchDescription.appendText("has extra properties called " + actualPropertyNames); return false; @@ -87,18 +87,17 @@ private boolean hasMatchingValues(Object actual, Description mismatchDescription return true; } - 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)) + private static List propertyMatchersFor(T bean, PropertyAccessor accessor, List ignoredFields) { + return accessor.readLenses().stream() + .filter(lens -> !ignoredFields.contains(lens.getName())) + .map(lens -> new PropertyMatcher(lens, bean)) .collect(Collectors.toList()); } - private static Set propertyNamesFrom(List accessors, List ignoredFields) { - return accessors.stream() - .map(PropertyAccessor::propertyName) - .filter(name -> !ignoredFields.contains(name)) - .collect(Collectors.toSet()); + private static Set propertyNamesFrom(PropertyAccessor accessor, List ignoredFields) { + Set fieldNames = new HashSet<>(accessor.fieldNames()); + ignoredFields.forEach(fieldNames::remove); + return fieldNames; } private static boolean isNotIgnored(List ignoredFields, FeatureDescriptor propertyDescriptor) { @@ -107,20 +106,20 @@ private static boolean isNotIgnored(List ignoredFields, FeatureDescripto @SuppressWarnings("WeakerAccess") private static class PropertyMatcher extends DiagnosingMatcher { - private final PropertyAccessor expectedAccessor; + private final PropertyReadLens expectedReadLens; private final Matcher matcher; - public PropertyMatcher(PropertyAccessor expectedAccessor, Object expectedObject) { - this.expectedAccessor = expectedAccessor; - this.matcher = equalTo(expectedAccessor.propertyValue()); + public PropertyMatcher(PropertyReadLens expectedReadLens, Object expectedObject) { + this.expectedReadLens = expectedReadLens; + this.matcher = equalTo(expectedReadLens.getValue()); } @Override public boolean matches(Object actual, Description mismatch) { - PropertyAccessor actualAccessor = PropertyUtil.getPropertyAccessor(expectedAccessor.propertyName(), actual); - Object actualValue = actualAccessor.propertyValue(); + PropertyAccessor actualAccessor = new PropertyAccessor(actual); + Object actualValue = actualAccessor.fieldValue(expectedReadLens.getName()); if (!matcher.matches(actualValue)) { - mismatch.appendText(expectedAccessor.propertyName() + " "); + mismatch.appendText(expectedReadLens.getName()+ " "); matcher.describeMismatch(actualValue, mismatch); return false; } @@ -129,7 +128,7 @@ public boolean matches(Object actual, Description mismatch) { @Override public void describeTo(Description description) { - description.appendText(expectedAccessor.propertyName() + ": ").appendDescriptionOf(matcher); + description.appendText(expectedReadLens.getName() + ": ").appendDescriptionOf(matcher); } } diff --git a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java index a7dcc00d..ca538cde 100644 --- a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java +++ b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java @@ -1,6 +1,8 @@ package org.hamcrest.beans; -import org.hamcrest.*; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.StringDescription; import org.hamcrest.core.IsEqual; import org.hamcrest.test.AbstractMatcherTest; import org.junit.jupiter.api.Test; @@ -11,12 +13,12 @@ import java.util.Objects; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.test.MatcherAssertions.*; import static org.hamcrest.Matchers.is; import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; import static org.hamcrest.beans.HasPropertyWithValue.hasPropertyAtPath; import static org.hamcrest.core.IsAnything.anything; import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.test.MatcherAssertions.*; import static org.junit.jupiter.api.Assertions.assertEquals; /** @@ -75,9 +77,9 @@ public void testMatchesRecordLikeBeanWithoutInfoWithMatchedNamedBooleanProperty( @Test public void testMatchesBeanWithInfoWithMatchedNamedProperty() { - assertMatches("with bean info", hasProperty("property", equalTo("with info")), beanWithInfo); + assertMatches(hasProperty("property", equalTo("with info")), beanWithInfo); assertMismatchDescription("property 'property' was \"with info\"", - hasProperty("property", equalTo("without info")), beanWithInfo); + hasProperty("property", equalTo("different")), beanWithInfo); } @Test @@ -253,7 +255,10 @@ public static class BeanWithInfo { public String property() { return propertyValue; } } - public static class BeanWithInfoBeanInfo extends SimpleBeanInfo { // TODO: No usage. Can be removed. + /** + * Used by bean introspector + */ + public static class BeanWithInfoBeanInfo extends SimpleBeanInfo { @Override public PropertyDescriptor[] getPropertyDescriptors() { try { diff --git a/hamcrest/src/test/java/org/hamcrest/beans/PropertyUtilTest.java b/hamcrest/src/test/java/org/hamcrest/beans/PropertyAccessorTest.java similarity index 60% rename from hamcrest/src/test/java/org/hamcrest/beans/PropertyUtilTest.java rename to hamcrest/src/test/java/org/hamcrest/beans/PropertyAccessorTest.java index 96f6f655..cfed2185 100644 --- a/hamcrest/src/test/java/org/hamcrest/beans/PropertyUtilTest.java +++ b/hamcrest/src/test/java/org/hamcrest/beans/PropertyAccessorTest.java @@ -2,71 +2,54 @@ import org.junit.jupiter.api.Test; -import java.beans.MethodDescriptor; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; -import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.hasItem; -class PropertyUtilTest { +class PropertyAccessorTest { @Test - void testReturnsTheNamesOfAllFieldsFromTargetClass() { + void testAccessesAllFieldsFromBean() { SamePropertyValuesAsTest.ExampleBean input = new SamePropertyValuesAsTest.ExampleBean("test", 1, null); + PropertyAccessor accessor = new PropertyAccessor(input); - Set output = PropertyUtil.getFieldNames(input); + Set fields = accessor.fieldNames(); - assertThat(output, hasSize(3)); - assertThat(output, hasItems("stringProperty", "intProperty", "valueProperty")); - assertThat(output, not(hasItem("nonexistentField"))); - } - - @Test - void testReturnsTheNamesOfAllFieldsFromTargetRecord() { - RecordLikeClass.SmallClass smallClass1 = new RecordLikeClass.SmallClass(); - 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))); - - Set output = PropertyUtil.getFieldNames(input); - - assertThat(output, hasSize(5)); - assertThat(output, hasItems("numberArray", "test", "smallClasses", "name", "age")); - assertThat(output, not(hasItem("notAGetter1"))); - assertThat(output, not(hasItem("notAGetter2"))); - assertThat(output, not(hasItem("getAge"))); - assertThat(output, not(hasItem("field1"))); - assertThat(output, not(hasItem("nonexistentField"))); - } - - @Test - void testReturnsArrayOfMethodDescriptorFromTargetClass() { - SamePropertyValuesAsTest.ExampleBean input = new SamePropertyValuesAsTest.ExampleBean("test", 1, null); + assertThat(fields, hasSize(3)); + assertThat(fields, hasItems("stringProperty", "intProperty", "valueProperty")); + assertThat(fields, not(hasItem("nonexistentField"))); - MethodDescriptor[] output = PropertyUtil.fieldReadDescriptorsFor(input); - - assertThat(output, arrayWithSize(0)); + assertThat(accessor.fieldValue("stringProperty"), equalTo("test")); + assertThat(accessor.fieldValue("intProperty"), equalTo(1)); + assertThat(accessor.fieldValue("valueProperty"), equalTo(null)); } @Test - void testReturnsArrayOfMethodDescriptorFromTargetRecord() { + void testReturnsTheNamesOfAllFieldsFromRecordLikeObject() { RecordLikeClass.SmallClass smallClass1 = new RecordLikeClass.SmallClass(); 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))); + RecordLikeClass input = new RecordLikeClass("uno", 22, true, new Long[] {1L, 2L, 3L}, Arrays.asList(smallClass1, smallClass2)); + PropertyAccessor accessor = new PropertyAccessor(input); - MethodDescriptor[] output = PropertyUtil.fieldReadDescriptorsFor(input); + Set fields = accessor.fieldNames(); - assertThat(output, arrayWithSize(5)); - assertThat(Arrays.stream(output).map(MethodDescriptor::getDisplayName).collect(Collectors.toList()), - hasItems("numberArray", "test", "smallClasses", "name", "age")); - } + assertThat(fields, hasSize(5)); + assertThat(fields, hasItems("numberArray", "test", "smallClasses", "name", "age")); + assertThat(fields, not(hasItem("notAGetter1"))); + assertThat(fields, not(hasItem("notAGetter2"))); + assertThat(fields, not(hasItem("field1"))); + assertThat(fields, not(hasItem("nonexistentField"))); + assertThat(accessor.fieldValue("name"), equalTo("uno")); + } /** * A Java Record-like class to test the functionality of - * {@link PropertyUtil} with Java Records in JDK 8 environment. + * {@link PropertyAccessor} with Java Records in JDK 8 environment. * * @see https://docs.oracle.com/en/java/javase/17/language/records.html */ @@ -76,9 +59,9 @@ static final class RecordLikeClass { private final int age; private final boolean test; private final Long[] numberArray; - private final List smallClasses; + private final List smallClasses; - public RecordLikeClass(String name, int age, boolean test, Long[] numberArray, List smallClasses) { + public RecordLikeClass(String name, int age, boolean test, Long[] numberArray, List smallClasses) { this.name = name; this.age = age; this.test = test; @@ -90,13 +73,12 @@ public RecordLikeClass(String name, int age, boolean test, Long[] numberArray, L public int age() { return age; } public boolean test() { return test; } public Long[] numberArray() { return numberArray; } - public List smallClasses() { return smallClasses; } + public List smallClasses() { return smallClasses; } public void notAGetter1() {} public String notAGetter2() { return "I'm nothing"; } public String name(String fake1, String fake2) { return name; } public void name(String fake1) {} - public int getAge() { return 0; } @Override public boolean equals(Object o) { @@ -147,4 +129,4 @@ public String toString() { } } -} +} \ No newline at end of file diff --git a/hamcrest/src/test/java/org/hamcrest/beans/SamePropertyValuesAsTest.java b/hamcrest/src/test/java/org/hamcrest/beans/SamePropertyValuesAsTest.java index bd5f1796..7df26392 100644 --- a/hamcrest/src/test/java/org/hamcrest/beans/SamePropertyValuesAsTest.java +++ b/hamcrest/src/test/java/org/hamcrest/beans/SamePropertyValuesAsTest.java @@ -107,14 +107,14 @@ public void testDescribesItself() { "same property values as ExampleBean [intProperty: <1>, stringProperty: \"same\", valueProperty: ]", samePropertyValuesAs(expectedBean)); assertDescription( - "same property values as ExampleRecord [valueProperty: , stringProperty: \"same\", intProperty: <1>]", + "same property values as ExampleRecord [intProperty: <1>, stringProperty: \"same\", valueProperty: ]", samePropertyValuesAs(expectedRecord)); assertDescription( "same property values as ExampleBean [intProperty: <1>, stringProperty: \"same\", valueProperty: ] ignoring [\"ignored1\", \"ignored2\"]", samePropertyValuesAs(expectedBean, "ignored1", "ignored2")); assertDescription( - "same property values as ExampleRecord [valueProperty: , stringProperty: \"same\", intProperty: <1>] ignoring [\"ignored1\", \"ignored2\"]", + "same property values as ExampleRecord [intProperty: <1>, stringProperty: \"same\", valueProperty: ] ignoring [\"ignored1\", \"ignored2\"]", samePropertyValuesAs(expectedRecord, "ignored1", "ignored2")); }