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 844fe59b57 Unit tests
844fe59b57 is described below

commit 844fe59b5706ba2e03a6b49b7f69faf51dc25761
Author: James Bognar <[email protected]>
AuthorDate: Tue Dec 2 06:55:42 2025 -0800

    Unit tests
---
 .../juneau/commons/reflect/AccessibleInfo.java     |  17 +-
 .../juneau/commons/reflect/ExecutableInfo.java     |  11 +-
 .../org/apache/juneau/commons/utils/Utils.java     |  33 +
 .../juneau/commons/collections/Cache2_Test.java    | 289 +++++++++
 .../juneau/commons/collections/Cache3_Test.java    | 264 ++++++++
 .../juneau/commons/collections/Cache4_Test.java    | 264 ++++++++
 .../juneau/commons/collections/Cache5_Test.java    | 264 ++++++++
 .../juneau/commons/collections/Cache_Test.java     | 290 +++++++++
 .../juneau/commons/reflect/AnnotationInfoTest.java |  57 --
 .../commons/reflect/AnnotationInfo_Test.java       | 706 +++++++++++++++++++++
 .../reflect/AnnotationInfo_ValueMethods_Test.java  | 188 ------
 ...ctorInfoTest.java => ConstructorInfo_Test.java} |  27 +-
 .../commons/reflect/ExecutableInfo_Test.java       |  33 +
 .../reflect/FieldInfo_AnnotationInfos_Test.java    | 117 ----
 .../commons/reflect/FieldInfo_FullName_Test.java   |  74 ---
 .../juneau/commons/reflect/FieldInfo_Test.java     | 157 +++++
 ...ameterInfoTest.java => ParameterInfo_Test.java} |   2 +-
 17 files changed, 2336 insertions(+), 457 deletions(-)

diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/AccessibleInfo.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/AccessibleInfo.java
index f5f41a6deb..5a52351c21 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/AccessibleInfo.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/AccessibleInfo.java
@@ -17,6 +17,7 @@
 package org.apache.juneau.commons.reflect;
 
 import static org.apache.juneau.commons.utils.Utils.*;
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
 
 import java.lang.reflect.*;
 
