This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 46f0e80533 New BeanCreator API
46f0e80533 is described below
commit 46f0e80533dd4aa89fd38625924e03267332b27f
Author: James Bognar <[email protected]>
AuthorDate: Wed Jan 21 16:51:43 2026 -0500
New BeanCreator API
---
.../apache/juneau/commons/reflect/MethodInfo.java | 4 +-
.../juneau/commons/utils/CollectionUtils.java | 30 +++
.../org/apache/juneau/commons/utils/Utils.java | 72 ++++++-
.../juneau/commons/reflect/ClassInfo_Test.java | 214 +++++++++++++++++++++
.../juneau/commons/reflect/MethodInfo_Test.java | 96 +++++++++
.../apache/juneau/commons/utils/Utils_Test.java | 49 +++++
6 files changed, 455 insertions(+), 10 deletions(-)
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/MethodInfo.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/MethodInfo.java
index b3b18938b9..529ecf2395 100644
---
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/MethodInfo.java
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/MethodInfo.java
@@ -188,9 +188,9 @@ public class MethodInfo extends ExecutableInfo implements
Comparable<MethodInfo>
if (i == 0) {
var declaringClass = getDeclaringClass();
var oDeclaringClass = o.getDeclaringClass();
- if (declaringClass.isParentOf(oDeclaringClass)) {
+ if (declaringClass.isChildOf(oDeclaringClass)) {
i = -1; // This method is from a child class,
so it comes first
- } else if (oDeclaringClass.isParentOf(declaringClass)) {
+ } else if (oDeclaringClass.isChildOf(declaringClass)) {
i = 1; // Other method is from a child class,
so it comes first
} else {
i = cmp(declaringClass.getName(),
oDeclaringClass.getName()); // Neither is a child of the other, compare by
class name for deterministic ordering
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/CollectionUtils.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/CollectionUtils.java
index d08269d10b..6a2a3e5eac 100644
---
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/CollectionUtils.java
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/CollectionUtils.java
@@ -852,6 +852,36 @@ public class CollectionUtils {
return e(l) ? null : l.get(l.size() - 1);
}
+ /**
+ * Returns the element at the specified index.
+ *
+ * <p>
+ * This is a null-safe convenience method that safely accesses list
elements.
+ * If the list is <jk>null</jk>, the index is negative, or the index is
greater than or equal to the list size,
+ * this method returns <jk>null</jk> instead of throwing an exception.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * List<String> <jv>list</jv> = <jsm>l</jsm>(<js>"a"</js>,
<js>"b"</js>, <js>"c"</js>);
+ *
+ * String <jv>first</jv> = <jsm>at</jsm>(<jv>list</jv>, 0);
<jc>// Returns "a"</jc>
+ * String <jv>second</jv> = <jsm>at</jsm>(<jv>list</jv>, 1);
<jc>// Returns "b"</jc>
+ * String <jv>outOfBounds</jv> = <jsm>at</jsm>(<jv>list</jv>, 10);
<jc>// Returns null</jc>
+ * String <jv>negative</jv> = <jsm>at</jsm>(<jv>list</jv>, -1);
<jc>// Returns null</jc>
+ * String <jv>nullList</jv> = <jsm>at</jsm>(<jk>null</jk>, 0);
<jc>// Returns null</jc>
+ * </p>
+ *
+ * @param <E> The element type.
+ * @param l The list. Can be <jk>null</jk>.
+ * @param index The index to access.
+ * @return The element at the specified index, or <jk>null</jk> if the
list is <jk>null</jk>, the index is out of bounds, or negative.
+ */
+ public static <E> E at(List<E> l, int index) {
+ if (l == null || index < 0 || index >= l.size())
+ return null;
+ return l.get(index);
+ }
+
/**
* Returns the length of the specified array.
*
diff --git
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Utils.java
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Utils.java
index 1fa5a3df69..89a018c3ae 100644
---
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Utils.java
+++
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/Utils.java
@@ -501,14 +501,14 @@ public class Utils {
* Tests two objects for equality, gracefully handling nulls and arrays.
*
* <p>
- * This method handles annotations specially by delegating to {@link
org.apache.juneau.commons.utils.AnnotationUtils#equals(java.lang.annotation.Annotation,
java.lang.annotation.Annotation)}
- * to ensure proper annotation comparison according to the annotation equality
contract.
- *
- * @param <T> The value types.
- * @param o1 Object 1.
- * @param o2 Object 2.
- * @return <jk>true</jk> if both objects are equal based on the {@link
Object#equals(Object)} method.
- * @see
org.apache.juneau.commons.utils.AnnotationUtils#equals(java.lang.annotation.Annotation,
java.lang.annotation.Annotation)
+ * This method handles annotations specially by delegating to {@link
org.apache.juneau.commons.utils.AnnotationUtils#equals(java.lang.annotation.Annotation,
java.lang.annotation.Annotation)}
+ * to ensure proper annotation comparison according to the annotation
equality contract.
+ *
+ * @param <T> The value types.
+ * @param o1 Object 1.
+ * @param o2 Object 2.
+ * @return <jk>true</jk> if both objects are equal based on the {@link
Object#equals(Object)} method.
+ * @see
org.apache.juneau.commons.utils.AnnotationUtils#equals(java.lang.annotation.Annotation,
java.lang.annotation.Annotation)
*/
public static <T> boolean eq(T o1, T o2) {
// Handle annotations specially
@@ -527,6 +527,37 @@ public class Utils {
return Objects.equals(o1, o2);
}
+ /**
+ * Returns <jk>true</jk> if the first argument equals any of the
varargs arguments.
+ *
+ * <p>
+ * Uses {@link #eq(Object, Object)} for equality comparison, which
handles nulls, arrays, and annotations gracefully.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jk>assertTrue</jk>(eqAny(<js>"apple"</js>, <js>"apple"</js>,
<js>"banana"</js>, <js>"cherry"</js>));
+ * <jk>assertFalse</jk>(eqAny(<js>"apple"</js>, <js>"banana"</js>,
<js>"cherry"</js>));
+ * <jk>assertTrue</jk>(eqAny(<jk>null</jk>, <js>"apple"</js>,
<jk>null</jk>, <js>"banana"</js>));
+ * <jk>assertFalse</jk>(eqAny(<js>"apple"</js>)); <jc>// Empty
varargs</jc>
+ * </p>
+ *
+ * @param <T> The value types.
+ * @param o1 The first object to compare.
+ * @param o2 The varargs array of objects to compare against.
+ * @return <jk>true</jk> if <c>o1</c> equals any of the values in
<c>o2</c>, <jk>false</jk> otherwise.
+ * Returns <jk>false</jk> if <c>o2</c> is <jk>null</jk> or empty.
+ * @see #eq(Object, Object)
+ */
+ @SafeVarargs
+ public static <T> boolean eqAny(T o1, T...o2) {
+ if (o2 == null || o2.length == 0)
+ return false;
+ for (var o : o2)
+ if (eq(o1, o))
+ return true;
+ return false;
+ }
+
/**
* Tests two objects for equality using a custom predicate, gracefully
handling nulls.
*
@@ -1557,6 +1588,31 @@ public class Utils {
return Optional.ofNullable(t);
}
+ /**
+ * Returns an Optional containing the element at the specified index in
the list, or empty if the index is out of bounds or the element is
<jk>null</jk>.
+ *
+ * <p>
+ * This is a convenience method that combines {@link
CollectionUtils#at(List, int)} with {@link #opt(Object)}.
+ * </p>
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * List<String> <jv>list</jv> =
<jsm>list</jsm>(<js>"a"</js>, <js>"b"</js>, <js>"c"</js>);
+ * Optional<String> <jv>result</jv> =
<jsm>opt</jsm>(<jv>list</jv>, 1); <jc>// Optional.of("b")</jc>
+ * Optional<String> <jv>result2</jv> =
<jsm>opt</jsm>(<jv>list</jv>, 10); <jc>// Optional.empty()</jc>
+ * </p>
+ *
+ * @param <T> The element type.
+ * @param l The list to get the element from.
+ * @param index The index of the element to retrieve.
+ * @return An Optional containing the element at the specified index,
or empty if the index is out of bounds or the element is <jk>null</jk>.
+ * @see CollectionUtils#at(List, int)
+ * @see #opt(Object)
+ */
+ public static final <T> Optional<T> opt(List<T> l, int index) {
+ return opt(CollectionUtils.at(l, index));
+ }
+
/**
* Returns an empty Optional.
*
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ClassInfo_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ClassInfo_Test.java
index 3629843542..917047be75 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ClassInfo_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ClassInfo_Test.java
@@ -1999,6 +1999,28 @@ public class ClassInfo_Test extends TestBase {
assertFalse(method2.isPresent());
}
+ // Test classes for bridge method testing
+ public interface GenericInterfaceForBridge<T> {
+ T getValue();
+ }
+ public static class BridgeMethodTestClass implements
GenericInterfaceForBridge<String> {
+ @Override
+ public String getValue() { return "value"; } // This creates a
bridge method
+ }
+
+ // Test classes for covariant return type (creates bridge method)
+ public static class ParentWithMethod {
+ public ParentWithMethod getInstance() {
+ return new ParentWithMethod();
+ }
+ }
+ public static class ChildWithCovariantReturn extends ParentWithMethod {
+ @Override
+ public ChildWithCovariantReturn getInstance() {
+ return new ChildWithCovariantReturn();
+ }
+ }
+
//====================================================================================================
// getPublicMethods()
//====================================================================================================
@@ -2014,6 +2036,35 @@ public class ClassInfo_Test extends TestBase {
check("", pTypeGenericArgInfo.getPublicMethods());
}
+ /**
+ * Tests that getPublicMethods() excludes both bridge and synthetic
methods.
+ * Some methods can be both bridge and synthetic, and they should be
excluded.
+ */
+ @Test
+ void a059d_getPublicMethods_excludesBridgeAndSyntheticMethods() {
+ // Test with ArrayList which implements List and has bridge
methods
+ var arrayListInfo = ClassInfo.of(java.util.ArrayList.class);
+ var publicMethods = arrayListInfo.getPublicMethods();
+
+ // Verify no bridge methods are included
+ var bridgeMethods = publicMethods.stream()
+ .filter(MethodInfo::isBridge)
+ .toList();
+ assertTrue(bridgeMethods.isEmpty(), "getPublicMethods() should
not include bridge methods. Found: " + bridgeMethods);
+
+ // Verify no synthetic methods are included
+ var syntheticMethods = publicMethods.stream()
+ .filter(MethodInfo::isSynthetic)
+ .toList();
+ assertTrue(syntheticMethods.isEmpty(), "getPublicMethods()
should not include synthetic methods. Found: " + syntheticMethods);
+
+ // Verify that methods that are both bridge and synthetic are
excluded
+ var bridgeOrSynthetic = publicMethods.stream()
+ .filter(m -> m.isBridge() || m.isSynthetic())
+ .toList();
+ assertTrue(bridgeOrSynthetic.isEmpty(), "getPublicMethods()
should not include methods that are bridge or synthetic. Found: " +
bridgeOrSynthetic);
+ }
+
//====================================================================================================
// getRecordComponents()
//====================================================================================================
@@ -3548,6 +3599,10 @@ public class ClassInfo_Test extends TestBase {
@java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD,
java.lang.annotation.ElementType.METHOD})
@interface Autowired {}
+
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+ @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
+ @interface PostConstruct {}
+
// Test classes for field injection
static class TestFieldInjection {
@Inject
@@ -3665,6 +3720,84 @@ public class ClassInfo_Test extends TestBase {
}
}
+ // Test class for PostConstruct injection
+ static class TestPostConstruct {
+ boolean postConstructCalled = false;
+ TestService injectedService;
+ boolean postConstruct2Called = false;
+
+ @Inject
+ TestService service;
+
+ @PostConstruct
+ void postConstruct() {
+ postConstructCalled = true;
+ // Verify injection happened before PostConstruct
+ injectedService = service;
+ }
+
+ @PostConstruct
+ void postConstruct2() {
+ postConstruct2Called = true;
+ }
+ }
+
+ // Parent class with PostConstruct for testing parent-to-child order
+ static class ParentWithPostConstruct {
+ final List<String> callOrder = new ArrayList<>();
+
+ @PostConstruct
+ void parentPostConstruct() {
+ callOrder.add("parent");
+ }
+ }
+
+ // Child class with PostConstruct for testing parent-to-child order
+ static class ChildWithPostConstruct extends ParentWithPostConstruct {
+ @PostConstruct
+ void childPostConstruct() {
+ callOrder.add("child");
+ }
+ }
+
+ // Test class with PostConstruct that has parameters (should be skipped)
+ static class TestPostConstructWithParams {
+ boolean called = false;
+
+ @PostConstruct
+ void postConstruct(TestService service) {
+ called = true; // Should not be called - has parameters
+ }
+ }
+
+ // Test class with PostConstruct that returns value (should still be
called)
+ static class TestPostConstructWithReturn {
+ boolean called = false;
+
+ @PostConstruct
+ public String postConstruct() {
+ called = true;
+ return "result"; // Return value should be ignored
+ }
+ }
+
+ // Test class with abstract PostConstruct method (should be skipped)
+ static abstract class TestAbstractPostConstruct {
+ @PostConstruct
+ abstract void postConstruct();
+ }
+
+ // Test class with PostConstruct and type parameters (should be skipped)
+ static class TestPostConstructWithTypeParams {
+ boolean called = false;
+
+ @PostConstruct
+ @SuppressWarnings("unused")
+ <T> void postConstruct() {
+ called = true; // Should not be called - has type
parameters
+ }
+ }
+
// Abstract test class for testing abstract method skipping
static abstract class TestAbstractMethodInjection {
@Inject
@@ -4093,5 +4226,86 @@ public class ClassInfo_Test extends TestBase {
assertSame(service, combined.field);
// Method should have been called (no exception means it worked)
}
+
+
//====================================================================================================
+ // injectBeans - PostConstruct tests
+
//====================================================================================================
+
+ @Test
+ void b030_injectBeans_postConstructCalled() {
+ var service = new TestService("test1");
+ beanStore.addBean(TestService.class, service);
+ var bean = new TestPostConstruct();
+ ClassInfo.of(bean).inject(bean, beanStore);
+ assertTrue(bean.postConstructCalled, "@PostConstruct method
should be called");
+ assertSame(service, bean.injectedService, "Injection should
happen before PostConstruct");
+ }
+
+ @Test
+ void b031_injectBeans_postConstructCalledAfterInjection() {
+ var service = new TestService("test1");
+ beanStore.addBean(TestService.class, service);
+ var bean = new TestPostConstruct();
+ ClassInfo.of(bean).inject(bean, beanStore);
+ // Verify that injection happened before PostConstruct
+ assertNotNull(bean.injectedService, "Service should be injected
before PostConstruct runs");
+ assertSame(service, bean.injectedService);
+ assertTrue(bean.postConstructCalled);
+ }
+
+ @Test
+ void b032_injectBeans_postConstructMultipleMethods() {
+ var service = new TestService("test1");
+ beanStore.addBean(TestService.class, service);
+ var bean = new TestPostConstruct();
+ ClassInfo.of(bean).inject(bean, beanStore);
+ assertTrue(bean.postConstructCalled, "First @PostConstruct
method should be called");
+ assertTrue(bean.postConstruct2Called, "Second @PostConstruct
method should be called");
+ }
+
+ @Test
+ void b033_injectBeans_postConstructParentToChildOrder() {
+ var bean = new ChildWithPostConstruct();
+ ClassInfo.of(bean).inject(bean, beanStore);
+ // Parent PostConstruct should be called before child
PostConstruct
+ assertEquals(2, bean.callOrder.size());
+ assertEquals("parent", bean.callOrder.get(0), "Parent
PostConstruct should be called first");
+ assertEquals("child", bean.callOrder.get(1), "Child
PostConstruct should be called second");
+ }
+
+ @Test
+ void b034_injectBeans_postConstructWithParametersSkipped() {
+ var service = new TestService("test1");
+ beanStore.addBean(TestService.class, service);
+ var bean = new TestPostConstructWithParams();
+ ClassInfo.of(bean).inject(bean, beanStore);
+ assertFalse(bean.called, "@PostConstruct method with parameters
should be skipped");
+ }
+
+ @Test
+ void b035_injectBeans_postConstructWithReturnValue() {
+ var bean = new TestPostConstructWithReturn();
+ ClassInfo.of(bean).inject(bean, beanStore);
+ assertFalse(bean.called, "@PostConstruct method with return
value should be skipped");
+ }
+
+ @Test
+ void b036_injectBeans_postConstructWithTypeParamsSkipped() {
+ var bean = new TestPostConstructWithTypeParams();
+ ClassInfo.of(bean).inject(bean, beanStore);
+ assertFalse(bean.called, "@PostConstruct method with type
parameters should be skipped");
+ }
+
+ @Test
+ void b037_injectBeans_postConstructWithFieldAndMethodInjection() {
+ var service = new TestService("test1");
+ beanStore.addBean(TestService.class, service);
+ var bean = new TestPostConstruct();
+ ClassInfo.of(bean).inject(bean, beanStore);
+ // Verify all injection happened before PostConstruct
+ assertSame(service, bean.service, "Field injection should
happen first");
+ assertSame(service, bean.injectedService, "PostConstruct should
see injected field");
+ assertTrue(bean.postConstructCalled, "PostConstruct should be
called after all injection");
+ }
}
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/MethodInfo_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/MethodInfo_Test.java
index 0b78d9047d..5717b560a5 100644
---
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/MethodInfo_Test.java
+++
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/MethodInfo_Test.java
@@ -267,6 +267,23 @@ class MethodInfo_Test extends TestBase {
assertSame(a_m, result);
}
+ // Test classes for compareTo tie-breaker testing
+ public static class CompareToParent {
+ public CompareToParent getInstance() {
+ return new CompareToParent();
+ }
+ public void method(String param) {}
+ }
+
+ public static class CompareToChild extends CompareToParent {
+ @Override
+ public CompareToChild getInstance() {
+ return new CompareToChild();
+ }
+ @Override
+ public void method(String param) {}
+ }
+
//====================================================================================================
// compareTo(MethodInfo)
//====================================================================================================
@@ -276,6 +293,42 @@ class MethodInfo_Test extends TestBase {
check("[G.a1(), G.a1(int), G.a1(String), G.a1(int,int), G.a2(),
G.a3()]", s);
}
+ /**
+ * Tests that compareTo() correctly handles tie-breaking for overridden
methods.
+ * When methods have the same name and parameters, child class methods
should
+ * come before parent class methods in sorted order. This ensures
consistent ordering
+ * and that methods with covariant return types are selected correctly.
+ */
+ @Test
+ void a002b_compareTo_tieBreakerForOverriddenMethods() throws Exception {
+ // Get methods from parent and child classes with same signature
+ var parentMethod =
MethodInfo.of(CompareToParent.class.getMethod("getInstance"));
+ var childMethod =
MethodInfo.of(CompareToChild.class.getMethod("getInstance"));
+
+ // Child method should come before parent method (negative
comparison)
+ assertTrue(childMethod.compareTo(parentMethod) < 0, "Child
method should come before parent method");
+ assertTrue(parentMethod.compareTo(childMethod) > 0, "Parent
method should come after child method");
+
+ // When sorted, child should come first
+ var sorted = new TreeSet<>(l(parentMethod, childMethod));
+ var list = new ArrayList<>(sorted);
+ assertEquals(childMethod, list.get(0), "Child method should be
first in sorted set");
+ assertEquals(parentMethod, list.get(1), "Parent method should
be second in sorted set");
+
+ // Test with methods that have parameters
+ var parentMethodWithParam =
MethodInfo.of(CompareToParent.class.getMethod("method", String.class));
+ var childMethodWithParam =
MethodInfo.of(CompareToChild.class.getMethod("method", String.class));
+
+
assertTrue(childMethodWithParam.compareTo(parentMethodWithParam) < 0, "Child
method with params should come before parent");
+
assertTrue(parentMethodWithParam.compareTo(childMethodWithParam) > 0, "Parent
method with params should come after child");
+
+ // Test with unrelated classes (should fall back to class name
comparison)
+ var unrelatedMethod = MethodInfo.of(A1.class.getMethod("m"));
+ var unrelatedMethod2 =
MethodInfo.of(EqualsTestClass.class.getMethod("method1"));
+ // These should have different names, so comparison should be
based on name, not tie-breaker
+ assertNotEquals(0, unrelatedMethod.compareTo(unrelatedMethod2));
+ }
+
//====================================================================================================
// getAnnotatedReturnType()
//====================================================================================================
@@ -424,6 +477,49 @@ class MethodInfo_Test extends TestBase {
check("Integer", d_a2.getReturnType());
}
+ // Test classes for covariant return type testing
+ public static class ParentWithMethod {
+ public ParentWithMethod getInstance() {
+ return new ParentWithMethod();
+ }
+ }
+
+ public static class ChildWithCovariantReturn extends ParentWithMethod {
+ @Override
+ public ChildWithCovariantReturn getInstance() {
+ return new ChildWithCovariantReturn();
+ }
+ }
+
+ /**
+ * Tests that getReturnType() correctly handles covariant return types.
+ * When a child class overrides a parent method with a more specific
return type,
+ * getReturnType() should return the child's return type, not the
parent's.
+ */
+ @Test
+ void a013b_getReturnType_covariantReturnType() throws Exception {
+ // Get the method from the child class
+ var childMethod =
MethodInfo.of(ChildWithCovariantReturn.class.getMethod("getInstance"));
+ var childReturnType = childMethod.getReturnType();
+
+ // Should return the child type, not the parent type
+ assertEquals(ChildWithCovariantReturn.class,
childReturnType.inner());
+ assertNotEquals(ParentWithMethod.class,
childReturnType.inner());
+
+ // Verify it's actually a child of the parent
+
assertTrue(childReturnType.isChildOf(ClassInfo.of(ParentWithMethod.class)));
+
+ // Get the method from the parent class for comparison
+ var parentMethod =
MethodInfo.of(ParentWithMethod.class.getMethod("getInstance"));
+ var parentReturnType = parentMethod.getReturnType();
+
+ // Parent method should return parent type
+ assertEquals(ParentWithMethod.class, parentReturnType.inner());
+
+ // Child and parent return types should be different
+ assertNotEquals(childReturnType.inner(),
parentReturnType.inner());
+ }
+
//====================================================================================================
// getSignature()
//====================================================================================================
diff --git
a/juneau-utest/src/test/java/org/apache/juneau/commons/utils/Utils_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/commons/utils/Utils_Test.java
index e64db1fd06..47f72ecbbe 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/commons/utils/Utils_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/commons/utils/Utils_Test.java
@@ -20,6 +20,7 @@ import static
org.apache.juneau.commons.utils.CollectionUtils.*;
import static org.apache.juneau.commons.utils.Utils.*;
import static org.junit.jupiter.api.Assertions.*;
+import java.io.*;
import java.lang.annotation.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
@@ -271,6 +272,54 @@ class Utils_Test extends TestBase {
assertFalse(eq("not an annotation", a1)); // o1 is not, o2 is
Annotation
}
+
//====================================================================================================
+ // eqAny(T, T...)
+
//====================================================================================================
+ @Test
+ void a014_eqAny() {
+ // Basic equality tests
+ assertTrue(eqAny("apple", "apple", "banana", "cherry"));
+ assertFalse(eqAny("apple", "banana", "cherry"));
+ assertTrue(eqAny(123, 123, 456, 789));
+ assertFalse(eqAny(123, 456, 789));
+
+ // Null handling
+ assertTrue(eqAny(null, "apple", null, "banana"));
+ assertFalse(eqAny(null, "apple", "banana"));
+ assertTrue(eqAny("apple", null, "apple", "banana"));
+ assertFalse(eqAny("apple", "banana", "cherry"));
+
+ // Empty varargs
+ assertFalse(eqAny("apple"));
+ assertFalse(eqAny(null));
+ assertFalse(eqAny(123));
+
+ // Null varargs array
+ String[] nullArray = null;
+ assertFalse(eqAny("apple", nullArray));
+
+ // Test arrays
+ var arr1 = a(1, 2, 3);
+ var arr2 = a(1, 2, 3);
+ var arr3 = a(1, 2, 4);
+ assertTrue(eqAny(arr1, arr2, arr3));
+ assertFalse(eqAny(arr1, (Serializable[])arr3));
+
+ // Test annotations
+ @TestAnnotation("test")
+ class T1 {}
+ @TestAnnotation("test")
+ class T2 {}
+ @TestAnnotation("different")
+ class T3 {}
+
+ var a1 = T1.class.getAnnotation(TestAnnotation.class);
+ var a2 = T2.class.getAnnotation(TestAnnotation.class);
+ var a3 = T3.class.getAnnotation(TestAnnotation.class);
+ assertTrue(eqAny(a1, a2, a3));
+ assertFalse(eqAny(a1, a3));
+ }
+
//====================================================================================================
// eq(T, U, BiPredicate<T,U>)
//====================================================================================================