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.utils.CollectionUtils.*; 023import static org.apache.juneau.commons.utils.Shorts.*; 024import static org.apache.juneau.commons.utils.ThrowableUtils.*; 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.inject.*; 034import org.apache.juneau.commons.utils.*; 035 036/** 037 * Lightweight utility class for introspecting information about a Java field. 038 * 039 * <p> 040 * This class provides a convenient wrapper around {@link Field} that extends the standard Java reflection 041 * API with additional functionality for field introspection, annotation handling, and value access. 042 * It extends {@link AccessibleInfo} to provide {@link AccessibleObject} functionality for accessing 043 * private fields. 044 * 045 * <h5 class='section'>Features:</h5> 046 * <ul class='spaced-list'> 047 * <li>Field introspection - access field metadata, type, modifiers 048 * <li>Annotation support - get annotations declared on the field 049 * <li>Value access - get and set field values with type safety 050 * <li>Accessibility control - make private fields accessible 051 * <li>Thread-safe - instances are immutable and safe for concurrent access 052 * </ul> 053 * 054 * <h5 class='section'>Use Cases:</h5> 055 * <ul class='spaced-list'> 056 * <li>Introspecting field metadata for code generation or analysis 057 * <li>Accessing field values in beans or data objects 058 * <li>Finding annotations on fields 059 * <li>Working with field types and modifiers 060 * <li>Building frameworks that need to analyze or manipulate field values 061 * </ul> 062 * 063 * <h5 class='section'>Usage:</h5> 064 * <p class='bjava'> 065 * <jc>// Get FieldInfo from a class</jc> 066 * ClassInfo <jv>ci</jv> = ClassInfo.<jsm>of</jsm>(MyClass.<jk>class</jk>); 067 * FieldInfo <jv>field</jv> = <jv>ci</jv>.getField(<js>"myField"</js>); 068 * 069 * <jc>// Get field type</jc> 070 * ClassInfo <jv>type</jv> = <jv>field</jv>.getType(); 071 * 072 * <jc>// Get annotations</jc> 073 * List<AnnotationInfo<MyAnnotation>> <jv>annotations</jv> = 074 * <jv>field</jv>.getAnnotations(MyAnnotation.<jk>class</jk>).toList(); 075 * 076 * <jc>// Access field value</jc> 077 * MyClass <jv>obj</jv> = <jk>new</jk> MyClass(); 078 * <jv>field</jv>.accessible(); <jc>// Make accessible if private</jc> 079 * Object <jv>value</jv> = <jv>field</jv>.get(<jv>obj</jv>); 080 * </p> 081 * 082 * <h5 class='section'>See Also:</h5><ul> 083 * <li class='jc'>{@link ClassInfo} - Class introspection 084 * <li class='jc'>{@link MethodInfo} - Method introspection 085 * <li class='jc'>{@link ConstructorInfo} - Constructor introspection 086 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauCommonsReflection">Reflection Package</a> 087 * </ul> 088 */ 089@SuppressWarnings({ 090 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 091 "java:S3011", // Reflection access needed for field introspection 092 "unchecked" // (T)inner.get(o) and the (AnnotationInfo<A>) cast are guarded by the caller-supplied type / type.isInstance filter 093}) 094public final class FieldInfo extends AccessibleInfo implements Comparable<FieldInfo>, Annotatable { 095 096 /** 097 * Creates a FieldInfo wrapper for the specified field. 098 * 099 * <h5 class='section'>Example:</h5> 100 * <p class='bjava'> 101 * ClassInfo <jv>ci</jv> = ClassInfo.<jsm>of</jsm>(MyClass.<jk>class</jk>); 102 * Field <jv>f</jv> = MyClass.<jk>class</jk>.getField(<js>"myField"</js>); 103 * FieldInfo <jv>fi</jv> = FieldInfo.<jsm>of</jsm>(<jv>ci</jv>, <jv>f</jv>); 104 * </p> 105 * 106 * @param declaringClass The ClassInfo for the class that declares this field. Must not be <jk>null</jk>. 107 * @param inner The field being wrapped. Must not be <jk>null</jk>. 108 * @return A new FieldInfo object wrapping the field. 109 */ 110 public static FieldInfo of(ClassInfo declaringClass, Field inner) { 111 reqnn("declaringClass", declaringClass); 112 return declaringClass.getField(inner); 113 } 114 115 /** 116 * Creates a FieldInfo wrapper for the specified field. 117 * 118 * <p> 119 * This convenience method automatically determines the declaring class from the field. 120 * 121 * <h5 class='section'>Example:</h5> 122 * <p class='bjava'> 123 * Field <jv>f</jv> = MyClass.<jk>class</jk>.getField(<js>"myField"</js>); 124 * FieldInfo <jv>fi</jv> = FieldInfo.<jsm>of</jsm>(<jv>f</jv>); 125 * </p> 126 * 127 * @param inner The field being wrapped. Must not be <jk>null</jk>. 128 * @return A new FieldInfo object wrapping the field. 129 */ 130 public static FieldInfo of(Field inner) { 131 reqnn("inner", inner); 132 return ClassInfo.of(inner.getDeclaringClass()).getField(inner); 133 } 134 135 private final Field inner; 136 private final ClassInfo declaringClass; 137 private final Supplier<ClassInfo> type; 138 private final Supplier<List<AnnotationInfo<Annotation>>> annotations; // All annotations declared directly on this field. 139 private final Supplier<String> nameFull; // Fully qualified field name (declaring-class.field-name). 140 private final Supplier<String> toString; // String representation with modifiers, type, and full name. 141 142 /** 143 * Constructor. 144 * 145 * <p> 146 * Creates a new FieldInfo wrapper for the specified field. This constructor is protected 147 * and should not be called directly. Use the static factory methods {@link #of(Field)} or 148 * obtain FieldInfo instances from {@link ClassInfo#getField(Field)}. 149 * 150 * @param declaringClass The ClassInfo for the class that declares this field. Must not be <jk>null</jk>. 151 * @param inner The field being wrapped. Must not be <jk>null</jk>. 152 */ 153 protected FieldInfo(ClassInfo declaringClass, Field inner) { 154 super(inner, inner.getModifiers()); 155 reqnn("inner", inner); 156 this.declaringClass = declaringClass; 157 this.inner = inner; 158 this.type = memoize(() -> ClassInfo.of(inner.getType(), inner.getGenericType())); 159 this.annotations = memoize(() -> stream(inner.getAnnotations()).flatMap(AnnotationUtils::streamRepeated).map(a -> ai(this, a)).toList()); 160 this.nameFull = memoize(this::findNameFull); 161 this.toString = memoize(this::findToString); 162 } 163 164 /** 165 * Attempts to call <code>x.setAccessible(<jk>true</jk>)</code> and quietly ignores security exceptions. 166 * 167 * @return This object. 168 */ 169 public FieldInfo accessible() { 170 setAccessible(); 171 return this; 172 } 173 174 @Override 175 public int compareTo(FieldInfo o) { 176 return cmp(getName(), o.getName()); 177 } 178 179 /** 180 * Returns the field value on the specified object. 181 * 182 * <p> 183 * If the underlying field is static, then the specified <c>o</c> argument is ignored. It may be <jk>null</jk>. 184 * 185 * @param <T> The object type to retrieve. 186 * @param o The object containing the field. Can be <jk>null</jk> for static fields. 187 * @return The field value. 188 * @throws BeanRuntimeException Field was not accessible or field does not belong to object. 189 */ 190 public <T> T get(Object o) throws BeanRuntimeException { 191 return safe(() -> { 192 inner.setAccessible(true); 193 return (T)inner.get(o); 194 }, Shorts::brex); 195 } 196 197 @Override /* Annotatable */ 198 public AnnotatableType getAnnotatableType() { return AnnotatableType.FIELD_TYPE; } 199 200 /** 201 * Returns an {@link AnnotatedType} object that represents the use of a type to specify the declared type of the field. 202 * 203 * <p> 204 * Same as calling {@link Field#getAnnotatedType()}. 205 * 206 * <h5 class='section'>Example:</h5> 207 * <p class='bjava'> 208 * <jc>// Get annotated type: @NotNull String name</jc> 209 * FieldInfo <jv>fi</jv> = ClassInfo.<jsm>of</jsm>(MyClass.<jk>class</jk>).getField(<js>"name"</js>); 210 * AnnotatedType <jv>aType</jv> = <jv>fi</jv>.getAnnotatedType(); 211 * <jc>// Check for @NotNull on the type</jc> 212 * </p> 213 * 214 * @return An {@link AnnotatedType} object representing the declared type. 215 * @see Field#getAnnotatedType() 216 */ 217 public AnnotatedType getAnnotatedType() { return inner.getAnnotatedType(); } 218 219 /** 220 * Returns all annotations declared on this field. 221 * 222 * <p> 223 * <b>Note on Repeatable Annotations:</b> 224 * Repeatable annotations (those marked with {@link Repeatable @Repeatable}) are automatically 225 * expanded into their individual annotation instances. For example, if a field has multiple {@code @Marshalled} annotations, 226 * this method returns each {@code @Marshalled} annotation separately, rather than the container annotation. 227 * 228 * @return 229 * An unmodifiable list of all annotations declared on this field. 230 * <br>Repeatable annotations are expanded into individual instances. 231 */ 232 public List<AnnotationInfo<Annotation>> getAnnotations() { return annotations.get(); } 233 234 /** 235 * Returns all annotations of the specified type declared on this field. 236 * 237 * @param <A> The annotation type. 238 * @param type The annotation type. 239 * @return A stream of all matching annotations. 240 */ 241 public <A extends Annotation> Stream<AnnotationInfo<A>> getAnnotations(Class<A> type) { 242 return annotations.get().stream().filter(x -> type.isInstance(x.inner())).map(x -> (AnnotationInfo<A>)x); 243 } 244 245 /** 246 * Returns metadata about the declaring class. 247 * 248 * @return Metadata about the declaring class. 249 */ 250 public ClassInfo getDeclaringClass() { return declaringClass; } 251 252 /** 253 * Returns the type of this field. 254 * 255 * @return The type of this field. 256 */ 257 public ClassInfo getFieldType() { return type.get(); } 258 259 /** 260 * Returns the full name of this field. 261 * 262 * <h5 class='section'>Examples:</h5> 263 * <ul> 264 * <li><js>"com.foo.MyClass.myField"</js> - Method. 265 * </ul> 266 * 267 * @return The underlying executable name. 268 */ 269 public String getNameFull() { return nameFull.get(); } 270 271 @Override /* Annotatable */ 272 public String getLabel() { return getDeclaringClass().getNameSimple() + "." + getName(); } 273 274 /** 275 * Returns the name of this field. 276 * 277 * @return The name of this field. 278 */ 279 public String getName() { return inner.getName(); } 280 281 /** 282 * Returns <jk>true</jk> if the specified annotation is present. 283 * 284 * @param <A> The annotation type to look for. 285 * @param type The annotation to look for. 286 * @return <jk>true</jk> if the specified annotation is present. 287 */ 288 public <A extends Annotation> boolean hasAnnotation(Class<A> type) { 289 return getAnnotations(type).findAny().isPresent(); 290 } 291 292 /** 293 * Returns <jk>true</jk> if the field has the specified name. 294 * 295 * @param name The name to compare against. 296 * @return <jk>true</jk> if the field has the specified name. 297 */ 298 public boolean hasName(String name) { 299 return inner.getName().equals(name); 300 } 301 302 /** 303 * Returns the wrapped field. 304 * 305 * @return The wrapped field. 306 */ 307 public Field inner() { 308 return inner; 309 } 310 311 /** 312 * Compares this FieldInfo with the specified object for equality. 313 * 314 * <p> 315 * Two FieldInfo objects are considered equal if they wrap the same underlying {@link Field} object. 316 * This delegates to the underlying {@link Field#equals(Object)} method. 317 * 318 * <p> 319 * This method makes FieldInfo suitable for use as keys in hash-based collections such as {@link HashMap} 320 * and {@link HashSet}. 321 * 322 * @param obj The object to compare with. 323 * @return <jk>true</jk> if the objects are equal, <jk>false</jk> otherwise. 324 */ 325 @Override 326 public boolean equals(Object obj) { 327 return obj instanceof FieldInfo obj2 && eq(this, obj2, (x, y) -> eq(x.inner, y.inner)); 328 } 329 330 /** 331 * Returns a hash code value for this FieldInfo. 332 * 333 * <p> 334 * This delegates to the underlying {@link Field#hashCode()} method. 335 * 336 * <p> 337 * This method makes FieldInfo suitable for use as keys in hash-based collections such as {@link HashMap} 338 * and {@link HashSet}. 339 * 340 * @return A hash code value for this FieldInfo. 341 */ 342 @Override 343 public int hashCode() { 344 return inner.hashCode(); 345 } 346 347 /** 348 * Returns <jk>true</jk> if all specified flags are applicable to this field. 349 * 350 * @param flag The flag to test for. 351 * @return <jk>true</jk> if all specified flags are applicable to this field. 352 */ 353 @Override 354 public boolean is(ElementFlag flag) { 355 return switch (flag) { 356 case DEPRECATED -> isDeprecated(); 357 case NOT_DEPRECATED -> isNotDeprecated(); 358 case ENUM_CONSTANT -> isEnumConstant(); 359 case NOT_ENUM_CONSTANT -> ! isEnumConstant(); 360 case SYNTHETIC -> isSynthetic(); 361 case NOT_SYNTHETIC -> ! isSynthetic(); // HTT 362 default -> super.is(flag); 363 }; 364 } 365 366 /** 367 * Returns <jk>true</jk> if this field has the {@link Deprecated @Deprecated} annotation on it. 368 * 369 * @return <jk>true</jk> if this field has the {@link Deprecated @Deprecated} annotation on it. 370 */ 371 public boolean isDeprecated() { return inner.isAnnotationPresent(Deprecated.class); } 372 373 /** 374 * Returns <jk>true</jk> if this field represents an element of an enumerated type. 375 * 376 * <p> 377 * Same as calling {@link Field#isEnumConstant()}. 378 * 379 * <h5 class='section'>Example:</h5> 380 * <p class='bjava'> 381 * <jc>// Check if field is an enum constant</jc> 382 * FieldInfo <jv>fi</jv> = ClassInfo.<jsm>of</jsm>(MyEnum.<jk>class</jk>).getField(<js>"VALUE1"</js>); 383 * <jk>if</jk> (<jv>fi</jv>.isEnumConstant()) { 384 * <jc>// Handle enum constant</jc> 385 * } 386 * </p> 387 * 388 * @return <jk>true</jk> if this field represents an enum constant. 389 * @see Field#isEnumConstant() 390 */ 391 public boolean isEnumConstant() { return inner.isEnumConstant(); } 392 393 /** 394 * Returns <jk>true</jk> if this field doesn't have the {@link Deprecated @Deprecated} annotation on it. 395 * 396 * @return <jk>true</jk> if this field doesn't have the {@link Deprecated @Deprecated} annotation on it. 397 */ 398 public boolean isNotDeprecated() { return ! inner.isAnnotationPresent(Deprecated.class); } 399 400 /** 401 * Returns <jk>true</jk> if this field is a synthetic field as defined by the Java Language Specification. 402 * 403 * <p> 404 * Same as calling {@link Field#isSynthetic()}. 405 * 406 * <h5 class='section'>Example:</h5> 407 * <p class='bjava'> 408 * <jc>// Filter out compiler-generated fields</jc> 409 * FieldInfo <jv>fi</jv> = ...; 410 * <jk>if</jk> (! <jv>fi</jv>.isSynthetic()) { 411 * <jc>// Process real field</jc> 412 * } 413 * </p> 414 * 415 * @return <jk>true</jk> if this field is a synthetic field. 416 * @see Field#isSynthetic() 417 */ 418 public boolean isSynthetic() { return inner.isSynthetic(); } 419 420 /** 421 * Identifies if the specified visibility matches this field. 422 * 423 * @param v The visibility to validate against. 424 * @return <jk>true</jk> if this visibility matches the modifier attribute of this field. 425 */ 426 public boolean isVisible(Visibility v) { 427 return v.isVisible(inner); 428 } 429 430 /** 431 * Sets the field value on the specified object. 432 * 433 * <p> 434 * If the underlying field is static, then the specified <c>o</c> argument is ignored. It may be <jk>null</jk>. 435 * 436 * @param o The object containing the field. Can be <jk>null</jk> for static fields. 437 * @param value The new field value. 438 * @throws BeanRuntimeException Field was not accessible or field does not belong to object. 439 */ 440 public void set(Object o, Object value) throws BeanRuntimeException { 441 safe((Snippet)() -> { 442 inner.setAccessible(true); 443 inner.set(o, value); 444 }, Shorts::brex); 445 } 446 447 //----------------------------------------------------------------------------------------------------------------- 448 // Field-Specific Methods 449 //----------------------------------------------------------------------------------------------------------------- 450 451 /** 452 * Sets the field value on the specified object if the value is <jk>null</jk>. 453 * 454 * @param o The object containing the field. Can be <jk>null</jk> for static fields. 455 * @param value The new field value. 456 * @throws BeanRuntimeException Field was not accessible or field does not belong to object. 457 */ 458 public void setIfNull(Object o, Object value) { 459 Object v = get(o); 460 if (v == null) 461 set(o, value); 462 } 463 464 /** 465 * Returns a string describing this field, including its generic type. 466 * 467 * <p> 468 * Same as calling {@link Field#toGenericString()}. 469 * 470 * <h5 class='section'>Example:</h5> 471 * <p class='bjava'> 472 * <jc>// Get generic string for: public static final List<String> VALUES</jc> 473 * FieldInfo <jv>fi</jv> = ClassInfo.<jsm>of</jsm>(MyClass.<jk>class</jk>).getField(<js>"VALUES"</js>); 474 * String <jv>str</jv> = <jv>fi</jv>.toGenericString(); 475 * <jc>// Returns: "public static final java.util.List<java.lang.String> com.example.MyClass.VALUES"</jc> 476 * </p> 477 * 478 * @return A string describing this field. 479 * @see Field#toGenericString() 480 */ 481 public String toGenericString() { 482 return inner.toGenericString(); 483 } 484 485 //----------------------------------------------------------------------------------------------------------------- 486 // Annotatable interface methods 487 //----------------------------------------------------------------------------------------------------------------- 488 489 /** 490 * Returns a detailed string representation of this field. 491 * 492 * <p> 493 * The returned string includes: 494 * <ul> 495 * <li>Modifiers (public, private, protected, static, final, volatile, transient) 496 * <li>Field type with generics (e.g., "List<String>") 497 * <li>Fully qualified field name (declaring-class.field-name) 498 * </ul> 499 * 500 * <h5 class='section'>Examples:</h5> 501 * <p class='bjava'> 502 * <jc>// Simple field</jc> 503 * FieldInfo <jv>fi</jv> = ...; 504 * <jv>fi</jv>.toString(); 505 * <jc>// Returns: "public java.lang.String org.example.MyClass.name"</jc> 506 * 507 * <jc>// Static final field</jc> 508 * <jc>// Returns: "public static final int org.example.MyClass.MAX_VALUE"</jc> 509 * 510 * <jc>// Generic field</jc> 511 * <jc>// Returns: "private java.util.List<java.lang.String> org.example.MyClass.items"</jc> 512 * 513 * <jc>// Volatile field</jc> 514 * <jc>// Returns: "volatile boolean org.example.MyClass.flag"</jc> 515 * </p> 516 * 517 * @return A detailed string representation including modifiers, type, and full name. 518 */ 519 @Override 520 public String toString() { 521 return toString.get(); 522 } 523 524 private String findToString() { 525 var sb = new StringBuilder(256); 526 527 // Modifiers 528 var mods = Modifier.toString(getModifiers()); 529 if (nn(mods) && ! mods.isEmpty()) { 530 sb.append(mods).append(" "); 531 } 532 533 // Field type (use generic type to show generics) 534 var genericType = inner.getGenericType(); 535 ClassInfo.of(genericType).appendNameFormatted(sb, FULL, true, '$', BRACKETS); 536 537 // Fully qualified field name 538 sb.append(" ").append(getNameFull()); 539 540 return sb.toString(); 541 } 542 543 private String findNameFull() { 544 var sb = new StringBuilder(128); 545 var dc = declaringClass; 546 var pi = dc.getPackage(); 547 if (nn(pi)) 548 sb.append(pi.getName()).append('.'); 549 // HTT - false branch (pi == null) is hard to test: some classloaders return Package objects 550 // even for default package classes, though Java API spec says it should return null 551 dc.appendNameFormatted(sb, SHORT, true, '$', BRACKETS); 552 sb.append('.').append(getName()); 553 return sb.toString(); 554 } 555 556 /** 557 * Resolves field value from the bean store and sets it on the specified bean instance. 558 * 559 * <p> 560 * This method resolves the field value using the same logic as parameter resolution, 561 * supporting single beans, {@code Optional}, arrays, {@code List}, {@code Set}, and {@code Map}. 562 * The field is made accessible before setting its value. 563 * 564 * <h5 class='section'>Field Resolution:</h5> 565 * <ul class='spaced-list'> 566 * <li><b>Single beans</b> - Resolved using {@link BeanStore#getBean(Class)} or {@link BeanStore#getBean(Class, String)}. 567 * Throws {@link ExecutableException} if not found. 568 * <li><b>Optional beans</b> - Wrapped in <c>Optional</c>, or <c>Optional.empty()</c> if not found. 569 * Never throws an exception. 570 * <li><b>Arrays</b> - All beans of the element type are collected into an array (may be empty). 571 * Never throws an exception. 572 * <li><b>Lists</b> - All beans of the element type are collected into a <c>List</c> (may be empty). 573 * Never throws an exception. 574 * <li><b>Sets</b> - All beans of the element type are collected into a <c>LinkedHashSet</c> (may be empty). 575 * Never throws an exception. 576 * <li><b>Maps</b> - All beans of the value type are collected into a <c>LinkedHashMap</c> keyed by bean name (may be empty). 577 * Unnamed beans use an empty string as the key. Never throws an exception. 578 * </ul> 579 * 580 * @param <T> The bean type. 581 * @param beanStore The bean store to resolve the field value from. 582 * @param bean The object instance containing the field. 583 * @return The same bean instance (for method chaining). 584 * @throws ExecutableException If a required field (non-Optional, non-collection) cannot be resolved from the bean store. 585 */ 586 @SuppressWarnings({ 587 "java:S3776" // Cognitive complexity acceptable for dependency injection 588 }) 589 public <T> T inject(BeanStore beanStore, T bean) { 590 accessible(); 591 var fieldType = getFieldType(); 592 593 // @Value sites resolve from Settings/VarResolver, not the bean store. 594 var annos = getAnnotations(); 595 var valueExpr = ValueResolver.findValueExpression(annos); 596 if (valueExpr != null) { 597 ValueResolver.checkInjectConflict(annos, this.toString()); 598 var unwrapped = fieldType.unwrap(Optional.class).inner(); 599 // Pass the declared (generic) field type so @Value Supplier<String> autodetects 600 // Field type IS the opt-in signal for re-evaluating reads (Supplier<String> autodetect). 601 // BeanStore is forwarded so caller-scoped PropertySource beans (e.g. per-RestContext 602 // @Rest(config=...) Configs) participate in expression resolution alongside Settings. 603 var resolved = ValueResolver.resolve(valueExpr, unwrapped, inner.getGenericType(), this.toString(), beanStore); 604 if (fieldType.is(Optional.class)) { 605 // VarResolver substitutes "" for a missing key with no default. Collapse both to 606 // Optional.empty() so @Value("${maybe}") Optional<T> behaves the same as Spring's. 607 set(bean, (resolved == null || (resolved instanceof String resolved2 && resolved2.isEmpty())) 608 ? oe() 609 : o(resolved)); 610 } else { 611 set(bean, resolved); 612 } 613 return bean; 614 } 615 616 // Find qualifier from @Named or @Qualifier annotation (same logic as ParameterInfo.getResolvedQualifier) 617 var beanQualifier = annos.stream() 618 .map(JsrSupport::qualifierValue) 619 .filter(Objects::nonNull) 620 .findFirst() 621 .orElse(null); 622 623 var ptUnwrapped = fieldType.unwrap(Optional.class); 624 625 // Handle collections and arrays 626 Object collectionValue = null; 627 if (ptUnwrapped.isInjectCollectionType()) { 628 // Extract element type from field type 629 Class<?> elementType = null; 630 631 if (ptUnwrapped.isArray()) { 632 elementType = ptUnwrapped.getComponentType().inner(); 633 } else { 634 Type parameterizedType = fieldType.innerType(); 635 var inner2 = o(ptUnwrapped.inner()).orElse(Object.class); 636 637 if (eqa(inner2, List.class, Set.class)) { 638 if (parameterizedType instanceof ParameterizedType parameterizedType2) { 639 var typeArgs = parameterizedType2.getActualTypeArguments(); 640 if (typeArgs.length > 0 && typeArgs[0] instanceof Class<?> elementClass) { 641 elementType = elementClass; 642 } 643 } 644 } else if (eq(inner2, Map.class) && parameterizedType instanceof ParameterizedType parameterizedType2) { 645 var typeArgs = parameterizedType2.getActualTypeArguments(); 646 if (typeArgs.length >= 2 && typeArgs[0] == String.class && typeArgs[1] instanceof Class<?> valueClass) { 647 elementType = valueClass; 648 } 649 } 650 } 651 652 collectionValue = ReflectionUtils.resolveCollectionValue(elementType, beanStore, ptUnwrapped); 653 } 654 655 Object value; 656 if (nn(collectionValue)) { 657 value = fieldType.is(Optional.class) ? o(collectionValue) : collectionValue; 658 } else { 659 // Handle single bean 660 var ptc = ptUnwrapped.inner(); 661 var o2 = beanQualifier == null ? beanStore.getBean(ptc) : beanStore.getBean(ptc, beanQualifier); 662 663 if (fieldType.is(Optional.class)) { 664 value = o2; 665 } else if (o2.isPresent()) { 666 value = o2.get(); 667 } else { 668 throw exex("Could not resolve value for field %s", this); 669 } 670 } 671 672 set(bean, value); 673 return bean; 674 } 675}