001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.reflect; 018 019import static org.apache.juneau.commons.function.Suppliers.*; 020import static org.apache.juneau.commons.reflect.ClassArrayFormat.*; 021import static org.apache.juneau.commons.reflect.ClassNameFormat.*; 022import static org.apache.juneau.commons.reflect.ReflectionUtils.*; 023import static org.apache.juneau.commons.utils.ClassUtils.*; 024import static org.apache.juneau.commons.utils.CollectionUtils.*; 025import static org.apache.juneau.commons.utils.Shorts.*; 026import static org.apache.juneau.commons.utils.ThrowableUtils.*; 027 028import java.lang.annotation.*; 029import java.util.*; 030import java.util.function.*; 031 032import org.apache.juneau.commons.*; 033import org.apache.juneau.commons.collections.*; 034 035/** 036 * Encapsulates information about an annotation instance and the element it's declared on. 037 * 038 * <p> 039 * This class provides a convenient wrapper around Java annotations that allows you to: 040 * <ul> 041 * <li>Access annotation values in a type-safe manner 042 * <li>Query annotation properties without reflection boilerplate 043 * <li>Track where the annotation was found (class, method, field, etc.) 044 * <li>Sort annotations by precedence using ranks 045 * </ul> 046 * 047 * <h5 class='section'>Example:</h5> 048 * <p class='bjava'> 049 * <jc>// Get annotation info from a class</jc> 050 * ClassInfo <jv>ci</jv> = ClassInfo.<jsm>of</jsm>(MyClass.<jk>class</jk>); 051 * Optional<AnnotationInfo<MyAnnotation>> <jv>ai</jv> = 052 * <jv>ci</jv>.getAnnotations(MyAnnotation.<jk>class</jk>).findFirst(); 053 * 054 * <jc>// Access annotation values</jc> 055 * <jv>ai</jv>.ifPresent(<jv>x</jv> -> { 056 * String <jv>value</jv> = <jv>x</jv>.getValue(String.<jk>class</jk>, <js>"value"</js>).orElse(<js>"default"</js>); 057 * <jk>int</jk> <jv>priority</jv> = <jv>x</jv>.getInt(<js>"priority"</js>).orElse(0); 058 * }); 059 * </p> 060 * 061 * <h5 class='section'>See Also:</h5><ul> 062 * <li class='jc'>{@link ClassInfo} 063 * <li class='jc'>{@link MethodInfo} 064 * <li class='jc'>{@link FieldInfo} 065 * <li class='jc'>{@link ConstructorInfo} 066 * <li class='jc'>{@link ParameterInfo} 067 * <li class='jc'>{@link PackageInfo} 068 * </ul> 069 * 070 * @param <T> The annotation type. 071 */ 072@SuppressWarnings({ 073 "hiding", // Field/variable hiding intentional for specific scoping 074 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 075 "java:S3740", // Raw Annotation type used intentionally for unparameterized annotation introspection 076 "unchecked" // Casts in cast() and the Class/Class[] value accessors follow isInstance/hasReturnType checks on the annotation member. 077}) 078public class AnnotationInfo<T extends Annotation> { 079 080 /** 081 * Creates a new annotation info object. 082 * 083 * <h5 class='section'>Example:</h5> 084 * <p class='bjava'> 085 * <jc>// Create annotation info for a class annotation</jc> 086 * ClassInfo <jv>ci</jv> = ClassInfo.<jsm>of</jsm>(MyClass.<jk>class</jk>); 087 * MyAnnotation <jv>annotation</jv> = <jv>ci</jv>.inner().getAnnotation(MyAnnotation.<jk>class</jk>); 088 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = AnnotationInfo.<jsm>of</jsm>(<jv>ci</jv>, <jv>annotation</jv>); 089 * </p> 090 * 091 * @param <A> The annotation type. 092 * @param on The annotatable object where the annotation was found (class, method, field, constructor, parameter, or package). Must not be <jk>null</jk>. 093 * @param value The annotation instance. Must not be <jk>null</jk>. 094 * @return A new {@link AnnotationInfo} object wrapping the annotation. 095 */ 096 public static <A extends Annotation> AnnotationInfo<A> of(Annotatable on, A value) { 097 return new AnnotationInfo<>(on, value); 098 } 099 100 private static int findRank(Object a) { 101 // @formatter:off 102 return ClassInfo.of(a).getAllMethods().stream() 103 .filter(m -> m.hasName("rank") && m.hasReturnType(int.class)) 104 .findFirst() 105 .map(m -> safe(() -> (int)m.invoke(a))) 106 .orElse(0); 107 // @formatter:on 108 } 109 110 private final Annotatable annotatable; 111 final int rank; 112 113 private final T a; 114 115 private final Supplier<List<MethodInfo>> methods; 116 private final Supplier<String> toString; // String representation with annotation type, location, and values. 117 118 /** 119 * Constructor. 120 * 121 * @param on The annotatable object where the annotation was found. 122 * @param a The annotation instance. 123 */ 124 AnnotationInfo(Annotatable on, T a) { 125 this.annotatable = reqnn("on", on); 126 this.a = reqnn("a", a); 127 this.methods = memoize(() -> stream(a.annotationType().getMethods()).map(m -> MethodInfo.of(info(a.annotationType()), m)).toList()); 128 this.rank = findRank(a); 129 this.toString = memoize(this::findToString); 130 } 131 132 /** 133 * Returns the annotatable object (class, method, field, etc.) where this annotation was found. 134 * 135 * @return The annotatable object where this annotation was found. 136 */ 137 public Annotatable getAnnotatable() { 138 return annotatable; 139 } 140 141 /** 142 * Returns the annotatable object cast to the specified type. 143 * 144 * @param <A> The annotatable type. 145 * @param type The class to cast to. 146 * @return The annotatable element cast to the specified type. 147 * @throws ClassCastException If the annotatable element is not of the specified type. 148 */ 149 public <A extends Annotatable> A getAnnotatable(Class<A> type) { 150 return type.cast(annotatable); 151 } 152 153 /** 154 * Returns the annotation type of this annotation. 155 * 156 * <p> 157 * Same as calling {@link Annotation#annotationType()}. 158 * 159 * <h5 class='section'>Example:</h5> 160 * <p class='bjava'> 161 * AnnotationInfo<Deprecated> <jv>ai</jv> = ClassInfo.<jsm>of</jsm>(MyClass.<jk>class</jk>).getAnnotation(Deprecated.<jk>class</jk>); 162 * Class<? <jk>extends</jk> Annotation> <jv>type</jv> = <jv>ai</jv>.annotationType(); <jc>// Returns Deprecated.class</jc> 163 * </p> 164 * 165 * @return The annotation type of this annotation. 166 * @see Annotation#annotationType() 167 */ 168 public Class<? extends Annotation> annotationType() { 169 return a.annotationType(); 170 } 171 172 /** 173 * Casts this annotation info to a specific annotation type. 174 * 175 * <p> 176 * This is useful when you have an {@code AnnotationInfo<?>} and need to narrow it to a specific type. 177 * 178 * <h5 class='section'>Example:</h5> 179 * <p class='bjava'> 180 * AnnotationInfo<?> <jv>ai</jv> = ...; 181 * 182 * <jc>// Safe cast</jc> 183 * AnnotationInfo<MyAnnotation> <jv>myAi</jv> = <jv>ai</jv>.cast(MyAnnotation.<jk>class</jk>); 184 * <jk>if</jk> (<jv>myAi</jv> != <jk>null</jk>) { 185 * <jc>// Use strongly-typed annotation info</jc> 186 * } 187 * </p> 188 * 189 * @param <A> The annotation type to cast to. 190 * @param type The annotation type to cast to. 191 * @return This annotation info cast to the specified type, or <jk>null</jk> if the annotation is not of the specified type. 192 */ 193 public <A extends Annotation> AnnotationInfo<A> cast(Class<A> type) { 194 return type.isInstance(a) ? (AnnotationInfo<A>)this : null; 195 } 196 197 /** 198 * Returns true if the specified object represents an annotation that is logically equivalent to this one. 199 * 200 * <p> 201 * Same as calling {@link Annotation#equals(Object)} on the wrapped annotation. 202 * 203 * <p> 204 * Two annotations are considered equal if: 205 * <ul> 206 * <li>They are of the same annotation type 207 * <li>All their corresponding member values are equal 208 * </ul> 209 * 210 * @param o The reference object with which to compare. 211 * @return <jk>true</jk> if the specified object is equal to this annotation. 212 * @see Annotation#equals(Object) 213 */ 214 @Override /* Overridden from Object */ 215 public boolean equals(Object o) { 216 if (o instanceof AnnotationInfo o2) 217 return a.equals(o2.a); 218 return a.equals(o); 219 } 220 221 /** 222 * Returns the value of the specified method on this annotation as a boolean. 223 * 224 * <h5 class='section'>Example:</h5> 225 * <p class='bjava'> 226 * <jc>// For annotation: @MyAnnotation(enabled=true)</jc> 227 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 228 * <jk>boolean</jk> <jv>enabled</jv> = <jv>ai</jv>.getBoolean(<js>"enabled"</js>).orElse(<jk>false</jk>); <jc>// Returns true</jc> 229 * </p> 230 * 231 * @param methodName The method name. 232 * @return An {@link Optional} containing the value as a boolean, or empty if not found or not a {@code boolean} type. 233 */ 234 public Optional<Boolean> getBoolean(String methodName) { 235 return getMethod(methodName).filter(x -> x.hasReturnType(boolean.class)).map(x -> (Boolean)x.invoke(a)); 236 } 237 238 /** 239 * Returns the value of the specified method on this annotation as a class array. 240 * 241 * <p> 242 * For type-safe access to an array of classes of a specific supertype, use {@link #getClassArray(String, Class)}. 243 * 244 * <h5 class='section'>Example:</h5> 245 * <p class='bjava'> 246 * <jc>// For annotation: @MyAnnotation(types={String.class, Integer.class})</jc> 247 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 248 * Class<?>[] <jv>types</jv> = <jv>ai</jv>.getClassArray(<js>"types"</js>).orElse(<jk>new</jk> Class[0]); <jc>// Returns [String.class, Integer.class]</jc> 249 * </p> 250 * 251 * @param methodName The method name. 252 * @return An {@link Optional} containing the class array value, or empty if not found or not a {@code Class[]} type. 253 */ 254 public Optional<Class<?>[]> getClassArray(String methodName) { 255 return (Optional<Class<?>[]>)(Optional<?>)getMethod(methodName).filter(x -> x.hasReturnType(Class[].class)).map(x -> x.invoke(a)); 256 } 257 258 /** 259 * Returns the value of the specified method on this annotation as a class array of a specific type. 260 * 261 * <h5 class='section'>Example:</h5> 262 * <p class='bjava'> 263 * <jc>// Get an array of serializer classes from an annotation</jc> 264 * Optional<Class<? <jk>extends</jk> Serializer>[]> <jv>serializerClasses</jv> = 265 * <jv>annotationInfo</jv>.getClassArray(<js>"serializers"</js>, Serializer.<jk>class</jk>); 266 * </p> 267 * 268 * @param <T> The expected supertype of the classes. 269 * @param methodName The method name. 270 * @param type The expected supertype of the class values. 271 * @return An optional containing the value of the specified method cast to the expected type, 272 * or empty if not found, not a class array, or any element is not assignable to the expected type. 273 */ 274 public <T> Optional<Class<? extends T>[]> getClassArray(String methodName, Class<T> type) { 275 // @formatter:off 276 return getMethod(methodName) 277 .filter(x -> x.hasReturnType(Class[].class)) 278 .map(x -> (Class<?>[])x.invoke(a)) 279 .filter(arr -> { 280 for (var c : arr) { 281 if (!type.isAssignableFrom(c)) 282 return false; 283 } 284 return true; 285 }) 286 .map(x -> (Class<? extends T>[])x); 287 // @formatter:on 288 } 289 290 /** 291 * Returns the value of the specified method on this annotation as a class. 292 * 293 * <p> 294 * For type-safe access to a class of a specific supertype, use {@link #getClassValue(String, Class)}. 295 * 296 * <h5 class='section'>Example:</h5> 297 * <p class='bjava'> 298 * <jc>// For annotation: @MyAnnotation(type=String.class)</jc> 299 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 300 * Class<?> <jv>type</jv> = <jv>ai</jv>.getClassValue(<js>"type"</js>).orElse(<jk>null</jk>); <jc>// Returns String.class</jc> 301 * </p> 302 * 303 * @param methodName The method name. 304 * @return An {@link Optional} containing the class value, or empty if not found or not a {@link Class} type. 305 */ 306 public Optional<Class<?>> getClassValue(String methodName) { 307 return (Optional<Class<?>>)(Optional<?>)getMethod(methodName).filter(x -> x.hasReturnType(Class.class)).map(x -> x.invoke(a)); 308 } 309 310 /** 311 * Returns the value of the specified method on this annotation as a class of a specific type. 312 * 313 * <h5 class='section'>Example:</h5> 314 * <p class='bjava'> 315 * <jc>// Get a serializer class from an annotation</jc> 316 * Optional<Class<? <jk>extends</jk> Serializer>> <jv>serializerClass</jv> = 317 * <jv>annotationInfo</jv>.getClassValue(<js>"serializer"</js>, Serializer.<jk>class</jk>); 318 * </p> 319 * 320 * @param <T> The expected supertype of the class. 321 * @param methodName The method name. 322 * @param type The expected supertype of the class value. 323 * @return An optional containing the value of the specified method cast to the expected type, 324 * or empty if not found, not a class, or not assignable to the expected type. 325 */ 326 public <T> Optional<Class<? extends T>> getClassValue(String methodName, Class<T> type) { 327 // @formatter:off 328 return getMethod(methodName) 329 .filter(x -> x.hasReturnType(Class.class)) 330 .map(x -> (Class<?>)x.invoke(a)) 331 .filter(type::isAssignableFrom) 332 .map(x -> (Class<? extends T>)x); 333 // @formatter:on 334 } 335 336 //----------------------------------------------------------------------------------------------------------------- 337 // Annotation interface methods 338 //----------------------------------------------------------------------------------------------------------------- 339 340 /** 341 * Returns the value of the specified method on this annotation as a double. 342 * 343 * <h5 class='section'>Example:</h5> 344 * <p class='bjava'> 345 * <jc>// For annotation: @MyAnnotation(threshold=0.95)</jc> 346 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 347 * <jk>double</jk> <jv>threshold</jv> = <jv>ai</jv>.getDouble(<js>"threshold"</js>).orElse(0.0); <jc>// Returns 0.95</jc> 348 * </p> 349 * 350 * @param methodName The method name. 351 * @return An {@link Optional} containing the value as a double, or empty if not found or not a {@code double} type. 352 */ 353 public Optional<Double> getDouble(String methodName) { 354 return getMethod(methodName).filter(x -> x.hasReturnType(double.class)).map(x -> (Double)x.invoke(a)); 355 } 356 357 /** 358 * Returns the value of the specified method on this annotation as a float. 359 * 360 * <h5 class='section'>Example:</h5> 361 * <p class='bjava'> 362 * <jc>// For annotation: @MyAnnotation(weight=0.5f)</jc> 363 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 364 * <jk>float</jk> <jv>weight</jv> = <jv>ai</jv>.getFloat(<js>"weight"</js>).orElse(0.0f); <jc>// Returns 0.5f</jc> 365 * </p> 366 * 367 * @param methodName The method name. 368 * @return An {@link Optional} containing the value as a float, or empty if not found or not a {@code float} type. 369 */ 370 public Optional<Float> getFloat(String methodName) { 371 return getMethod(methodName).filter(x -> x.hasReturnType(float.class)).map(x -> (Float)x.invoke(a)); 372 } 373 374 /** 375 * Returns the value of the specified method on this annotation as an integer. 376 * 377 * <h5 class='section'>Example:</h5> 378 * <p class='bjava'> 379 * <jc>// For annotation: @MyAnnotation(priority=5)</jc> 380 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 381 * <jk>int</jk> <jv>priority</jv> = <jv>ai</jv>.getInt(<js>"priority"</js>).orElse(0); <jc>// Returns 5</jc> 382 * </p> 383 * 384 * @param methodName The method name. 385 * @return An {@link Optional} containing the value as an integer, or empty if not found or not an {@code int} type. 386 */ 387 public Optional<Integer> getInt(String methodName) { 388 return getMethod(methodName).filter(x -> x.hasReturnType(int.class)).map(x -> (Integer)x.invoke(a)); 389 } 390 391 /** 392 * Returns the value of the specified method on this annotation as a long. 393 * 394 * <h5 class='section'>Example:</h5> 395 * <p class='bjava'> 396 * <jc>// For annotation: @MyAnnotation(timestamp=1234567890L)</jc> 397 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 398 * <jk>long</jk> <jv>timestamp</jv> = <jv>ai</jv>.getLong(<js>"timestamp"</js>).orElse(0L); <jc>// Returns 1234567890L</jc> 399 * </p> 400 * 401 * @param methodName The method name. 402 * @return An {@link Optional} containing the value as a long, or empty if not found or not a {@code long} type. 403 */ 404 public Optional<Long> getLong(String methodName) { 405 return getMethod(methodName).filter(x -> x.hasReturnType(long.class)).map(x -> (Long)x.invoke(a)); 406 } 407 408 /** 409 * Returns the method with the specified name on this annotation. 410 * 411 * <h5 class='section'>Example:</h5> 412 * <p class='bjava'> 413 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 414 * Optional<MethodInfo> <jv>method</jv> = <jv>ai</jv>.getMethod(<js>"value"</js>); 415 * <jv>method</jv>.ifPresent(<jv>m</jv> -> System.<jsf>out</jsf>.println(<jv>m</jv>.getReturnType())); 416 * </p> 417 * 418 * @param methodName The method name to look for. 419 * @return An {@link Optional} containing the method info, or empty if method not found. 420 */ 421 public Optional<MethodInfo> getMethod(String methodName) { 422 return methods.get().stream().filter(x -> eq(methodName, x.getNameSimple())).findFirst(); 423 } 424 425 //----------------------------------------------------------------------------------------------------------------- 426 // Private helper methods 427 //----------------------------------------------------------------------------------------------------------------- 428 429 /** 430 * Returns the class name of this annotation. 431 * 432 * <h5 class='section'>Example:</h5> 433 * <p class='bjava'> 434 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 435 * String <jv>name</jv> = <jv>ai</jv>.getName(); <jc>// Returns "mypackage.MyAnnotation"</jc> 436 * </p> 437 * 438 * @return The simple class name of the annotation (e.g., {@code "Override"} for {@code @Override}). 439 */ 440 public String getName() { return cn(a.annotationType()); } 441 442 /** 443 * Returns the simple class name of this annotation. 444 * 445 * <h5 class='section'>Example:</h5> 446 * <p class='bjava'> 447 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 448 * String <jv>name</jv> = <jv>ai</jv>.getNameSimple(); <jc>// Returns "MyAnnotation"</jc> 449 * </p> 450 * 451 * @return The simple class name of the annotation (e.g., {@code "Override"} for {@code @Override}). 452 * @see #getName() 453 */ 454 public String getNameSimple() { return cns(a.annotationType()); } 455 456 /** 457 * Returns the rank of this annotation for sorting by precedence. 458 * 459 * <p> 460 * The rank is determined by checking if the annotation has a {@code rank()} method that returns an {@code int}. 461 * If found, that value is used; otherwise the rank defaults to {@code 0}. 462 * 463 * <p> 464 * Higher rank values indicate higher precedence when multiple annotations of the same type are present. 465 * 466 * <h5 class='section'>Example:</h5> 467 * <p class='bjava'> 468 * <jc>// Annotation with rank method</jc> 469 * <ja>@interface</ja> MyAnnotation { 470 * <jk>int</jk> rank() <jk>default</jk> 0; 471 * } 472 * 473 * <jc>// Get rank from annotation info</jc> 474 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 475 * <jk>int</jk> <jv>rank</jv> = <jv>ai</jv>.getRank(); <jc>// Returns value from rank() method</jc> 476 * </p> 477 * 478 * @return The rank of this annotation, or {@code 0} if no rank method exists. 479 */ 480 public int getRank() { return rank; } 481 482 /** 483 * Returns the return type of the specified method on this annotation. 484 * 485 * <h5 class='section'>Example:</h5> 486 * <p class='bjava'> 487 * Optional<ClassInfo> <jv>returnType</jv> = <jv>annotationInfo</jv>.getReturnType(<js>"value"</js>); 488 * </p> 489 * 490 * @param methodName The method name. 491 * @return An optional containing the return type of the specified method, or empty if method not found. 492 */ 493 public Optional<ClassInfo> getReturnType(String methodName) { 494 return getMethod(methodName).map(x -> x.getReturnType()); 495 } 496 497 /** 498 * Returns the value of the specified method on this annotation as a string. 499 * 500 * <h5 class='section'>Example:</h5> 501 * <p class='bjava'> 502 * <jc>// For annotation: @MyAnnotation(name="John", age=30)</jc> 503 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 504 * String <jv>name</jv> = <jv>ai</jv>.getString(<js>"name"</js>).orElse(<js>"unknown"</js>); <jc>// Returns "John"</jc> 505 * </p> 506 * 507 * @param methodName The method name. 508 * @return An {@link Optional} containing the value as a string, or empty if not found or not a string type. 509 */ 510 public Optional<String> getString(String methodName) { 511 return getMethod(methodName).filter(x -> x.hasReturnType(String.class)).map(x -> s(x.invoke(a))); 512 } 513 514 /** 515 * Returns the value of the specified method on this annotation as a string array. 516 * 517 * <h5 class='section'>Example:</h5> 518 * <p class='bjava'> 519 * <jc>// For annotation: @MyAnnotation(tags={"foo", "bar"})</jc> 520 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 521 * String[] <jv>tags</jv> = <jv>ai</jv>.getStringArray(<js>"tags"</js>).orElse(<jk>new</jk> String[0]); <jc>// Returns ["foo", "bar"]</jc> 522 * </p> 523 * 524 * @param methodName The method name. 525 * @return An {@link Optional} containing the string array value, or empty if not found or not a {@code String[]} type. 526 */ 527 public Optional<String[]> getStringArray(String methodName) { 528 return getMethod(methodName).filter(x -> x.hasReturnType(String[].class)).map(x -> (String[])x.invoke(a)); 529 } 530 531 /** 532 * Returns the value of the {@code value()} method on this annotation as a string. 533 * 534 * <p> 535 * This is a convenience method equivalent to calling {@link #getString(String) getString("value")}. 536 * 537 * <h5 class='section'>Example:</h5> 538 * <p class='bjava'> 539 * <jc>// For annotation: @MyAnnotation("foo")</jc> 540 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 541 * String <jv>value</jv> = <jv>ai</jv>.getValue().orElse(<js>"default"</js>); <jc>// Returns "foo"</jc> 542 * </p> 543 * 544 * @return An {@link Optional} containing the value of the {@code value()} method, or empty if not found or not a string. 545 */ 546 public Optional<String> getValue() { return getString("value"); } 547 548 /** 549 * Returns the value of a specific annotation method. 550 * 551 * <p> 552 * This method provides type-safe access to annotation field values without requiring 553 * explicit reflection calls or casting. 554 * 555 * <h5 class='section'>Example:</h5> 556 * <p class='bjava'> 557 * <jc>// For annotation: @interface MyAnnotation { String value(); int priority(); }</jc> 558 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 559 * 560 * <jc>// Get string value</jc> 561 * Optional<String> <jv>value</jv> = <jv>ai</jv>.getValue(String.<jk>class</jk>, <js>"value"</js>); 562 * 563 * <jc>// Get int value</jc> 564 * Optional<Integer> <jv>priority</jv> = <jv>ai</jv>.getValue(Integer.<jk>class</jk>, <js>"priority"</js>); 565 * </p> 566 * 567 * @param <V> The expected type of the annotation field value. 568 * @param type The expected class of the annotation field value. 569 * @param name The name of the annotation method (field). 570 * @return An {@link Optional} containing the value if found and type matches, empty otherwise. 571 */ 572 public <V> Optional<V> getValue(Class<V> type, String name) { 573 // @formatter:off 574 return methods.get().stream() 575 .filter(m -> eq(m.getName(), name) && eq(m.getReturnType().inner(), type)) 576 .map(m -> safe(() -> (V)m.invoke(a))) 577 .findFirst(); 578 // @formatter:on 579 } 580 581 /** 582 * Returns <jk>true</jk> if this annotation is itself annotated with the specified annotation. 583 * 584 * <p> 585 * This checks for meta-annotations on the annotation type. 586 * 587 * <h5 class='section'>Example:</h5> 588 * <p class='bjava'> 589 * <jc>// Check if @MyAnnotation is annotated with @Documented</jc> 590 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 591 * <jk>boolean</jk> <jv>isDocumented</jv> = <jv>ai</jv>.hasAnnotation(Documented.<jk>class</jk>); 592 * </p> 593 * 594 * @param <A> The meta-annotation type. 595 * @param type The meta-annotation to test for. 596 * @return <jk>true</jk> if this annotation is annotated with the specified annotation. 597 */ 598 public <A extends Annotation> boolean hasAnnotation(Class<A> type) { 599 return nn(this.a.annotationType().getAnnotation(type)); 600 } 601 602 /** 603 * Returns the hash code of this annotation. 604 * 605 * <p> 606 * Same as calling {@link Annotation#hashCode()} on the wrapped annotation. 607 * 608 * <p> 609 * The hash code of an annotation is the sum of the hash codes of its members (including those with default values). 610 * 611 * @return The hash code of this annotation. 612 * @see Annotation#hashCode() 613 */ 614 @Override /* Overridden from Object */ 615 public int hashCode() { 616 return a.hashCode(); 617 } 618 619 /** 620 * Returns <jk>true</jk> if this annotation has the specified fully-qualified name. 621 * 622 * <h5 class='section'>Example:</h5> 623 * <p class='bjava'> 624 * <jk>boolean</jk> <jv>isName</jv> = <jv>annotationInfo</jv>.hasName(<js>"org.apache.juneau.commons.bean.Name"</js>); 625 * </p> 626 * 627 * @param value The fully-qualified name to check. 628 * @return <jk>true</jk> if this annotation has the specified fully-qualified name. 629 */ 630 public boolean hasName(String value) { 631 return eq(value, getName()); 632 } 633 634 /** 635 * Returns <jk>true</jk> if this annotation has the specified simple name. 636 * 637 * <h5 class='section'>Example:</h5> 638 * <p class='bjava'> 639 * <jk>boolean</jk> <jv>isName</jv> = <jv>annotationInfo</jv>.hasSimpleName(<js>"Name"</js>); 640 * </p> 641 * 642 * @param value The simple name to check. 643 * @return <jk>true</jk> if this annotation has the specified simple name. 644 */ 645 public boolean hasNameSimple(String value) { 646 return eq(value, getNameSimple()); 647 } 648 649 /** 650 * Returns the wrapped annotation instance. 651 * 652 * <h5 class='section'>Example:</h5> 653 * <p class='bjava'> 654 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 655 * MyAnnotation <jv>annotation</jv> = <jv>ai</jv>.inner(); 656 * 657 * <jc>// Access annotation methods directly</jc> 658 * String <jv>value</jv> = <jv>annotation</jv>.value(); 659 * </p> 660 * 661 * @return The wrapped annotation instance. 662 */ 663 public T inner() { 664 return a; 665 } 666 667 /** 668 * Returns <jk>true</jk> if this annotation is in the specified {@link AnnotationGroup}. 669 * 670 * <p> 671 * Annotation groups are used to logically group related annotations together. 672 * This checks if the annotation is annotated with {@link AnnotationGroup} and if 673 * the group value matches the specified type. 674 * 675 * <h5 class='section'>Example:</h5> 676 * <p class='bjava'> 677 * <jc>// Define an annotation group</jc> 678 * <ja>@interface</ja> MyGroup {} 679 * 680 * <jc>// Annotation in the group</jc> 681 * <ja>@AnnotationGroup</ja>(MyGroup.<jk>class</jk>) 682 * <ja>@interface</ja> MyAnnotation {} 683 * 684 * <jc>// Check if annotation is in group</jc> 685 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 686 * <jk>boolean</jk> <jv>inGroup</jv> = <jv>ai</jv>.isInGroup(MyGroup.<jk>class</jk>); <jc>// Returns true</jc> 687 * </p> 688 * 689 * @param <A> The group annotation type. 690 * @param group The group annotation class to test for. 691 * @return <jk>true</jk> if this annotation is in the specified group. 692 * @see AnnotationGroup 693 */ 694 public <A extends Annotation> boolean isInGroup(Class<A> group) { 695 var x = a.annotationType().getAnnotation(AnnotationGroup.class); 696 return (nn(x) && x.value().equals(group)); 697 } 698 699 /** 700 * Returns <jk>true</jk> if this annotation is of the specified type. 701 * 702 * <h5 class='section'>Example:</h5> 703 * <p class='bjava'> 704 * AnnotationInfo<?> <jv>ai</jv> = ...; 705 * 706 * <jk>if</jk> (<jv>ai</jv>.isType(MyAnnotation.<jk>class</jk>)) { 707 * <jc>// Handle MyAnnotation specifically</jc> 708 * } 709 * </p> 710 * 711 * @param <A> The annotation type to test for. 712 * @param type The annotation type to test against. 713 * @return <jk>true</jk> if this annotation's type is exactly the specified type. 714 */ 715 public <A extends Annotation> boolean isType(Class<A> type) { 716 return this.a.annotationType() == type; 717 } 718 719 /** 720 * Converts this annotation info to a map representation for debugging purposes. 721 * 722 * <p> 723 * The returned map contains: 724 * <ul> 725 * <li>The annotatable element's type and label (e.g., {@code "CLASS_TYPE" -> "com.example.MyClass"}) 726 * <li>A nested map with the annotation's simple name as key and its non-default values 727 * </ul> 728 * 729 * <p> 730 * Only annotation values that differ from their default values are included. 731 * 732 * <h5 class='section'>Example:</h5> 733 * <p class='bjava'> 734 * AnnotationInfo<MyAnnotation> <jv>ai</jv> = ...; 735 * LinkedHashMap<String,Object> <jv>map</jv> = <jv>ai</jv>.toMap(); 736 * <jc>// Returns: {"CLASS_TYPE": "MyClass", "@MyAnnotation": {"value": "foo", "priority": 5}}</jc> 737 * </p> 738 * 739 * @return A new map showing the attributes of this annotation info. 740 */ 741 public FluentMap<String,Object> properties() { 742 // @formatter:off 743 var ca = info(a.annotationType()); 744 var ja = mapBuilder().sorted().buildFluent(); // NOAI 745 ca.getDeclaredMethods().forEach(x -> safeOptCatch(() -> { 746 var val = x.invoke(a); 747 var d = x.inner().getDefaultValue(); 748 // Add values only if they're different from the default. 749 if (neq(val, d) && ! (isArray(val) && length(val) == 0 && isArray(d) && length(d) == 0)) 750 return val; 751 return null; 752 }, e -> localizedMessage(e)).ifPresent(v -> ja.a(x.getName(), v))); 753 return filteredBeanPropertyMap() 754 .a(s(annotatable.getAnnotatableType()), annotatable.getLabel()) 755 .a("@" + ca.getNameSimple(), ja); 756 // @formatter:on 757 } 758 759 /** 760 * Returns a simple string representation of this annotation showing the annotation type and location. 761 * 762 * <p> 763 * Format: {@code @AnnotationName(on=location)} 764 * 765 * <h5 class='section'>Examples:</h5> 766 * <ul> 767 * <li>{@code @Marshalled(on=MyClass)} - Annotation on a class 768 * <li>{@code @Inject(on=MyClass.myField)} - Annotation on a field 769 * <li>{@code @PackageAnnotation(on=my.package)} - Annotation on a package 770 * </ul> 771 * 772 * @return A simple string representation of this annotation. 773 */ 774 public String toSimpleString() { 775 return "@" + cns(a.annotationType()) + "(on=" + annotatable.getLabel() + ")"; 776 } 777 778 /** 779 * Returns a detailed string representation of this annotation. 780 * 781 * <p> 782 * The returned string includes: 783 * <ul> 784 * <li>Annotation type (fully qualified name with @ prefix) 785 * <li>Location where the annotation is declared (on=...) 786 * <li>Annotation values (non-default values only, in key=value format) 787 * </ul> 788 * 789 * <h5 class='section'>Examples:</h5> 790 * <p class='bjava'> 791 * <jc>// Simple annotation on a class</jc> 792 * AnnotationInfo <jv>ai</jv> = ...; 793 * <jv>ai</jv>.toString(); 794 * <jc>// Returns: "@java.lang.Deprecated(on=org.example.MyClass)"</jc> 795 * 796 * <jc>// Annotation with value</jc> 797 * <jc>// Returns: "@org.example.MyAnnotation(value=foo, on=org.example.MyClass)"</jc> 798 * 799 * <jc>// Annotation on a method</jc> 800 * <jc>// Returns: "@org.example.RestGet(path=/api, on=org.example.MyClass.myMethod)"</jc> 801 * 802 * <jc>// Annotation on a field</jc> 803 * <jc>// Returns: "@org.example.Inject(name=myBean, on=org.example.MyClass.myField)"</jc> 804 * </p> 805 * 806 * <h5 class='section'>Comparison with Other Methods:</h5> 807 * <ul class='spaced-list'> 808 * <li>{@link #toSimpleString()} - Returns simple format: "@AnnotationName(on=location)" 809 * <li>{@link #properties()} - Returns map representation for debugging 810 * </ul> 811 * 812 * @return A detailed string representation including annotation type, location, and values. 813 */ 814 @Override /* Overridden from Object */ 815 public String toString() { 816 return toString.get(); 817 } 818 819 private String findToString() { 820 var sb = new StringBuilder(256); 821 822 // Annotation type (with @ prefix and full package name) 823 sb.append("@"); 824 var annotationType = a.annotationType(); 825 ClassInfo.of(annotationType).appendNameFormatted(sb, FULL, true, '$', BRACKETS); 826 827 // Get annotation values (non-default only) 828 var ca = info(annotationType); 829 var values = new ArrayList<String>(); 830 ca.getDeclaredMethods().forEach(m -> safeOptCatch(() -> { 831 var val = m.invoke(a); 832 var d = m.inner().getDefaultValue(); 833 // Add values only if they're different from the default 834 if (neq(val, d) && ! (isArray(val) && length(val) == 0 && isArray(d) && length(d) == 0)) { 835 var valueStr = formatAnnotationValue(val); 836 values.add(m.getName() + "=" + valueStr); 837 } 838 return null; 839 }, e -> null)); 840 841 // Build the string: @AnnotationType(key1=value1, key2=value2, on=location) 842 if (! values.isEmpty()) { 843 sb.append("("); 844 sb.append(String.join(", ", values)); 845 sb.append(", on="); 846 } else { 847 sb.append("(on="); 848 } 849 sb.append(annotatable.getLabel()); 850 sb.append(")"); 851 852 return sb.toString(); 853 } 854 855 private String formatAnnotationValue(Object value) { 856 if (value == null) 857 return "null"; 858 if (value instanceof String) 859 return "\"" + value + "\""; 860 if (isArray(value)) { 861 var array = toList(value, Object.class); 862 if (array.isEmpty()) 863 return "{}"; 864 var elements = array.stream() 865 .map(this::formatAnnotationValue) 866 .collect(java.util.stream.Collectors.joining(", ")); 867 return "{" + elements + "}"; 868 } 869 if (value instanceof Class<?> value2) { 870 var sb = new StringBuilder(); 871 ClassInfo.of(value2).appendNameFormatted(sb, FULL, true, '$', BRACKETS); 872 return sb.toString() + ".class"; 873 } 874 if (value instanceof Enum<?> value3) { 875 return value3.getDeclaringClass().getSimpleName() + "." + value3.name(); 876 } 877 if (value instanceof Annotation value2) { 878 // For nested annotations, use simple format 879 return "@" + ClassInfo.of(value2.annotationType()).getNameSimple() + "(...)"; 880 } 881 return String.valueOf(value); 882 } 883}