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.junit.jupiter.api.Assertions.*;
22  
23  import java.lang.annotation.*;
24  import org.apache.juneau.*;
25  import org.apache.juneau.commons.annotation.*;
26  import org.junit.jupiter.api.*;
27  
28  class AnnotationInfo_Test extends TestBase {
29  
30  	//====================================================================================================
31  	// Test annotations and classes
32  	//====================================================================================================
33  
34  	@Target(TYPE)
35  	@Retention(RUNTIME)
36  	public static @interface TestAnnotation {
37  		String value() default "default";
38  	}
39  
40  	@Target(TYPE)
41  	@Retention(RUNTIME)
42  	public static @interface MultiTypeAnnotation {
43  		String stringValue() default "default";
44  
45  		int intValue() default 0;
46  
47  		boolean boolValue() default true;
48  
49  		long longValue() default 100L;
50  
51  		double doubleValue() default 3.14;
52  
53  		float floatValue() default 2.5f;
54  
55  		Class<?> classValue() default String.class;
56  
57  		String[] stringArray() default { "a", "b" };
58  
59  		Class<?>[] classArray() default { String.class, Integer.class };
60  	}
61  
62  	@Target(TYPE)
63  	@Retention(RUNTIME)
64  	public static @interface RankedAnnotation {
65  		int rank() default 0;
66  	}
67  
68  	@Target(TYPE)
69  	@Retention(RUNTIME)
70  	public static @interface UnrankedAnnotation {
71  		String value() default "";
72  	}
73  
74  	@Target(TYPE)
75  	@Retention(RUNTIME)
76  	public static @interface RankWithWrongReturnTypeAnnotation {
77  		String rank() default "";  // Returns String, not int, should not match
78  	}
79  
80  	@Target(TYPE)
81  	@Retention(RUNTIME)
82  	public static @interface ClassArrayAnnotation {
83  		Class<?>[] classes() default {};
84  	}
85  
86  	@Target(TYPE)
87  	@Retention(RUNTIME)
88  	public static @interface ClassValueAnnotation {
89  		Class<?> value() default String.class;
90  	}
91  
92  	@Target(TYPE)
93  	@Retention(RUNTIME)
94  	@Documented
95  	public static @interface DocumentedAnnotation {}
96  
97  	@Target(TYPE)
98  	@Retention(RUNTIME)
99  	@AnnotationGroup(GroupAnnotation.class)
100 	public static @interface GroupAnnotation {}
101 
102 	@Target(TYPE)
103 	@Retention(RUNTIME)
104 	@AnnotationGroup(GroupAnnotation.class)
105 	public static @interface GroupMember1 {}
106 
107 	@Target(TYPE)
108 	@Retention(RUNTIME)
109 	@AnnotationGroup(GroupAnnotation.class)
110 	public static @interface GroupMember2 {}
111 
112 	@Target(TYPE)
113 	@Retention(RUNTIME)
114 	public static @interface NotInGroup {}
115 
116 	@TestAnnotation("test")
117 	public static class TestClass {}
118 
119 	@MultiTypeAnnotation(stringValue = "test", intValue = 123, boolValue = false, longValue = 999L, doubleValue = 1.23, floatValue = 4.56f, classValue = Integer.class, stringArray = { "x", "y",
120 		"z" }, classArray = { Long.class, Double.class })
121 	public static class MultiTypeClass {}
122 
123 	@RankedAnnotation(rank = 5)
124 	public static class RankedClass {}
125 
126 	@UnrankedAnnotation
127 	public static class UnrankedClass {}
128 
129 	@RankWithWrongReturnTypeAnnotation
130 	public static class RankWithWrongReturnTypeClass {}
131 
132 	@ClassArrayAnnotation(classes = { String.class, Integer.class })
133 	public static class ClassArrayClass {}
134 
135 	@ClassValueAnnotation(Integer.class)
136 	public static class ClassValueClass {}
137 
138 	@DocumentedAnnotation
139 	public static class DocumentedClass {}
140 
141 	@GroupMember1
142 	@GroupMember2
143 	@NotInGroup
144 	public static class GroupTestClass {}
145 
146 	@Target(TYPE)
147 	@Retention(RUNTIME)
148 	public static @interface ToMapTestAnnotation {
149 		String value() default "default";
150 		String[] arrayValue() default {};
151 		String[] nonEmptyArray() default {"a", "b"};
152 		String[] emptyArrayWithNonEmptyDefault() default {"default"};
153 	}
154 
155 	@ToMapTestAnnotation(value = "custom", arrayValue = {}, nonEmptyArray = {"x"}, emptyArrayWithNonEmptyDefault = {})
156 	public static class ToMapTestClass {}
157 
158 	//====================================================================================================
159 	// annotationType()
160 	//====================================================================================================
161 	@Test
162 	void a001_annotationType() {
163 		var ci = ClassInfo.of(TestClass.class);
164 		var ai = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
165 		assertNotNull(ai);
166 		assertEquals(TestAnnotation.class, ai.annotationType());
167 	}
168 
169 	//====================================================================================================
170 	// cast(Class<A>)
171 	//====================================================================================================
172 	@Test
173 	void a002_cast() {
174 		var ci = ClassInfo.of(TestClass.class);
175 		var ai = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
176 		assertNotNull(ai);
177 
178 		// Same type returns this
179 		var casted = ai.cast(TestAnnotation.class);
180 		assertNotNull(casted);
181 		assertSame(ai, casted);
182 
183 		// Different type returns null
184 		var casted2 = ai.cast(Deprecated.class);
185 		assertNull(casted2);
186 	}
187 
188 	//====================================================================================================
189 	// equals(Object)
190 	//====================================================================================================
191 	@Test
192 	void a003_equals() {
193 		var ci = ClassInfo.of(TestClass.class);
194 		var ai1 = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
195 		var ai2 = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
196 
197 		assertNotNull(ai1);
198 		assertNotNull(ai2);
199 		// Same annotation should be equal
200 		assertEquals(ai1, ai2);
201 		assertEquals(ai1.hashCode(), ai2.hashCode());
202 
203 		// Different annotation should not be equal
204 		@Deprecated
205 		class DeprecatedClass {}
206 		var ci2 = ClassInfo.of(DeprecatedClass.class);
207 		var ai3 = ci2.getAnnotations(Deprecated.class).findFirst().orElse(null);
208 		assertNotNull(ai3);
209 		assertNotEquals(ai1, ai3);
210 
211 		// With AnnotationInfo
212 		var ai4 = AnnotationInfo.of(ci, ci.inner().getAnnotation(TestAnnotation.class));
213 		assertEquals(ai1, ai4);
214 
215 		// With Annotation
216 		var annotation = ci.inner().getAnnotation(TestAnnotation.class);
217 		assertEquals(ai1, annotation);
218 	}
219 
220 	//====================================================================================================
221 	// getBoolean(String)
222 	//====================================================================================================
223 	@Test
224 	void a004_getBoolean() {
225 		var ci = ClassInfo.of(MultiTypeClass.class);
226 		var ai = ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
227 		assertNotNull(ai);
228 
229 		assertTrue(ai.getBoolean("boolValue").isPresent());
230 		assertEquals(false, ai.getBoolean("boolValue").get());
231 		assertFalse(ai.getBoolean("nonexistent").isPresent());
232 	}
233 
234 	//====================================================================================================
235 	// getClassArray(String)
236 	//====================================================================================================
237 	@Test
238 	void a005_getClassArray() {
239 		var ci = ClassInfo.of(MultiTypeClass.class);
240 		var ai = ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
241 		assertNotNull(ai);
242 
243 		assertTrue(ai.getClassArray("classArray").isPresent());
244 		var array = ai.getClassArray("classArray").get();
245 		assertNotNull(array);
246 		assertEquals(2, array.length);
247 		assertEquals(Long.class, array[0]);
248 		assertEquals(Double.class, array[1]);
249 		assertFalse(ai.getClassArray("nonexistent").isPresent());
250 	}
251 
252 	//====================================================================================================
253 	// getClassArray(String, Class<T>)
254 	//====================================================================================================
255 	@Test
256 	void a006_getClassArray_typed() {
257 		var ci = ClassInfo.of(ClassArrayClass.class);
258 		var ai = ci.getAnnotations(ClassArrayAnnotation.class).findFirst().orElse(null);
259 		assertNotNull(ai);
260 
261 		// Both String and Integer are assignable to Object
262 		var classes = ai.getClassArray("classes", Object.class);
263 		assertTrue(classes.isPresent());
264 		var array = classes.get();
265 		assertEquals(2, array.length);
266 		assertEquals(String.class, array[0]);
267 		assertEquals(Integer.class, array[1]);
268 
269 		// String and Integer are not assignable to Exception
270 		var classes2 = ai.getClassArray("classes", Exception.class);
271 		assertFalse(classes2.isPresent());
272 	}
273 
274 	//====================================================================================================
275 	// getClassValue(String)
276 	//====================================================================================================
277 	@Test
278 	void a007_getClassValue() {
279 		var ci = ClassInfo.of(MultiTypeClass.class);
280 		var ai = ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
281 		assertNotNull(ai);
282 
283 		assertTrue(ai.getClassValue("classValue").isPresent());
284 		assertEquals(Integer.class, ai.getClassValue("classValue").get());
285 		assertFalse(ai.getClassValue("nonexistent").isPresent());
286 	}
287 
288 	//====================================================================================================
289 	// getClassValue(String, Class<T>)
290 	//====================================================================================================
291 	@Test
292 	void a008_getClassValue_typed() {
293 		var ci = ClassInfo.of(ClassValueClass.class);
294 		var ai = ci.getAnnotations(ClassValueAnnotation.class).findFirst().orElse(null);
295 		assertNotNull(ai);
296 
297 		// Integer is assignable to Number
298 		var numberClass = ai.getClassValue("value", Number.class);
299 		assertTrue(numberClass.isPresent());
300 		assertEquals(Integer.class, numberClass.get());
301 
302 		// Integer is not assignable to Exception
303 		var exceptionClass = ai.getClassValue("value", Exception.class);
304 		assertFalse(exceptionClass.isPresent());
305 	}
306 
307 	//====================================================================================================
308 	// getDouble(String)
309 	//====================================================================================================
310 	@Test
311 	void a009_getDouble() {
312 		var ci = ClassInfo.of(MultiTypeClass.class);
313 		var ai = ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
314 		assertNotNull(ai);
315 
316 		assertTrue(ai.getDouble("doubleValue").isPresent());
317 		assertEquals(1.23, ai.getDouble("doubleValue").get(), 0.001);
318 		assertFalse(ai.getDouble("nonexistent").isPresent());
319 	}
320 
321 	//====================================================================================================
322 	// getFloat(String)
323 	//====================================================================================================
324 	@Test
325 	void a010_getFloat() {
326 		var ci = ClassInfo.of(MultiTypeClass.class);
327 		var ai = ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
328 		assertNotNull(ai);
329 
330 		assertTrue(ai.getFloat("floatValue").isPresent());
331 		assertEquals(4.56f, ai.getFloat("floatValue").get(), 0.001);
332 		assertFalse(ai.getFloat("nonexistent").isPresent());
333 	}
334 
335 	//====================================================================================================
336 	// getInt(String)
337 	//====================================================================================================
338 	@Test
339 	void a011_getInt() {
340 		var ci = ClassInfo.of(MultiTypeClass.class);
341 		var ai = ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
342 		assertNotNull(ai);
343 
344 		assertTrue(ai.getInt("intValue").isPresent());
345 		assertEquals(123, ai.getInt("intValue").get());
346 		assertFalse(ai.getInt("nonexistent").isPresent());
347 	}
348 
349 	//====================================================================================================
350 	// getLong(String)
351 	//====================================================================================================
352 	@Test
353 	void a012_getLong() {
354 		var ci = ClassInfo.of(MultiTypeClass.class);
355 		var ai = ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
356 		assertNotNull(ai);
357 
358 		assertTrue(ai.getLong("longValue").isPresent());
359 		assertEquals(999L, ai.getLong("longValue").get());
360 		assertFalse(ai.getLong("nonexistent").isPresent());
361 	}
362 
363 	//====================================================================================================
364 	// getMethod(String)
365 	//====================================================================================================
366 	@Test
367 	void a013_getMethod() {
368 		var ci = ClassInfo.of(TestClass.class);
369 		var ai = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
370 		assertNotNull(ai);
371 
372 		// Existing method
373 		var method = ai.getMethod("value");
374 		assertTrue(method.isPresent());
375 		assertEquals("value", method.get().getSimpleName());
376 
377 		// Non-existent method
378 		var method2 = ai.getMethod("nonexistent");
379 		assertFalse(method2.isPresent());
380 	}
381 
382 	//====================================================================================================
383 	// getName()
384 	//====================================================================================================
385 	@Test
386 	void a014_getName() {
387 		var ci = ClassInfo.of(TestClass.class);
388 		var ai = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
389 		assertNotNull(ai);
390 		assertEquals("TestAnnotation", ai.getName());
391 	}
392 
393 	//====================================================================================================
394 	// getRank()
395 	//====================================================================================================
396 	@Test
397 	void a015_getRank() {
398 		// With rank method
399 		var ci1 = ClassInfo.of(RankedClass.class);
400 		var ai1 = ci1.getAnnotations(RankedAnnotation.class).findFirst().orElse(null);
401 		assertNotNull(ai1);
402 		assertEquals(5, ai1.getRank());
403 
404 		// Without rank method
405 		var ci2 = ClassInfo.of(UnrankedClass.class);
406 		var ai2 = ci2.getAnnotations(UnrankedAnnotation.class).findFirst().orElse(null);
407 		assertNotNull(ai2);
408 		assertEquals(0, ai2.getRank());
409 
410 		// With rank method but wrong return type (String instead of int)
411 		var ci3 = ClassInfo.of(RankWithWrongReturnTypeClass.class);
412 		var ai3 = ci3.getAnnotations(RankWithWrongReturnTypeAnnotation.class).findFirst().orElse(null);
413 		assertNotNull(ai3);
414 		assertEquals(0, ai3.getRank());
415 	}
416 
417 	//====================================================================================================
418 	// getReturnType(String)
419 	//====================================================================================================
420 	@Test
421 	void a016_getReturnType() {
422 		var ci = ClassInfo.of(MultiTypeClass.class);
423 		var ai = ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
424 		assertNotNull(ai);
425 
426 		assertTrue(ai.getReturnType("stringValue").isPresent());
427 		assertEquals(String.class, ai.getReturnType("stringValue").get().inner());
428 		assertEquals(int.class, ai.getReturnType("intValue").get().inner());
429 		assertEquals(boolean.class, ai.getReturnType("boolValue").get().inner());
430 		assertEquals(long.class, ai.getReturnType("longValue").get().inner());
431 		assertEquals(double.class, ai.getReturnType("doubleValue").get().inner());
432 		assertEquals(float.class, ai.getReturnType("floatValue").get().inner());
433 		assertEquals(Class.class, ai.getReturnType("classValue").get().inner());
434 		assertEquals(String[].class, ai.getReturnType("stringArray").get().inner());
435 		assertEquals(Class[].class, ai.getReturnType("classArray").get().inner());
436 
437 		// Non-existent method
438 		assertFalse(ai.getReturnType("nonexistent").isPresent());
439 	}
440 
441 	//====================================================================================================
442 	// getString(String)
443 	//====================================================================================================
444 	@Test
445 	void a017_getString() {
446 		var ci = ClassInfo.of(MultiTypeClass.class);
447 		var ai = ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
448 		assertNotNull(ai);
449 
450 		assertTrue(ai.getString("stringValue").isPresent());
451 		assertEquals("test", ai.getString("stringValue").get());
452 		assertFalse(ai.getString("nonexistent").isPresent());
453 	}
454 
455 	//====================================================================================================
456 	// getStringArray(String)
457 	//====================================================================================================
458 	@Test
459 	void a018_getStringArray() {
460 		var ci = ClassInfo.of(MultiTypeClass.class);
461 		var ai = ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
462 		assertNotNull(ai);
463 
464 		assertTrue(ai.getStringArray("stringArray").isPresent());
465 		var array = ai.getStringArray("stringArray").get();
466 		assertNotNull(array);
467 		assertEquals(3, array.length);
468 		assertEquals("x", array[0]);
469 		assertEquals("y", array[1]);
470 		assertEquals("z", array[2]);
471 		assertFalse(ai.getStringArray("nonexistent").isPresent());
472 	}
473 
474 	//====================================================================================================
475 	// getValue()
476 	//====================================================================================================
477 	@Test
478 	void a019_getValue() {
479 		var ci = ClassInfo.of(TestClass.class);
480 		var ai = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
481 		assertNotNull(ai);
482 
483 		var value = ai.getValue();
484 		assertTrue(value.isPresent());
485 		assertEquals("test", value.get());
486 	}
487 
488 	//====================================================================================================
489 	// getValue(Class<V>, String)
490 	//====================================================================================================
491 	@Test
492 	void a020_getValue_typed() {
493 		var ci = ClassInfo.of(MultiTypeClass.class);
494 		var ai = ci.getAnnotations(MultiTypeAnnotation.class).findFirst().orElse(null);
495 		assertNotNull(ai);
496 
497 		// String value
498 		var stringValue = ai.getValue(String.class, "stringValue");
499 		assertTrue(stringValue.isPresent());
500 		assertEquals("test", stringValue.get());
501 
502 		// int value (primitive)
503 		var intValue = ai.getValue(int.class, "intValue");
504 		assertTrue(intValue.isPresent());
505 		assertEquals(123, intValue.get());
506 
507 		// Wrong type returns empty
508 		var intValue2 = ai.getValue(Integer.class, "stringValue");
509 		assertFalse(intValue2.isPresent());
510 	}
511 
512 	//====================================================================================================
513 	// hasAnnotation(Class<A>)
514 	//====================================================================================================
515 	@Test
516 	void a021_hasAnnotation() {
517 		var ci = ClassInfo.of(DocumentedClass.class);
518 		var ai = ci.getAnnotations(DocumentedAnnotation.class).findFirst().orElse(null);
519 		assertNotNull(ai);
520 
521 		// Has meta-annotation
522 		assertTrue(ai.hasAnnotation(Documented.class));
523 
524 		// Doesn't have meta-annotation
525 		var ci2 = ClassInfo.of(TestClass.class);
526 		var ai2 = ci2.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
527 		assertNotNull(ai2);
528 		assertFalse(ai2.hasAnnotation(Documented.class));
529 	}
530 
531 	//====================================================================================================
532 	// hasName(String)
533 	//====================================================================================================
534 	@Test
535 	void a022_hasName() {
536 		var ci = ClassInfo.of(TestClass.class);
537 		var ai = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
538 		assertNotNull(ai);
539 
540 		var fullyQualifiedName = TestAnnotation.class.getName();
541 		assertTrue(ai.hasName(fullyQualifiedName));
542 		assertFalse(ai.hasName("TestAnnotation"));
543 	}
544 
545 	//====================================================================================================
546 	// hasSimpleName(String)
547 	//====================================================================================================
548 	@Test
549 	void a023_hasSimpleName() {
550 		var ci = ClassInfo.of(TestClass.class);
551 		var ai = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
552 		assertNotNull(ai);
553 
554 		assertTrue(ai.hasSimpleName("TestAnnotation"));
555 		assertFalse(ai.hasSimpleName(TestAnnotation.class.getName()));
556 	}
557 
558 	//====================================================================================================
559 	// hashCode()
560 	//====================================================================================================
561 	@Test
562 	void a024_hashCode() {
563 		var ci = ClassInfo.of(TestClass.class);
564 		var ai1 = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
565 		var ai2 = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
566 
567 		assertNotNull(ai1);
568 		assertNotNull(ai2);
569 		assertEquals(ai1.hashCode(), ai2.hashCode());
570 	}
571 
572 	//====================================================================================================
573 	// inner()
574 	//====================================================================================================
575 	@Test
576 	void a025_inner() {
577 		var ci = ClassInfo.of(TestClass.class);
578 		var ai = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
579 		assertNotNull(ai);
580 
581 		var annotation = ai.inner();
582 		assertNotNull(annotation);
583 		assertEquals(TestAnnotation.class, annotation.annotationType());
584 		assertEquals("test", annotation.value());
585 	}
586 
587 	//====================================================================================================
588 	// isInGroup(Class<A>)
589 	//====================================================================================================
590 	@Test
591 	void a026_isInGroup() {
592 		var ci = ClassInfo.of(GroupTestClass.class);
593 		var groupMember1 = ci.getAnnotations(GroupMember1.class).findFirst().orElse(null);
594 		var groupMember2 = ci.getAnnotations(GroupMember2.class).findFirst().orElse(null);
595 		var notInGroup = ci.getAnnotations(NotInGroup.class).findFirst().orElse(null);
596 
597 		assertNotNull(groupMember1);
598 		assertNotNull(groupMember2);
599 		assertNotNull(notInGroup);
600 
601 		assertTrue(groupMember1.isInGroup(GroupAnnotation.class));
602 		assertTrue(groupMember2.isInGroup(GroupAnnotation.class));
603 		assertFalse(notInGroup.isInGroup(GroupAnnotation.class));
604 	}
605 
606 	//====================================================================================================
607 	// isType(Class<A>)
608 	//====================================================================================================
609 	@Test
610 	void a027_isType() {
611 		var ci = ClassInfo.of(TestClass.class);
612 		var ai = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
613 		assertNotNull(ai);
614 
615 		assertTrue(ai.isType(TestAnnotation.class));
616 		assertFalse(ai.isType(Deprecated.class));
617 	}
618 
619 	//====================================================================================================
620 	// of(Annotatable, A)
621 	//====================================================================================================
622 	@Test
623 	void a028_of() {
624 		var ci = ClassInfo.of(TestClass.class);
625 		var annotation = ci.inner().getAnnotation(TestAnnotation.class);
626 		var ai = AnnotationInfo.of(ci, annotation);
627 
628 		assertNotNull(ai);
629 		assertEquals(TestAnnotation.class, ai.annotationType());
630 		assertEquals("test", ai.getValue().orElse(null));
631 
632 		// Null annotation should throw
633 		assertThrows(IllegalArgumentException.class, () -> AnnotationInfo.of(ci, null));
634 	}
635 
636 	//====================================================================================================
637 	// properties()
638 	//====================================================================================================
639 	@Test
640 	void a029_properties() {
641 		var ci = ClassInfo.of(TestClass.class);
642 		var ai = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
643 		assertNotNull(ai);
644 
645 		var map = ai.properties();
646 		assertNotNull(map);
647 		assertTrue(map.containsKey("CLASS_TYPE"));
648 		assertTrue(map.containsKey("@TestAnnotation"));
649 
650 		var annotationMap = (java.util.Map<String,Object>)map.get("@TestAnnotation");
651 		assertNotNull(annotationMap);
652 		assertEquals("test", annotationMap.get("value"));
653 
654 		// Test with non-default values
655 		var ci2 = ClassInfo.of(ToMapTestClass.class);
656 		var ai2 = ci2.getAnnotations(ToMapTestAnnotation.class).findFirst().orElse(null);
657 		assertNotNull(ai2);
658 
659 		var map2 = ai2.properties();
660 		assertNotNull(map2);
661 		var annotationMap2 = (java.util.Map<String,Object>)map2.get("@ToMapTestAnnotation");
662 		assertNotNull(annotationMap2);
663 		
664 		// value differs from default (non-array), should be included
665 		assertEquals("custom", annotationMap2.get("value"));
666 		
667 		// nonEmptyArray differs from default (non-empty array), should be included
668 		assertTrue(annotationMap2.containsKey("nonEmptyArray"));
669 		
670 		// emptyArrayWithNonEmptyDefault is empty array but default is non-empty, should be included
671 		assertTrue(annotationMap2.containsKey("emptyArrayWithNonEmptyDefault"));
672 		
673 		// arrayValue is empty array matching default empty array, should NOT be included
674 		assertFalse(annotationMap2.containsKey("arrayValue"));
675 
676 		// Test with exception handling
677 		var annotationType = ToMapTestAnnotation.class;
678 		var handler = new java.lang.reflect.InvocationHandler() {
679 			@Override
680 			public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) throws Throwable {
681 				if (method.getName().equals("value")) {
682 					throw new RuntimeException("Test exception");
683 				}
684 				if (method.getName().equals("annotationType")) {
685 					return annotationType;
686 				}
687 				if (method.getName().equals("toString")) {
688 					return "@ToMapTestAnnotation";
689 				}
690 				if (method.getName().equals("hashCode")) {
691 					return 0;
692 				}
693 				if (method.getName().equals("equals")) {
694 					return false;
695 				}
696 				return method.getDefaultValue();
697 			}
698 		};
699 		
700 		var proxyAnnotation = (ToMapTestAnnotation)java.lang.reflect.Proxy.newProxyInstance(
701 			annotationType.getClassLoader(),
702 			new Class[]{annotationType},
703 			handler
704 		);
705 		
706 		var ci3 = ClassInfo.of(ToMapTestClass.class);
707 		var ai3 = AnnotationInfo.of(ci3, proxyAnnotation);
708 		
709 		var map3 = ai3.properties();
710 		assertNotNull(map3);
711 		var annotationMap3 = (java.util.Map<String,Object>)map3.get("@ToMapTestAnnotation");
712 		assertNotNull(annotationMap3);
713 		
714 		// The exception should be caught and stored as a localized message
715 		assertTrue(annotationMap3.containsKey("value"));
716 		var value = annotationMap3.get("value");
717 		assertNotNull(value);
718 		// The value should be a string representation of the exception
719 		assertTrue(value instanceof String);
720 	}
721 
722 	//====================================================================================================
723 	// toSimpleString()
724 	//====================================================================================================
725 	@Test
726 	void a030_toSimpleString() {
727 		var ci = ClassInfo.of(TestClass.class);
728 		var ai = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
729 		assertNotNull(ai);
730 
731 		var str = ai.toSimpleString();
732 		assertNotNull(str);
733 		assertTrue(str.contains("@TestAnnotation"));
734 		assertTrue(str.contains("on="));
735 	}
736 
737 	//====================================================================================================
738 	// toString()
739 	//====================================================================================================
740 	@Test
741 	void a031_toString() {
742 		var ci = ClassInfo.of(TestClass.class);
743 		var ai = ci.getAnnotations(TestAnnotation.class).findFirst().orElse(null);
744 		assertNotNull(ai);
745 
746 		var str = ai.toString();
747 		assertNotNull(str);
748 		// toString() returns the map representation
749 		assertTrue(str.contains("CLASS_TYPE") || str.contains("@TestAnnotation"));
750 	}
751 }
752