From 820157714dcd28773f922c4a43eee186857f6776 Mon Sep 17 00:00:00 2001 From: Uno Kim Date: Sun, 10 Nov 2024 16:22:52 +0900 Subject: [PATCH 01/10] Add `MethodUtil` to make `hasProperty()` work for Java Records The current `hasProperty()` matcher can't deal with Java Records, as they are not compatible with JavaBeans specification. In this change, `HasProperty` first tries to do the original thing and then tries to find a method of which name is equivalent to the given property name, if there's no matching property. So for example, if the class has a getter method named `property()`, `hasProperty()` would regard it as the class has a property named `property`. I hope this change might be properly and easily removed when the time comes and Hamcrest starts to support Java 17. --- .../java/org/hamcrest/beans/HasProperty.java | 3 +- .../java/org/hamcrest/beans/MethodUtil.java | 58 +++++++++++++++++++ .../org/hamcrest/beans/HasPropertyTest.java | 5 ++ .../beans/HasPropertyWithValueTest.java | 39 +++++++++++++ 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java index cce9a8b3..87fb0792 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java @@ -31,7 +31,8 @@ public HasProperty(String propertyName) { @Override public boolean matchesSafely(T obj) { try { - return PropertyUtil.getPropertyDescriptor(propertyName, obj) != null; + return PropertyUtil.getPropertyDescriptor(propertyName, obj) != null || + MethodUtil.getMethodDescriptor(propertyName, obj) != null; } catch (IllegalArgumentException e) { return false; } diff --git a/hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java b/hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java new file mode 100644 index 00000000..be92edc4 --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java @@ -0,0 +1,58 @@ +package org.hamcrest.beans; + +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.MethodDescriptor; + +/** + * Utility class with static methods for accessing methods on regular Java objects. + * This utility class is mainly used to get property information from Java Records + * as they don't conform to the JavaBeans conventions. + * This might not be necessary in the next major release of Hamcrest which includes JDK upgrade to 17. + * + * @see PropertyUtil + * @see More information on JavaBeans + * @see Java Records + * @author Uno Kim + * @since 1.3.2 + */ +public class MethodUtil { + + private MethodUtil() {} + + /** + * Returns the description of the method with the provided + * name on the provided object's interface. + * + * @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 + */ + public static MethodDescriptor getMethodDescriptor(String propertyName, Object fromObj) throws IllegalArgumentException { + for (MethodDescriptor method : methodDescriptorsFor(fromObj, null)) { + if (method.getName().equals(propertyName)) { + return method; + } + } + + return null; + } + + /** + * Returns all the method 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 Method descriptors + * @throws IllegalArgumentException if there's an introspection failure + */ + public static MethodDescriptor[] methodDescriptorsFor(Object fromObj, Class stopClass) throws IllegalArgumentException { + try { + return Introspector.getBeanInfo(fromObj.getClass(), stopClass).getMethodDescriptors(); + } catch (IntrospectionException e) { + throw new IllegalArgumentException("Could not get method descriptors for " + fromObj.getClass(), e); + } + } + +} diff --git a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyTest.java b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyTest.java index 5143a3ad..8ba1f225 100644 --- a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyTest.java +++ b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyTest.java @@ -16,6 +16,7 @@ public final class HasPropertyTest { private final HasPropertyWithValueTest.BeanWithoutInfo bean = new HasPropertyWithValueTest.BeanWithoutInfo("a bean", false); + private final HasPropertyWithValueTest.RecordLikeBeanWithoutInfo record = new HasPropertyWithValueTest.RecordLikeBeanWithoutInfo("a record", false); @Test public void copesWithNullsAndUnknownTypes() { @@ -28,11 +29,13 @@ public final class HasPropertyTest { @Test public void matchesWhenThePropertyExists() { assertMatches(hasProperty("writeOnlyProperty"), bean); + assertMatches(hasProperty("property"), record); } @Test public void doesNotMatchIfPropertyDoesNotExist() { assertDoesNotMatch(hasProperty("aNonExistentProp"), bean); + assertDoesNotMatch(hasProperty("aNonExistentProp"), record); } @Test public void @@ -44,6 +47,8 @@ public final class HasPropertyTest { describesAMismatch() { assertMismatchDescription("no \"aNonExistentProp\" in <[Person: a bean]>", hasProperty("aNonExistentProp"), bean); + assertMismatchDescription("no \"aNonExistentProp\" in <[Person: a record]>", + hasProperty("aNonExistentProp"), record); } } diff --git a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java index 6fec083f..1b53d3ea 100644 --- a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java +++ b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java @@ -8,6 +8,7 @@ import java.beans.IntrospectionException; import java.beans.PropertyDescriptor; import java.beans.SimpleBeanInfo; +import java.util.Objects; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.test.MatcherAssertions.*; @@ -157,6 +158,44 @@ public String toString() { } } + /** + * A Java Record-like class to test the functionality of + * {@link org.hamcrest.beans.HasProperty#hasProperty(String) hasProperty(String)} + * with Java Records in JDK 8 environment. + * + * @see https://docs.oracle.com/en/java/javase/17/language/records.html + */ + public static final class RecordLikeBeanWithoutInfo { + private final String property; + private final boolean booleanProperty; + + public RecordLikeBeanWithoutInfo(String property, boolean booleanProperty) { + this.property = property; + this.booleanProperty = booleanProperty; + } + + public String property() { return this.property; } + public boolean booleanProperty() { return this.booleanProperty; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RecordLikeBeanWithoutInfo)) return false; + RecordLikeBeanWithoutInfo that = (RecordLikeBeanWithoutInfo) o; + return Objects.equals(this.property, that.property) && this.booleanProperty == that.booleanProperty; + } + + @Override + public int hashCode() { + return Objects.hash(property, booleanProperty); + } + + @Override + public String toString() { + return "[Person: " + property + "]"; + } + } + @SuppressWarnings("WeakerAccess") public static class BeanWithInner { private final Object inner; From f71c10bf8d5dca3e745e440e4f32b10671f4d578 Mon Sep 17 00:00:00 2001 From: Uno Kim Date: Sun, 10 Nov 2024 16:23:14 +0900 Subject: [PATCH 02/10] Fix typo in javadoc of `PropertyUtil` --- hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java index 71b7dcea..b85d94ac 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java @@ -27,7 +27,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 a introspection failure + * @throws IllegalArgumentException if there's an introspection failure */ public static PropertyDescriptor getPropertyDescriptor(String propertyName, Object fromObj) throws IllegalArgumentException { for (PropertyDescriptor property : propertyDescriptorsFor(fromObj, null)) { @@ -45,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 a introspection failure + * @throws IllegalArgumentException if there's an introspection failure */ public static PropertyDescriptor[] propertyDescriptorsFor(Object fromObj, Class stopClass) throws IllegalArgumentException { try { From b1809a2a0dabac656bea55584c5d63fae10c5a7a Mon Sep 17 00:00:00 2001 From: Uno Kim Date: Sun, 10 Nov 2024 17:24:11 +0900 Subject: [PATCH 03/10] Fix `MethodUtil` to see if a method is a getter There is a flaw in 8201577, which is that the logic can't distinguish if the found method is a getter. Let's put a simple additional rule: The property must be a getter, so it should return something. --- hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java | 2 +- hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java | 5 +++-- .../src/test/java/org/hamcrest/beans/HasPropertyTest.java | 1 + .../java/org/hamcrest/beans/HasPropertyWithValueTest.java | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java index 87fb0792..276d6505 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java @@ -32,7 +32,7 @@ public HasProperty(String propertyName) { public boolean matchesSafely(T obj) { try { return PropertyUtil.getPropertyDescriptor(propertyName, obj) != null || - MethodUtil.getMethodDescriptor(propertyName, obj) != null; + MethodUtil.getMethodDescriptor(propertyName, obj, true) != null; } catch (IllegalArgumentException e) { return false; } diff --git a/hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java b/hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java index be92edc4..e3cc9feb 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java @@ -26,12 +26,13 @@ private MethodUtil() {} * * @param propertyName the object property name. * @param fromObj the object to check. + * @param isNonVoid whether the method is non-void, which means the method has a return value. * @return the descriptor of the method, or null if the method does not exist. * @throws IllegalArgumentException if there's an introspection failure */ - public static MethodDescriptor getMethodDescriptor(String propertyName, Object fromObj) throws IllegalArgumentException { + public static MethodDescriptor getMethodDescriptor(String propertyName, Object fromObj, boolean isNonVoid) throws IllegalArgumentException { for (MethodDescriptor method : methodDescriptorsFor(fromObj, null)) { - if (method.getName().equals(propertyName)) { + if (method.getName().equals(propertyName) && (!isNonVoid || method.getMethod().getReturnType() != void.class)) { return method; } } diff --git a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyTest.java b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyTest.java index 8ba1f225..9a5b06d4 100644 --- a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyTest.java +++ b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyTest.java @@ -36,6 +36,7 @@ public final class HasPropertyTest { doesNotMatchIfPropertyDoesNotExist() { assertDoesNotMatch(hasProperty("aNonExistentProp"), bean); assertDoesNotMatch(hasProperty("aNonExistentProp"), record); + assertDoesNotMatch(hasProperty("notAGetterMethod"), record); } @Test public void diff --git a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java index 1b53d3ea..b040ba39 100644 --- a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java +++ b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java @@ -176,6 +176,7 @@ public RecordLikeBeanWithoutInfo(String property, boolean booleanProperty) { public String property() { return this.property; } public boolean booleanProperty() { return this.booleanProperty; } + public void notAGetterMethod() {} @Override public boolean equals(Object o) { From 587ca1221d5449398a2e4d290ea69263d60e2d75 Mon Sep 17 00:00:00 2001 From: Uno Kim Date: Sun, 10 Nov 2024 17:24:45 +0900 Subject: [PATCH 04/10] Polish unused imports in `HasProperty` --- hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java | 1 - 1 file changed, 1 deletion(-) diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java index 276d6505..c752026e 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java @@ -3,7 +3,6 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; -import org.hamcrest.collection.ArrayMatching; /** * A matcher that checks if an object has a JavaBean property with the From 958682ef2c75b9c58320e1b0871d7d58c6664116 Mon Sep 17 00:00:00 2001 From: Uno Kim Date: Tue, 19 Nov 2024 02:50:48 +0900 Subject: [PATCH 05/10] Move functionalities of `MethodUtil` to `PropertyUtil` After the debate of pr #426, We decided to move all the methods of `MethodUtil` to `PropertyUtil`. There was a confusion of understanding the name of the class `PropertyUtil`. First I thought it was kind of a technical name indicating that `PropertyUtil` deals with java beans property. But actually it wasn't just a technical name. `PropertyUtil` means a tool to find out some properties in the target class. So to say, it is like a `PropertyFindingUtil`. We don't have to change the utility name even if it finds some methods of the target class, not properties of it, as it does what it is meant to do. --- .../java/org/hamcrest/beans/HasProperty.java | 2 +- .../java/org/hamcrest/beans/MethodUtil.java | 59 ------------------- .../java/org/hamcrest/beans/PropertyUtil.java | 42 +++++++++++++ 3 files changed, 43 insertions(+), 60 deletions(-) delete mode 100644 hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java index c752026e..6bcebe41 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java @@ -31,7 +31,7 @@ public HasProperty(String propertyName) { public boolean matchesSafely(T obj) { try { return PropertyUtil.getPropertyDescriptor(propertyName, obj) != null || - MethodUtil.getMethodDescriptor(propertyName, obj, true) != null; + PropertyUtil.getMethodDescriptor(propertyName, obj, true) != null; } catch (IllegalArgumentException e) { return false; } diff --git a/hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java b/hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java deleted file mode 100644 index e3cc9feb..00000000 --- a/hamcrest/src/main/java/org/hamcrest/beans/MethodUtil.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.hamcrest.beans; - -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.MethodDescriptor; - -/** - * Utility class with static methods for accessing methods on regular Java objects. - * This utility class is mainly used to get property information from Java Records - * as they don't conform to the JavaBeans conventions. - * This might not be necessary in the next major release of Hamcrest which includes JDK upgrade to 17. - * - * @see PropertyUtil - * @see More information on JavaBeans - * @see Java Records - * @author Uno Kim - * @since 1.3.2 - */ -public class MethodUtil { - - private MethodUtil() {} - - /** - * Returns the description of the method with the provided - * name on the provided object's interface. - * - * @param propertyName the object property name. - * @param fromObj the object to check. - * @param isNonVoid whether the method is non-void, which means the method has a return value. - * @return the descriptor of the method, or null if the method does not exist. - * @throws IllegalArgumentException if there's an introspection failure - */ - public static MethodDescriptor getMethodDescriptor(String propertyName, Object fromObj, boolean isNonVoid) throws IllegalArgumentException { - for (MethodDescriptor method : methodDescriptorsFor(fromObj, null)) { - if (method.getName().equals(propertyName) && (!isNonVoid || method.getMethod().getReturnType() != void.class)) { - return method; - } - } - - return null; - } - - /** - * Returns all the method 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 Method descriptors - * @throws IllegalArgumentException if there's an introspection failure - */ - public static MethodDescriptor[] methodDescriptorsFor(Object fromObj, Class stopClass) throws IllegalArgumentException { - try { - return Introspector.getBeanInfo(fromObj.getClass(), stopClass).getMethodDescriptors(); - } catch (IntrospectionException e) { - throw new IllegalArgumentException("Could not get method descriptors for " + fromObj.getClass(), e); - } - } - -} diff --git a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java index b85d94ac..f80f8497 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java @@ -2,6 +2,7 @@ import java.beans.IntrospectionException; import java.beans.Introspector; +import java.beans.MethodDescriptor; import java.beans.PropertyDescriptor; /** @@ -11,6 +12,7 @@ * * @author Iain McGinniss * @author Steve Freeman + * @author Uno Kim * @since 1.1.0 */ public class PropertyUtil { @@ -55,6 +57,46 @@ public static PropertyDescriptor[] propertyDescriptorsFor(Object fromObj, Class< } } + /** + * Returns the description of the 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. + * @param isNonVoid whether the method is non-void, which means the method has a return value. + * @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, boolean isNonVoid) throws IllegalArgumentException { + for (MethodDescriptor method : methodDescriptorsFor(fromObj, null)) { + if (method.getName().equals(propertyName) && (!isNonVoid || method.getMethod().getReturnType() != void.class)) { + return method; + } + } + + return null; + } + + /** + * Returns all the method 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 Method descriptors + * @throws IllegalArgumentException if there's an introspection failure + */ + public static MethodDescriptor[] methodDescriptorsFor(Object fromObj, Class stopClass) throws IllegalArgumentException { + try { + return Introspector.getBeanInfo(fromObj.getClass(), stopClass).getMethodDescriptors(); + } catch (IntrospectionException e) { + throw new IllegalArgumentException("Could not get method descriptors for " + fromObj.getClass(), e); + } + } + /** * Empty object array, used for documenting that we are deliberately passing no arguments to a method. */ From dbe26b4f5d6c8de12ea56d42e9773293b13a17ee Mon Sep 17 00:00:00 2001 From: Uno Kim Date: Wed, 20 Nov 2024 03:42:56 +0900 Subject: [PATCH 06/10] Refactor `HasPropertyWithValue` using lambda expression and polished its javadoc. --- .../src/main/java/org/hamcrest/Condition.java | 1 + .../hamcrest/beans/HasPropertyWithValue.java | 46 ++++++++----------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/hamcrest/src/main/java/org/hamcrest/Condition.java b/hamcrest/src/main/java/org/hamcrest/Condition.java index 85293eb1..8518ec6c 100644 --- a/hamcrest/src/main/java/org/hamcrest/Condition.java +++ b/hamcrest/src/main/java/org/hamcrest/Condition.java @@ -19,6 +19,7 @@ public abstract class Condition { * @param the initial value type * @param the next step value type */ + @FunctionalInterface public interface Step { /** * Apply this condition to a value diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java b/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java index a734f800..a52c3f75 100644 --- a/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java +++ b/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java @@ -26,7 +26,7 @@ *

Example Usage

* Consider the situation where we have a class representing a person, which * follows the basic JavaBean convention of having get() and possibly set() - * methods for it's properties: + * methods for its properties: *
{@code  public class Person {
  *   private String name;
  *   public Person(String person) {
@@ -122,22 +122,19 @@ private Condition propertyOn(T bean, Description mismatch) {
     }
 
     private Condition.Step withPropertyValue(final T bean) {
-        return new Condition.Step() {
-            @Override
-            public Condition apply(Method readMethod, Description mismatch) {
-                try {
-                    return matched(readMethod.invoke(bean, NO_ARGUMENTS), mismatch);
-                } catch (InvocationTargetException e) {
-                    mismatch
-                      .appendText("Calling '")
-                      .appendText(readMethod.toString())
-                      .appendText("': ")
-                      .appendValue(e.getTargetException().getMessage());
-                    return notMatched();
-                } catch (Exception e) {
-                    throw new IllegalStateException(
-                      "Calling: '" + readMethod + "' should not have thrown " + e);
-                }
+        return (readMethod, mismatch) -> {
+            try {
+                return matched(readMethod.invoke(bean, NO_ARGUMENTS), mismatch);
+            } catch (InvocationTargetException e) {
+                mismatch
+                  .appendText("Calling '")
+                  .appendText(readMethod.toString())
+                  .appendText("': ")
+                  .appendValue(e.getTargetException().getMessage());
+                return notMatched();
+            } catch (Exception e) {
+                throw new IllegalStateException(
+                  "Calling: '" + readMethod + "' should not have thrown " + e);
             }
         };
     }
@@ -148,16 +145,13 @@ private static Matcher nastyGenericsWorkaround(Matcher valueMatcher)
     }
 
     private static Condition.Step withReadMethod() {
-        return new Condition.Step() {
-            @Override
-            public Condition apply(PropertyDescriptor property, Description mismatch) {
-                final Method readMethod = property.getReadMethod();
-                if (null == readMethod) {
-                    mismatch.appendText("property \"" + property.getName() + "\" is not readable");
-                    return notMatched();
-                }
-                return matched(readMethod, mismatch);
+        return (property, mismatch) -> {
+            final Method readMethod = property.getReadMethod();
+            if (null == readMethod) {
+                mismatch.appendText("property \"" + property.getName() + "\" is not readable");
+                return notMatched();
             }
+            return matched(readMethod, mismatch);
         };
     }
 

From 84c238bb88646dc1006099000c1f352daa3394ec Mon Sep 17 00:00:00 2001
From: Uno Kim 
Date: Wed, 20 Nov 2024 19:31:43 +0900
Subject: [PATCH 07/10] Implement matching conditions of `HasPropertyWithValue`
 for Java Records

The basic approach is as same as the `HasProperty`
in 8201577.
`hasProperty(propertyName, valueMatcher)` will
recognize properties in java record classes if:

1. There's a method of which name is same to the given property name
2. The found method returns something

If it doesn't meet condition 1, the property does not exist.
If it doesn't meet condition 2, the property is write-only.
---
 .../hamcrest/beans/HasPropertyWithValue.java  | 19 ++++---
 .../beans/HasPropertyWithValueTest.java       | 53 +++++++++++++++++--
 2 files changed, 61 insertions(+), 11 deletions(-)

diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java b/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java
index a52c3f75..d0674774 100644
--- a/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java
+++ b/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java
@@ -5,6 +5,8 @@
 import org.hamcrest.Matcher;
 import org.hamcrest.TypeSafeDiagnosingMatcher;
 
+import java.beans.FeatureDescriptor;
+import java.beans.MethodDescriptor;
 import java.beans.PropertyDescriptor;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -69,7 +71,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,8 +113,11 @@ public void describeTo(Description description) {
                    .appendDescriptionOf(valueMatcher).appendText(")");
     }
 
-    private Condition propertyOn(T bean, Description mismatch) {
-        PropertyDescriptor property = PropertyUtil.getPropertyDescriptor(propertyName, bean);
+    private Condition propertyOn(T bean, Description mismatch) {
+        FeatureDescriptor property = PropertyUtil.getPropertyDescriptor(propertyName, bean);
+        if (property == null) {
+            property = PropertyUtil.getMethodDescriptor(propertyName, bean, false);
+        }
         if (property == null) {
             mismatch.appendText("No property \"" + propertyName + "\"");
             return notMatched();
@@ -144,10 +149,12 @@ private static Matcher nastyGenericsWorkaround(Matcher valueMatcher)
         return (Matcher) valueMatcher;
     }
 
-    private static Condition.Step withReadMethod() {
+    private static Condition.Step withReadMethod() {
         return (property, mismatch) -> {
-            final Method readMethod = property.getReadMethod();
-            if (null == readMethod) {
+            final Method readMethod = property instanceof PropertyDescriptor ?
+                    ((PropertyDescriptor) property).getReadMethod() :
+                    (((MethodDescriptor) property).getMethod());
+            if (null == readMethod || readMethod.getReturnType() == void.class) {
                 mismatch.appendText("property \"" + property.getName() + "\" is not readable");
                 return notMatched();
             }
diff --git a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java
index b040ba39..4bfd149d 100644
--- a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java
+++ b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java
@@ -31,6 +31,9 @@ public class HasPropertyWithValueTest extends AbstractMatcherTest {
   private final BeanWithoutInfo shouldMatch = new BeanWithoutInfo("is expected", true);
   private final BeanWithoutInfo shouldNotMatch = new BeanWithoutInfo("not expected", false);
 
+  private final RecordLikeBeanWithoutInfo recordShouldMatch = new RecordLikeBeanWithoutInfo("is expected", true);
+  private final RecordLikeBeanWithoutInfo recordShouldNotMatch = new RecordLikeBeanWithoutInfo("not expected", false);
+
   private final BeanWithInfo beanWithInfo = new BeanWithInfo("with info");
 
   @Override
@@ -46,6 +49,14 @@ public void testMatchesBeanWithoutInfoWithMatchedNamedProperty() {
     assertMismatchDescription("property 'property' was \"not expected\"", propertyMatcher, shouldNotMatch);
   }
 
+  @Test
+  public void testMatchesRecordLikeBeanWithoutInfoWithMatchedNamedProperty() {
+    final Matcher propertyMatcher = hasProperty("property", equalTo("is expected"));
+
+    assertMatches("with property", propertyMatcher, recordShouldMatch);
+    assertMismatchDescription("property 'property' was \"not expected\"", propertyMatcher, recordShouldNotMatch);
+  }
+
   @Test
   public void testMatchesBeanWithoutInfoWithMatchedNamedBooleanProperty() {
     final Matcher booleanPropertyMatcher = hasProperty("booleanProperty", is(true));
@@ -54,6 +65,14 @@ public void testMatchesBeanWithoutInfoWithMatchedNamedBooleanProperty() {
     assertMismatchDescription("property 'booleanProperty' was ", booleanPropertyMatcher, shouldNotMatch);
   }
 
+  @Test
+  public void testMatchesRecordLikeBeanWithoutInfoWithMatchedNamedBooleanProperty() {
+    final Matcher booleanPropertyMatcher = hasProperty("booleanProperty", is(true));
+
+    assertMatches("with property", booleanPropertyMatcher, recordShouldMatch);
+    assertMismatchDescription("property 'booleanProperty' was ", booleanPropertyMatcher, recordShouldNotMatch);
+  }
+
   @Test
   public void testMatchesBeanWithInfoWithMatchedNamedProperty() {
     assertMatches("with bean info", hasProperty("property", equalTo("with info")), beanWithInfo);
@@ -65,12 +84,20 @@ public void testMatchesBeanWithInfoWithMatchedNamedProperty() {
   public void testDoesNotMatchBeanWithoutInfoOrMatchedNamedProperty() {
     assertMismatchDescription("No property \"nonExistentProperty\"",
                               hasProperty("nonExistentProperty", anything()), shouldNotMatch);
-   }
+  }
 
-   @Test
+  @Test
+  public void testDoesNotMatchRecordLikeBeanWithoutInfoOrMatchedNamedProperty() {
+    assertMismatchDescription("No property \"nonExistentProperty\"",
+                              hasProperty("nonExistentProperty", anything()), recordShouldNotMatch);
+  }
+
+  @Test
   public void testDoesNotMatchWriteOnlyProperty() {
     assertMismatchDescription("property \"writeOnlyProperty\" is not readable",
                               hasProperty("writeOnlyProperty", anything()), shouldNotMatch);
+    assertMismatchDescription("property \"writeOnlyProperty\" is not readable",
+                              hasProperty("writeOnlyProperty", anything()), recordShouldNotMatch);
   }
 
   @Test
@@ -83,6 +110,16 @@ public void testMatchesPath() {
     assertMismatchDescription("inner.inner.property.was \"not expected\"", hasPropertyAtPath("inner.inner.property", equalTo("something")), new BeanWithInner(new BeanWithInner(shouldNotMatch)));
   }
 
+  @Test
+  public void testMatchesPathForJavaRecords() {
+    assertMatches("1-step path", hasPropertyAtPath("property", equalTo("is expected")), recordShouldMatch);
+    assertMatches("2-step path", hasPropertyAtPath("inner.property", equalTo("is expected")), new BeanWithInner(recordShouldMatch));
+    assertMatches("3-step path", hasPropertyAtPath("inner.inner.property", equalTo("is expected")), new BeanWithInner(new BeanWithInner(recordShouldMatch)));
+
+    assertMismatchDescription("inner.No property \"wrong\"", hasPropertyAtPath("inner.wrong.property", anything()), new BeanWithInner(new BeanWithInner(recordShouldMatch)));
+    assertMismatchDescription("inner.inner.property.was \"not expected\"", hasPropertyAtPath("inner.inner.property", equalTo("something")), new BeanWithInner(new BeanWithInner(recordShouldNotMatch)));
+  }
+
   @Test
   public void testDescribeTo() {
     assertDescription("hasProperty(\"property\", )", hasProperty("property", equalTo(true)));
@@ -93,6 +130,11 @@ public void testMatchesPropertyAndValue() {
     assertMatches("property with value", hasProperty("property", anything()), beanWithInfo);
   }
 
+  @Test
+  public void testMatchesPropertyAndValueWithJavaRecords() {
+    assertMatches("property with value", hasProperty("property", anything()), recordShouldMatch);
+  }
+
   @Test
   public void testDoesNotWriteMismatchIfPropertyMatches() {
     Description description = new StringDescription();
@@ -160,7 +202,7 @@ public String toString() {
 
   /**
    * A Java Record-like class to test the functionality of
-   * {@link org.hamcrest.beans.HasProperty#hasProperty(String) hasProperty(String)}
+   * {@link HasProperty}, {@link HasPropertyWithValue}
    * with Java Records in JDK 8 environment.
    *
    * @see https://docs.oracle.com/en/java/javase/17/language/records.html
@@ -175,8 +217,9 @@ public RecordLikeBeanWithoutInfo(String property, boolean booleanProperty) {
     }
 
     public String property() { return this.property; }
-    public boolean booleanProperty()  { return this.booleanProperty; }
+    public boolean booleanProperty() { return this.booleanProperty; }
     public void notAGetterMethod() {}
+    public void writeOnlyProperty(float property) {}
 
     @Override
     public boolean equals(Object o) {
@@ -213,7 +256,7 @@ public static class BeanWithInfo {
     public String property() { return propertyValue; }
   }
 
-  public static class BeanWithInfoBeanInfo extends SimpleBeanInfo {
+  public static class BeanWithInfoBeanInfo extends SimpleBeanInfo { // TODO: No usage. Can be removed.
     @Override
     public PropertyDescriptor[] getPropertyDescriptors() {
       try {

From 0fb657c89db8b28bd4585927ad7773cf1a14e1cf Mon Sep 17 00:00:00 2001
From: Uno Kim 
Date: Mon, 25 Nov 2024 02:56:36 +0900
Subject: [PATCH 08/10] Refactor method name in `SamePropertyValuesAs`

---
 .../main/java/org/hamcrest/beans/SamePropertyValuesAs.java  | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java b/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java
index 94f4dba1..8bef0c9d 100644
--- a/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java
+++ b/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java
@@ -90,7 +90,7 @@ private boolean hasMatchingValues(Object actual, Description mismatchDescription
     private static  List propertyMatchersFor(T bean, PropertyDescriptor[] descriptors, List ignoredFields) {
         List result = new ArrayList<>(descriptors.length);
         for (PropertyDescriptor propertyDescriptor : descriptors) {
-            if (isIgnored(ignoredFields, propertyDescriptor)) {
+            if (isNotIgnored(ignoredFields, propertyDescriptor)) {
                 result.add(new PropertyMatcher(propertyDescriptor, bean));
             }
         }
@@ -100,14 +100,14 @@ private static  List propertyMatchersFor(T bean, PropertyDes
     private static Set propertyNamesFrom(PropertyDescriptor[] descriptors, List ignoredFields) {
         HashSet result = new HashSet<>();
         for (PropertyDescriptor propertyDescriptor : descriptors) {
-            if (isIgnored(ignoredFields, propertyDescriptor)) {
+            if (isNotIgnored(ignoredFields, propertyDescriptor)) {
                 result.add(propertyDescriptor.getDisplayName());
             }
         }
         return result;
     }
 
-    private static boolean isIgnored(List ignoredFields, PropertyDescriptor propertyDescriptor) {
+    private static boolean isNotIgnored(List ignoredFields, PropertyDescriptor propertyDescriptor) {
         return ! ignoredFields.contains(propertyDescriptor.getDisplayName());
     }
 

From 5068e6726a8f483a3e8e29def2bcd0a1a7786d04 Mon Sep 17 00:00:00 2001
From: Uno Kim 
Date: Mon, 25 Nov 2024 04:24:44 +0900
Subject: [PATCH 09/10] Implement matching conditions of `SamePropertyValuesAs`
 for Java Records

To achieve this goal I needed a different approach
from 8201577.
`samePropertyValuesAs(expectedBean, ignoredProperties)`
will recognize properties in java record classes if:

* There's a read accessor method of which name is same to the given property name

A read accessor method is a method
which acts like a getter for a field.
To check if the method is a read accessor method,
the algorithm filters the following:

1. The name of it should be identical to the name of the field.
2. It should return something.
3. It should not have any method parameters.

In this manner, we can now assure that
the collected methods are getters.

## Reference

* https://docs.oracle.com/en/java/javase/14/language/records.html
---
 .../java/org/hamcrest/beans/PropertyUtil.java |  51 ++++++
 .../hamcrest/beans/SamePropertyValuesAs.java  |  33 ++--
 .../org/hamcrest/beans/PropertyUtilTest.java  | 150 ++++++++++++++++++
 .../beans/SamePropertyValuesAsTest.java       |  68 ++++++++
 4 files changed, 290 insertions(+), 12 deletions(-)
 create mode 100644 hamcrest/src/test/java/org/hamcrest/beans/PropertyUtilTest.java

diff --git a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java
index f80f8497..41cad6ae 100644
--- a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java
+++ b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java
@@ -4,6 +4,11 @@
 import java.beans.Introspector;
 import java.beans.MethodDescriptor;
 import java.beans.PropertyDescriptor;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Utility class with static methods for accessing properties on JavaBean objects.
@@ -84,11 +89,13 @@ public static MethodDescriptor getMethodDescriptor(String propertyName, Object f
     /**
      * Returns all the method descriptors for the class associated with the given object
      *
+     * @deprecated Use {@link #recordReadAccessorMethodDescriptorsFor(Object, Class)} instead.
      * @param fromObj Use the class of this object
      * @param stopClass Don't include any properties from this ancestor class upwards.
      * @return Method descriptors
      * @throws IllegalArgumentException if there's an introspection failure
      */
+    @Deprecated
     public static MethodDescriptor[] methodDescriptorsFor(Object fromObj, Class stopClass) throws IllegalArgumentException {
         try {
             return Introspector.getBeanInfo(fromObj.getClass(), stopClass).getMethodDescriptors();
@@ -97,6 +104,50 @@ public static MethodDescriptor[] methodDescriptorsFor(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 8bef0c9d..fbb0175f 100644
--- a/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java
+++ b/hamcrest/src/main/java/org/hamcrest/beans/SamePropertyValuesAs.java
@@ -4,6 +4,8 @@
 import org.hamcrest.DiagnosingMatcher;
 import org.hamcrest.Matcher;
 
+import java.beans.FeatureDescriptor;
+import java.beans.MethodDescriptor;
 import java.beans.PropertyDescriptor;
 import java.lang.reflect.Method;
 import java.util.*;
@@ -11,6 +13,7 @@
 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;
 
 /**
@@ -33,7 +36,11 @@ public class SamePropertyValuesAs extends DiagnosingMatcher {
      */
     @SuppressWarnings("WeakerAccess")
     public SamePropertyValuesAs(T expectedBean, List ignoredProperties) {
-        PropertyDescriptor[] descriptors = propertyDescriptorsFor(expectedBean, Object.class);
+        FeatureDescriptor[] descriptors = propertyDescriptorsFor(expectedBean, Object.class);
+        if (descriptors == null || descriptors.length == 0) {
+            descriptors = recordReadAccessorMethodDescriptorsFor(expectedBean, Object.class);
+        }
+
         this.expectedBean = expectedBean;
         this.ignoredFields = ignoredProperties;
         this.propertyNames = propertyNamesFrom(descriptors, ignoredProperties);
@@ -87,27 +94,27 @@ private boolean hasMatchingValues(Object actual, Description mismatchDescription
         return true;
     }
 
-    private static  List propertyMatchersFor(T bean, PropertyDescriptor[] descriptors, List ignoredFields) {
+    private static  List propertyMatchersFor(T bean, FeatureDescriptor[] descriptors, List ignoredFields) {
         List result = new ArrayList<>(descriptors.length);
-        for (PropertyDescriptor propertyDescriptor : descriptors) {
-            if (isNotIgnored(ignoredFields, propertyDescriptor)) {
-                result.add(new PropertyMatcher(propertyDescriptor, bean));
+        for (FeatureDescriptor descriptor : descriptors) {
+            if (isNotIgnored(ignoredFields, descriptor)) {
+                result.add(new PropertyMatcher(descriptor, bean));
             }
         }
         return result;
     }
 
-    private static Set propertyNamesFrom(PropertyDescriptor[] descriptors, List ignoredFields) {
+    private static Set propertyNamesFrom(FeatureDescriptor[] descriptors, List ignoredFields) {
         HashSet result = new HashSet<>();
-        for (PropertyDescriptor propertyDescriptor : descriptors) {
-            if (isNotIgnored(ignoredFields, propertyDescriptor)) {
-                result.add(propertyDescriptor.getDisplayName());
+        for (FeatureDescriptor descriptor : descriptors) {
+            if (isNotIgnored(ignoredFields, descriptor)) {
+                result.add(descriptor.getDisplayName());
             }
         }
         return result;
     }
 
-    private static boolean isNotIgnored(List ignoredFields, PropertyDescriptor propertyDescriptor) {
+    private static boolean isNotIgnored(List ignoredFields, FeatureDescriptor propertyDescriptor) {
         return ! ignoredFields.contains(propertyDescriptor.getDisplayName());
     }
 
@@ -117,9 +124,11 @@ private static class PropertyMatcher extends DiagnosingMatcher {
         private final Matcher matcher;
         private final String propertyName;
 
-        public PropertyMatcher(PropertyDescriptor descriptor, Object expectedObject) {
+        public PropertyMatcher(FeatureDescriptor descriptor, Object expectedObject) {
             this.propertyName = descriptor.getDisplayName();
-            this.readMethod = descriptor.getReadMethod();
+            this.readMethod = descriptor instanceof PropertyDescriptor ?
+                    ((PropertyDescriptor) descriptor).getReadMethod() :
+                    ((MethodDescriptor) descriptor).getMethod();
             this.matcher = equalTo(readProperty(readMethod, expectedObject));
         }
 
diff --git a/hamcrest/src/test/java/org/hamcrest/beans/PropertyUtilTest.java b/hamcrest/src/test/java/org/hamcrest/beans/PropertyUtilTest.java
new file mode 100644
index 00000000..bc9ba6a0
--- /dev/null
+++ b/hamcrest/src/test/java/org/hamcrest/beans/PropertyUtilTest.java
@@ -0,0 +1,150 @@
+package org.hamcrest.beans;
+
+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.*;
+
+class PropertyUtilTest {
+
+    @Test
+    void testReturnsTheNamesOfAllFieldsFromTargetClass() {
+        SamePropertyValuesAsTest.ExampleBean input = new SamePropertyValuesAsTest.ExampleBean("test", 1, null);
+
+        Set output = PropertyUtil.getFieldNames(input);
+
+        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);
+
+        MethodDescriptor[] output = PropertyUtil.recordReadAccessorMethodDescriptorsFor(input, Object.class);
+
+        assertThat(output, arrayWithSize(0));
+    }
+
+    @Test
+    void testReturnsArrayOfMethodDescriptorFromTargetRecord() {
+        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)));
+
+        MethodDescriptor[] output = PropertyUtil.recordReadAccessorMethodDescriptorsFor(input, Object.class);
+
+        assertThat(output, arrayWithSize(5));
+        assertThat(Arrays.stream(output).map(MethodDescriptor::getDisplayName).collect(Collectors.toList()),
+                   hasItems("numberArray", "test", "smallClasses", "name", "age"));
+    }
+
+
+    /**
+     * A Java Record-like class to test the functionality of
+     * {@link PropertyUtil} with Java Records in JDK 8 environment.
+     *
+     * @see https://docs.oracle.com/en/java/javase/17/language/records.html
+     */
+    @SuppressWarnings("unused")
+    static final class RecordLikeClass {
+        private final String name;
+        private final int age;
+        private final boolean test;
+        private final Long[] numberArray;
+        private final List smallClasses;
+
+        public RecordLikeClass(String name, int age, boolean test, Long[] numberArray, List smallClasses) {
+            this.name = name;
+            this.age = age;
+            this.test = test;
+            this.numberArray = numberArray;
+            this.smallClasses = smallClasses;
+        }
+
+        public String name() { return name; }
+        public int age() { return age; }
+        public boolean test() { return test; }
+        public Long[] numberArray() { return numberArray; }
+        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) {
+            if (!(o instanceof RecordLikeClass)) return false;
+            RecordLikeClass that = (RecordLikeClass) o;
+            return this.age() == that.age() &&
+                    this.test() == that.test() &&
+                    Objects.equals(this.name(), that.name()) &&
+                    Objects.deepEquals(this.numberArray(), that.numberArray())&&
+                    Objects.equals(this.smallClasses(), that.smallClasses());
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(name(), age(), test(), Arrays.hashCode(numberArray()), smallClasses());
+        }
+
+        @Override
+        public String toString() {
+            return "RecordLikeClass{" +
+                    "name='" + name + '\'' +
+                    ", age=" + age +
+                    ", test=" + test +
+                    ", numberArray=" + Arrays.toString(numberArray) +
+                    ", smallClasses=" + smallClasses +
+                    '}';
+        }
+
+        static class SmallClass {
+            private String field1;
+            private Integer field2;
+            private BigDecimal field3;
+            private LocalDateTime field4;
+
+            public SmallClass() {}
+
+            public SmallClass(String field1, Integer field2, BigDecimal field3, LocalDateTime field4) {
+                this.field1 = field1;
+                this.field2 = field2;
+                this.field3 = field3;
+                this.field4 = field4;
+            }
+
+            @Override
+            public String toString() {
+                return "SmallClass{field1='" + field1 + "', field2=" + field2 + ", field3=" + field3 + ", field4=" + field4 + '}';
+            }
+        }
+    }
+
+}
diff --git a/hamcrest/src/test/java/org/hamcrest/beans/SamePropertyValuesAsTest.java b/hamcrest/src/test/java/org/hamcrest/beans/SamePropertyValuesAsTest.java
index 3af77cf0..bd5f1796 100644
--- a/hamcrest/src/test/java/org/hamcrest/beans/SamePropertyValuesAsTest.java
+++ b/hamcrest/src/test/java/org/hamcrest/beans/SamePropertyValuesAsTest.java
@@ -4,6 +4,8 @@
 import org.hamcrest.Matcher;
 import org.junit.jupiter.api.Test;
 
+import java.util.Objects;
+
 import static org.hamcrest.test.MatcherAssertions.*;
 import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
 
@@ -13,6 +15,9 @@ public class SamePropertyValuesAsTest extends AbstractMatcherTest {
   private static final Value aValue = new Value("expected");
   private static final ExampleBean expectedBean = new ExampleBean("same", 1, aValue);
   private static final ExampleBean actualBean = new ExampleBean("same", 1, aValue);
+  private static final ExampleRecord expectedRecord = new ExampleRecord("same", 1, aValue);
+  private static final ExampleRecord actualRecord = new ExampleRecord("same", 1, aValue);
+
 
   @Override
   protected Matcher createMatcher() {
@@ -22,12 +27,15 @@ protected Matcher createMatcher() {
   @Test
   public void test_reports_match_when_all_properties_match() {
     assertMatches("matched properties", samePropertyValuesAs(expectedBean), actualBean);
+    assertMatches("matched properties", samePropertyValuesAs(expectedRecord), actualRecord);
   }
 
   @Test
   public void test_reports_mismatch_when_actual_type_is_not_assignable_to_expected_type() {
     assertMismatchDescription("is incompatible type: ExampleBean",
                               samePropertyValuesAs((Object)aValue), actualBean);
+    assertMismatchDescription("is incompatible type: ExampleRecord",
+                              samePropertyValuesAs((Object)aValue), actualRecord);
   }
 
   @Test
@@ -38,6 +46,13 @@ public void test_reports_mismatch_on_first_property_difference() {
         samePropertyValuesAs(expectedBean), new ExampleBean("same", 2, aValue));
     assertMismatchDescription("valueProperty was ",
         samePropertyValuesAs(expectedBean), new ExampleBean("same", 1, new Value("other")));
+
+    assertMismatchDescription("stringProperty was \"different\"",
+        samePropertyValuesAs(expectedRecord), new ExampleRecord("different", 1, aValue));
+    assertMismatchDescription("intProperty was <2>",
+        samePropertyValuesAs(expectedRecord), new ExampleRecord("same", 2, aValue));
+    assertMismatchDescription("valueProperty was ",
+        samePropertyValuesAs(expectedRecord), new ExampleRecord("same", 1, new Value("other")));
   }
 
   @Test
@@ -61,21 +76,29 @@ public void test_ignores_extra_subtype_properties() {
   @Test
   public void test_ignores_different_properties() {
     final ExampleBean differentBean = new ExampleBean("different", 1, aValue);
+    final ExampleRecord differentRecord = new ExampleRecord("different", 1, aValue);
     assertMatches("different property", samePropertyValuesAs(expectedBean, "stringProperty"), differentBean);
+    assertMatches("different property", samePropertyValuesAs(expectedRecord, "stringProperty"), differentRecord);
   }
 
   @Test
   public void test_accepts_missing_properties_to_ignore() {
     assertMatches("ignored property", samePropertyValuesAs(expectedBean, "notAProperty"), actualBean);
+    assertMatches("ignored property", samePropertyValuesAs(expectedRecord, "notAProperty"), actualRecord);
   }
 
   @Test
   public void test_can_ignore_all_properties() {
     final ExampleBean differentBean = new ExampleBean("different", 2, new Value("not expected"));
+    final ExampleRecord differentRecord = new ExampleRecord("different", 2, new Value("not expected"));
     assertMatches(
             "different property",
             samePropertyValuesAs(expectedBean, "stringProperty", "intProperty", "valueProperty"),
             differentBean);
+    assertMatches(
+            "different property",
+            samePropertyValuesAs(expectedRecord, "stringProperty", "intProperty", "valueProperty"),
+            differentRecord);
   }
 
   @Test
@@ -83,10 +106,16 @@ public void testDescribesItself() {
     assertDescription(
             "same property values as ExampleBean [intProperty: <1>, stringProperty: \"same\", valueProperty: ]",
             samePropertyValuesAs(expectedBean));
+    assertDescription(
+            "same property values as ExampleRecord [valueProperty: , stringProperty: \"same\", intProperty: <1>]",
+            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\"]",
+            samePropertyValuesAs(expectedRecord, "ignored1", "ignored2"));
   }
 
   public static class Value {
@@ -126,6 +155,45 @@ public Value getValueProperty() {
     @Override public String toString() { return "an ExampleBean"; }
   }
 
+  /**
+   * A Java Record-like class to test the functionality of
+   * {@link SamePropertyValuesAs} with Java Records in JDK 8 environment.
+   * The basic property structure is the same as {@link ExampleBean ExampleBean} for the exact comparison.
+   *
+   * @see ExampleBean ExampleBean
+   * @see https://docs.oracle.com/en/java/javase/17/language/records.html
+   */
+  @SuppressWarnings("unused")
+  public static final class ExampleRecord {
+    private final String stringProperty;
+    private final int intProperty;
+    private final Value valueProperty;
+
+    public ExampleRecord(String stringProperty, int intProperty, Value valueProperty) {
+      this.stringProperty = stringProperty;
+      this.intProperty = intProperty;
+      this.valueProperty = valueProperty;
+    }
+
+    public String stringProperty() { return stringProperty; }
+    public int intProperty() { return intProperty; }
+    public Value valueProperty() { return valueProperty; }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof ExampleRecord)) return false;
+      ExampleRecord that = (ExampleRecord) o;
+      return this.intProperty == that.intProperty && Objects.equals(this.stringProperty, that.stringProperty) && Objects.equals(this.valueProperty, that.valueProperty);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(stringProperty, intProperty, valueProperty);
+    }
+
+    @Override public String toString() { return "an ExampleRecord"; }
+  }
+
   public static class SubBeanWithNoExtraProperties extends ExampleBean {
     public SubBeanWithNoExtraProperties(String stringProperty, int intProperty, Value valueProperty) {
       super(stringProperty, intProperty, valueProperty);

From c397cbfd52c2e5731b8966eaec8c041370a32efe Mon Sep 17 00:00:00 2001
From: Uno Kim 
Date: Mon, 25 Nov 2024 04:29:27 +0900
Subject: [PATCH 10/10] Remove unused method in `PropertyUtil`

The former implementation
`methodDescriptorsFor()` can be
replaced to the new method
`recordReadAccessorMethodDescriptorsFor()`.
This doesn't change current behavior
except one, the case for the non-readable property.
Actually, there can't be any non-readable property in
Java Records by its specification.
---
 .../java/org/hamcrest/beans/HasProperty.java  |  2 +-
 .../hamcrest/beans/HasPropertyWithValue.java  |  2 +-
 .../java/org/hamcrest/beans/PropertyUtil.java | 28 +++----------------
 .../beans/HasPropertyWithValueTest.java       |  3 --
 4 files changed, 6 insertions(+), 29 deletions(-)

diff --git a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java
index 6bcebe41..8494c9a3 100644
--- a/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java
+++ b/hamcrest/src/main/java/org/hamcrest/beans/HasProperty.java
@@ -31,7 +31,7 @@ public HasProperty(String propertyName) {
     public boolean matchesSafely(T obj) {
         try {
             return PropertyUtil.getPropertyDescriptor(propertyName, obj) != null ||
-                    PropertyUtil.getMethodDescriptor(propertyName, obj, true) != null;
+                    PropertyUtil.getMethodDescriptor(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 d0674774..f45c1264 100644
--- a/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java
+++ b/hamcrest/src/main/java/org/hamcrest/beans/HasPropertyWithValue.java
@@ -116,7 +116,7 @@ public void describeTo(Description description) {
     private Condition propertyOn(T bean, Description mismatch) {
         FeatureDescriptor property = PropertyUtil.getPropertyDescriptor(propertyName, bean);
         if (property == null) {
-            property = PropertyUtil.getMethodDescriptor(propertyName, bean, false);
+            property = PropertyUtil.getMethodDescriptor(propertyName, bean);
         }
         if (property == null) {
             mismatch.appendText("No property \"" + propertyName + "\"");
diff --git a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java
index 41cad6ae..40d5a37d 100644
--- a/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java
+++ b/hamcrest/src/main/java/org/hamcrest/beans/PropertyUtil.java
@@ -6,7 +6,6 @@
 import java.beans.PropertyDescriptor;
 import java.lang.reflect.Field;
 import java.util.Arrays;
-import java.util.LinkedHashSet;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -63,22 +62,21 @@ public static PropertyDescriptor[] propertyDescriptorsFor(Object fromObj, Class<
     }
 
     /**
-     * Returns the description of the method with the provided
+     * 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.
-     * @param isNonVoid whether the method is non-void, which means the method has a return value.
      * @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, boolean isNonVoid) throws IllegalArgumentException {
-        for (MethodDescriptor method : methodDescriptorsFor(fromObj, null)) {
-            if (method.getName().equals(propertyName) && (!isNonVoid || method.getMethod().getReturnType() != void.class)) {
+    public static MethodDescriptor getMethodDescriptor(String propertyName, Object fromObj) throws IllegalArgumentException {
+        for (MethodDescriptor method : recordReadAccessorMethodDescriptorsFor(fromObj, null)) {
+            if (method.getName().equals(propertyName)) {
                 return method;
             }
         }
@@ -86,24 +84,6 @@ public static MethodDescriptor getMethodDescriptor(String propertyName, Object f
         return null;
     }
 
-    /**
-     * Returns all the method descriptors for the class associated with the given object
-     *
-     * @deprecated Use {@link #recordReadAccessorMethodDescriptorsFor(Object, Class)} instead.
-     * @param fromObj Use the class of this object
-     * @param stopClass Don't include any properties from this ancestor class upwards.
-     * @return Method descriptors
-     * @throws IllegalArgumentException if there's an introspection failure
-     */
-    @Deprecated
-    public static MethodDescriptor[] methodDescriptorsFor(Object fromObj, Class stopClass) throws IllegalArgumentException {
-        try {
-            return Introspector.getBeanInfo(fromObj.getClass(), stopClass).getMethodDescriptors();
-        } catch (IntrospectionException e) {
-            throw new IllegalArgumentException("Could not get method descriptors for " + fromObj.getClass(), e);
-        }
-    }
-
     /**
      * 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
diff --git a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java
index 4bfd149d..a7dcc00d 100644
--- a/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java
+++ b/hamcrest/src/test/java/org/hamcrest/beans/HasPropertyWithValueTest.java
@@ -96,8 +96,6 @@ public void testDoesNotMatchRecordLikeBeanWithoutInfoOrMatchedNamedProperty() {
   public void testDoesNotMatchWriteOnlyProperty() {
     assertMismatchDescription("property \"writeOnlyProperty\" is not readable",
                               hasProperty("writeOnlyProperty", anything()), shouldNotMatch);
-    assertMismatchDescription("property \"writeOnlyProperty\" is not readable",
-                              hasProperty("writeOnlyProperty", anything()), recordShouldNotMatch);
   }
 
   @Test
@@ -219,7 +217,6 @@ public RecordLikeBeanWithoutInfo(String property, boolean booleanProperty) {
     public String property() { return this.property; }
     public boolean booleanProperty() { return this.booleanProperty; }
     public void notAGetterMethod() {}
-    public void writeOnlyProperty(float property) {}
 
     @Override
     public boolean equals(Object o) {