View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau.commons.reflect;
18  
19  import static java.lang.annotation.ElementType.*;
20  import static java.lang.annotation.RetentionPolicy.*;
21  import static org.apache.juneau.commons.reflect.AnnotationTraversal.*;
22  import static org.junit.jupiter.api.Assertions.*;
23  
24  import java.lang.annotation.*;
25  import org.apache.juneau.*;
26  import org.apache.juneau.commons.collections.*;
27  import org.junit.jupiter.api.*;
28  
29  class AnnotationProvider_Test extends TestBase {
30  
31  	//====================================================================================================
32  	// Test annotations and classes
33  	//====================================================================================================
34  
35  	@Target(TYPE)
36  	@Retention(RUNTIME)
37  	public static @interface TestAnnotation {
38  		String value() default "default";
39  	}
40  
41  	@Target({TYPE, METHOD, FIELD, CONSTRUCTOR, PARAMETER})
42  	@Retention(RUNTIME)
43  	public static @interface MultiTargetAnnotation {
44  		int value() default 0;
45  	}
46  
47  	@Target(TYPE)
48  	@Retention(RUNTIME)
49  	public static @interface ParentAnnotation {
50  		String value() default "";
51  	}
52  
53  	@TestAnnotation("class")
54  	public static class TestClass {
55  		@MultiTargetAnnotation(1)
56  		public String field1;
57  
58  		@MultiTargetAnnotation(2)
59  		public TestClass() {}
60  
61  		@MultiTargetAnnotation(3)
62  		public void method1() {}
63  
64  		public void method2(@MultiTargetAnnotation(4) String param) {}
65  	}
66  
67  	@ParentAnnotation("parent")
68  	public static class ParentClass {}
69  
70  	@TestAnnotation("child")
71  	public static class ChildClass extends ParentClass {}
72  
73  	// Test classes for MATCHING_METHODS traversal
74  	public static interface MatchingMethodInterface {
75  		@MultiTargetAnnotation(10)
76  		void matchingMethod(String param);
77  	}
78  
79  	public static class MatchingMethodParent {
80  		@MultiTargetAnnotation(20)
81  		public void matchingMethod(String param) {}
82  	}
83  
84  	public static class MatchingMethodChild extends MatchingMethodParent implements MatchingMethodInterface {
85  		@MultiTargetAnnotation(30)
86  		@Override
87  		public void matchingMethod(String param) {}
88  	}
89  
90  	// Test classes for MATCHING_PARAMETERS traversal
91  	public static interface MatchingParameterInterface {
92  		void matchingParameterMethod(@MultiTargetAnnotation(100) String param);
93  	}
94  
95  	public static class MatchingParameterParent {
96  		public void matchingParameterMethod(@MultiTargetAnnotation(200) String param) {}
97  	}
98  
99  	public static class MatchingParameterChild extends MatchingParameterParent implements MatchingParameterInterface {
100 		@Override
101 		public void matchingParameterMethod(@MultiTargetAnnotation(300) String param) {}
102 	}
103 
104 	//====================================================================================================
105 	// create() and INSTANCE
106 	//====================================================================================================
107 
108 	@Test
109 	void a01_create_returnsBuilder() {
110 		var builder = AnnotationProvider.create();
111 		assertNotNull(builder);
112 		var provider = builder.build();
113 		assertNotNull(provider);
114 	}
115 
116 	@Test
117 	void a02_instance_isNotNull() {
118 		assertNotNull(AnnotationProvider.INSTANCE);
119 	}
120 
121 	//====================================================================================================
122 	// find(Class, ClassInfo, ...) - typed find for classes
123 	//====================================================================================================
124 
125 	@Test
126 	void b01_find_typedClass_returnsAnnotations() {
127 		var provider = AnnotationProvider.create().build();
128 		var ci = ClassInfo.of(TestClass.class);
129 		var annotations = provider.find(TestAnnotation.class, ci, SELF);
130 
131 		assertNotNull(annotations);
132 		assertEquals(1, annotations.size());
133 		assertEquals("class", annotations.get(0).getValue().orElse(null));
134 	}
135 
136 	@Test
137 	void b02_find_typedClass_withParents_returnsAnnotationsFromHierarchy() {
138 		var provider = AnnotationProvider.create().build();
139 		var ci = ClassInfo.of(ChildClass.class);
140 		var annotations = provider.find(TestAnnotation.class, ci, SELF, PARENTS);
141 
142 		assertNotNull(annotations);
143 		// Should find annotation on child class
144 		assertTrue(annotations.size() >= 1);
145 		var childAnnotation = annotations.stream()
146 			.filter(a -> a.getValue().orElse("").equals("child"))
147 			.findFirst();
148 		assertTrue(childAnnotation.isPresent());
149 	}
150 
151 	@Test
152 	void b03_find_typedClass_notFound_returnsEmptyList() {
153 		var provider = AnnotationProvider.create().build();
154 		var ci = ClassInfo.of(TestClass.class);
155 		var annotations = provider.find(ParentAnnotation.class, ci, SELF);
156 
157 		assertNotNull(annotations);
158 		assertTrue(annotations.isEmpty());
159 	}
160 
161 	@Test
162 	void b04_find_typedClass_withNullType_throwsException() {
163 		var provider = AnnotationProvider.create().build();
164 		var ci = ClassInfo.of(TestClass.class);
165 		assertThrows(IllegalArgumentException.class, () -> provider.find(null, ci, SELF));
166 	}
167 
168 	@Test
169 	void b05_find_typedClass_withNullClassInfo_throwsException() {
170 		var provider = AnnotationProvider.create().build();
171 		ClassInfo nullClassInfo = null;
172 		assertThrows(IllegalArgumentException.class, () -> provider.find(TestAnnotation.class, nullClassInfo, SELF));
173 	}
174 
175 	//====================================================================================================
176 	// find(ClassInfo, ...) - untyped find for classes
177 	//====================================================================================================
178 
179 	@Test
180 	void c01_find_untypedClass_returnsAllAnnotations() {
181 		var provider = AnnotationProvider.create().build();
182 		var ci = ClassInfo.of(TestClass.class);
183 		var annotations = provider.find(ci, SELF);
184 
185 		assertNotNull(annotations);
186 		assertTrue(annotations.size() >= 1);
187 		assertTrue(annotations.stream().anyMatch(a -> a.isType(TestAnnotation.class)));
188 	}
189 
190 	@Test
191 	void c02_find_untypedClass_withNullClassInfo_throwsException() {
192 		var provider = AnnotationProvider.create().build();
193 		assertThrows(IllegalArgumentException.class, () -> provider.find((ClassInfo)null, SELF));
194 	}
195 
196 	//====================================================================================================
197 	// find(Class, FieldInfo, ...) - typed find for fields
198 	//====================================================================================================
199 
200 	@Test
201 	void d01_find_typedField_returnsAnnotations() {
202 		var provider = AnnotationProvider.create().build();
203 		var ci = ClassInfo.of(TestClass.class);
204 		var field = ci.getPublicField(x -> x.hasName("field1")).orElse(null);
205 		assertNotNull(field);
206 
207 		var annotations = provider.find(MultiTargetAnnotation.class, field, SELF);
208 
209 		assertNotNull(annotations);
210 		assertEquals(1, annotations.size());
211 		assertEquals(1, annotations.get(0).getInt("value").orElse(0));
212 	}
213 
214 	@Test
215 	void d03_find_typedField_withRuntimeAnnotations_includesRuntimeAndDeclared() {
216 		// This test covers lines 1039-1041 - SELF traversal for FieldInfo with runtime annotations
217 		var runtimeAnnotation = new RuntimeOnAnnotation(new String[]{"org.apache.juneau.commons.reflect.AnnotationProvider_Test$TestClass.field1"}, "runtimeField");
218 		var provider = AnnotationProvider.create()
219 			.addRuntimeAnnotations(runtimeAnnotation)
220 			.build();
221 		var ci = ClassInfo.of(TestClass.class);
222 		var field = ci.getPublicField(x -> x.hasName("field1")).orElse(null);
223 		assertNotNull(field);
224 
225 		// Call with SELF traversal - should include both runtime annotations (line 1040) and declared annotations (line 1041)
226 		var annotations = provider.find(TestAnnotation.class, field, SELF);
227 
228 		assertNotNull(annotations);
229 		// Should find both the runtime annotation and the declared annotation (if any)
230 		assertTrue(annotations.size() >= 1, "Should find at least the runtime annotation");
231 
232 		// Verify runtime annotation is found
233 		var runtimeAnnotationFound = annotations.stream()
234 			.filter(a -> a.getValue().orElse("").equals("runtimeField"))
235 			.findFirst();
236 		assertTrue(runtimeAnnotationFound.isPresent(), "Should find runtime annotation on field");
237 	}
238 
239 	@Test
240 	void d02_find_typedField_notFound_returnsEmptyList() {
241 		var provider = AnnotationProvider.create().build();
242 		var ci = ClassInfo.of(TestClass.class);
243 		var field = ci.getPublicField(x -> x.hasName("field1")).orElse(null);
244 		assertNotNull(field);
245 
246 		var annotations = provider.find(TestAnnotation.class, field, SELF);
247 
248 		assertNotNull(annotations);
249 		assertTrue(annotations.isEmpty());
250 	}
251 
252 	//====================================================================================================
253 	// find(FieldInfo, ...) - untyped find for fields
254 	//====================================================================================================
255 
256 	@Test
257 	void e01_find_untypedField_returnsAllAnnotations() {
258 		var provider = AnnotationProvider.create().build();
259 		var ci = ClassInfo.of(TestClass.class);
260 		var field = ci.getPublicField(x -> x.hasName("field1")).orElse(null);
261 		assertNotNull(field);
262 
263 		var annotations = provider.find(field, SELF);
264 
265 		assertNotNull(annotations);
266 		assertTrue(annotations.size() >= 1);
267 		assertTrue(annotations.stream().anyMatch(a -> a.isType(MultiTargetAnnotation.class)));
268 	}
269 
270 	//====================================================================================================
271 	// find(Class, ConstructorInfo, ...) - typed find for constructors
272 	//====================================================================================================
273 
274 	@Test
275 	void f01_find_typedConstructor_returnsAnnotations() {
276 		var provider = AnnotationProvider.create().build();
277 		var ci = ClassInfo.of(TestClass.class);
278 		var constructor = ci.getPublicConstructor(x -> x.getParameterCount() == 0).orElse(null);
279 		assertNotNull(constructor);
280 
281 		var annotations = provider.find(MultiTargetAnnotation.class, constructor, SELF);
282 
283 		assertNotNull(annotations);
284 		assertEquals(1, annotations.size());
285 		assertEquals(2, annotations.get(0).getInt("value").orElse(0));
286 	}
287 
288 	//====================================================================================================
289 	// find(ConstructorInfo, ...) - untyped find for constructors
290 	//====================================================================================================
291 
292 	@Test
293 	void g01_find_untypedConstructor_returnsAllAnnotations() {
294 		var provider = AnnotationProvider.create().build();
295 		var ci = ClassInfo.of(TestClass.class);
296 		var constructor = ci.getPublicConstructor(x -> x.getParameterCount() == 0).orElse(null);
297 		assertNotNull(constructor);
298 
299 		var annotations = provider.find(constructor, SELF);
300 
301 		assertNotNull(annotations);
302 		assertTrue(annotations.size() >= 1);
303 		assertTrue(annotations.stream().anyMatch(a -> a.isType(MultiTargetAnnotation.class)));
304 	}
305 
306 	//====================================================================================================
307 	// find(Class, MethodInfo, ...) - typed find for methods
308 	//====================================================================================================
309 
310 	@Test
311 	void h01_find_typedMethod_returnsAnnotations() {
312 		var provider = AnnotationProvider.create().build();
313 		var ci = ClassInfo.of(TestClass.class);
314 		MethodInfo method = ci.getPublicMethod(x -> x.hasName("method1")).orElse(null);
315 		assertNotNull(method);
316 
317 		var annotations = provider.find(MultiTargetAnnotation.class, method, SELF);
318 
319 		assertNotNull(annotations);
320 		assertEquals(1, annotations.size());
321 		assertEquals(3, annotations.get(0).getInt("value").orElse(0));
322 	}
323 
324 	//====================================================================================================
325 	// find(MethodInfo, ...) - untyped find for methods
326 	//====================================================================================================
327 
328 	@Test
329 	void i01_find_untypedMethod_returnsAllAnnotations() {
330 		var provider = AnnotationProvider.create().build();
331 		var ci = ClassInfo.of(TestClass.class);
332 		var method = ci.getPublicMethod(x -> x.hasName("method1")).orElse(null);
333 		assertNotNull(method);
334 
335 		var annotations = provider.find(method, SELF);
336 
337 		assertNotNull(annotations);
338 		assertTrue(annotations.size() >= 1);
339 		assertTrue(annotations.stream().anyMatch(a -> a.isType(MultiTargetAnnotation.class)));
340 	}
341 
342 	//====================================================================================================
343 	// find(Class, ParameterInfo, ...) - typed find for parameters
344 	//====================================================================================================
345 
346 	@Test
347 	void j01_find_typedParameter_returnsAnnotations() {
348 		var provider = AnnotationProvider.create().build();
349 		var ci = ClassInfo.of(TestClass.class);
350 		var method = ci.getPublicMethod(x -> x.hasName("method2")).orElse(null);
351 		assertNotNull(method);
352 		ParameterInfo param = method.getParameter(0);
353 		assertNotNull(param);
354 
355 		var annotations = provider.find(MultiTargetAnnotation.class, param, SELF);
356 
357 		assertNotNull(annotations);
358 		assertEquals(1, annotations.size());
359 		assertEquals(4, annotations.get(0).getInt("value").orElse(0));
360 	}
361 
362 	//====================================================================================================
363 	// find(ParameterInfo, ...) - untyped find for parameters
364 	//====================================================================================================
365 
366 	@Test
367 	void k01_find_untypedParameter_returnsAllAnnotations() {
368 		var provider = AnnotationProvider.create().build();
369 		var ci = ClassInfo.of(TestClass.class);
370 		var method = ci.getPublicMethod(x -> x.hasName("method2")).orElse(null);
371 		assertNotNull(method);
372 		var param = method.getParameter(0);
373 		assertNotNull(param);
374 
375 		var annotations = provider.find(param, SELF);
376 
377 		assertNotNull(annotations);
378 		assertTrue(annotations.size() >= 1);
379 		assertTrue(annotations.stream().anyMatch(a -> a.isType(MultiTargetAnnotation.class)));
380 	}
381 
382 	//====================================================================================================
383 	// has(Class, ClassInfo, ...) - check existence for classes
384 	//====================================================================================================
385 
386 	@Test
387 	void l01_has_typedClass_exists_returnsTrue() {
388 		var provider = AnnotationProvider.create().build();
389 		var ci = ClassInfo.of(TestClass.class);
390 
391 		assertTrue(provider.has(TestAnnotation.class, ci, SELF));
392 	}
393 
394 	@Test
395 	void l02_has_typedClass_notExists_returnsFalse() {
396 		var provider = AnnotationProvider.create().build();
397 		var ci = ClassInfo.of(TestClass.class);
398 
399 		assertFalse(provider.has(ParentAnnotation.class, ci, SELF));
400 	}
401 
402 	//====================================================================================================
403 	// has(Class, ConstructorInfo, ...) - check existence for constructors
404 	//====================================================================================================
405 
406 	@Test
407 	void l03_has_typedConstructor_exists_returnsTrue() {
408 		var provider = AnnotationProvider.create().build();
409 		var ci = ClassInfo.of(TestClass.class);
410 		ConstructorInfo constructor = ci.getPublicConstructor(x -> x.getParameterCount() == 0).orElse(null);
411 		assertNotNull(constructor);
412 
413 		assertTrue(provider.has(MultiTargetAnnotation.class, constructor, SELF));
414 	}
415 
416 	@Test
417 	void l04_has_typedConstructor_notExists_returnsFalse() {
418 		var provider = AnnotationProvider.create().build();
419 		var ci = ClassInfo.of(TestClass.class);
420 		ConstructorInfo constructor = ci.getPublicConstructor(x -> x.getParameterCount() == 0).orElse(null);
421 		assertNotNull(constructor);
422 
423 		assertFalse(provider.has(TestAnnotation.class, constructor, SELF));
424 	}
425 
426 	//====================================================================================================
427 	// has(Class, FieldInfo, ...) - check existence for fields
428 	//====================================================================================================
429 
430 	@Test
431 	void l05_has_typedField_exists_returnsTrue() {
432 		var provider = AnnotationProvider.create().build();
433 		var ci = ClassInfo.of(TestClass.class);
434 		FieldInfo field = ci.getPublicField(x -> x.hasName("field1")).orElse(null);
435 		assertNotNull(field);
436 
437 		assertTrue(provider.has(MultiTargetAnnotation.class, field, SELF));
438 	}
439 
440 	@Test
441 	void l06_has_typedField_notExists_returnsFalse() {
442 		var provider = AnnotationProvider.create().build();
443 		var ci = ClassInfo.of(TestClass.class);
444 		FieldInfo field = ci.getPublicField(x -> x.hasName("field1")).orElse(null);
445 		assertNotNull(field);
446 
447 		assertFalse(provider.has(TestAnnotation.class, field, SELF));
448 	}
449 
450 	//====================================================================================================
451 	// has(Class, MethodInfo, ...) - check existence for methods
452 	//====================================================================================================
453 
454 	@Test
455 	void l07_has_typedMethod_exists_returnsTrue() {
456 		var provider = AnnotationProvider.create().build();
457 		var ci = ClassInfo.of(TestClass.class);
458 		MethodInfo method = ci.getPublicMethod(x -> x.hasName("method1")).orElse(null);
459 		assertNotNull(method);
460 
461 		assertTrue(provider.has(MultiTargetAnnotation.class, method, SELF));
462 	}
463 
464 	@Test
465 	void l08_has_typedMethod_notExists_returnsFalse() {
466 		var provider = AnnotationProvider.create().build();
467 		var ci = ClassInfo.of(TestClass.class);
468 		MethodInfo method = ci.getPublicMethod(x -> x.hasName("method1")).orElse(null);
469 		assertNotNull(method);
470 
471 		assertFalse(provider.has(TestAnnotation.class, method, SELF));
472 	}
473 
474 	//====================================================================================================
475 	// has(Class, ParameterInfo, ...) - check existence for parameters
476 	//====================================================================================================
477 
478 	@Test
479 	void l09_has_typedParameter_exists_returnsTrue() {
480 		var provider = AnnotationProvider.create().build();
481 		var ci = ClassInfo.of(TestClass.class);
482 		var method = ci.getPublicMethod(x -> x.hasName("method2")).orElse(null);
483 		assertNotNull(method);
484 		ParameterInfo param = method.getParameter(0);
485 		assertNotNull(param);
486 
487 		assertTrue(provider.has(MultiTargetAnnotation.class, param, SELF));
488 	}
489 
490 	@Test
491 	void l10_has_typedParameter_notExists_returnsFalse() {
492 		var provider = AnnotationProvider.create().build();
493 		var ci = ClassInfo.of(TestClass.class);
494 		var method = ci.getPublicMethod(x -> x.hasName("method2")).orElse(null);
495 		assertNotNull(method);
496 		ParameterInfo param = method.getParameter(0);
497 		assertNotNull(param);
498 
499 		assertFalse(provider.has(TestAnnotation.class, param, SELF));
500 	}
501 
502 	//====================================================================================================
503 	// Default traversal logic (lines 991-998) - when no traversals specified
504 	//====================================================================================================
505 
506 	@Test
507 	void l11_find_typedClass_noTraversals_usesDefaultTraversals() {
508 		// This test covers line 991-992 - default traversals for ClassInfo (PARENTS, PACKAGE)
509 		var provider = AnnotationProvider.create().build();
510 		var ci = ClassInfo.of(ChildClass.class);
511 
512 		// Call with no traversals - should use default (PARENTS, PACKAGE)
513 		var annotations = provider.find(TestAnnotation.class, ci);
514 
515 		assertNotNull(annotations);
516 		// Should find annotation from child class
517 		assertTrue(annotations.size() >= 1);
518 	}
519 
520 	@Test
521 	void l12_find_typedMethod_noTraversals_usesDefaultTraversals() {
522 		// This test covers line 993-994 - default traversals for MethodInfo
523 		var provider = AnnotationProvider.create().build();
524 		var ci = ClassInfo.of(TestClass.class);
525 		MethodInfo method = ci.getPublicMethod(x -> x.hasName("method1")).orElse(null);
526 		assertNotNull(method);
527 
528 		// Call with no traversals - should use default (SELF, MATCHING_METHODS, DECLARING_CLASS, RETURN_TYPE, PACKAGE)
529 		var annotations = provider.find(MultiTargetAnnotation.class, method);
530 
531 		assertNotNull(annotations);
532 		assertTrue(annotations.size() >= 1);
533 	}
534 
535 	@Test
536 	void l13_find_typedField_noTraversals_usesDefaultTraversals() {
537 		// This test covers line 995-996 - default traversals for FieldInfo (SELF)
538 		var provider = AnnotationProvider.create().build();
539 		var ci = ClassInfo.of(TestClass.class);
540 		FieldInfo field = ci.getPublicField(x -> x.hasName("field1")).orElse(null);
541 		assertNotNull(field);
542 
543 		// Call with no traversals - should use default (SELF)
544 		var annotations = provider.find(MultiTargetAnnotation.class, field);
545 
546 		assertNotNull(annotations);
547 		assertEquals(1, annotations.size());
548 	}
549 
550 	@Test
551 	void l14_find_typedConstructor_noTraversals_usesDefaultTraversals() {
552 		// This test covers line 995-996 - default traversals for ConstructorInfo (SELF)
553 		var provider = AnnotationProvider.create().build();
554 		var ci = ClassInfo.of(TestClass.class);
555 		ConstructorInfo constructor = ci.getPublicConstructor(x -> x.getParameterCount() == 0).orElse(null);
556 		assertNotNull(constructor);
557 
558 		// Call with no traversals - should use default (SELF)
559 		var annotations = provider.find(MultiTargetAnnotation.class, constructor);
560 
561 		assertNotNull(annotations);
562 		assertEquals(1, annotations.size());
563 	}
564 
565 	@Test
566 	void l15_find_typedParameter_noTraversals_usesDefaultTraversals() {
567 		// This test covers line 997-998 - default traversals for ParameterInfo
568 		var provider = AnnotationProvider.create().build();
569 		var ci = ClassInfo.of(TestClass.class);
570 		var method = ci.getPublicMethod(x -> x.hasName("method2")).orElse(null);
571 		assertNotNull(method);
572 		ParameterInfo param = method.getParameter(0);
573 		assertNotNull(param);
574 
575 		// Call with no traversals - should use default (SELF, MATCHING_PARAMETERS, PARAMETER_TYPE)
576 		var annotations = provider.find(MultiTargetAnnotation.class, param);
577 
578 		assertNotNull(annotations);
579 		assertTrue(annotations.size() >= 1);
580 	}
581 
582 	@Test
583 	void l16_find_typedClass_withPackageTraversal_nullPackage_handlesGracefully() {
584 		// This test covers line 1014 - when getPackage() returns null
585 		// Primitive types and arrays of primitives have no package
586 		var provider = AnnotationProvider.create().build();
587 
588 		// int.class has no package (getPackage() returns null)
589 		var ci = ClassInfo.of(int.class);
590 		assertNull(ci.getPackage(), "int.class should have no package");
591 
592 		// Call with PACKAGE traversal - should handle null package gracefully
593 		var annotations = provider.find(TestAnnotation.class, ci, AnnotationTraversal.PACKAGE);
594 
595 		// Should not throw exception, just return empty list
596 		assertNotNull(annotations);
597 		assertEquals(0, annotations.size());
598 	}
599 
600 	@Test
601 	void l17_find_typedMethod_withMatchingMethodsTraversal_includesParentAndInterfaceMethods() {
602 		// This test covers lines 1024-1025 - MATCHING_METHODS traversal
603 		var provider = AnnotationProvider.create().build();
604 		var ci = ClassInfo.of(MatchingMethodChild.class);
605 		MethodInfo method = ci.getPublicMethod(x -> x.hasName("matchingMethod")).orElse(null);
606 		assertNotNull(method);
607 
608 		// Verify the matching methods order: [child, interface, parent]
609 		var matchingMethods = method.getMatchingMethods();
610 		assertTrue(matchingMethods.size() >= 3, "Should have at least child, interface, and parent methods. Found: " + matchingMethods.size());
611 
612 		// Verify we can find the parent method in the matching methods
613 		var parentMethod = matchingMethods.stream()
614 			.filter(m -> MatchingMethodParent.class.equals(m.getDeclaringClass().inner()))
615 			.findFirst();
616 		assertTrue(parentMethod.isPresent(), "Parent method should be in matching methods");
617 
618 		// Verify the parent method has the annotation
619 		var parentMethodAnnotations = parentMethod.get().getDeclaredAnnotations(MultiTargetAnnotation.class).toList();
620 		assertTrue(parentMethodAnnotations.size() > 0, "Parent method should have annotation. Found " + parentMethodAnnotations.size() + " annotations");
621 		var parentMethodAnnotation = parentMethodAnnotations.get(0);
622 		// Try getInt first, then getValue as fallback
623 		var parentAnnotationValue = parentMethodAnnotation.getInt("value").orElse(null);
624 		if (parentAnnotationValue == null) {
625 			parentAnnotationValue = parentMethodAnnotation.getValue(Integer.class, "value").orElse(null);
626 		}
627 		assertEquals(20, parentAnnotationValue, "Parent method annotation should have value 20. Annotation: " + parentMethodAnnotation);
628 
629 		// Skip the first (child method) - should have interface and parent
630 		var methodsAfterSkip = matchingMethods.stream().skip(1).toList();
631 		assertTrue(methodsAfterSkip.size() >= 2, "Should have interface and parent methods after skipping child");
632 
633 		// Call with MATCHING_METHODS traversal - should include annotations from parent and interface methods
634 		// Note: skip(1) skips the child method itself, so we get interface and parent
635 		var annotations = provider.find(MultiTargetAnnotation.class, method, MATCHING_METHODS);
636 
637 		assertNotNull(annotations);
638 		// Should find annotations from:
639 		// 1. Interface method (value=10) - from declared interfaces of child class
640 		// 2. Parent class method (value=20) - from parent class
641 		// Note: The child method itself is skipped by .skip(1)
642 		assertTrue(annotations.size() >= 2, "Should find annotations from parent and interface matching methods. Found: " + annotations.size());
643 
644 		// Debug: print what we found
645 		var foundValues = annotations.stream()
646 			.map(a -> {
647 				Integer val = a.getInt("value").orElse(null);
648 				if (val == null) {
649 					val = a.getValue(Integer.class, "value").orElse(null);
650 				}
651 				return val;
652 			})
653 			.filter(v -> v != null)
654 			.toList();
655 
656 		// Verify we have the interface annotation (value=10) - comes first after skip(1)
657 		var interfaceAnnotation = annotations.stream()
658 			.filter(a -> {
659 				Integer val = a.getInt("value").orElse(null);
660 				if (val == null) {
661 					val = a.getValue(Integer.class, "value").orElse(null);
662 				}
663 				return val != null && val.intValue() == 10;
664 			})
665 			.findFirst();
666 		assertTrue(interfaceAnnotation.isPresent(), "Should find annotation from interface method. Found values: " + foundValues);
667 
668 		// Verify we have the parent annotation (value=20) - comes after interface
669 		var parentAnnotation = annotations.stream()
670 			.filter(a -> {
671 				Integer val = a.getInt("value").orElse(null);
672 				if (val == null) {
673 					val = a.getValue(Integer.class, "value").orElse(null);
674 				}
675 				return val != null && val.intValue() == 20;
676 			})
677 			.findFirst();
678 		assertTrue(parentAnnotation.isPresent(), "Should find annotation from parent class method. Found values: " + foundValues + ", matching methods count: " + matchingMethods.size());
679 	}
680 
681 	@Test
682 	void l18_find_typedParameter_withMatchingParametersTraversal_includesParentAndInterfaceParameters() {
683 		// This test covers line 1054 - MATCHING_PARAMETERS traversal
684 		var provider = AnnotationProvider.create().build();
685 		var ci = ClassInfo.of(MatchingParameterChild.class);
686 		MethodInfo method = ci.getPublicMethod(x -> x.hasName("matchingParameterMethod")).orElse(null);
687 		assertNotNull(method);
688 		ParameterInfo param = method.getParameter(0);
689 		assertNotNull(param);
690 
691 		// Verify the matching parameters order: [child, interface, parent]
692 		var matchingParameters = param.getMatchingParameters();
693 		assertTrue(matchingParameters.size() >= 3, "Should have at least child, interface, and parent parameters. Found: " + matchingParameters.size());
694 
695 		// Verify we can find the parent parameter in the matching parameters
696 		var parentParameter = matchingParameters.stream()
697 			.filter(p -> {
698 				var paramMethod = p.getMethod();
699 				if (paramMethod != null) {
700 					return MatchingParameterParent.class.equals(paramMethod.getDeclaringClass().inner());
701 				}
702 				return false;
703 			})
704 			.findFirst();
705 		assertTrue(parentParameter.isPresent(), "Parent parameter should be in matching parameters");
706 
707 		// Verify the parent parameter has the annotation
708 		var parentParameterAnnotations = parentParameter.get().getAnnotations(MultiTargetAnnotation.class).toList();
709 		assertTrue(parentParameterAnnotations.size() > 0, "Parent parameter should have annotation. Found " + parentParameterAnnotations.size() + " annotations");
710 		var parentParameterAnnotation = parentParameterAnnotations.get(0);
711 		var parentParameterValue = parentParameterAnnotation.getInt("value").orElse(null);
712 		if (parentParameterValue == null) {
713 			parentParameterValue = parentParameterAnnotation.getValue(Integer.class, "value").orElse(null);
714 		}
715 		assertEquals(200, parentParameterValue, "Parent parameter annotation should have value 200. Annotation: " + parentParameterAnnotation);
716 
717 		// Skip the first (child parameter) - should have interface and parent
718 		var parametersAfterSkip = matchingParameters.stream().skip(1).toList();
719 		assertTrue(parametersAfterSkip.size() >= 2, "Should have interface and parent parameters after skipping child");
720 
721 		// Call with MATCHING_PARAMETERS traversal - should include annotations from parent and interface parameters
722 		// Note: skip(1) skips the child parameter itself, so we get interface and parent
723 		var annotations = provider.find(MultiTargetAnnotation.class, param, MATCHING_PARAMETERS);
724 
725 		assertNotNull(annotations);
726 		// Should find annotations from:
727 		// 1. Interface parameter (value=100) - from declared interfaces of child class
728 		// 2. Parent class parameter (value=200) - from parent class
729 		// Note: The child parameter itself is skipped by .skip(1)
730 		assertTrue(annotations.size() >= 2, "Should find annotations from parent and interface matching parameters. Found: " + annotations.size());
731 
732 		// Debug: print what we found
733 		var foundValues = annotations.stream()
734 			.map(a -> {
735 				Integer val = a.getInt("value").orElse(null);
736 				if (val == null) {
737 					val = a.getValue(Integer.class, "value").orElse(null);
738 				}
739 				return val;
740 			})
741 			.filter(v -> v != null)
742 			.toList();
743 
744 		// Verify we have the interface parameter annotation (value=100) - comes first after skip(1)
745 		var interfaceAnnotation = annotations.stream()
746 			.filter(a -> {
747 				Integer val = a.getInt("value").orElse(null);
748 				if (val == null) {
749 					val = a.getValue(Integer.class, "value").orElse(null);
750 				}
751 				return val != null && val.intValue() == 100;
752 			})
753 			.findFirst();
754 		assertTrue(interfaceAnnotation.isPresent(), "Should find annotation from interface parameter. Found values: " + foundValues);
755 
756 		// Verify we have the parent parameter annotation (value=200) - comes after interface
757 		var parentAnnotation = annotations.stream()
758 			.filter(a -> {
759 				Integer val = a.getInt("value").orElse(null);
760 				if (val == null) {
761 					val = a.getValue(Integer.class, "value").orElse(null);
762 				}
763 				return val != null && val.intValue() == 200;
764 			})
765 			.findFirst();
766 		assertTrue(parentAnnotation.isPresent(), "Should find annotation from parent class parameter. Found values: " + foundValues + ", matching parameters count: " + matchingParameters.size());
767 	}
768 
769 	@Test
770 	void l19_find_typedMethod_withRuntimeAnnotation_loadsFromAnnotationMap() {
771 		// This test covers line 1072 - load() method for Method objects
772 		var runtimeAnnotation = new RuntimeOnAnnotation(new String[]{"org.apache.juneau.commons.reflect.AnnotationProvider_Test$TestClass.method1"}, "runtimeMethod");
773 		var provider = AnnotationProvider.create()
774 			.addRuntimeAnnotations(runtimeAnnotation)
775 			.build();
776 		var ci = ClassInfo.of(TestClass.class);
777 		MethodInfo method = ci.getPublicMethod(x -> x.hasName("method1")).orElse(null);
778 		assertNotNull(method);
779 
780 		// Call with SELF traversal - should trigger runtimeCache.get() which calls load() for Method
781 		// This covers line 1072: annotationMap.find(mi.inner())
782 		var annotations = provider.find(TestAnnotation.class, method, SELF);
783 
784 		assertNotNull(annotations);
785 		// Should find the runtime annotation
786 		assertTrue(annotations.size() >= 1, "Should find at least the runtime annotation");
787 
788 		// Verify runtime annotation is found
789 		var runtimeAnnotationFound = annotations.stream()
790 			.filter(a -> a.getValue().orElse("").equals("runtimeMethod"))
791 			.findFirst();
792 		assertTrue(runtimeAnnotationFound.isPresent(), "Should find runtime annotation on method");
793 	}
794 
795 	@Test
796 	void l20_find_typedConstructor_withRuntimeAnnotation_loadsFromAnnotationMap() {
797 		// This test covers line 1080 - load() method for Constructor objects
798 		// Constructor format for ReflectionMap is "ClassName()" for no-arg constructor
799 		var runtimeAnnotation = new RuntimeOnAnnotation(new String[]{"org.apache.juneau.commons.reflect.AnnotationProvider_Test$TestClass()"}, "runtimeConstructor");
800 		var provider = AnnotationProvider.create()
801 			.addRuntimeAnnotations(runtimeAnnotation)
802 			.build();
803 		var ci = ClassInfo.of(TestClass.class);
804 		ConstructorInfo constructor = ci.getPublicConstructor(x -> x.getParameterCount() == 0).orElse(null);
805 		assertNotNull(constructor);
806 
807 		// Call with SELF traversal - should trigger runtimeCache.get() which calls load() for Constructor
808 		// This covers line 1080: annotationMap.find(ci.inner())
809 		var annotations = provider.find(TestAnnotation.class, constructor, SELF);
810 
811 		assertNotNull(annotations);
812 		// Should find the runtime annotation
813 		assertTrue(annotations.size() >= 1, "Should find at least the runtime annotation");
814 
815 		// Verify runtime annotation is found
816 		var runtimeAnnotationFound = annotations.stream()
817 			.filter(a -> a.getValue().orElse("").equals("runtimeConstructor"))
818 			.findFirst();
819 		assertTrue(runtimeAnnotationFound.isPresent(), "Should find runtime annotation on constructor");
820 	}
821 
822 	//====================================================================================================
823 	// Builder methods
824 	//====================================================================================================
825 
826 	@Test
827 	void m01_builder_cacheMode_buildsProvider() {
828 		var provider = AnnotationProvider.create()
829 			.cacheMode(CacheMode.NONE)
830 			.build();
831 		assertNotNull(provider);
832 	}
833 
834 	@Test
835 	void m02_builder_logOnExit_buildsProvider() {
836 		var provider = AnnotationProvider.create()
837 			.logOnExit()
838 			.build();
839 		assertNotNull(provider);
840 	}
841 
842 	@Test
843 	void m03_builder_logOnExit_withBoolean_coversLine402() {
844 		// This test covers line 402 - logOnExit(boolean value)
845 		var provider1 = AnnotationProvider.create()
846 			.logOnExit(true)
847 			.build();
848 		assertNotNull(provider1);
849 
850 		var provider2 = AnnotationProvider.create()
851 			.logOnExit(false)
852 			.build();
853 		assertNotNull(provider2);
854 	}
855 
856 	@Test
857 	void m04_builder_chaining_buildsProvider() {
858 		var provider = AnnotationProvider.create()
859 			.cacheMode(CacheMode.NONE)
860 			.logOnExit()
861 			.build();
862 		assertNotNull(provider);
863 	}
864 
865 	//====================================================================================================
866 	// addRuntimeAnnotations(Annotation...) - varargs version
867 	//====================================================================================================
868 
869 	// Simple runtime annotation implementation for testing with onClass()
870 	private static class RuntimeTestAnnotation implements TestAnnotation {
871 		private final Class<?>[] onClass;
872 		private final String value;
873 
874 		RuntimeTestAnnotation(Class<?>[] onClass, String value) {
875 			this.onClass = onClass;
876 			this.value = value;
877 		}
878 
879 		@Override
880 		public String value() {
881 			return value;
882 		}
883 
884 		@Override
885 		public Class<? extends Annotation> annotationType() {
886 			return TestAnnotation.class;
887 		}
888 
889 		@SuppressWarnings("unused")
890 		public Class<?>[] onClass() {
891 			return onClass;
892 		}
893 
894 		@Override
895 		public boolean equals(Object obj) {
896 			if (!(obj instanceof TestAnnotation))
897 				return false;
898 			TestAnnotation other = (TestAnnotation)obj;
899 			return value.equals(other.value());
900 		}
901 
902 		@Override
903 		public int hashCode() {
904 			return value.hashCode();
905 		}
906 
907 		@Override
908 		public String toString() {
909 			return "@TestAnnotation(value=" + value + ")";
910 		}
911 	}
912 
913 	// Runtime annotation implementation with on() method (String[] targeting)
914 	private static class RuntimeOnAnnotation implements TestAnnotation {
915 		private final String[] on;
916 		private final String value;
917 
918 		RuntimeOnAnnotation(String[] on, String value) {
919 			this.on = on;
920 			this.value = value;
921 		}
922 
923 		@Override
924 		public String value() {
925 			return value;
926 		}
927 
928 		@Override
929 		public Class<? extends Annotation> annotationType() {
930 			return TestAnnotation.class;
931 		}
932 
933 		@SuppressWarnings("unused")
934 		public String[] on() {
935 			return on;
936 		}
937 
938 		@Override
939 		public boolean equals(Object obj) {
940 			if (!(obj instanceof TestAnnotation))
941 				return false;
942 			TestAnnotation other = (TestAnnotation)obj;
943 			return value.equals(other.value());
944 		}
945 
946 		@Override
947 		public int hashCode() {
948 			return value.hashCode();
949 		}
950 
951 		@Override
952 		public String toString() {
953 			return "@TestAnnotation(value=" + value + ")";
954 		}
955 	}
956 
957 	@Test
958 	void n01_addRuntimeAnnotations_varargs_callsListVersion() {
959 		// This test covers line 245 - the varargs version that converts to list
960 		var runtimeAnnotation1 = new RuntimeTestAnnotation(new Class<?>[]{TestClass.class}, "runtime1");
961 		var runtimeAnnotation2 = new RuntimeTestAnnotation(new Class<?>[]{ParentClass.class}, "runtime2");
962 
963 		var provider = AnnotationProvider.create()
964 			.addRuntimeAnnotations(runtimeAnnotation1, runtimeAnnotation2)  // Varargs - covers line 245
965 			.build();
966 
967 		assertNotNull(provider);
968 
969 		// Verify the runtime annotations are applied
970 		var ci = ClassInfo.of(TestClass.class);
971 		var annotations = provider.find(TestAnnotation.class, ci, SELF);
972 
973 		// Should find the runtime annotation
974 		assertTrue(annotations.size() >= 1);
975 		var runtimeAnnotation = annotations.stream()
976 			.filter(a -> a.getValue().orElse("").equals("runtime1"))
977 			.findFirst();
978 		assertTrue(runtimeAnnotation.isPresent());
979 	}
980 
981 	@Test
982 	void n04_addRuntimeAnnotations_withOnMethod_coversLine343() {
983 		// This test covers line 343 - the on() method returning String[] is processed
984 		var className = TestClass.class.getName();
985 		var runtimeAnnotation = new RuntimeOnAnnotation(new String[]{className}, "runtimeOn");
986 
987 		var provider = AnnotationProvider.create()
988 			.addRuntimeAnnotations(runtimeAnnotation)
989 			.build();
990 
991 		assertNotNull(provider);
992 
993 		// Verify the runtime annotation is applied using on() method
994 		var ci = ClassInfo.of(TestClass.class);
995 		var annotations = provider.find(TestAnnotation.class, ci, SELF);
996 
997 		// Should find the runtime annotation
998 		assertTrue(annotations.size() >= 1);
999 		var runtimeAnnotationFound = annotations.stream()
1000 			.filter(a -> a.getValue().orElse("").equals("runtimeOn"))
1001 			.findFirst();
1002 		assertTrue(runtimeAnnotationFound.isPresent());
1003 	}
1004 
1005 	// Runtime annotation with invalid onClass() return type (not Class[])
1006 	private static class InvalidOnClassAnnotation implements TestAnnotation {
1007 		@Override
1008 		public String value() {
1009 			return "invalid";
1010 		}
1011 
1012 		@Override
1013 		public Class<? extends Annotation> annotationType() {
1014 			return TestAnnotation.class;
1015 		}
1016 
1017 		@SuppressWarnings("unused")
1018 		public String onClass() {  // Wrong return type - should be Class[]
1019 			return "invalid";
1020 		}
1021 
1022 		@Override
1023 		public boolean equals(Object obj) {
1024 			return obj instanceof TestAnnotation;
1025 		}
1026 
1027 		@Override
1028 		public int hashCode() {
1029 			return 0;
1030 		}
1031 
1032 		@Override
1033 		public String toString() {
1034 			return "@TestAnnotation";
1035 		}
1036 	}
1037 
1038 	// Runtime annotation with invalid on() return type (not String[])
1039 	private static class InvalidOnAnnotation implements TestAnnotation {
1040 		@Override
1041 		public String value() {
1042 			return "invalid";
1043 		}
1044 
1045 		@Override
1046 		public Class<? extends Annotation> annotationType() {
1047 			return TestAnnotation.class;
1048 		}
1049 
1050 		@SuppressWarnings("unused")
1051 		public String on() {  // Wrong return type - should be String[]
1052 			return "invalid";
1053 		}
1054 
1055 		@Override
1056 		public boolean equals(Object obj) {
1057 			return obj instanceof TestAnnotation;
1058 		}
1059 
1060 		@Override
1061 		public int hashCode() {
1062 			return 0;
1063 		}
1064 
1065 		@Override
1066 		public String toString() {
1067 			return "@TestAnnotation";
1068 		}
1069 	}
1070 
1071 	@Test
1072 	void n02_addRuntimeAnnotations_invalidOnClassReturnType_throwsException() {
1073 		// This test covers line 334 - onClass() method with wrong return type
1074 		var invalidAnnotation = new InvalidOnClassAnnotation();
1075 
1076 		assertThrows(BeanRuntimeException.class, () -> {
1077 			AnnotationProvider.create()
1078 				.addRuntimeAnnotations(invalidAnnotation)
1079 				.build();
1080 		});
1081 	}
1082 
1083 	@Test
1084 	void n03_addRuntimeAnnotations_invalidOnReturnType_throwsException() {
1085 		// This test covers line 341 - on() method with wrong return type
1086 		var invalidAnnotation = new InvalidOnAnnotation();
1087 
1088 		assertThrows(BeanRuntimeException.class, () -> {
1089 			AnnotationProvider.create()
1090 				.addRuntimeAnnotations(invalidAnnotation)
1091 				.build();
1092 		});
1093 	}
1094 
1095 	// Runtime annotation that throws an exception when onClass() is invoked
1096 	private static class ThrowingOnClassAnnotation implements TestAnnotation {
1097 		@Override
1098 		public String value() {
1099 			return "throwing";
1100 		}
1101 
1102 		@Override
1103 		public Class<? extends Annotation> annotationType() {
1104 			return TestAnnotation.class;
1105 		}
1106 
1107 		@SuppressWarnings("unused")
1108 		public Class<?>[] onClass() {
1109 			throw new RuntimeException("Test exception from onClass()");
1110 		}
1111 
1112 		@Override
1113 		public boolean equals(Object obj) {
1114 			return obj instanceof TestAnnotation;
1115 		}
1116 
1117 		@Override
1118 		public int hashCode() {
1119 			return 0;
1120 		}
1121 
1122 		@Override
1123 		public String toString() {
1124 			return "@TestAnnotation";
1125 		}
1126 	}
1127 
1128 	@Test
1129 	void n05_addRuntimeAnnotations_throwingOnClass_coversLine349() {
1130 		// This test covers line 349 - exception during method invocation (not BeanRuntimeException)
1131 		var throwingAnnotation = new ThrowingOnClassAnnotation();
1132 
1133 		// The exception from onClass() will be caught and wrapped in BeanRuntimeException
1134 		assertThrows(BeanRuntimeException.class, () -> {
1135 			AnnotationProvider.create()
1136 				.addRuntimeAnnotations(throwingAnnotation)
1137 				.build();
1138 		});
1139 	}
1140 }
1141