diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java index 8494c9a3..59f351b1 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java @@ -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; } diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java b/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java index f45c1264..c65f460f 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.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; @@ -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; /** *

A matcher that checks if an object has a JavaBean property with the @@ -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,17 +111,14 @@ public void describeTo(Description description) { .appendDescriptionOf(valueMatcher).appendText(")"); } - private Condition propertyOn(T bean, Description mismatch) { - FeatureDescriptor property = PropertyUtil.getPropertyDescriptor(propertyName, bean); - if (property == null) { - property = PropertyUtil.getMethodDescriptor(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) { @@ -149,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 (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); 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..4dd414cd --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/beans/PropertyAccessor.java @@ -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. + *

+ * 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. + *

+ * See https://docs.oracle.com/javase/8/docs/technotes/guides/beans/index.html for + * more information on JavaBeans. + */ +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 = 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, 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 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]; + +} diff --git a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java index 40d5a37d..1971ebab 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java @@ -2,12 +2,7 @@ import java.beans.IntrospectionException; import java.beans.Introspector; -import java.beans.MethodDescriptor; import java.beans.PropertyDescriptor; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; /** * Utility class with static methods for accessing properties on JavaBean objects. @@ -16,8 +11,8 @@ * * @author Iain McGinniss * @author Steve Freeman - * @author Uno Kim * @since 1.1.0 + * @deprecated Replaced by {@link PropertyAccessor} */ public class PropertyUtil { @@ -33,7 +28,7 @@ private PropertyUtil() { * @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)) { @@ -51,7 +46,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 { @@ -61,73 +56,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, null)) { - 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 - * 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 - * @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 stopClass) throws IllegalArgumentException { - try { - Set recordComponentNames = getFieldNames(fromObj); - MethodDescriptor[] methodDescriptors = Introspector.getBeanInfo(fromObj.getClass(), stopClass).getMethodDescriptors(); - - return Arrays.stream(methodDescriptors) - .filter(x -> recordComponentNames.contains(x.getDisplayName())) - .filter(x -> x.getMethod().getReturnType() != void.class) - .filter(x -> x.getMethod().getParameterCount() == 0) - .toArray(MethodDescriptor[]::new); - } catch (IntrospectionException e) { - throw new IllegalArgumentException("Could not get method descriptors for " + fromObj.getClass(), e); - } - } - - /** - * 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 fbb0175f..58425eee 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java @@ -3,17 +3,13 @@ import org.hamcrest.Description; import org.hamcrest.DiagnosingMatcher; import org.hamcrest.Matcher; +import org.hamcrest.beans.PropertyAccessor.PropertyReadLens; 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.beans.PropertyUtil.recordReadAccessorMethodDescriptorsFor; import static org.hamcrest.core.IsEqual.equalTo; /** @@ -36,15 +32,11 @@ public class SamePropertyValuesAs extends DiagnosingMatcher { */ @SuppressWarnings("WeakerAccess") public SamePropertyValuesAs(T expectedBean, List ignoredProperties) { - FeatureDescriptor[] descriptors = propertyDescriptorsFor(expectedBean, Object.class); - if (descriptors == null || descriptors.length == 0) { - descriptors = recordReadAccessorMethodDescriptorsFor(expectedBean, Object.class); - } - + PropertyAccessor accessor = new PropertyAccessor(expectedBean); this.expectedBean = expectedBean; this.ignoredFields = ignoredProperties; - this.propertyNames = propertyNamesFrom(descriptors, ignoredProperties); - this.propertyMatchers = propertyMatchersFor(expectedBean, descriptors, ignoredProperties); + this.propertyNames = propertyNamesFrom(accessor, ignoredProperties); + this.propertyMatchers = propertyMatchersFor(expectedBean, accessor, ignoredProperties); } @Override @@ -75,8 +67,9 @@ private boolean isCompatibleType(Object actual, Description mismatchDescription) } private boolean hasNoExtraProperties(Object actual, Description mismatchDescription) { - Set actualPropertyNames = propertyNamesFrom(propertyDescriptorsFor(actual, Object.class), 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; @@ -94,24 +87,17 @@ 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, 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(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(PropertyAccessor accessor, List ignoredFields) { + Set fieldNames = new HashSet<>(accessor.fieldNames()); + ignoredFields.forEach(fieldNames::remove); + return fieldNames; } private static boolean isNotIgnored(List ignoredFields, FeatureDescriptor propertyDescriptor) { @@ -120,23 +106,20 @@ private static boolean isNotIgnored(List ignoredFields, FeatureDescripto @SuppressWarnings("WeakerAccess") private static class PropertyMatcher extends DiagnosingMatcher { - private final Method readMethod; + private final PropertyReadLens expectedReadLens; 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(PropertyReadLens expectedReadLens, Object expectedObject) { + this.expectedReadLens = expectedReadLens; + this.matcher = equalTo(expectedReadLens.getValue()); } @Override public boolean matches(Object actual, Description mismatch) { - final Object actualValue = readProperty(readMethod, actual); + PropertyAccessor actualAccessor = new PropertyAccessor(actual); + Object actualValue = actualAccessor.fieldValue(expectedReadLens.getName()); if (!matcher.matches(actualValue)) { - mismatch.appendText(propertyName + " "); + mismatch.appendText(expectedReadLens.getName()+ " "); matcher.describeMismatch(actualValue, mismatch); return false; } @@ -145,15 +128,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(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 59% rename from hamcrest/src/test/java/org/hamcrest/beans/PropertyUtilTest.java rename to hamcrest/src/test/java/org/hamcrest/beans/PropertyAccessorTest.java index bc9ba6a0..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.recordReadAccessorMethodDescriptorsFor(input, Object.class); - - 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.recordReadAccessorMethodDescriptorsFor(input, Object.class); + 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")); }