@@ -83,7 +84,7 @@ public abstract class AccessibleInfo extends ElementInfo {
         */
        protected AccessibleInfo(AccessibleObject inner, int modifiers) {
                super(modifiers);
-               this.inner = inner;
+               this.inner = assertArgNotNull("inner", inner);
        }
 
        
//-----------------------------------------------------------------------------------------------------------------
@@ -107,11 +108,7 @@ public abstract class AccessibleInfo extends ElementInfo {
         * @return <jk>true</jk> if this object is accessible, <jk>false</jk> 
otherwise or if not supported.
         */
        public boolean isAccessible() {
-               try {
-                       return 
(boolean)AccessibleObject.class.getMethod("isAccessible").invoke(inner);
-               } catch (@SuppressWarnings("unused") Exception ex) {
-                       return false;
-               }
+               return safeOpt(() -> 
(boolean)AccessibleObject.class.getMethod("isAccessible").invoke(inner)).orElse(false);
        }
 
        /**
@@ -120,12 +117,6 @@ public abstract class AccessibleInfo extends ElementInfo {
         * @return <jk>true</jk> if call was successful.
         */
        public boolean setAccessible() {
-               try {
-                       if (nn(inner))
-                               inner.setAccessible(true);
-                       return true;
-               } catch (@SuppressWarnings("unused") SecurityException e) {
-                       return false;
-               }
+               return safeOpt(() -> { inner.setAccessible(true); return 
true;}).orElse(false);
        }
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/ExecutableInfo.java
 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/ExecutableInfo.java
index cc99b5f9a9..dc32bd735b 100644
--- 
a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/ExecutableInfo.java
+++ 
b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/reflect/ExecutableInfo.java
@@ -711,13 +711,12 @@ public abstract class ExecutableInfo extends 
AccessibleInfo {
         */
        @Override
        public final boolean setAccessible() {
-               try {
-                       if (nn(inner))
-                               inner.setAccessible(true);
-                       return true;
-               } catch (@SuppressWarnings("unused") SecurityException e) {
+               if (!nn(inner))
                        return false;
-               }
+               return safeOpt(() -> {
+                       inner.setAccessible(true);
+                       return true;
+               }).orElse(false);
        }
 
        /**
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 1e4c6c9d88..b43b7ced47 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
@@ -1395,6 +1395,39 @@ public class Utils {
                }
        }
 
+       /**
+        * Executes a supplier that may throw an exception and returns an 
Optional.
+        *
+        * <p>
+        * If the supplier executes successfully, returns {@link 
Optional#of(Object)} with the result.
+        * If the supplier throws any exception, returns {@link 
Optional#empty()}.
+        *
+        * <p>
+        * This is useful for operations that may fail but you want to handle 
the failure
+        * gracefully by returning an empty Optional instead of throwing an 
exception.
+        *
+        * <h5 class='section'>Example:</h5>
+        * <p class='bjava'>
+        *      <jc>// Check if AccessibleObject.isAccessible() method exists 
(Java 9+)</jc>
+        *      <jk>boolean</jk> <jv>isAccessible</jv> = <jsm>safeOpt</jsm>(() 
-&gt;
+        *              
(<jk>boolean</jk>)AccessibleObject.<jk>class</jk>.getMethod(<js>"isAccessible"</js>).invoke(<jv>obj</jv>)
+        *      ).orElse(<jk>false</jk>);
+        * </p>
+        *
+        * @param <T> The return type.
+        * @param s The supplier that may throw an exception.
+        * @return An Optional containing the result if successful, or empty if 
an exception was thrown.
+        * @see #safe(ThrowingSupplier)
+        * @see #opt(Object)
+        */
+       public static <T> Optional<T> safeOpt(ThrowingSupplier<T> s) {
+               try {
+                       return Optional.of(s.get());
+               } catch (@SuppressWarnings("unused") Exception e) {
+                       return Optional.empty();
+               }
+       }
+
        /**
         * Allows you to wrap a supplier that throws an exception so that it 
can be used in a fluent interface.
         *
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache2_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache2_Test.java
index 392a9927df..ee9cd12df3 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache2_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache2_Test.java
@@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.*;
 import static org.apache.juneau.commons.collections.CacheMode.*;
 import static org.apache.juneau.junit.bct.BctAssertions.*;
 
+import java.util.concurrent.*;
 import java.util.concurrent.atomic.*;
 
 import org.apache.juneau.*;
@@ -301,6 +302,46 @@ class Cache2_Test extends TestBase {
                assertSize(1, x);
        }
 
+       @Test
+       void d06_weakMethod_basicCaching() {
+               // Test the weak() convenience method
+               var callCount = new AtomicInteger();
+               var x = Cache2.of(String.class, Integer.class, String.class)
+                       .weak()
+                       .supplier((k1, k2) -> {
+                               callCount.incrementAndGet();
+                               return k1 + ":" + k2;
+                       })
+                       .build();
+
+               // First call - cache miss
+               var result1 = x.get("user", 123);
+
+               // Second call - cache hit
+               var result2 = x.get("user", 123);
+
+               assertEquals("user:123", result1);
+               assertEquals("user:123", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, x);
+               assertEquals(1, x.getCacheHits());
+       }
+
+       @Test
+       void d07_weakMethod_chaining() {
+               // Test that weak() can be chained with other builder methods
+               var x = Cache2.of(String.class, Integer.class, String.class)
+                       .weak()
+                       .maxSize(100)
+                       .supplier((k1, k2) -> k1 + ":" + k2)
+                       .build();
+
+               var result = x.get("user", 123);
+               assertEquals("user:123", result);
+               assertSize(1, x);
+       }
+
        
//====================================================================================================
        // e - Max size and eviction
        
//====================================================================================================
@@ -523,6 +564,17 @@ class Cache2_Test extends TestBase {
                assertFalse(x.containsValue("value1"));
        }
 
+       @Test
+       void m04_containsValue_nullValue() {
+               var x = Cache2.of(String.class, Integer.class, 
String.class).build();
+               // Null values can't be cached, so containsValue(null) should 
return false
+               x.get("user", 123, () -> null);
+               assertFalse(x.containsValue(null));
+               // Also test with empty cache
+               var x2 = Cache2.of(String.class, Integer.class, 
String.class).build();
+               assertFalse(x2.containsValue(null));
+       }
+
        
//====================================================================================================
        // n - logOnExit() builder methods
        
//====================================================================================================
@@ -616,5 +668,242 @@ class Cache2_Test extends TestBase {
                assertEquals(2, callCount.get());
                assertTrue(x.isEmpty());
        }
+
+       
//====================================================================================================
+       // o - Thread-local cache mode
+       
//====================================================================================================
+
+       @Test
+       void o01_threadLocal_basicCaching() {
+               var callCount = new AtomicInteger();
+               var x = Cache2.of(String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .supplier((k1, k2) -> {
+                               callCount.incrementAndGet();
+                               return k1 + ":" + k2;
+                       })
+                       .build();
+
+               // First call - cache miss
+               var result1 = x.get("user", 123);
+
+               // Second call - cache hit
+               var result2 = x.get("user", 123);
+
+               assertEquals("user:123", result1);
+               assertEquals("user:123", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, x);
+               assertEquals(1, x.getCacheHits());
+       }
+
+       @Test
+       void o02_threadLocal_eachThreadHasOwnCache() throws Exception {
+               var x = Cache2.of(String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .build();
+               var executor = 
java.util.concurrent.Executors.newFixedThreadPool(2);
+               var threadValues = new ConcurrentHashMap<Thread, String>();
+
+               // Each thread caches ("user", 123) with its own value
+               var future1 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("user", 123, () -> "thread1-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               var future2 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("user", 123, () -> "thread2-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               java.util.concurrent.CompletableFuture.allOf(future1, 
future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
+
+               // Verify both threads cached their own values
+               assertEquals(2, threadValues.size());
+               assertTrue(threadValues.containsValue("thread1-value"));
+               assertTrue(threadValues.containsValue("thread2-value"));
+
+               // Verify each thread's cache is independent - same thread 
should get same cached value
+               var threadValues2 = new ConcurrentHashMap<Thread, String>();
+               var threads = new 
java.util.ArrayList<Thread>(threadValues.keySet());
+
+               future1 = java.util.concurrent.CompletableFuture.runAsync(() -> 
{
+                       var value = x.get("user", 123, () -> 
"should-not-be-called");
+                       threadValues2.put(Thread.currentThread(), value);
+               }, executor);
+
+               future2 = java.util.concurrent.CompletableFuture.runAsync(() -> 
{
+                       var value = x.get("user", 123, () -> 
"should-not-be-called");
+                       threadValues2.put(Thread.currentThread(), value);
+               }, executor);
+
+               java.util.concurrent.CompletableFuture.allOf(future1, 
future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
+
+               // Each thread should get its own cached value (same as what it 
cached before)
+               for (var thread : threads) {
+                       if (threadValues2.containsKey(thread)) {
+                               assertEquals(threadValues.get(thread), 
threadValues2.get(thread),
+                                       "Thread " + thread + " should get its 
own cached value");
+                       }
+               }
+
+               executor.shutdown();
+       }
+
+       @Test
+       void o03_threadLocal_multipleKeys() {
+               var x = Cache2.of(String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .supplier((k1, k2) -> k1 + ":" + k2)
+                       .build();
+
+               x.get("user", 123);
+               x.get("admin", 456);
+               x.get("guest", 789);
+
+               assertSize(3, x);
+               assertEquals(0, x.getCacheHits());
+
+               // Verify all cached
+               assertEquals("user:123", x.get("user", 123));
+               assertEquals("admin:456", x.get("admin", 456));
+               assertEquals("guest:789", x.get("guest", 789));
+               assertEquals(3, x.getCacheHits());
+       }
+
+       @Test
+       void o04_threadLocal_clear() {
+               var x = Cache2.of(String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .supplier((k1, k2) -> k1 + ":" + k2)
+                       .build();
+
+               x.get("user", 123);
+               x.get("admin", 456);
+               assertSize(2, x);
+
+               x.clear();
+               assertEmpty(x);
+       }
+
+       @Test
+       void o05_threadLocal_maxSize() {
+               var x = Cache2.of(String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .maxSize(2)
+                       .supplier((k1, k2) -> k1 + ":" + k2)
+                       .build();
+
+               x.get("k1", 1);
+               x.get("k2", 2);
+               assertSize(2, x);
+
+               // 3rd item doesn't trigger eviction yet
+               x.get("k3", 3);
+               assertSize(3, x);
+
+               // 4th item triggers eviction
+               x.get("k4", 4);
+               assertSize(1, x);
+       }
+
+       
//====================================================================================================
+       // p - Thread-local + weak mode combination
+       
//====================================================================================================
+
+       @Test
+       void p01_threadLocal_weakMode_basicCaching() {
+               var callCount = new AtomicInteger();
+               var x = Cache2.of(String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .supplier((k1, k2) -> {
+                               callCount.incrementAndGet();
+                               return k1 + ":" + k2;
+                       })
+                       .build();
+
+               // First call - cache miss
+               var result1 = x.get("user", 123);
+
+               // Second call - cache hit
+               var result2 = x.get("user", 123);
+
+               assertEquals("user:123", result1);
+               assertEquals("user:123", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, x);
+               assertEquals(1, x.getCacheHits());
+       }
+
+       @Test
+       void p02_threadLocal_weakMode_eachThreadHasOwnCache() throws Exception {
+               var x = Cache2.of(String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .build();
+               var executor = 
java.util.concurrent.Executors.newFixedThreadPool(2);
+               var threadValues = new ConcurrentHashMap<Thread, String>();
+
+               // Each thread caches ("user", 123) with its own value
+               var future1 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("user", 123, () -> "thread1-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               var future2 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("user", 123, () -> "thread2-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               java.util.concurrent.CompletableFuture.allOf(future1, 
future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
+
+               // Verify both threads cached their own values
+               assertEquals(2, threadValues.size());
+               assertTrue(threadValues.containsValue("thread1-value"));
+               assertTrue(threadValues.containsValue("thread2-value"));
+
+               executor.shutdown();
+       }
+
+       @Test
+       void p03_threadLocal_weakMode_clear() {
+               var x = Cache2.of(String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .supplier((k1, k2) -> k1 + ":" + k2)
+                       .build();
+
+               x.get("user", 123);
+               x.get("admin", 456);
+               assertSize(2, x);
+
+               x.clear();
+               assertEmpty(x);
+       }
+
+       @Test
+       void p04_threadLocal_weakMode_maxSize() {
+               var x = Cache2.of(String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .maxSize(2)
+                       .supplier((k1, k2) -> k1 + ":" + k2)
+                       .build();
+
+               x.get("k1", 1);
+               x.get("k2", 2);
+               assertSize(2, x);
+
+               // 3rd item doesn't trigger eviction yet
+               x.get("k3", 3);
+               assertSize(3, x);
+
+               // 4th item triggers eviction
+               x.get("k4", 4);
+               assertSize(1, x);
+       }
 }
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache3_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache3_Test.java
index a89408d392..560092ac8d 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache3_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache3_Test.java
@@ -178,6 +178,46 @@ class Cache3_Test extends TestBase {
                assertSize(1, x);
        }
 
+       @Test
+       void a04f_weakMethod_basicCaching() {
+               // Test the weak() convenience method
+               var callCount = new AtomicInteger();
+               var x = Cache3.of(String.class, String.class, Integer.class, 
String.class)
+                       .weak()
+                       .supplier((k1, k2, k3) -> {
+                               callCount.incrementAndGet();
+                               return k1 + ":" + k2 + ":" + k3;
+                       })
+                       .build();
+
+               // First call - cache miss
+               var result1 = x.get("en", "US", 1);
+
+               // Second call - cache hit
+               var result2 = x.get("en", "US", 1);
+
+               assertEquals("en:US:1", result1);
+               assertEquals("en:US:1", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, x);
+               assertEquals(1, x.getCacheHits());
+       }
+
+       @Test
+       void a04g_weakMethod_chaining() {
+               // Test that weak() can be chained with other builder methods
+               var x = Cache3.of(String.class, String.class, Integer.class, 
String.class)
+                       .weak()
+                       .maxSize(100)
+                       .supplier((k1, k2, k3) -> k1 + ":" + k2 + ":" + k3)
+                       .build();
+
+               var result = x.get("en", "US", 1);
+               assertEquals("en:US:1", result);
+               assertSize(1, x);
+       }
+
        @Test
        void a05_maxSize() {
                var x = Cache3.of(String.class, String.class, Integer.class, 
String.class)
@@ -316,6 +356,17 @@ class Cache3_Test extends TestBase {
                assertFalse(x.containsValue("value2"));
        }
 
+       @Test
+       void d03_containsValue_nullValue() {
+               var x = Cache3.of(String.class, String.class, Integer.class, 
String.class).build();
+               // Null values can't be cached, so containsValue(null) should 
return false
+               x.get("en", "US", 1, () -> null);
+               assertFalse(x.containsValue(null));
+               // Also test with empty cache
+               var x2 = Cache3.of(String.class, String.class, Integer.class, 
String.class).build();
+               assertFalse(x2.containsValue(null));
+       }
+
        
//====================================================================================================
        // e - logOnExit() builder methods
        
//====================================================================================================
@@ -339,5 +390,218 @@ class Cache3_Test extends TestBase {
                x.get("en", "US", 1);
                assertSize(1, x);
        }
+
+       
//====================================================================================================
+       // f - Thread-local cache mode
+       
//====================================================================================================
+
+       @Test
+       void f01_threadLocal_basicCaching() {
+               var callCount = new AtomicInteger();
+               var x = Cache3.of(String.class, String.class, Integer.class, 
String.class)
+                       .threadLocal()
+                       .supplier((k1, k2, k3) -> {
+                               callCount.incrementAndGet();
+                               return k1 + ":" + k2 + ":" + k3;
+                       })
+                       .build();
+
+               // First call - cache miss
+               var result1 = x.get("en", "US", 1);
+
+               // Second call - cache hit
+               var result2 = x.get("en", "US", 1);
+
+               assertEquals("en:US:1", result1);
+               assertEquals("en:US:1", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, x);
+               assertEquals(1, x.getCacheHits());
+       }
+
+       @Test
+       void f02_threadLocal_eachThreadHasOwnCache() throws Exception {
+               var x = Cache3.of(String.class, String.class, Integer.class, 
String.class)
+                       .threadLocal()
+                       .build();
+               var executor = 
java.util.concurrent.Executors.newFixedThreadPool(2);
+               var threadValues = new 
java.util.concurrent.ConcurrentHashMap<Thread, String>();
+
+               // Each thread caches ("en", "US", 1) with its own value
+               var future1 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("en", "US", 1, () -> "thread1-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               var future2 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("en", "US", 1, () -> "thread2-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               java.util.concurrent.CompletableFuture.allOf(future1, 
future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
+
+               // Verify both threads cached their own values
+               assertEquals(2, threadValues.size());
+               assertTrue(threadValues.containsValue("thread1-value"));
+               assertTrue(threadValues.containsValue("thread2-value"));
+
+               executor.shutdown();
+       }
+
+       @Test
+       void f03_threadLocal_multipleKeys() {
+               var x = Cache3.of(String.class, String.class, Integer.class, 
String.class)
+                       .threadLocal()
+                       .supplier((k1, k2, k3) -> k1 + ":" + k2 + ":" + k3)
+                       .build();
+
+               x.get("en", "US", 1);
+               x.get("fr", "FR", 2);
+               x.get("de", "DE", 3);
+
+               assertSize(3, x);
+               assertEquals(0, x.getCacheHits());
+
+               // Verify all cached
+               assertEquals("en:US:1", x.get("en", "US", 1));
+               assertEquals("fr:FR:2", x.get("fr", "FR", 2));
+               assertEquals("de:DE:3", x.get("de", "DE", 3));
+               assertEquals(3, x.getCacheHits());
+       }
+
+       @Test
+       void f04_threadLocal_clear() {
+               var x = Cache3.of(String.class, String.class, Integer.class, 
String.class)
+                       .threadLocal()
+                       .supplier((k1, k2, k3) -> "value")
+                       .build();
+
+               x.get("en", "US", 1);
+               x.get("fr", "FR", 2);
+               assertSize(2, x);
+
+               x.clear();
+               assertEmpty(x);
+       }
+
+       @Test
+       void f05_threadLocal_maxSize() {
+               var x = Cache3.of(String.class, String.class, Integer.class, 
String.class)
+                       .threadLocal()
+                       .maxSize(2)
+                       .supplier((k1, k2, k3) -> "value")
+                       .build();
+
+               x.get("en", "US", 1);
+               x.get("fr", "FR", 2);
+               assertSize(2, x);
+
+               // 3rd item doesn't trigger eviction yet
+               x.get("de", "DE", 3);
+               assertSize(3, x);
+
+               // 4th item triggers eviction
+               x.get("es", "ES", 4);
+               assertSize(1, x);
+       }
+
+       
//====================================================================================================
+       // g - Thread-local + weak mode combination
+       
//====================================================================================================
+
+       @Test
+       void g01_threadLocal_weakMode_basicCaching() {
+               var callCount = new AtomicInteger();
+               var x = Cache3.of(String.class, String.class, Integer.class, 
String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .supplier((k1, k2, k3) -> {
+                               callCount.incrementAndGet();
+                               return k1 + ":" + k2 + ":" + k3;
+                       })
+                       .build();
+
+               // First call - cache miss
+               var result1 = x.get("en", "US", 1);
+
+               // Second call - cache hit
+               var result2 = x.get("en", "US", 1);
+
+               assertEquals("en:US:1", result1);
+               assertEquals("en:US:1", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, x);
+               assertEquals(1, x.getCacheHits());
+       }
+
+       @Test
+       void g02_threadLocal_weakMode_eachThreadHasOwnCache() throws Exception {
+               var x = Cache3.of(String.class, String.class, Integer.class, 
String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .build();
+               var executor = 
java.util.concurrent.Executors.newFixedThreadPool(2);
+               var threadValues = new 
java.util.concurrent.ConcurrentHashMap<Thread, String>();
+
+               // Each thread caches ("en", "US", 1) with its own value
+               var future1 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("en", "US", 1, () -> "thread1-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               var future2 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("en", "US", 1, () -> "thread2-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               java.util.concurrent.CompletableFuture.allOf(future1, 
future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
+
+               // Verify both threads cached their own values
+               assertEquals(2, threadValues.size());
+               assertTrue(threadValues.containsValue("thread1-value"));
+               assertTrue(threadValues.containsValue("thread2-value"));
+
+               executor.shutdown();
+       }
+
+       @Test
+       void g03_threadLocal_weakMode_clear() {
+               var x = Cache3.of(String.class, String.class, Integer.class, 
String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .supplier((k1, k2, k3) -> "value")
+                       .build();
+
+               x.get("en", "US", 1);
+               x.get("fr", "FR", 2);
+               assertSize(2, x);
+
+               x.clear();
+               assertEmpty(x);
+       }
+
+       @Test
+       void g04_threadLocal_weakMode_maxSize() {
+               var x = Cache3.of(String.class, String.class, Integer.class, 
String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .maxSize(2)
+                       .supplier((k1, k2, k3) -> "value")
+                       .build();
+
+               x.get("en", "US", 1);
+               x.get("fr", "FR", 2);
+               assertSize(2, x);
+
+               // 3rd item doesn't trigger eviction yet
+               x.get("de", "DE", 3);
+               assertSize(3, x);
+
+               // 4th item triggers eviction
+               x.get("es", "ES", 4);
+               assertSize(1, x);
+       }
 }
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache4_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache4_Test.java
index 76821a4ec6..b1b2e55259 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache4_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache4_Test.java
@@ -179,6 +179,46 @@ class Cache4_Test extends TestBase {
                assertSize(1, x);
        }
 
+       @Test
+       void a04f_weakMethod_basicCaching() {
+               // Test the weak() convenience method
+               var callCount = new AtomicInteger();
+               var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class)
+                       .weak()
+                       .supplier((k1, k2, k3, k4) -> {
+                               callCount.incrementAndGet();
+                               return k1 + ":" + k2 + ":" + k3 + ":" + k4;
+                       })
+                       .build();
+
+               // First call - cache miss
+               var result1 = x.get("en", "US", "formal", 1);
+
+               // Second call - cache hit
+               var result2 = x.get("en", "US", "formal", 1);
+
+               assertEquals("en:US:formal:1", result1);
+               assertEquals("en:US:formal:1", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, x);
+               assertEquals(1, x.getCacheHits());
+       }
+
+       @Test
+       void a04g_weakMethod_chaining() {
+               // Test that weak() can be chained with other builder methods
+               var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class)
+                       .weak()
+                       .maxSize(100)
+                       .supplier((k1, k2, k3, k4) -> k1 + ":" + k2 + ":" + k3 
+ ":" + k4)
+                       .build();
+
+               var result = x.get("en", "US", "formal", 1);
+               assertEquals("en:US:formal:1", result);
+               assertSize(1, x);
+       }
+
        @Test
        void a05_maxSize() {
                var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class)
@@ -316,6 +356,17 @@ class Cache4_Test extends TestBase {
                assertFalse(x.containsValue("value2"));
        }
 
+       @Test
+       void d03_containsValue_nullValue() {
+               var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class).build();
+               // Null values can't be cached, so containsValue(null) should 
return false
+               x.get("en", "US", "formal", 1, () -> null);
+               assertFalse(x.containsValue(null));
+               // Also test with empty cache
+               var x2 = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class).build();
+               assertFalse(x2.containsValue(null));
+       }
+
        
//====================================================================================================
        // e - logOnExit() builder methods
        
//====================================================================================================
@@ -339,5 +390,218 @@ class Cache4_Test extends TestBase {
                x.get("en", "US", "formal", 1);
                assertSize(1, x);
        }
+
+       
//====================================================================================================
+       // f - Thread-local cache mode
+       
//====================================================================================================
+
+       @Test
+       void f01_threadLocal_basicCaching() {
+               var callCount = new AtomicInteger();
+               var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class)
+                       .threadLocal()
+                       .supplier((k1, k2, k3, k4) -> {
+                               callCount.incrementAndGet();
+                               return k1 + ":" + k2 + ":" + k3 + ":" + k4;
+                       })
+                       .build();
+
+               // First call - cache miss
+               var result1 = x.get("en", "US", "formal", 1);
+
+               // Second call - cache hit
+               var result2 = x.get("en", "US", "formal", 1);
+
+               assertEquals("en:US:formal:1", result1);
+               assertEquals("en:US:formal:1", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, x);
+               assertEquals(1, x.getCacheHits());
+       }
+
+       @Test
+       void f02_threadLocal_eachThreadHasOwnCache() throws Exception {
+               var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class)
+                       .threadLocal()
+                       .build();
+               var executor = 
java.util.concurrent.Executors.newFixedThreadPool(2);
+               var threadValues = new 
java.util.concurrent.ConcurrentHashMap<Thread, String>();
+
+               // Each thread caches ("en", "US", "formal", 1) with its own 
value
+               var future1 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("en", "US", "formal", 1, () -> 
"thread1-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               var future2 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("en", "US", "formal", 1, () -> 
"thread2-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               java.util.concurrent.CompletableFuture.allOf(future1, 
future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
+
+               // Verify both threads cached their own values
+               assertEquals(2, threadValues.size());
+               assertTrue(threadValues.containsValue("thread1-value"));
+               assertTrue(threadValues.containsValue("thread2-value"));
+
+               executor.shutdown();
+       }
+
+       @Test
+       void f03_threadLocal_multipleKeys() {
+               var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class)
+                       .threadLocal()
+                       .supplier((k1, k2, k3, k4) -> k1 + ":" + k2 + ":" + k3 
+ ":" + k4)
+                       .build();
+
+               x.get("en", "US", "formal", 1);
+               x.get("fr", "FR", "informal", 2);
+               x.get("de", "DE", "formal", 3);
+
+               assertSize(3, x);
+               assertEquals(0, x.getCacheHits());
+
+               // Verify all cached
+               assertEquals("en:US:formal:1", x.get("en", "US", "formal", 1));
+               assertEquals("fr:FR:informal:2", x.get("fr", "FR", "informal", 
2));
+               assertEquals("de:DE:formal:3", x.get("de", "DE", "formal", 3));
+               assertEquals(3, x.getCacheHits());
+       }
+
+       @Test
+       void f04_threadLocal_clear() {
+               var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class)
+                       .threadLocal()
+                       .supplier((k1, k2, k3, k4) -> "value")
+                       .build();
+
+               x.get("en", "US", "formal", 1);
+               x.get("fr", "FR", "informal", 2);
+               assertSize(2, x);
+
+               x.clear();
+               assertEmpty(x);
+       }
+
+       @Test
+       void f05_threadLocal_maxSize() {
+               var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class)
+                       .threadLocal()
+                       .maxSize(2)
+                       .supplier((k1, k2, k3, k4) -> "value")
+                       .build();
+
+               x.get("en", "US", "formal", 1);
+               x.get("fr", "FR", "informal", 2);
+               assertSize(2, x);
+
+               // 3rd item doesn't trigger eviction yet
+               x.get("de", "DE", "formal", 3);
+               assertSize(3, x);
+
+               // 4th item triggers eviction
+               x.get("es", "ES", "informal", 4);
+               assertSize(1, x);
+       }
+
+       
//====================================================================================================
+       // g - Thread-local + weak mode combination
+       
//====================================================================================================
+
+       @Test
+       void g01_threadLocal_weakMode_basicCaching() {
+               var callCount = new AtomicInteger();
+               var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .supplier((k1, k2, k3, k4) -> {
+                               callCount.incrementAndGet();
+                               return k1 + ":" + k2 + ":" + k3 + ":" + k4;
+                       })
+                       .build();
+
+               // First call - cache miss
+               var result1 = x.get("en", "US", "formal", 1);
+
+               // Second call - cache hit
+               var result2 = x.get("en", "US", "formal", 1);
+
+               assertEquals("en:US:formal:1", result1);
+               assertEquals("en:US:formal:1", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, x);
+               assertEquals(1, x.getCacheHits());
+       }
+
+       @Test
+       void g02_threadLocal_weakMode_eachThreadHasOwnCache() throws Exception {
+               var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .build();
+               var executor = 
java.util.concurrent.Executors.newFixedThreadPool(2);
+               var threadValues = new 
java.util.concurrent.ConcurrentHashMap<Thread, String>();
+
+               // Each thread caches ("en", "US", "formal", 1) with its own 
value
+               var future1 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("en", "US", "formal", 1, () -> 
"thread1-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               var future2 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("en", "US", "formal", 1, () -> 
"thread2-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               java.util.concurrent.CompletableFuture.allOf(future1, 
future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
+
+               // Verify both threads cached their own values
+               assertEquals(2, threadValues.size());
+               assertTrue(threadValues.containsValue("thread1-value"));
+               assertTrue(threadValues.containsValue("thread2-value"));
+
+               executor.shutdown();
+       }
+
+       @Test
+       void g03_threadLocal_weakMode_clear() {
+               var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .supplier((k1, k2, k3, k4) -> "value")
+                       .build();
+
+               x.get("en", "US", "formal", 1);
+               x.get("fr", "FR", "informal", 2);
+               assertSize(2, x);
+
+               x.clear();
+               assertEmpty(x);
+       }
+
+       @Test
+       void g04_threadLocal_weakMode_maxSize() {
+               var x = Cache4.of(String.class, String.class, String.class, 
Integer.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .maxSize(2)
+                       .supplier((k1, k2, k3, k4) -> "value")
+                       .build();
+
+               x.get("en", "US", "formal", 1);
+               x.get("fr", "FR", "informal", 2);
+               assertSize(2, x);
+
+               // 3rd item doesn't trigger eviction yet
+               x.get("de", "DE", "formal", 3);
+               assertSize(3, x);
+
+               // 4th item triggers eviction
+               x.get("es", "ES", "informal", 4);
+               assertSize(1, x);
+       }
 }
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache5_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache5_Test.java
index e74ad86bfb..b0a1bf3eea 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache5_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache5_Test.java
@@ -180,6 +180,46 @@ class Cache5_Test extends TestBase {
                assertSize(1, x);
        }
 
+       @Test
+       void a04f_weakMethod_basicCaching() {
+               // Test the weak() convenience method
+               var callCount = new AtomicInteger();
+               var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class)
+                       .weak()
+                       .supplier((k1, k2, k3, k4, k5) -> {
+                               callCount.incrementAndGet();
+                               return k1 + ":" + k2 + ":" + k3 + ":" + k4 + 
":" + k5;
+                       })
+                       .build();
+
+               // First call - cache miss
+               var result1 = x.get("en", "US", "west", "formal", 1);
+
+               // Second call - cache hit
+               var result2 = x.get("en", "US", "west", "formal", 1);
+
+               assertEquals("en:US:west:formal:1", result1);
+               assertEquals("en:US:west:formal:1", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, x);
+               assertEquals(1, x.getCacheHits());
+       }
+
+       @Test
+       void a04g_weakMethod_chaining() {
+               // Test that weak() can be chained with other builder methods
+               var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class)
+                       .weak()
+                       .maxSize(100)
+                       .supplier((k1, k2, k3, k4, k5) -> k1 + ":" + k2 + ":" + 
k3 + ":" + k4 + ":" + k5)
+                       .build();
+
+               var result = x.get("en", "US", "west", "formal", 1);
+               assertEquals("en:US:west:formal:1", result);
+               assertSize(1, x);
+       }
+
        @Test
        void a05_maxSize() {
                var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class)
@@ -317,6 +357,17 @@ class Cache5_Test extends TestBase {
                assertFalse(x.containsValue("value2"));
        }
 
+       @Test
+       void d03_containsValue_nullValue() {
+               var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class).build();
+               // Null values can't be cached, so containsValue(null) should 
return false
+               x.get("en", "US", "west", "formal", 1, () -> null);
+               assertFalse(x.containsValue(null));
+               // Also test with empty cache
+               var x2 = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class).build();
+               assertFalse(x2.containsValue(null));
+       }
+
        
//====================================================================================================
        // e - logOnExit() builder methods
        
//====================================================================================================
@@ -340,5 +391,218 @@ class Cache5_Test extends TestBase {
                x.get("en", "US", "west", "formal", 1);
                assertSize(1, x);
        }
+
+       
//====================================================================================================
+       // f - Thread-local cache mode
+       
//====================================================================================================
+
+       @Test
+       void f01_threadLocal_basicCaching() {
+               var callCount = new AtomicInteger();
+               var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .supplier((k1, k2, k3, k4, k5) -> {
+                               callCount.incrementAndGet();
+                               return k1 + ":" + k2 + ":" + k3 + ":" + k4 + 
":" + k5;
+                       })
+                       .build();
+
+               // First call - cache miss
+               var result1 = x.get("en", "US", "west", "formal", 1);
+
+               // Second call - cache hit
+               var result2 = x.get("en", "US", "west", "formal", 1);
+
+               assertEquals("en:US:west:formal:1", result1);
+               assertEquals("en:US:west:formal:1", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, x);
+               assertEquals(1, x.getCacheHits());
+       }
+
+       @Test
+       void f02_threadLocal_eachThreadHasOwnCache() throws Exception {
+               var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .build();
+               var executor = 
java.util.concurrent.Executors.newFixedThreadPool(2);
+               var threadValues = new 
java.util.concurrent.ConcurrentHashMap<Thread, String>();
+
+               // Each thread caches ("en", "US", "west", "formal", 1) with 
its own value
+               var future1 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("en", "US", "west", "formal", 1, () 
-> "thread1-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               var future2 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("en", "US", "west", "formal", 1, () 
-> "thread2-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               java.util.concurrent.CompletableFuture.allOf(future1, 
future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
+
+               // Verify both threads cached their own values
+               assertEquals(2, threadValues.size());
+               assertTrue(threadValues.containsValue("thread1-value"));
+               assertTrue(threadValues.containsValue("thread2-value"));
+
+               executor.shutdown();
+       }
+
+       @Test
+       void f03_threadLocal_multipleKeys() {
+               var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .supplier((k1, k2, k3, k4, k5) -> k1 + ":" + k2 + ":" + 
k3 + ":" + k4 + ":" + k5)
+                       .build();
+
+               x.get("en", "US", "west", "formal", 1);
+               x.get("fr", "FR", "east", "informal", 2);
+               x.get("de", "DE", "north", "formal", 3);
+
+               assertSize(3, x);
+               assertEquals(0, x.getCacheHits());
+
+               // Verify all cached
+               assertEquals("en:US:west:formal:1", x.get("en", "US", "west", 
"formal", 1));
+               assertEquals("fr:FR:east:informal:2", x.get("fr", "FR", "east", 
"informal", 2));
+               assertEquals("de:DE:north:formal:3", x.get("de", "DE", "north", 
"formal", 3));
+               assertEquals(3, x.getCacheHits());
+       }
+
+       @Test
+       void f04_threadLocal_clear() {
+               var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .supplier((k1, k2, k3, k4, k5) -> "value")
+                       .build();
+
+               x.get("en", "US", "west", "formal", 1);
+               x.get("fr", "FR", "east", "informal", 2);
+               assertSize(2, x);
+
+               x.clear();
+               assertEmpty(x);
+       }
+
+       @Test
+       void f05_threadLocal_maxSize() {
+               var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .maxSize(2)
+                       .supplier((k1, k2, k3, k4, k5) -> "value")
+                       .build();
+
+               x.get("en", "US", "west", "formal", 1);
+               x.get("fr", "FR", "east", "informal", 2);
+               assertSize(2, x);
+
+               // 3rd item doesn't trigger eviction yet
+               x.get("de", "DE", "north", "formal", 3);
+               assertSize(3, x);
+
+               // 4th item triggers eviction
+               x.get("es", "ES", "south", "informal", 4);
+               assertSize(1, x);
+       }
+
+       
//====================================================================================================
+       // g - Thread-local + weak mode combination
+       
//====================================================================================================
+
+       @Test
+       void g01_threadLocal_weakMode_basicCaching() {
+               var callCount = new AtomicInteger();
+               var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .supplier((k1, k2, k3, k4, k5) -> {
+                               callCount.incrementAndGet();
+                               return k1 + ":" + k2 + ":" + k3 + ":" + k4 + 
":" + k5;
+                       })
+                       .build();
+
+               // First call - cache miss
+               var result1 = x.get("en", "US", "west", "formal", 1);
+
+               // Second call - cache hit
+               var result2 = x.get("en", "US", "west", "formal", 1);
+
+               assertEquals("en:US:west:formal:1", result1);
+               assertEquals("en:US:west:formal:1", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, x);
+               assertEquals(1, x.getCacheHits());
+       }
+
+       @Test
+       void g02_threadLocal_weakMode_eachThreadHasOwnCache() throws Exception {
+               var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .build();
+               var executor = 
java.util.concurrent.Executors.newFixedThreadPool(2);
+               var threadValues = new 
java.util.concurrent.ConcurrentHashMap<Thread, String>();
+
+               // Each thread caches ("en", "US", "west", "formal", 1) with 
its own value
+               var future1 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("en", "US", "west", "formal", 1, () 
-> "thread1-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               var future2 = 
java.util.concurrent.CompletableFuture.runAsync(() -> {
+                       var value = x.get("en", "US", "west", "formal", 1, () 
-> "thread2-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               java.util.concurrent.CompletableFuture.allOf(future1, 
future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
+
+               // Verify both threads cached their own values
+               assertEquals(2, threadValues.size());
+               assertTrue(threadValues.containsValue("thread1-value"));
+               assertTrue(threadValues.containsValue("thread2-value"));
+
+               executor.shutdown();
+       }
+
+       @Test
+       void g03_threadLocal_weakMode_clear() {
+               var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .supplier((k1, k2, k3, k4, k5) -> "value")
+                       .build();
+
+               x.get("en", "US", "west", "formal", 1);
+               x.get("fr", "FR", "east", "informal", 2);
+               assertSize(2, x);
+
+               x.clear();
+               assertEmpty(x);
+       }
+
+       @Test
+       void g04_threadLocal_weakMode_maxSize() {
+               var x = Cache5.of(String.class, String.class, String.class, 
String.class, Integer.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .maxSize(2)
+                       .supplier((k1, k2, k3, k4, k5) -> "value")
+                       .build();
+
+               x.get("en", "US", "west", "formal", 1);
+               x.get("fr", "FR", "east", "informal", 2);
+               assertSize(2, x);
+
+               // 3rd item doesn't trigger eviction yet
+               x.get("de", "DE", "north", "formal", 3);
+               assertSize(3, x);
+
+               // 4th item triggers eviction
+               x.get("es", "ES", "south", "informal", 4);
+               assertSize(1, x);
+       }
 }
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache_Test.java
index d6c237b050..04d7a155b3 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/collections/Cache_Test.java
@@ -332,6 +332,46 @@ class Cache_Test extends TestBase {
                assertSize(1, cache);
        }
 
+       @Test void a16b_weakMethod_basicCaching() {
+               // Test the weak() convenience method
+               var cache = Cache.of(String.class, String.class)
+                       .weak()
+                       .build();
+               var callCount = new AtomicInteger();
+
+               // First call - cache miss
+               var result1 = cache.get("key1", () -> {
+                       callCount.incrementAndGet();
+                       return "value1";
+               });
+
+               // Second call - cache hit
+               var result2 = cache.get("key1", () -> {
+                       callCount.incrementAndGet();
+                       return "should not be called";
+               });
+
+               assertEquals("value1", result1);
+               assertEquals("value1", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, cache);
+               assertEquals(1, cache.getCacheHits());
+       }
+
+       @Test void a16c_weakMethod_chaining() {
+               // Test that weak() can be chained with other builder methods
+               var cache = Cache.of(String.class, Integer.class)
+                       .weak()
+                       .maxSize(100)
+                       .supplier(k -> k.length())
+                       .build();
+
+               var result = cache.get("hello");
+               assertEquals(5, result);
+               assertSize(1, cache);
+       }
+
        
//====================================================================================================
        // Builder configuration
        
//====================================================================================================
@@ -925,5 +965,255 @@ class Cache_Test extends TestBase {
                assertTrue(cache.isEmpty());
                assertEquals(0, cache.getCacheHits());
        }
+
+       
//====================================================================================================
+       // Thread-local cache mode
+       
//====================================================================================================
+
+       @Test void a54_threadLocal_basicCaching() throws Exception {
+               var cache = Cache.of(String.class, String.class)
+                       .threadLocal()
+                       .build();
+               var callCount = new AtomicInteger();
+
+               // First call - cache miss
+               var result1 = cache.get("key1", () -> {
+                       callCount.incrementAndGet();
+                       return "value1";
+               });
+
+               // Second call - cache hit
+               var result2 = cache.get("key1", () -> {
+                       callCount.incrementAndGet();
+                       return "should not be called";
+               });
+
+               assertEquals("value1", result1);
+               assertEquals("value1", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, cache);
+               assertEquals(1, cache.getCacheHits());
+       }
+
+       @Test void a55_threadLocal_eachThreadHasOwnCache() throws Exception {
+               var cache = Cache.of(String.class, String.class)
+                       .threadLocal()
+                       .build();
+               var executor = Executors.newFixedThreadPool(2);
+               var threadValues = new ConcurrentHashMap<Thread, String>();
+
+               // Each thread caches "key1" with its own value
+               var future1 = CompletableFuture.runAsync(() -> {
+                       var value = cache.get("key1", () -> "thread1-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               var future2 = CompletableFuture.runAsync(() -> {
+                       var value = cache.get("key1", () -> "thread2-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               CompletableFuture.allOf(future1, future2).get(5, 
TimeUnit.SECONDS);
+
+               // Verify both threads cached their own values
+               assertEquals(2, threadValues.size());
+               assertTrue(threadValues.containsValue("thread1-value"));
+               assertTrue(threadValues.containsValue("thread2-value"));
+
+               // Verify each thread's cache is independent - same thread 
should get same cached value
+               var threadValues2 = new ConcurrentHashMap<Thread, String>();
+               var threads = new 
java.util.ArrayList<Thread>(threadValues.keySet());
+
+               future1 = CompletableFuture.runAsync(() -> {
+                       var value = cache.get("key1", () -> 
"should-not-be-called");
+                       threadValues2.put(Thread.currentThread(), value);
+               }, executor);
+
+               future2 = CompletableFuture.runAsync(() -> {
+                       var value = cache.get("key1", () -> 
"should-not-be-called");
+                       threadValues2.put(Thread.currentThread(), value);
+               }, executor);
+
+               CompletableFuture.allOf(future1, future2).get(5, 
TimeUnit.SECONDS);
+
+               // Each thread should get its own cached value (same as what it 
cached before)
+               for (var thread : threads) {
+                       if (threadValues2.containsKey(thread)) {
+                               assertEquals(threadValues.get(thread), 
threadValues2.get(thread),
+                                       "Thread " + thread + " should get its 
own cached value");
+                       }
+               }
+
+               executor.shutdown();
+       }
+
+       @Test void a56_threadLocal_multipleKeys() {
+               var cache = Cache.of(String.class, Integer.class)
+                       .threadLocal()
+                       .build();
+
+               cache.get("one", () -> 1);
+               cache.get("two", () -> 2);
+               cache.get("three", () -> 3);
+
+               assertSize(3, cache);
+               assertEquals(0, cache.getCacheHits());
+
+               // Verify all cached
+               assertEquals(1, cache.get("one", () -> 999));
+               assertEquals(2, cache.get("two", () -> 999));
+               assertEquals(3, cache.get("three", () -> 999));
+               assertEquals(3, cache.getCacheHits());
+       }
+
+       @Test void a57_threadLocal_clear() {
+               var cache = Cache.of(String.class, Integer.class)
+                       .threadLocal()
+                       .build();
+
+               cache.get("one", () -> 1);
+               cache.get("two", () -> 2);
+               assertSize(2, cache);
+
+               cache.clear();
+               assertEmpty(cache);
+       }
+
+       @Test void a58_threadLocal_maxSize() {
+               var cache = Cache.of(String.class, Integer.class)
+                       .threadLocal()
+                       .maxSize(3)
+                       .build();
+
+               cache.get("one", () -> 1);
+               cache.get("two", () -> 2);
+               cache.get("three", () -> 3);
+               assertSize(3, cache);
+
+               // 4th item doesn't trigger eviction yet
+               cache.get("four", () -> 4);
+               assertSize(4, cache);
+
+               // 5th item triggers eviction
+               cache.get("five", () -> 5);
+               assertSize(1, cache);
+       }
+
+       @Test void a59_threadLocal_cacheHits() {
+               var cache = Cache.of(String.class, Integer.class)
+                       .threadLocal()
+                       .build();
+
+               assertEquals(0, cache.getCacheHits());
+
+               cache.get("one", () -> 1); // Miss
+               assertEquals(0, cache.getCacheHits());
+
+               cache.get("one", () -> 999); // Hit
+               assertEquals(1, cache.getCacheHits());
+
+               cache.get("two", () -> 2); // Miss
+               assertEquals(1, cache.getCacheHits());
+
+               cache.get("one", () -> 999); // Hit
+               cache.get("two", () -> 999); // Hit
+               assertEquals(3, cache.getCacheHits());
+       }
+
+       
//====================================================================================================
+       // Thread-local + weak mode combination
+       
//====================================================================================================
+
+       @Test void a60_threadLocal_weakMode_basicCaching() {
+               var cache = Cache.of(String.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .build();
+               var callCount = new AtomicInteger();
+
+               // First call - cache miss
+               var result1 = cache.get("key1", () -> {
+                       callCount.incrementAndGet();
+                       return "value1";
+               });
+
+               // Second call - cache hit
+               var result2 = cache.get("key1", () -> {
+                       callCount.incrementAndGet();
+                       return "should not be called";
+               });
+
+               assertEquals("value1", result1);
+               assertEquals("value1", result2);
+               assertSame(result1, result2);
+               assertEquals(1, callCount.get()); // Supplier only called once
+               assertSize(1, cache);
+               assertEquals(1, cache.getCacheHits());
+       }
+
+       @Test void a61_threadLocal_weakMode_eachThreadHasOwnCache() throws 
Exception {
+               var cache = Cache.of(String.class, String.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .build();
+               var executor = Executors.newFixedThreadPool(2);
+               var threadValues = new ConcurrentHashMap<Thread, String>();
+
+               // Each thread caches "key1" with its own value
+               var future1 = CompletableFuture.runAsync(() -> {
+                       var value = cache.get("key1", () -> "thread1-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               var future2 = CompletableFuture.runAsync(() -> {
+                       var value = cache.get("key1", () -> "thread2-value");
+                       threadValues.put(Thread.currentThread(), value);
+               }, executor);
+
+               CompletableFuture.allOf(future1, future2).get(5, 
TimeUnit.SECONDS);
+
+               // Verify both threads cached their own values
+               assertEquals(2, threadValues.size());
+               assertTrue(threadValues.containsValue("thread1-value"));
+               assertTrue(threadValues.containsValue("thread2-value"));
+
+               executor.shutdown();
+       }
+
+       @Test void a62_threadLocal_weakMode_clear() {
+               var cache = Cache.of(String.class, Integer.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .build();
+
+               cache.get("one", () -> 1);
+               cache.get("two", () -> 2);
+               assertSize(2, cache);
+
+               cache.clear();
+               assertEmpty(cache);
+       }
+
+       @Test void a63_threadLocal_weakMode_maxSize() {
+               var cache = Cache.of(String.class, Integer.class)
+                       .threadLocal()
+                       .cacheMode(WEAK)
+                       .maxSize(3)
+                       .build();
+
+               cache.get("one", () -> 1);
+               cache.get("two", () -> 2);
+               cache.get("three", () -> 3);
+               assertSize(3, cache);
+
+               // 4th item doesn't trigger eviction yet
+               cache.get("four", () -> 4);
+               assertSize(4, cache);
+
+               // 5th item triggers eviction
+               cache.get("five", () -> 5);
+               assertSize(1, cache);
+       }
 }
 
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/AnnotationInfoTest.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/AnnotationInfoTest.java
deleted file mode 100644
index a61c82fe0f..0000000000
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/AnnotationInfoTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.juneau.commons.reflect;
-
-import static java.lang.annotation.ElementType.*;
-import static java.lang.annotation.RetentionPolicy.*;
-import static org.apache.juneau.commons.utils.CollectionUtils.*;
-import static org.apache.juneau.junit.bct.BctAssertions.*;
-
-import java.lang.annotation.*;
-import org.apache.juneau.*;
-import org.apache.juneau.commons.annotation.*;
-import org.junit.jupiter.api.*;
-
-class AnnotationInfoTest extends TestBase {
-
-       
//-----------------------------------------------------------------------------------------------------------------
-       // Is in group.
-       
//-----------------------------------------------------------------------------------------------------------------
-
-       @Target(TYPE)
-       @Retention(RUNTIME)
-       @AnnotationGroup(D1.class)
-       public static @interface D1 {}
-
-       @Target(TYPE)
-       @Retention(RUNTIME)
-       @AnnotationGroup(D1.class)
-       public static @interface D2 {}
-
-       @Target(TYPE)
-       @Retention(RUNTIME)
-       public static @interface D3 {}
-
-       @D1 @D2 @D3
-       public static class D {}
-
-       @Test void d01_isInGroup() {
-               var d = ClassInfo.of(D.class);
-               var l = rstream(d.getAnnotations()).filter(x -> 
x.isInGroup(D1.class));
-               assertSize(2, l);
-       }
-}
\ No newline at end of file
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/AnnotationInfo_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/AnnotationInfo_Test.java
new file mode 100644
index 0000000000..fe27bf4730
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/AnnotationInfo_Test.java
@@ -0,0 +1,706 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.commons.reflect;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.annotation.*;
+import org.apache.juneau.*;
+import org.apache.juneau.commons.annotation.*;
+import org.junit.jupiter.api.*;
+
+class AnnotationInfo_Test extends TestBase {
+
+       
//====================================================================================================
+       // Test annotations and classes
+       
//====================================================================================================
+
+       @Target(TYPE)
+       @Retention(RUNTIME)
+       public static @interface TestAnnotation {
+               String value() default "default";
+       }
+
+       @Target(TYPE)
+       @Retention(RUNTIME)
+       public static @interface MultiTypeAnnotation {
+               String stringValue() default "default";
+
+               int intValue() default 0;
+
+               boolean boolValue() default true;
+
+               long longValue() default 100L;
+
+               double doubleValue() default 3.14;
+
+               float floatValue() default 2.5f;
+
+               Class<?> classValue() default String.class;
+
+               String[] stringArray() default { "a", "b" };
+
+               Class<?>[] classArray() default { String.class, Integer.class };
+       }
+
+       @Target(TYPE)
+       @Retention(RUNTIME)
+       public static @interface RankedAnnotation {
+               int rank() default 0;
+       }
+
+       @Target(TYPE)
+       @Retention(RUNTIME)
+       public static @interface UnrankedAnnotation {
+               String value() default "";
+       }
+
+       @Target(TYPE)
+       @Retention(RUNTIME)
+       public static @interface ClassArrayAnnotation {
+               Class<?>[] classes() default {};
+       }
+
+       @Target(TYPE)
+       @Retention(RUNTIME)
+       public static @interface ClassValueAnnotation {
+               Class<?> value() default String.class;
+       }
+
+       @Target(TYPE)
+       @Retention(RUNTIME)
+       @Documented
+       public static @interface DocumentedAnnotation {}
+
+       @Target(TYPE)
+       @Retention(RUNTIME)
+       @AnnotationGroup(GroupAnnotation.class)
+       public static @interface GroupAnnotation {}
+
+       @Target(TYPE)
+       @Retention(RUNTIME)
+       @AnnotationGroup(GroupAnnotation.class)
+       public static @interface GroupMember1 {}
+
+       @Target(TYPE)
+       @Retention(RUNTIME)
+       @AnnotationGroup(GroupAnnotation.class)
+       public static @interface GroupMember2 {}
+
+       @Target(TYPE)
+       @Retention(RUNTIME)
+       public static @interface NotInGroup {}
+
+       @TestAnnotation("test")
+       public static class TestClass {}
+
+       @MultiTypeAnnotation(stringValue = "test", intValue = 123, boolValue = 
false, longValue = 999L, doubleValue = 1.23, floatValue = 4.56f, classValue = 
Integer.class, stringArray = { "x", "y",
+               "z" }, classArray = { Long.class, Double.class })
+       public static class MultiTypeClass {}
+
+       @RankedAnnotation(rank = 5)
+       public static class RankedClass {}
+
+       @UnrankedAnnotation
+       public static class UnrankedClass {}
+
+       @ClassArrayAnnotation(classes = { String.class, Integer.class })
+       public static class ClassArrayClass {}
+
+       @ClassValueAnnotation(Integer.class)
+       public static class ClassValueClass {}
+
+       @DocumentedAnnotation
+       public static class DocumentedClass {}
+
+       @GroupMember1
+       @GroupMember2
+       @NotInGroup
+       public static class GroupTestClass {}
+
+       
//====================================================================================================
+       // of() - Static factory method
+       
//====================================================================================================
+
+       @Test
+       void a01_of_createsAnnotationInfo() {
+               var ci = ClassInfo.of(TestClass.class);
+               var annotation = ci.inner().getAnnotation(TestAnnotation.class);
+               var ai = AnnotationInfo.of(ci, annotation);
+
+               assertNotNull(ai);
+               assertEquals(TestAnnotation.class, ai.annotationType());
+               assertEquals("test", ai.getValue().orElse(null));
+       }
+
+       @Test
+       void a02_of_withNullAnnotation_throwsException() {
+               var ci = ClassInfo.of(TestClass.class);
+               assertThrows(IllegalArgumentException.class, () -> 
AnnotationInfo.of(ci, null));
+       }
+
+       
//====================================================================================================
+       // annotationType()
+       
//====================================================================================================
+
+       @Test
+       void b01_annotationType_returnsCorrectType() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+               assertEquals(TestAnnotation.class, ai.annotationType());
+       }
+
+       
//====================================================================================================
+       // cast()
+       
//====================================================================================================
+
+       @Test
+       void c01_cast_sameType_returnsThis() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               var casted = ai.cast(TestAnnotation.class);
+               assertNotNull(casted);
+               assertSame(ai, casted);
+       }
+
+       @Test
+       void c02_cast_differentType_returnsNull() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               var casted = ai.cast(Deprecated.class);
+               assertNull(casted);
+       }
+
+       
//====================================================================================================
+       // equals() and hashCode()
+       
//====================================================================================================
+
+       @Test
+       void d01_equals_sameAnnotation_returnsTrue() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai1 = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               var ai2 = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+
+               assertNotNull(ai1);
+               assertNotNull(ai2);
+               assertEquals(ai1, ai2);
+               assertEquals(ai1.hashCode(), ai2.hashCode());
+       }
+
+       @Test
+       void d02_equals_differentAnnotation_returnsFalse() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai1 = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+
+               @Deprecated
+               class DeprecatedClass {}
+               var ci2 = ClassInfo.of(DeprecatedClass.class);
+               var ai2 = 
ci2.getAnnotations(Deprecated.class).findFirst().orElse(null);
+
+               assertNotNull(ai1);
+               assertNotNull(ai2);
+               assertNotEquals(ai1, ai2);
+       }
+
+       @Test
+       void d03_equals_withAnnotationInfo_comparesAnnotations() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai1 = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               var ai2 = AnnotationInfo.of(ci, 
ci.inner().getAnnotation(TestAnnotation.class));
+
+               assertNotNull(ai1);
+               assertEquals(ai1, ai2);
+       }
+
+       @Test
+       void d04_equals_withAnnotation_comparesAnnotations() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               var annotation = ci.inner().getAnnotation(TestAnnotation.class);
+
+               assertNotNull(ai);
+               assertEquals(ai, annotation);
+       }
+
+       
//====================================================================================================
+       // getName()
+       
//====================================================================================================
+
+       @Test
+       void e01_getName_returnsSimpleName() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+               assertEquals("TestAnnotation", ai.getName());
+       }
+
+       
//====================================================================================================
+       // getRank()
+       
//====================================================================================================
+
+       @Test
+       void f01_getRank_withRankMethod_returnsRank() {
+               var ci = ClassInfo.of(RankedClass.class);
+               var ai = 
ci.getAnnotations(RankedAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+               assertEquals(5, ai.getRank());
+       }
+
+       @Test
+       void f02_getRank_withoutRankMethod_returnsZero() {
+               var ci = ClassInfo.of(UnrankedClass.class);
+               var ai = 
ci.getAnnotations(UnrankedAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+               assertEquals(0, ai.getRank());
+       }
+
+       
//====================================================================================================
+       // getMethod()
+       
//====================================================================================================
+
+       @Test
+       void g01_getMethod_existingMethod_returnsMethodInfo() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               var method = ai.getMethod("value");
+               assertTrue(method.isPresent());
+               assertEquals("value", method.get().getSimpleName());
+       }
+
+       @Test
+       void g02_getMethod_nonexistentMethod_returnsEmpty() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               var method = ai.getMethod("nonexistent");
+               assertFalse(method.isPresent());
+       }
+
+       
//====================================================================================================
+       // getReturnType()
+       
//====================================================================================================
+
+       @Test
+       void h01_getReturnType_returnsCorrectType() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.getReturnType("stringValue").isPresent());
+               assertEquals(String.class, 
ai.getReturnType("stringValue").get().inner());
+               assertEquals(int.class, 
ai.getReturnType("intValue").get().inner());
+               assertEquals(boolean.class, 
ai.getReturnType("boolValue").get().inner());
+               assertEquals(long.class, 
ai.getReturnType("longValue").get().inner());
+               assertEquals(double.class, 
ai.getReturnType("doubleValue").get().inner());
+               assertEquals(float.class, 
ai.getReturnType("floatValue").get().inner());
+               assertEquals(Class.class, 
ai.getReturnType("classValue").get().inner());
+               assertEquals(String[].class, 
ai.getReturnType("stringArray").get().inner());
+               assertEquals(Class[].class, 
ai.getReturnType("classArray").get().inner());
+       }
+
+       @Test
+       void h02_getReturnType_nonexistentMethod_returnsEmpty() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertFalse(ai.getReturnType("nonexistent").isPresent());
+       }
+
+       
//====================================================================================================
+       // Value retrieval methods - getString(), getInt(), getBoolean(), etc.
+       
//====================================================================================================
+
+       @Test
+       void i01_getString_returnsStringValue() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.getString("stringValue").isPresent());
+               assertEquals("test", ai.getString("stringValue").get());
+               assertFalse(ai.getString("nonexistent").isPresent());
+       }
+
+       @Test
+       void i02_getInt_returnsIntValue() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.getInt("intValue").isPresent());
+               assertEquals(123, ai.getInt("intValue").get());
+               assertFalse(ai.getInt("nonexistent").isPresent());
+       }
+
+       @Test
+       void i03_getBoolean_returnsBooleanValue() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.getBoolean("boolValue").isPresent());
+               assertEquals(false, ai.getBoolean("boolValue").get());
+               assertFalse(ai.getBoolean("nonexistent").isPresent());
+       }
+
+       @Test
+       void i04_getLong_returnsLongValue() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.getLong("longValue").isPresent());
+               assertEquals(999L, ai.getLong("longValue").get());
+               assertFalse(ai.getLong("nonexistent").isPresent());
+       }
+
+       @Test
+       void i05_getDouble_returnsDoubleValue() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.getDouble("doubleValue").isPresent());
+               assertEquals(1.23, ai.getDouble("doubleValue").get(), 0.001);
+               assertFalse(ai.getDouble("nonexistent").isPresent());
+       }
+
+       @Test
+       void i06_getFloat_returnsFloatValue() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.getFloat("floatValue").isPresent());
+               assertEquals(4.56f, ai.getFloat("floatValue").get(), 0.001);
+               assertFalse(ai.getFloat("nonexistent").isPresent());
+       }
+
+       @Test
+       void i07_getClassValue_returnsClassValue() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.getClassValue("classValue").isPresent());
+               assertEquals(Integer.class, 
ai.getClassValue("classValue").get());
+               assertFalse(ai.getClassValue("nonexistent").isPresent());
+       }
+
+       @Test
+       void i08_getStringArray_returnsStringArray() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.getStringArray("stringArray").isPresent());
+               var array = ai.getStringArray("stringArray").get();
+               assertNotNull(array);
+               assertEquals(3, array.length);
+               assertEquals("x", array[0]);
+               assertEquals("y", array[1]);
+               assertEquals("z", array[2]);
+               assertFalse(ai.getStringArray("nonexistent").isPresent());
+       }
+
+       @Test
+       void i09_getClassArray_returnsClassArray() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.getClassArray("classArray").isPresent());
+               var array = ai.getClassArray("classArray").get();
+               assertNotNull(array);
+               assertEquals(2, array.length);
+               assertEquals(Long.class, array[0]);
+               assertEquals(Double.class, array[1]);
+               assertFalse(ai.getClassArray("nonexistent").isPresent());
+       }
+
+       
//====================================================================================================
+       // getValue() - Convenience method
+       
//====================================================================================================
+
+       @Test
+       void j01_getValue_returnsValueMethod() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               var value = ai.getValue();
+               assertTrue(value.isPresent());
+               assertEquals("test", value.get());
+       }
+
+       
//====================================================================================================
+       // getValue(Class<V> type, String name)
+       
//====================================================================================================
+
+       @Test
+       void k01_getValue_withType_returnsTypedValue() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               var stringValue = ai.getValue(String.class, "stringValue");
+               assertTrue(stringValue.isPresent());
+               assertEquals("test", stringValue.get());
+
+               // intValue returns int.class, not Integer.class
+               var intValue = ai.getValue(int.class, "intValue");
+               assertTrue(intValue.isPresent());
+               assertEquals(123, intValue.get());
+       }
+
+       @Test
+       void k02_getValue_wrongType_returnsEmpty() {
+               var ci = ClassInfo.of(MultiTypeClass.class);
+               var ai = 
ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               // Try to get stringValue as Integer
+               var intValue = ai.getValue(Integer.class, "stringValue");
+               assertFalse(intValue.isPresent());
+       }
+
+       
//====================================================================================================
+       // getClassArray(String, Class<T>)
+       
//====================================================================================================
+
+       @Test
+       void l01_getClassArray_typed_returnsTypedArray() {
+               var ci = ClassInfo.of(ClassArrayClass.class);
+               var ai = 
ci.getAnnotations(ClassArrayAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               // Both String and Integer are assignable to Object
+               var classes = ai.getClassArray("classes", Object.class);
+               assertTrue(classes.isPresent());
+               var array = classes.get();
+               assertEquals(2, array.length);
+               assertEquals(String.class, array[0]);
+               assertEquals(Integer.class, array[1]);
+       }
+
+       @Test
+       void l02_getClassArray_typed_notAssignable_returnsEmpty() {
+               var ci = ClassInfo.of(ClassArrayClass.class);
+               var ai = 
ci.getAnnotations(ClassArrayAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               // String and Integer are not assignable to Exception
+               var classes = ai.getClassArray("classes", Exception.class);
+               assertFalse(classes.isPresent());
+       }
+
+       
//====================================================================================================
+       // getClassValue(String, Class<T>)
+       
//====================================================================================================
+
+       @Test
+       void m01_getClassValue_typed_returnsTypedClass() {
+               var ci = ClassInfo.of(ClassValueClass.class);
+               var ai = 
ci.getAnnotations(ClassValueAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               var numberClass = ai.getClassValue("value", Number.class);
+               assertTrue(numberClass.isPresent());
+               assertEquals(Integer.class, numberClass.get());
+       }
+
+       @Test
+       void m02_getClassValue_typed_notAssignable_returnsEmpty() {
+               var ci = ClassInfo.of(ClassValueClass.class);
+               var ai = 
ci.getAnnotations(ClassValueAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               // Integer is not assignable to Exception
+               var exceptionClass = ai.getClassValue("value", Exception.class);
+               assertFalse(exceptionClass.isPresent());
+       }
+
+       
//====================================================================================================
+       // hasName() and hasSimpleName()
+       
//====================================================================================================
+
+       @Test
+       void n01_hasName_returnsTrueForFullyQualifiedName() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               var fullyQualifiedName = TestAnnotation.class.getName();
+               assertTrue(ai.hasName(fullyQualifiedName));
+               assertFalse(ai.hasName("TestAnnotation"));
+       }
+
+       @Test
+       void n02_hasSimpleName_returnsTrueForSimpleName() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.hasSimpleName("TestAnnotation"));
+               assertFalse(ai.hasSimpleName(TestAnnotation.class.getName()));
+       }
+
+       
//====================================================================================================
+       // hasAnnotation()
+       
//====================================================================================================
+
+       @Test
+       void o01_hasAnnotation_withMetaAnnotation_returnsTrue() {
+               var ci = ClassInfo.of(DocumentedClass.class);
+               var ai = 
ci.getAnnotations(DocumentedAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.hasAnnotation(Documented.class));
+       }
+
+       @Test
+       void o02_hasAnnotation_withoutMetaAnnotation_returnsFalse() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertFalse(ai.hasAnnotation(Documented.class));
+       }
+
+       
//====================================================================================================
+       // inner()
+       
//====================================================================================================
+
+       @Test
+       void p01_inner_returnsWrappedAnnotation() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               var annotation = ai.inner();
+               assertNotNull(annotation);
+               assertEquals(TestAnnotation.class, annotation.annotationType());
+               assertEquals("test", annotation.value());
+       }
+
+       
//====================================================================================================
+       // isInGroup()
+       
//====================================================================================================
+
+       @Test
+       void q01_isInGroup_returnsTrueForGroupMembers() {
+               var ci = ClassInfo.of(GroupTestClass.class);
+               var groupMember1 = 
ci.getAnnotations(GroupMember1.class).findFirst().orElse(null);
+               var groupMember2 = 
ci.getAnnotations(GroupMember2.class).findFirst().orElse(null);
+               var notInGroup = 
ci.getAnnotations(NotInGroup.class).findFirst().orElse(null);
+
+               assertNotNull(groupMember1);
+               assertNotNull(groupMember2);
+               assertNotNull(notInGroup);
+
+               assertTrue(groupMember1.isInGroup(GroupAnnotation.class));
+               assertTrue(groupMember2.isInGroup(GroupAnnotation.class));
+               assertFalse(notInGroup.isInGroup(GroupAnnotation.class));
+       }
+
+       
//====================================================================================================
+       // isType()
+       
//====================================================================================================
+
+       @Test
+       void r01_isType_sameType_returnsTrue() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertTrue(ai.isType(TestAnnotation.class));
+       }
+
+       @Test
+       void r02_isType_differentType_returnsFalse() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               assertFalse(ai.isType(Deprecated.class));
+       }
+
+       
//====================================================================================================
+       // toMap()
+       
//====================================================================================================
+
+       @Test
+       void s01_toMap_returnsMapRepresentation() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               var map = ai.toMap();
+               assertNotNull(map);
+               assertTrue(map.containsKey("CLASS_TYPE"));
+               assertTrue(map.containsKey("@TestAnnotation"));
+
+               var annotationMap = 
(java.util.Map<String,Object>)map.get("@TestAnnotation");
+               assertNotNull(annotationMap);
+               assertEquals("test", annotationMap.get("value"));
+       }
+
+       
//====================================================================================================
+       // toSimpleString()
+       
//====================================================================================================
+
+       @Test
+       void t01_toSimpleString_returnsFormattedString() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               var str = ai.toSimpleString();
+               assertNotNull(str);
+               assertTrue(str.contains("@TestAnnotation"));
+               assertTrue(str.contains("on="));
+       }
+
+       
//====================================================================================================
+       // toString()
+       
//====================================================================================================
+
+       @Test
+       void u01_toString_returnsStringRepresentation() {
+               var ci = ClassInfo.of(TestClass.class);
+               var ai = 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
+               assertNotNull(ai);
+
+               var str = ai.toString();
+               assertNotNull(str);
+               // toString() returns the map representation
+               assertTrue(str.contains("CLASS_TYPE") || 
str.contains("@TestAnnotation"));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/AnnotationInfo_ValueMethods_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/AnnotationInfo_ValueMethods_Test.java
deleted file mode 100644
index ad62cab056..0000000000
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/AnnotationInfo_ValueMethods_Test.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.juneau.commons.reflect;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import java.lang.annotation.*;
-
-import org.junit.jupiter.api.*;
-
-/**
- * Tests for AnnotationInfo value retrieval methods.
- */
-public class AnnotationInfo_ValueMethods_Test {
-
-       @Target(ElementType.TYPE)
-       @Retention(RetentionPolicy.RUNTIME)
-       public @interface TestAnnotation {
-               String stringValue() default "default";
-               int intValue() default 42;
-               boolean boolValue() default true;
-               long longValue() default 100L;
-               double doubleValue() default 3.14;
-               float floatValue() default 2.5f;
-               Class<?> classValue() default String.class;
-               String[] stringArray() default {"a", "b"};
-               Class<?>[] classArray() default {String.class, Integer.class};
-       }
-
-       @TestAnnotation(
-               stringValue = "test",
-               intValue = 123,
-               boolValue = false,
-               longValue = 999L,
-               doubleValue = 1.23,
-               floatValue = 4.56f,
-               classValue = Integer.class,
-               stringArray = {"x", "y", "z"},
-               classArray = {Long.class, Double.class}
-       )
-       public static class AnnotatedClass {}
-
-       private static AnnotationInfo<TestAnnotation> getTestAnnotationInfo() {
-               var ci = ClassInfo.of(AnnotatedClass.class);
-               return 
ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
-       }
-
-       @Test
-       public void testHasName() {
-               var ai = getTestAnnotationInfo();
-
-               
assertTrue(ai.hasName("org.apache.juneau.commons.reflect.AnnotationInfo_ValueMethods_Test$TestAnnotation"));
-               assertFalse(ai.hasName("TestAnnotation"));
-       }
-
-       @Test
-       public void testHasSimpleName() {
-               var ai = getTestAnnotationInfo();
-
-               assertTrue(ai.hasSimpleName("TestAnnotation"));
-               
assertFalse(ai.hasSimpleName("org.apache.juneau.commons.reflect.AnnotationInfo_ValueMethods_Test$TestAnnotation"));
-       }
-
-       @Test
-       public void testGetString() {
-               var ai = getTestAnnotationInfo();
-
-               assertTrue(ai.getString("stringValue").isPresent());
-               assertEquals("test", ai.getString("stringValue").get());
-               assertTrue(ai.getString("nonexistent").isEmpty());
-       }
-
-       @Test
-       public void testGetInt() {
-               var ai = getTestAnnotationInfo();
-
-               assertTrue(ai.getInt("intValue").isPresent());
-               assertEquals(123, ai.getInt("intValue").get());
-               assertTrue(ai.getInt("nonexistent").isEmpty());
-       }
-
-       @Test
-       public void testGetBoolean() {
-               var ai = getTestAnnotationInfo();
-
-               assertTrue(ai.getBoolean("boolValue").isPresent());
-               assertEquals(false, ai.getBoolean("boolValue").get());
-               assertTrue(ai.getBoolean("nonexistent").isEmpty());
-       }
-
-       @Test
-       public void testGetLong() {
-               var ai = getTestAnnotationInfo();
-
-               assertTrue(ai.getLong("longValue").isPresent());
-               assertEquals(999L, ai.getLong("longValue").get());
-               assertTrue(ai.getLong("nonexistent").isEmpty());
-       }
-
-       @Test
-       public void testGetDouble() {
-               var ai = getTestAnnotationInfo();
-
-               assertTrue(ai.getDouble("doubleValue").isPresent());
-               assertEquals(1.23, ai.getDouble("doubleValue").get(), 0.001);
-               assertTrue(ai.getDouble("nonexistent").isEmpty());
-       }
-
-       @Test
-       public void testGetFloat() {
-               var ai = getTestAnnotationInfo();
-
-               assertTrue(ai.getFloat("floatValue").isPresent());
-               assertEquals(4.56f, ai.getFloat("floatValue").get(), 0.001);
-               assertTrue(ai.getFloat("nonexistent").isEmpty());
-       }
-
-       @Test
-       public void testGetClassValue() {
-               var ai = getTestAnnotationInfo();
-
-               assertTrue(ai.getClassValue("classValue").isPresent());
-               assertEquals(Integer.class, 
ai.getClassValue("classValue").get());
-               assertTrue(ai.getClassValue("nonexistent").isEmpty());
-       }
-
-       @Test
-       public void testGetStringArray() {
-               var ai = getTestAnnotationInfo();
-
-               assertTrue(ai.getStringArray("stringArray").isPresent());
-               String[] array = ai.getStringArray("stringArray").get();
-               assertNotNull(array);
-               assertEquals(3, array.length);
-               assertEquals("x", array[0]);
-               assertEquals("y", array[1]);
-               assertEquals("z", array[2]);
-               assertTrue(ai.getStringArray("nonexistent").isEmpty());
-       }
-
-       @Test
-       public void testGetClassArray() {
-               var ai = getTestAnnotationInfo();
-
-               assertTrue(ai.getClassArray("classArray").isPresent());
-               Class<?>[] array = ai.getClassArray("classArray").get();
-               assertNotNull(array);
-               assertEquals(2, array.length);
-               assertEquals(Long.class, array[0]);
-               assertEquals(Double.class, array[1]);
-               assertTrue(ai.getClassArray("nonexistent").isEmpty());
-       }
-
-       @Test
-       public void testGetReturnType() {
-               var ai = getTestAnnotationInfo();
-
-               // Test various return types
-               assertTrue(ai.getReturnType("stringValue").isPresent());
-               assertEquals(String.class, 
ai.getReturnType("stringValue").get().inner());
-               assertEquals(int.class, 
ai.getReturnType("intValue").get().inner());
-               assertEquals(boolean.class, 
ai.getReturnType("boolValue").get().inner());
-               assertEquals(long.class, 
ai.getReturnType("longValue").get().inner());
-               assertEquals(double.class, 
ai.getReturnType("doubleValue").get().inner());
-               assertEquals(float.class, 
ai.getReturnType("floatValue").get().inner());
-               assertEquals(Class.class, 
ai.getReturnType("classValue").get().inner());
-               assertEquals(String[].class, 
ai.getReturnType("stringArray").get().inner());
-               assertEquals(Class[].class, 
ai.getReturnType("classArray").get().inner());
-
-               // Nonexistent method
-               assertTrue(ai.getReturnType("nonexistent").isEmpty());
-       }
-}
-
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ConstructorInfoTest.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ConstructorInfo_Test.java
similarity index 79%
rename from 
juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ConstructorInfoTest.java
rename to 
juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ConstructorInfo_Test.java
index a75b1916c9..2b6ad852d6 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ConstructorInfoTest.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ConstructorInfo_Test.java
@@ -28,7 +28,7 @@ import java.util.stream.*;
 import org.apache.juneau.*;
 import org.junit.jupiter.api.*;
 
-class ConstructorInfoTest extends TestBase {
+class ConstructorInfo_Test extends TestBase {
 
        private static void check(String expected, Object o) {
                assertEquals(expected, TO_STRING.apply(o));
@@ -122,6 +122,31 @@ class ConstructorInfoTest extends TestBase {
                assertEquals(null, b_c3.newInstanceLenient(123).toString());
        }
 
+       @Test void isAccessible() {
+               // Test isAccessible() before and after setAccessible()
+               // Note: isAccessible() was added in Java 9, so behavior may 
vary
+               
+               // Before setAccessible(), private/protected constructors 
should not be accessible
+               // (unless they're already accessible due to module system)
+               var privateBefore = b_c3.isAccessible();
+               
+               // Make it accessible
+               b_c3.setAccessible();
+               
+               // After setAccessible(), it should be accessible (if Java 9+)
+               // If Java 8 or earlier, isAccessible() will return false
+               var privateAfter = b_c3.isAccessible();
+               
+               // Verify the method doesn't throw and returns a boolean
+               // The actual value depends on Java version, but it should be 
consistent
+               assertTrue(privateAfter || !privateBefore, "After 
setAccessible(), isAccessible() should return true (Java 9+) or false (Java 
8)");
+               
+               // Public constructors might already be accessible
+               var publicAccessible = b_c1.isAccessible();
+               // Should return a boolean (either true or false depending on 
Java version)
+               assertNotNull(Boolean.valueOf(publicAccessible));
+       }
+
        @Test void compareTo() {
                var s = new TreeSet<>(l(b_c1, b_c2, b_c3, b_c4, a));
                check("A(),B(),B(int),B(String),B(String,String)", s);
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ExecutableInfo_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ExecutableInfo_Test.java
index fdb3e3e79f..630af13e08 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ExecutableInfo_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ExecutableInfo_Test.java
@@ -462,6 +462,39 @@ class ExecutableInfo_Test extends TestBase {
                assertDoesNotThrow(()->f_isDefault.accessible());
        }
 
+       @Test void isAccessible() {
+               // Test isAccessible() before and after setAccessible()
+               // Note: isAccessible() was added in Java 9, so behavior may 
vary
+               
+               // Before setAccessible(), private/protected/default methods 
should not be accessible
+               // (unless they're already accessible due to module system)
+               var privateBefore = f_isPrivate.isAccessible();
+               var protectedBefore = f_isProtected.isAccessible();
+               var defaultBefore = f_isDefault.isAccessible();
+               
+               // Make them accessible
+               f_isPrivate.setAccessible();
+               f_isProtected.setAccessible();
+               f_isDefault.setAccessible();
+               
+               // After setAccessible(), they should be accessible (if Java 9+)
+               // If Java 8 or earlier, isAccessible() will return false
+               var privateAfter = f_isPrivate.isAccessible();
+               var protectedAfter = f_isProtected.isAccessible();
+               var defaultAfter = f_isDefault.isAccessible();
+               
+               // Verify the method doesn't throw and returns a boolean
+               // The actual value depends on Java version, but it should be 
consistent
+               assertTrue(privateAfter || !privateBefore, "After 
setAccessible(), isAccessible() should return true (Java 9+) or false (Java 
8)");
+               assertTrue(protectedAfter || !protectedBefore, "After 
setAccessible(), isAccessible() should return true (Java 9+) or false (Java 
8)");
+               assertTrue(defaultAfter || !defaultBefore, "After 
setAccessible(), isAccessible() should return true (Java 9+) or false (Java 
8)");
+               
+               // Public methods might already be accessible
+               var publicAccessible = f_isPublic.isAccessible();
+               // Should return a boolean (either true or false depending on 
Java version)
+               assertNotNull(Boolean.valueOf(publicAccessible));
+       }
+
        @Test void isVisible() {
                assertTrue(f_isPublic.isVisible(Visibility.PUBLIC));
                assertTrue(f_isPublic.isVisible(Visibility.PROTECTED));
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/FieldInfo_AnnotationInfos_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/FieldInfo_AnnotationInfos_Test.java
deleted file mode 100644
index 8bfc2a48ba..0000000000
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/FieldInfo_AnnotationInfos_Test.java
+++ /dev/null
@@ -1,117 +0,0 @@
-// 
***************************************************************************************************************************
-// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
-// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
-// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
-// * with the License.  You may obtain a copy of the License at                
                                              *
-// *                                                                           
                                              *
-// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
-// *                                                                           
                                              *
-// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
-// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
-// * specific language governing permissions and limitations under the 
License.                                              *
-// 
***************************************************************************************************************************
-package org.apache.juneau.commons.reflect;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import java.lang.annotation.*;
-
-import org.junit.jupiter.api.*;
-
-/**
- * Tests for {@link FieldInfo#getAnnotations()} methods.
- */
-public class FieldInfo_AnnotationInfos_Test {
-
-       @Retention(RetentionPolicy.RUNTIME)
-       @Target(ElementType.FIELD)
-       public @interface TestAnnotation1 {
-               String value() default "";
-       }
-
-       @Retention(RetentionPolicy.RUNTIME)
-       @Target(ElementType.FIELD)
-       public @interface TestAnnotation2 {
-               int value() default 0;
-       }
-
-       @Retention(RetentionPolicy.RUNTIME)
-       @Target(ElementType.FIELD)
-       public @interface TestAnnotation3 {
-               String value() default "";
-       }
-
-       public static class TestClass {
-               @TestAnnotation1("test1")
-               @TestAnnotation2(42)
-               public String field1;
-
-               @TestAnnotation1("test2")
-               public String field2;
-
-               public String field3;
-       }
-
-       @Test
-       public void testGetAnnotationInfos() {
-               var ci = ClassInfo.of(TestClass.class);
-               var field1 = ci.getPublicField(x -> 
x.getName().equals("field1")).get();
-               var field2 = ci.getPublicField(x -> 
x.getName().equals("field2")).get();
-               var field3 = ci.getPublicField(x -> 
x.getName().equals("field3")).get();
-
-               // field1 has 2 annotations
-               var annotations1 = field1.getAnnotations();
-               assertEquals(2, annotations1.size());
-               assertTrue(annotations1.stream().anyMatch(a -> 
a.hasSimpleName("TestAnnotation1")));
-               assertTrue(annotations1.stream().anyMatch(a -> 
a.hasSimpleName("TestAnnotation2")));
-
-               // field2 has 1 annotation
-               var annotations2 = field2.getAnnotations();
-               assertEquals(1, annotations2.size());
-               assertTrue(annotations2.stream().anyMatch(a -> 
a.hasSimpleName("TestAnnotation1")));
-
-               // field3 has no annotations
-               var annotations3 = field3.getAnnotations();
-               assertEquals(0, annotations3.size());
-       }
-
-       @Test
-       public void testGetAnnotationInfosTyped() {
-               var ci = ClassInfo.of(TestClass.class);
-               var field1 = ci.getPublicField(x -> 
x.getName().equals("field1")).get();
-               var field2 = ci.getPublicField(x -> 
x.getName().equals("field2")).get();
-
-               // Test filtering by type for field1
-               var ann1_type1 = 
field1.getAnnotations(TestAnnotation1.class).toList();
-               assertEquals(1, ann1_type1.size());
-               assertEquals("test1", ann1_type1.get(0).getValue().get());
-
-               var ann1_type2 = 
field1.getAnnotations(TestAnnotation2.class).toList();
-               assertEquals(1, ann1_type2.size());
-               assertEquals(42, ann1_type2.get(0).getInt("value").get());
-
-               // Test filtering by type that doesn't exist
-               var ann1_type3 = 
field1.getAnnotations(TestAnnotation3.class).toList();
-               assertEquals(0, ann1_type3.size());
-
-               // Test filtering for field2
-               var ann2_type1 = 
field2.getAnnotations(TestAnnotation1.class).toList();
-               assertEquals(1, ann2_type1.size());
-               assertEquals("test2", ann2_type1.get(0).getValue().get());
-
-               var ann2_type2 = 
field2.getAnnotations(TestAnnotation2.class).toList();
-               assertEquals(0, ann2_type2.size());
-       }
-
-       @Test
-       public void testGetAnnotationInfosMemoization() {
-               var ci = ClassInfo.of(TestClass.class);
-               var field1 = ci.getPublicField(x -> 
x.getName().equals("field1")).get();
-
-               // Calling getDeclaredAnnotationInfos() multiple times should 
return the same list instance
-               var annotations1 = field1.getAnnotations();
-               var annotations2 = field1.getAnnotations();
-               assertSame(annotations1, annotations2);
-       }
-}
-
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/FieldInfo_FullName_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/FieldInfo_FullName_Test.java
deleted file mode 100644
index bcda6c1154..0000000000
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/FieldInfo_FullName_Test.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// 
***************************************************************************************************************************
-// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
-// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
-// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
-// * with the License.  You may obtain a copy of the License at                
                                              *
-// *                                                                           
                                              *
-// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
-// *                                                                           
                                              *
-// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
-// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
-// * specific language governing permissions and limitations under the 
License.                                              *
-// 
***************************************************************************************************************************
-package org.apache.juneau.commons.reflect;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import org.junit.jupiter.api.*;
-
-/**
- * Tests for {@link FieldInfo#getFullName()} method.
- */
-public class FieldInfo_FullName_Test {
-
-       public static class TestClass {
-               public String field1;
-               public int field2;
-       }
-
-       @Test
-       public void testGetFullName() {
-               var ci = ClassInfo.of(TestClass.class);
-               var field1 = ci.getPublicField(x -> 
x.getName().equals("field1")).get();
-               var field2 = ci.getPublicField(x -> 
x.getName().equals("field2")).get();
-
-               // Verify full names are correct
-               String fullName1 = field1.getFullName();
-               String fullName2 = field2.getFullName();
-
-               
assertTrue(fullName1.endsWith("FieldInfo_FullName_Test$TestClass.field1"));
-               
assertTrue(fullName2.endsWith("FieldInfo_FullName_Test$TestClass.field2"));
-               
-               // Verify package is included
-               
assertTrue(fullName1.startsWith("org.apache.juneau.commons.reflect."));
-               
assertTrue(fullName2.startsWith("org.apache.juneau.commons.reflect."));
-       }
-
-       @Test
-       public void testGetFullNameMemoization() {
-               var ci = ClassInfo.of(TestClass.class);
-               var field1 = ci.getPublicField(x -> 
x.getName().equals("field1")).get();
-
-               // Calling getFullName() multiple times should return the same 
String instance (memoized)
-               String name1 = field1.getFullName();
-               String name2 = field1.getFullName();
-               assertSame(name1, name2);
-       }
-
-       public static class InnerClass {
-               public String innerField;
-       }
-
-       @Test
-       public void testGetFullNameWithInnerClass() {
-               var ci = ClassInfo.of(InnerClass.class);
-               var field = ci.getPublicField(x -> 
x.getName().equals("innerField")).get();
-
-               String fullName = field.getFullName();
-               
-               // Verify $ separator is used for inner class
-               
assertTrue(fullName.contains("FieldInfo_FullName_Test$InnerClass"));
-               assertTrue(fullName.endsWith(".innerField"));
-       }
-}
-
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/FieldInfo_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/FieldInfo_Test.java
index 7810833eab..4d8a359223 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/FieldInfo_Test.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/FieldInfo_Test.java
@@ -47,6 +47,24 @@ class FieldInfo_Test extends TestBase {
                String value();
        }
 
+       @Target(FIELD)
+       @Retention(RUNTIME)
+       public static @interface TestAnnotation1 {
+               String value() default "";
+       }
+
+       @Target(FIELD)
+       @Retention(RUNTIME)
+       public static @interface TestAnnotation2 {
+               int value() default 0;
+       }
+
+       @Target(FIELD)
+       @Retention(RUNTIME)
+       public static @interface TestAnnotation3 {
+               String value() default "";
+       }
+
        private static void check(String expected, Object o) {
                assertEquals(expected, TO_STRING.apply(o));
        }
@@ -252,6 +270,39 @@ class FieldInfo_Test extends TestBase {
                assertDoesNotThrow(()->d_isDefault.setAccessible());
        }
 
+       @Test void isAccessible() {
+               // Test isAccessible() before and after setAccessible()
+               // Note: isAccessible() was added in Java 9, so behavior may 
vary
+               
+               // Before setAccessible(), private/protected/default fields 
should not be accessible
+               // (unless they're already accessible due to module system)
+               var privateBefore = d_isPrivate.isAccessible();
+               var protectedBefore = d_isProtected.isAccessible();
+               var defaultBefore = d_isDefault.isAccessible();
+               
+               // Make them accessible
+               d_isPrivate.setAccessible();
+               d_isProtected.setAccessible();
+               d_isDefault.setAccessible();
+               
+               // After setAccessible(), they should be accessible (if Java 9+)
+               // If Java 8 or earlier, isAccessible() will return false
+               var privateAfter = d_isPrivate.isAccessible();
+               var protectedAfter = d_isProtected.isAccessible();
+               var defaultAfter = d_isDefault.isAccessible();
+               
+               // Verify the method doesn't throw and returns a boolean
+               // The actual value depends on Java version, but it should be 
consistent
+               assertTrue(privateAfter || !privateBefore, "After 
setAccessible(), isAccessible() should return true (Java 9+) or false (Java 
8)");
+               assertTrue(protectedAfter || !protectedBefore, "After 
setAccessible(), isAccessible() should return true (Java 9+) or false (Java 
8)");
+               assertTrue(defaultAfter || !defaultBefore, "After 
setAccessible(), isAccessible() should return true (Java 9+) or false (Java 
8)");
+               
+               // Public fields might already be accessible
+               var publicAccessible = d_isPublic.isAccessible();
+               // Should return a boolean (either true or false depending on 
Java version)
+               assertNotNull(Boolean.valueOf(publicAccessible));
+       }
+
        @Test void isVisible() {
                assertTrue(d_isPublic.isVisible(Visibility.PUBLIC));
                assertTrue(d_isPublic.isVisible(Visibility.PROTECTED));
@@ -301,4 +352,110 @@ class FieldInfo_Test extends TestBase {
        @Test void toString2() {
                
assertEquals("org.apache.juneau.commons.reflect.FieldInfo_Test$E.a1", 
e_a1.toString());
        }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // getAnnotations()
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       public static class F {
+               @TestAnnotation1("test1")
+               @TestAnnotation2(42)
+               public String field1;
+
+               @TestAnnotation1("test2")
+               public String field2;
+
+               public String field3;
+       }
+
+       static ClassInfo f = ClassInfo.of(F.class);
+       static FieldInfo
+               f_field1 = f.getPublicField(x -> x.hasName("field1")).get(),
+               f_field2 = f.getPublicField(x -> x.hasName("field2")).get(),
+               f_field3 = f.getPublicField(x -> x.hasName("field3")).get();
+
+       @Test void getAnnotations_returnsAllAnnotations() {
+               var annotations1 = f_field1.getAnnotations();
+               assertEquals(2, annotations1.size());
+               assertTrue(annotations1.stream().anyMatch(a -> 
a.hasSimpleName("TestAnnotation1")));
+               assertTrue(annotations1.stream().anyMatch(a -> 
a.hasSimpleName("TestAnnotation2")));
+
+               var annotations2 = f_field2.getAnnotations();
+               assertEquals(1, annotations2.size());
+               assertTrue(annotations2.stream().anyMatch(a -> 
a.hasSimpleName("TestAnnotation1")));
+
+               var annotations3 = f_field3.getAnnotations();
+               assertEquals(0, annotations3.size());
+       }
+
+       @Test void getAnnotations_typed_filtersByType() {
+               var ann1_type1 = 
f_field1.getAnnotations(TestAnnotation1.class).toList();
+               assertEquals(1, ann1_type1.size());
+               assertEquals("test1", ann1_type1.get(0).getValue().get());
+
+               var ann1_type2 = 
f_field1.getAnnotations(TestAnnotation2.class).toList();
+               assertEquals(1, ann1_type2.size());
+               assertEquals(42, ann1_type2.get(0).getInt("value").get());
+
+               var ann1_type3 = 
f_field1.getAnnotations(TestAnnotation3.class).toList();
+               assertEquals(0, ann1_type3.size());
+
+               var ann2_type1 = 
f_field2.getAnnotations(TestAnnotation1.class).toList();
+               assertEquals(1, ann2_type1.size());
+               assertEquals("test2", ann2_type1.get(0).getValue().get());
+
+               var ann2_type2 = 
f_field2.getAnnotations(TestAnnotation2.class).toList();
+               assertEquals(0, ann2_type2.size());
+       }
+
+       @Test void getAnnotations_memoization_returnsSameInstance() {
+               var annotations1 = f_field1.getAnnotations();
+               var annotations2 = f_field1.getAnnotations();
+               assertSame(annotations1, annotations2);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // getFullName()
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       public static class G {
+               public String field1;
+               public int field2;
+       }
+
+       static ClassInfo g = ClassInfo.of(G.class);
+       static FieldInfo
+               g_field1 = g.getPublicField(x -> x.hasName("field1")).get(),
+               g_field2 = g.getPublicField(x -> x.hasName("field2")).get();
+
+       @Test void getFullName_returnsFullyQualifiedName() {
+               String fullName1 = g_field1.getFullName();
+               String fullName2 = g_field2.getFullName();
+
+               assertTrue(fullName1.endsWith("FieldInfo_Test$G.field1"));
+               assertTrue(fullName2.endsWith("FieldInfo_Test$G.field2"));
+               
+               
assertTrue(fullName1.startsWith("org.apache.juneau.commons.reflect."));
+               
assertTrue(fullName2.startsWith("org.apache.juneau.commons.reflect."));
+       }
+
+       @Test void getFullName_memoization_returnsSameInstance() {
+               String name1 = g_field1.getFullName();
+               String name2 = g_field1.getFullName();
+               assertSame(name1, name2);
+       }
+
+       public static class InnerClass {
+               public String innerField;
+       }
+
+       static ClassInfo inner = ClassInfo.of(InnerClass.class);
+       static FieldInfo inner_field = inner.getPublicField(x -> 
x.hasName("innerField")).get();
+
+       @Test void getFullName_withInnerClass_usesDollarSeparator() {
+               String fullName = inner_field.getFullName();
+               
+               assertTrue(fullName.contains("FieldInfo_Test$InnerClass"));
+               assertTrue(fullName.endsWith(".innerField"));
+       }
 }
\ No newline at end of file
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ParameterInfoTest.java
 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ParameterInfo_Test.java
similarity index 99%
rename from 
juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ParameterInfoTest.java
rename to 
juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ParameterInfo_Test.java
index 8268a3d14a..9583354b06 100644
--- 
a/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ParameterInfoTest.java
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/commons/reflect/ParameterInfo_Test.java
@@ -35,7 +35,7 @@ import org.apache.juneau.annotation.Name;
 /**
  * ParameterInfo tests.
  */
-class ParameterInfoTest extends TestBase {
+class ParameterInfo_Test extends TestBase {
 
        private static String originalDisableParamNameDetection;
 

Reply via email to