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.reflect.ClassArrayFormat.*; 020import static org.apache.juneau.commons.reflect.ClassNameFormat.*; 021import static org.apache.juneau.commons.utils.AssertionUtils.*; 022import static org.apache.juneau.commons.utils.CollectionUtils.*; 023import static org.apache.juneau.commons.utils.ThrowableUtils.*; 024import static org.apache.juneau.commons.utils.Utils.*; 025 026import java.lang.annotation.*; 027import java.lang.reflect.*; 028import java.util.*; 029import java.util.function.*; 030import java.util.stream.*; 031 032import org.apache.juneau.commons.function.*; 033import org.apache.juneau.commons.utils.*; 034 035/** 036 * Lightweight utility class for introspecting information about a Java field. 037 * 038 * <p> 039 * This class provides a convenient wrapper around {@link Field} that extends the standard Java reflection 040 * API with additional functionality for field introspection, annotation handling, and value access. 041 * It extends {@link AccessibleInfo} to provide {@link AccessibleObject} functionality for accessing 042 * private fields. 043 * 044 * <h5 class='section'>Features:</h5> 045 * <ul class='spaced-list'> 046 * <li>Field introspection - access field metadata, type, modifiers 047 * <li>Annotation support - get annotations declared on the field 048 * <li>Value access - get and set field values with type safety 049 * <li>Accessibility control - make private fields accessible 050 * <li>Thread-safe - instances are immutable and safe for concurrent access 051 * </ul> 052 * 053 * <h5 class='section'>Use Cases:</h5> 054 * <ul class='spaced-list'> 055 * <li>Introspecting field metadata for code generation or analysis 056 * <li>Accessing field values in beans or data objects 057 * <li>Finding annotations on fields 058 * <li>Working with field types and modifiers 059 * <li>Building frameworks that need to analyze or manipulate field values 060 * </ul> 061 * 062 * <h5 class='section'>Usage:</h5> 063 * <p class='bjava'> 064 * <jc>// Get FieldInfo from a class</jc> 065 * ClassInfo <jv>ci</jv> = ClassInfo.<jsm>of</jsm>(MyClass.<jk>class</jk>); 066 * FieldInfo <jv>field</jv> = <jv>ci</jv>.getField(<js>"myField"</js>); 067 * 068 * <jc>// Get field type</jc> 069 * ClassInfo <jv>type</jv> = <jv>field</jv>.getType(); 070 * 071 * <jc>// Get annotations</jc> 072 * List<AnnotationInfo<MyAnnotation>> <jv>annotations</jv> = 073 * <jv>field</jv>.getAnnotations(MyAnnotation.<jk>class</jk>).toList(); 074 * 075 * <jc>// Access field value</jc> 076 * MyClass <jv>obj</jv> = <jk>new</jk> MyClass(); 077 * <jv>field</jv>.accessible(); <jc>// Make accessible if private</jc> 078 * Object <jv>value</jv> = <jv>field</jv>.get(<jv>obj</jv>); 079 * </p> 080 * 081 * <h5 class='section'>See Also:</h5><ul> 082 * <li class='jc'>{@link ClassInfo} - Class introspection 083 * <li class='jc'>{@link MethodInfo} - Method introspection 084 * <li class='jc'>{@link ConstructorInfo} - Constructor introspection 085 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauCommonsReflection">Reflection Package</a> 086 * </ul> 087 */ 088public class FieldInfo extends AccessibleInfo implements Comparable<FieldInfo>, Annotatable { 089 /** 090 * Creates a FieldInfo wrapper for the specified field. 091 * 092 * <h5 class='section'>Example:</h5> 093 * <p class='bjava'> 094 * ClassInfo <jv>ci</jv> = ClassInfo.<jsm>of</jsm>(MyClass.<jk>class</jk>); 095 * Field <jv>f</jv> = MyClass.<jk>class</jk>.getField(<js>"myField"</js>); 096 * FieldInfo <jv>fi</jv> = FieldInfo.<jsm>of</jsm>(<jv>ci</jv>, <jv>f</jv>); 097 * </p> 098 * 099 * @param declaringClass The ClassInfo for the class that declares this field. Must not be <jk>null</jk>. 100 * @param inner The field being wrapped. Must not be <jk>null</jk>. 101 * @return A new FieldInfo object wrapping the field. 102 */ 103 public static FieldInfo of(ClassInfo declaringClass, Field inner) { 104 assertArgNotNull("declaringClass", declaringClass); 105 return declaringClass.getField(inner); 106 } 107 108 /** 109 * Creates a FieldInfo wrapper for the specified field. 110 * 111 * <p> 112 * This convenience method automatically determines the declaring class from the field. 113 * 114 * <h5 class='section'>Example:</h5> 115 * <p class='bjava'> 116 * Field <jv>f</jv> = MyClass.<jk>class</jk>.getField(<js>"myField"</js>); 117 * FieldInfo <jv>fi</jv> = FieldInfo.<jsm>of</jsm>(<jv>f</jv>); 118 * </p> 119 * 120 * @param inner The field being wrapped. Must not be <jk>null</jk>. 121 * @return A new FieldInfo object wrapping the field. 122 */ 123 public static FieldInfo of(Field inner) { 124 assertArgNotNull("inner", inner); 125 return ClassInfo.of(inner.getDeclaringClass()).getField(inner); 126 } 127 128 private final Field inner; 129 private final ClassInfo declaringClass; 130 private final Supplier<ClassInfo> type; 131 private final Supplier<List<AnnotationInfo<Annotation>>> annotations; // All annotations declared directly on this field. 132 private final Supplier<String> fullName; // Fully qualified field name (declaring-class.field-name). 133 134 /** 135 * Constructor. 136 * 137 * <p> 138 * Creates a new FieldInfo wrapper for the specified field. This constructor is protected 139 * and should not be called directly. Use the static factory methods {@link #of(Field)} or 140 * obtain FieldInfo instances from {@link ClassInfo#getField(Field)}. 141 * 142 * @param declaringClass The ClassInfo for the class that declares this field. 143 * @param inner The field being wrapped. 144 */ 145 protected FieldInfo(ClassInfo declaringClass, Field inner) { 146 super(inner, inner.getModifiers()); 147 assertArgNotNull("inner", inner); 148 this.declaringClass = declaringClass; 149 this.inner = inner; 150 this.type = mem(() -> ClassInfo.of(inner.getType(), inner.getGenericType())); 151 this.annotations = mem(() -> stream(inner.getAnnotations()).flatMap(a -> AnnotationUtils.streamRepeated(a)).map(a -> ai(this, a)).toList()); 152 this.fullName = mem(this::findFullName); 153 } 154 155 /** 156 * Attempts to call <code>x.setAccessible(<jk>true</jk>)</code> and quietly ignores security exceptions. 157 * 158 * @return This object. 159 */ 160 public FieldInfo accessible() { 161 setAccessible(); 162 return this; 163 } 164 165 @Override 166 public int compareTo(FieldInfo o) { 167 return cmp(getName(), o.getName()); 168 } 169 170 /** 171 * Returns the field value on the specified object. 172 * 173 * @param o The object containing the field. 174 * @param <T> The object type to retrieve. 175 * @return The field value. 176 * @throws BeanRuntimeException Field was not accessible or field does not belong to object. 177 */ 178 @SuppressWarnings("unchecked") 179 public <T> T get(Object o) throws BeanRuntimeException { 180 return safe(() -> { 181 inner.setAccessible(true); 182 return (T)inner.get(o); 183 }, e -> bex(e)); 184 } 185 186 @Override /* Annotatable */ 187 public AnnotatableType getAnnotatableType() { return AnnotatableType.FIELD_TYPE; } 188 189 /** 190 * Returns an {@link AnnotatedType} object that represents the use of a type to specify the declared type of the field. 191 * 192 * <p> 193 * Same as calling {@link Field#getAnnotatedType()}. 194 * 195 * <h5 class='section'>Example:</h5> 196 * <p class='bjava'> 197 * <jc>// Get annotated type: @NotNull String name</jc> 198 * FieldInfo <jv>fi</jv> = ClassInfo.<jsm>of</jsm>(MyClass.<jk>class</jk>).getField(<js>"name"</js>); 199 * AnnotatedType <jv>aType</jv> = <jv>fi</jv>.getAnnotatedType(); 200 * <jc>// Check for @NotNull on the type</jc> 201 * </p> 202 * 203 * @return An {@link AnnotatedType} object representing the declared type. 204 * @see Field#getAnnotatedType() 205 */ 206 public AnnotatedType getAnnotatedType() { return inner.getAnnotatedType(); } 207 208 /** 209 * Returns all annotations declared on this field. 210 * 211 * <p> 212 * <b>Note on Repeatable Annotations:</b> 213 * Repeatable annotations (those marked with {@link java.lang.annotation.Repeatable @Repeatable}) are automatically 214 * expanded into their individual annotation instances. For example, if a field has multiple {@code @Bean} annotations, 215 * this method returns each {@code @Bean} annotation separately, rather than the container annotation. 216 * 217 * @return 218 * An unmodifiable list of all annotations declared on this field. 219 * <br>Repeatable annotations are expanded into individual instances. 220 */ 221 public List<AnnotationInfo<Annotation>> getAnnotations() { return annotations.get(); } 222 223 /** 224 * Returns all annotations of the specified type declared on this field. 225 * 226 * @param <A> The annotation type. 227 * @param type The annotation type. 228 * @return A stream of all matching annotations. 229 */ 230 @SuppressWarnings("unchecked") 231 public <A extends Annotation> Stream<AnnotationInfo<A>> getAnnotations(Class<A> type) { 232 return annotations.get().stream().filter(x -> type.isInstance(x.inner())).map(x -> (AnnotationInfo<A>)x); 233 } 234 235 /** 236 * Returns metadata about the declaring class. 237 * 238 * @return Metadata about the declaring class. 239 */ 240 public ClassInfo getDeclaringClass() { return declaringClass; } 241 242 /** 243 * Returns the type of this field. 244 * 245 * @return The type of this field. 246 */ 247 public ClassInfo getFieldType() { return type.get(); } 248 249 /** 250 * Returns the full name of this field. 251 * 252 * <h5 class='section'>Examples:</h5> 253 * <ul> 254 * <li><js>"com.foo.MyClass.myField"</js> - Method. 255 * </ul> 256 * 257 * @return The underlying executable name. 258 */ 259 public String getFullName() { return fullName.get(); } 260 261 @Override /* Annotatable */ 262 public String getLabel() { return getDeclaringClass().getNameSimple() + "." + getName(); } 263 264 /** 265 * Returns the name of this field. 266 * 267 * @return The name of this field. 268 */ 269 public String getName() { return inner.getName(); } 270 271 /** 272 * Returns <jk>true</jk> if the specified annotation is present. 273 * 274 * @param <A> The annotation type to look for. 275 * @param type The annotation to look for. 276 * @return <jk>true</jk> if the specified annotation is present. 277 */ 278 public <A extends Annotation> boolean hasAnnotation(Class<A> type) { 279 return getAnnotations(type).findAny().isPresent(); 280 } 281 282 /** 283 * Returns <jk>true</jk> if the field has the specified name. 284 * 285 * @param name The name to compare against. 286 * @return <jk>true</jk> if the field has the specified name. 287 */ 288 public boolean hasName(String name) { 289 return inner.getName().equals(name); 290 } 291 292 /** 293 * Returns the wrapped field. 294 * 295 * @return The wrapped field. 296 */ 297 public Field inner() { 298 return inner; 299 } 300 301 /** 302 * Compares this FieldInfo with the specified object for equality. 303 * 304 * <p> 305 * Two FieldInfo objects are considered equal if they wrap the same underlying {@link Field} object. 306 * This delegates to the underlying {@link Field#equals(Object)} method. 307 * 308 * <p> 309 * This method makes FieldInfo suitable for use as keys in hash-based collections such as {@link HashMap} 310 * and {@link HashSet}. 311 * 312 * @param obj The object to compare with. 313 * @return <jk>true</jk> if the objects are equal, <jk>false</jk> otherwise. 314 */ 315 @Override 316 public boolean equals(Object obj) { 317 return obj instanceof FieldInfo other && eq(this, other, (x, y) -> eq(x.inner, y.inner)); 318 } 319 320 /** 321 * Returns a hash code value for this FieldInfo. 322 * 323 * <p> 324 * This delegates to the underlying {@link Field#hashCode()} method. 325 * 326 * <p> 327 * This method makes FieldInfo suitable for use as keys in hash-based collections such as {@link HashMap} 328 * and {@link HashSet}. 329 * 330 * @return A hash code value for this FieldInfo. 331 */ 332 @Override 333 public int hashCode() { 334 return inner.hashCode(); 335 } 336 337 /** 338 * Returns <jk>true</jk> if all specified flags are applicable to this field. 339 * 340 * @param flag The flag to test for. 341 * @return <jk>true</jk> if all specified flags are applicable to this field. 342 */ 343 @Override 344 public boolean is(ElementFlag flag) { 345 return switch (flag) { 346 case DEPRECATED -> isDeprecated(); 347 case NOT_DEPRECATED -> isNotDeprecated(); 348 case ENUM_CONSTANT -> isEnumConstant(); 349 case NOT_ENUM_CONSTANT -> ! isEnumConstant(); 350 case SYNTHETIC -> isSynthetic(); 351 case NOT_SYNTHETIC -> ! isSynthetic(); // HTT 352 default -> super.is(flag); 353 }; 354 } 355 356 /** 357 * Returns <jk>true</jk> if this field has the {@link Deprecated @Deprecated} annotation on it. 358 * 359 * @return <jk>true</jk> if this field has the {@link Deprecated @Deprecated} annotation on it. 360 */ 361 public boolean isDeprecated() { return inner.isAnnotationPresent(Deprecated.class); } 362 363 /** 364 * Returns <jk>true</jk> if this field represents an element of an enumerated type. 365 * 366 * <p> 367 * Same as calling {@link Field#isEnumConstant()}. 368 * 369 * <h5 class='section'>Example:</h5> 370 * <p class='bjava'> 371 * <jc>// Check if field is an enum constant</jc> 372 * FieldInfo <jv>fi</jv> = ClassInfo.<jsm>of</jsm>(MyEnum.<jk>class</jk>).getField(<js>"VALUE1"</js>); 373 * <jk>if</jk> (<jv>fi</jv>.isEnumConstant()) { 374 * <jc>// Handle enum constant</jc> 375 * } 376 * </p> 377 * 378 * @return <jk>true</jk> if this field represents an enum constant. 379 * @see Field#isEnumConstant() 380 */ 381 public boolean isEnumConstant() { return inner.isEnumConstant(); } 382 383 /** 384 * Returns <jk>true</jk> if this field doesn't have the {@link Deprecated @Deprecated} annotation on it. 385 * 386 * @return <jk>true</jk> if this field doesn't have the {@link Deprecated @Deprecated} annotation on it. 387 */ 388 public boolean isNotDeprecated() { return ! inner.isAnnotationPresent(Deprecated.class); } 389 390 /** 391 * Returns <jk>true</jk> if this field is a synthetic field as defined by the Java Language Specification. 392 * 393 * <p> 394 * Same as calling {@link Field#isSynthetic()}. 395 * 396 * <h5 class='section'>Example:</h5> 397 * <p class='bjava'> 398 * <jc>// Filter out compiler-generated fields</jc> 399 * FieldInfo <jv>fi</jv> = ...; 400 * <jk>if</jk> (! <jv>fi</jv>.isSynthetic()) { 401 * <jc>// Process real field</jc> 402 * } 403 * </p> 404 * 405 * @return <jk>true</jk> if this field is a synthetic field. 406 * @see Field#isSynthetic() 407 */ 408 public boolean isSynthetic() { return inner.isSynthetic(); } 409 410 /** 411 * Identifies if the specified visibility matches this field. 412 * 413 * @param v The visibility to validate against. 414 * @return <jk>true</jk> if this visibility matches the modifier attribute of this field. 415 */ 416 public boolean isVisible(Visibility v) { 417 return v.isVisible(inner); 418 } 419 420 /** 421 * Sets the field value on the specified object. 422 * 423 * @param o The object containing the field. 424 * @param value The new field value. 425 * @throws BeanRuntimeException Field was not accessible or field does not belong to object. 426 */ 427 public void set(Object o, Object value) throws BeanRuntimeException { 428 safe((Snippet)() -> { 429 inner.setAccessible(true); 430 inner.set(o, value); 431 }, e -> bex(e)); 432 } 433 434 //----------------------------------------------------------------------------------------------------------------- 435 // Field-Specific Methods 436 //----------------------------------------------------------------------------------------------------------------- 437 438 /** 439 * Sets the field value on the specified object if the value is <jk>null</jk>. 440 * 441 * @param o The object containing the field. 442 * @param value The new field value. 443 * @throws BeanRuntimeException Field was not accessible or field does not belong to object. 444 */ 445 public void setIfNull(Object o, Object value) { 446 Object v = get(o); 447 if (v == null) 448 set(o, value); 449 } 450 451 /** 452 * Returns a string describing this field, including its generic type. 453 * 454 * <p> 455 * Same as calling {@link Field#toGenericString()}. 456 * 457 * <h5 class='section'>Example:</h5> 458 * <p class='bjava'> 459 * <jc>// Get generic string for: public static final List<String> VALUES</jc> 460 * FieldInfo <jv>fi</jv> = ClassInfo.<jsm>of</jsm>(MyClass.<jk>class</jk>).getField(<js>"VALUES"</js>); 461 * String <jv>str</jv> = <jv>fi</jv>.toGenericString(); 462 * <jc>// Returns: "public static final java.util.List<java.lang.String> com.example.MyClass.VALUES"</jc> 463 * </p> 464 * 465 * @return A string describing this field. 466 * @see Field#toGenericString() 467 */ 468 public String toGenericString() { 469 return inner.toGenericString(); 470 } 471 472 //----------------------------------------------------------------------------------------------------------------- 473 // Annotatable interface methods 474 //----------------------------------------------------------------------------------------------------------------- 475 476 @Override 477 public String toString() { 478 return cn(inner.getDeclaringClass()) + "." + inner.getName(); 479 } 480 481 private String findFullName() { 482 var sb = new StringBuilder(128); 483 var dc = declaringClass; 484 var pi = dc.getPackage(); 485 if (nn(pi)) 486 sb.append(pi.getName()).append('.'); 487 // HTT - false branch (pi == null) is hard to test: some classloaders return Package objects 488 // even for default package classes, though Java API spec says it should return null 489 dc.appendNameFormatted(sb, SHORT, true, '$', BRACKETS); 490 sb.append('.').append(getName()); 491 return sb.toString(); 492 } 493}