• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.05
today-context/src/main/java/infra/context/properties/bind/JavaBeanBinder.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.context.properties.bind;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.beans.Introspector;
23
import java.lang.annotation.Annotation;
24
import java.lang.reflect.Field;
25
import java.lang.reflect.Method;
26
import java.lang.reflect.Modifier;
27
import java.util.Arrays;
28
import java.util.Comparator;
29
import java.util.LinkedHashMap;
30
import java.util.LinkedHashSet;
31
import java.util.Map;
32
import java.util.Set;
33
import java.util.concurrent.ConcurrentHashMap;
34
import java.util.function.BiConsumer;
35
import java.util.function.Function;
36
import java.util.function.Supplier;
37

38
import infra.beans.BeanUtils;
39
import infra.context.properties.source.ConfigurationPropertyName;
40
import infra.context.properties.source.ConfigurationPropertySource;
41
import infra.context.properties.source.ConfigurationPropertyState;
42
import infra.core.BridgeMethodResolver;
43
import infra.core.MethodParameter;
44
import infra.core.ResolvableType;
45
import infra.lang.Constant;
46
import infra.util.ReflectionUtils;
47

48
/**
49
 * {@link DataObjectBinder} for mutable Java Beans.
50
 *
51
 * @author Phillip Webb
52
 * @author Madhura Bhave
53
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
54
 * @since 4.0
55
 */
56
@SuppressWarnings("NullAway")
57
class JavaBeanBinder implements DataObjectBinder {
3✔
58

59
  private static final String HAS_KNOWN_BINDABLE_PROPERTIES_CACHE = JavaBeanBinder.class.getName()
4✔
60
          + ".HAS_KNOWN_BINDABLE_PROPERTIES_CACHE";
61

62
  static final JavaBeanBinder INSTANCE = new JavaBeanBinder();
5✔
63

64
  @Nullable
65
  @Override
66
  public <T> T bind(ConfigurationPropertyName name, Bindable<T> target,
67
          Binder.Context context, DataObjectPropertyBinder propertyBinder) {
68
    boolean hasKnownBindableProperties = target.getValue() != null && hasKnownBindableProperties(name, context);
12✔
69
    Bean<T> bean = Bean.get(target, context, hasKnownBindableProperties);
5✔
70
    if (bean == null) {
2✔
71
      return null;
2✔
72
    }
73
    BeanSupplier<T> beanSupplier = bean.getSupplier(target);
4✔
74
    boolean bound = bind(propertyBinder, bean, beanSupplier, context);
7✔
75
    return (bound ? beanSupplier.get() : null);
7✔
76
  }
77

78
  @Nullable
79
  @Override
80
  @SuppressWarnings("unchecked")
81
  public <T> T create(Bindable<T> target, Binder.Context context) {
82
    Class<T> type = (Class<T>) target.getType().resolve();
4✔
83
    return type != null ? BeanUtils.newInstance(type) : null;
6!
84
  }
85

86
  private boolean hasKnownBindableProperties(ConfigurationPropertyName name, Binder.Context context) {
87
    var cache = getHasKnownBindablePropertiesCache(context);
4✔
88
    Boolean hasKnownBindableProperties = cache.get(name);
5✔
89
    if (hasKnownBindableProperties == null) {
2✔
90
      hasKnownBindableProperties = computeHasKnownBindableProperties(name, context);
6✔
91
      cache.put(name, hasKnownBindableProperties);
5✔
92
    }
93
    return hasKnownBindableProperties;
3✔
94
  }
95

96
  private boolean computeHasKnownBindableProperties(ConfigurationPropertyName name, Binder.Context context) {
97
    for (ConfigurationPropertySource source : context.getSources()) {
11✔
98
      if (source.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT) {
5✔
99
        return true;
2✔
100
      }
101
    }
1✔
102
    return false;
2✔
103
  }
104

105
  @SuppressWarnings("unchecked")
106
  private Map<ConfigurationPropertyName, Boolean> getHasKnownBindablePropertiesCache(Binder.Context context) {
107
    Object cache = context.getCache().get(HAS_KNOWN_BINDABLE_PROPERTIES_CACHE);
5✔
108
    if (cache == null) {
2✔
109
      cache = new ConcurrentHashMap<ConfigurationPropertyName, Boolean>();
4✔
110
      context.getCache().put(HAS_KNOWN_BINDABLE_PROPERTIES_CACHE, cache);
6✔
111
    }
112
    return (Map<ConfigurationPropertyName, Boolean>) cache;
3✔
113
  }
114

115
  private <T> boolean bind(DataObjectPropertyBinder propertyBinder,
116
          Bean<T> bean, BeanSupplier<T> beanSupplier, Binder.Context context) {
117
    boolean bound = false;
2✔
118
    for (BeanProperty beanProperty : bean.getProperties().values()) {
12✔
119
      bound |= bind(beanSupplier, propertyBinder, beanProperty);
8✔
120
      context.clearConfigurationProperty();
2✔
121
    }
1✔
122
    return bound;
2✔
123
  }
124

125
  private <T> boolean bind(BeanSupplier<T> beanSupplier, DataObjectPropertyBinder propertyBinder, BeanProperty property) {
126
    String propertyName = determinePropertyName(property);
4✔
127
    ResolvableType type = property.getType();
3✔
128
    Supplier<Object> value = property.getValue(beanSupplier);
4✔
129
    Annotation[] annotations = property.getAnnotations();
3✔
130
    Object bound = propertyBinder.bindProperty(propertyName,
5✔
131
            Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations));
5✔
132
    if (bound == null) {
2✔
133
      return false;
2✔
134
    }
135
    if (property.isSettable()) {
3✔
136
      property.setValue(beanSupplier, bound);
5✔
137
    }
138
    else if (value == null || !bound.equals(value.get())) {
7!
139
      throw new IllegalStateException("No setter found for property: " + property.name);
7✔
140
    }
141
    return true;
2✔
142
  }
143

144
  private String determinePropertyName(BeanProperty property) {
145
    Annotation[] annotations = property.getAnnotations();
3✔
146
    return Arrays.stream((annotations != null) ? annotations : Constant.EMPTY_ANNOTATIONS)
8✔
147
            .filter(annotation -> annotation.annotationType() == Name.class)
9✔
148
            .findFirst()
3✔
149
            .map(Name.class::cast)
5✔
150
            .map(Name::value)
3✔
151
            .orElse(property.name);
2✔
152
  }
153

154
  /**
155
   * The properties of a bean that may be bound.
156
   */
157
  static class BeanProperties {
158

159
    private final Map<String, BeanProperty> properties = new LinkedHashMap<>();
5✔
160

161
    private final ResolvableType type;
162

163
    @Nullable
164
    private final Class<?> resolvedType;
165

166
    BeanProperties(ResolvableType type, @Nullable Class<?> resolvedType) {
2✔
167
      this.type = type;
3✔
168
      this.resolvedType = resolvedType;
3✔
169
      addProperties(resolvedType);
3✔
170
    }
1✔
171

172
    private void addProperties(@Nullable Class<?> type) {
173
      while (type != null && !Object.class.equals(type)) {
6✔
174
        Method[] declaredMethods = getSorted(type, this::getDeclaredMethods, Method::getName);
8✔
175
        Field[] declaredFields = getSorted(type, Class::getDeclaredFields, Field::getName);
7✔
176
        addProperties(declaredMethods, declaredFields);
4✔
177
        type = type.getSuperclass();
3✔
178
      }
1✔
179
    }
1✔
180

181
    private Method[] getDeclaredMethods(Class<?> type) {
182
      Method[] methods = type.getDeclaredMethods();
3✔
183
      Set<Method> result = new LinkedHashSet<>(methods.length);
6✔
184
      for (Method method : methods) {
16✔
185
        result.add(BridgeMethodResolver.findBridgedMethod(method));
5✔
186
      }
187
      return result.toArray(new Method[0]);
6✔
188
    }
189

190
    private <S, E> E[] getSorted(S source, Function<S, E[]> elements, Function<E, String> name) {
191
      E[] result = elements.apply(source);
5✔
192
      Arrays.sort(result, Comparator.comparing(name));
4✔
193
      return result;
2✔
194
    }
195

196
    protected void addProperties(Method[] declaredMethods, Field[] declaredFields) {
197
      for (Method method : declaredMethods) {
16✔
198
        if (isCandidate(method)) {
4✔
199
          addMethodIfPossible(method, "is", 0, BeanProperty::addGetter);
6✔
200
          addMethodIfPossible(method, "get", 0, BeanProperty::addGetter);
6✔
201
          addMethodIfPossible(method, "set", 1, BeanProperty::addSetter);
6✔
202
        }
203
      }
204

205
      for (Field field : declaredFields) {
16✔
206
        addField(field);
3✔
207
      }
208
    }
1✔
209

210
    private boolean isCandidate(Method method) {
211
      int modifiers = method.getModifiers();
3✔
212
      return !Modifier.isPrivate(modifiers)
5✔
213
              && !Modifier.isProtected(modifiers)
3✔
214
              && !Modifier.isAbstract(modifiers)
3✔
215
              && !Modifier.isStatic(modifiers)
3✔
216
              && !method.isBridge()
4!
217
              && !Object.class.equals(method.getDeclaringClass())
5!
218
              && !Class.class.equals(method.getDeclaringClass())
4!
219
              && method.getName().indexOf('$') == -1;
8!
220
    }
221

222
    private void addMethodIfPossible(@Nullable Method method, String prefix, int parameterCount,
223
            BiConsumer<BeanProperty, Method> consumer) {
224
      if (method != null
3!
225
              && method.getParameterCount() == parameterCount
4✔
226
              && method.getName().startsWith(prefix)
5✔
227
              && method.getName().length() > prefix.length()) {
5✔
228
        String propertyName = Introspector.decapitalize(method.getName().substring(prefix.length()));
7✔
229
        consumer.accept(this.properties.computeIfAbsent(propertyName, this::createBeanProperty), method);
10✔
230
      }
231
    }
1✔
232

233
    private BeanProperty createBeanProperty(String name) {
234
      return new BeanProperty(name, this.type);
7✔
235
    }
236

237
    private void addField(Field field) {
238
      String propertyName = field.getName();
3✔
239
      BeanProperty property = properties.get(propertyName);
6✔
240
      if (property != null) {
2✔
241
        ReflectionUtils.makeAccessible(field);
3✔
242
        property.field = field;
4✔
243
      }
244
      else if (Modifier.isPublic(field.getModifiers())) {
4✔
245
        // add public field
246
        property = createBeanProperty(propertyName);
4✔
247
        property.field = field;
3✔
248
        properties.put(propertyName, property);
6✔
249
      }
250
    }
1✔
251

252
    protected final ResolvableType getType() {
253
      return this.type;
×
254
    }
255

256
    @Nullable
257
    protected final Class<?> getResolvedType() {
258
      return this.resolvedType;
3✔
259
    }
260

261
    final Map<String, BeanProperty> getProperties() {
262
      return this.properties;
3✔
263
    }
264

265
    static BeanProperties of(Bindable<?> bindable) {
266
      ResolvableType type = bindable.getType();
3✔
267
      Class<?> resolvedType = type.resolve(Object.class);
4✔
268
      return new BeanProperties(type, resolvedType);
6✔
269
    }
270

271
  }
272

273
  /**
274
   * The bean being bound.
275
   *
276
   * @param <T> the bean type
277
   */
278
  static class Bean<T> extends BeanProperties {
279

280
    Bean(ResolvableType type, Class<?> resolvedType) {
281
      super(type, resolvedType);
4✔
282
    }
1✔
283

284
    @SuppressWarnings({ "unchecked", "NullAway" })
285
    BeanSupplier<T> getSupplier(Bindable<T> target) {
286
      return new BeanSupplier<>(() -> {
7✔
287
        T instance = null;
2✔
288
        if (target.getValue() != null) {
3✔
289
          instance = target.getValue().get();
4✔
290
        }
291
        if (instance == null) {
2✔
292
          instance = (T) BeanUtils.newInstance(getResolvedType());
4✔
293
        }
294
        return instance;
2✔
295
      });
296
    }
297

298
    @Nullable
299
    @SuppressWarnings("unchecked")
300
    static <T> Bean<T> get(Bindable<T> bindable, Binder.Context context, boolean canCallGetValue) {
301
      ResolvableType type = bindable.getType();
3✔
302
      Class<?> resolvedType = type.resolve(Object.class);
4✔
303
      Supplier<T> value = bindable.getValue();
3✔
304
      T instance = null;
2✔
305
      if (canCallGetValue && value != null) {
4!
306
        instance = value.get();
3✔
307
        resolvedType = (instance != null) ? instance.getClass() : resolvedType;
7✔
308
      }
309
      if (instance == null && !isInstantiable(resolvedType)) {
5✔
310
        return null;
2✔
311
      }
312
      Map<CacheKey, Bean<?>> cache = getCache(context);
3✔
313
      CacheKey cacheKey = new CacheKey(type, resolvedType);
6✔
314
      Bean<?> bean = cache.get(cacheKey);
5✔
315
      if (bean == null) {
2✔
316
        bean = new Bean<>(type, resolvedType);
6✔
317
        cache.put(cacheKey, bean);
5✔
318
      }
319
      return (Bean<T>) bean;
2✔
320
    }
321

322
    @SuppressWarnings("unchecked")
323
    private static Map<CacheKey, Bean<?>> getCache(Binder.Context context) {
324
      Map<CacheKey, Bean<?>> cache = (Map<CacheKey, Bean<?>>) context.getCache().get(Bean.class);
6✔
325
      if (cache == null) {
2✔
326
        cache = new ConcurrentHashMap<>();
4✔
327
        context.getCache().put(Bean.class, cache);
6✔
328
      }
329
      return cache;
2✔
330
    }
331

332
    private static boolean isInstantiable(Class<?> type) {
333
      if (type.isInterface()) {
3✔
334
        return false;
2✔
335
      }
336
      try {
337
        type.getDeclaredConstructor();
5✔
338
        return true;
2✔
339
      }
340
      catch (Exception ex) {
1✔
341
        return false;
2✔
342
      }
343
    }
344

345
    private record CacheKey(ResolvableType type, Class<?> resolvedType) {
9✔
346

347
    }
348

349
  }
350

351
  private static class BeanSupplier<T> implements Supplier<T> {
352

353
    private final Supplier<T> factory;
354

355
    @Nullable
356
    private T instance;
357

358
    BeanSupplier(Supplier<T> factory) {
2✔
359
      this.factory = factory;
3✔
360
    }
1✔
361

362
    @Override
363
    public T get() {
364
      if (this.instance == null) {
3✔
365
        this.instance = this.factory.get();
5✔
366
      }
367
      return this.instance;
3✔
368
    }
369

370
  }
371

372
  /**
373
   * A bean property being bound.
374
   */
375
  static class BeanProperty {
376

377
    public final String name;
378

379
    public final ResolvableType declaringClassType;
380

381
    @Nullable
382
    public Method getter;
383

384
    @Nullable
385
    public Method setter;
386

387
    @Nullable
388
    public Field field;
389

390
    BeanProperty(String name, ResolvableType declaringClassType) {
2✔
391
      this.name = DataObjectPropertyName.toDashedForm(name);
4✔
392
      this.declaringClassType = declaringClassType;
3✔
393
    }
1✔
394

395
    void addGetter(Method getter) {
396
      if (this.getter == null || this.getter.getName().startsWith("is")) {
9!
397
        this.getter = getter;
3✔
398
      }
399
    }
1✔
400

401
    void addSetter(Method setter) {
402
      if (this.setter == null || isBetterSetter(setter)) {
7✔
403
        this.setter = setter;
3✔
404
      }
405
    }
1✔
406

407
    private boolean isBetterSetter(Method setter) {
408
      return this.getter != null && this.getter.getReturnType().equals(setter.getParameterTypes()[0]);
16✔
409
    }
410

411
    public ResolvableType getType() {
412
      if (this.setter != null) {
3✔
413
        MethodParameter methodParameter = new MethodParameter(this.setter, 0);
7✔
414
        return ResolvableType.forMethodParameter(methodParameter, this.declaringClassType);
5✔
415
      }
416
      if (getter != null) {
3✔
417
        MethodParameter methodParameter = new MethodParameter(this.getter, -1);
7✔
418
        return ResolvableType.forMethodParameter(methodParameter, this.declaringClassType);
5✔
419
      }
420
      else {
421
        return ResolvableType.forField(field);
4✔
422
      }
423
    }
424

425
    Annotation @Nullable [] getAnnotations() {
426
      try {
427
        return (this.field != null) ? this.field.getDeclaredAnnotations() : null;
9✔
428
      }
429
      catch (Exception ex) {
×
430
        return null;
×
431
      }
432
    }
433

434
    @Nullable
435
    Supplier<Object> getValue(Supplier<?> instance) {
436
      if (getter != null) {
3✔
437
        return () -> {
4✔
438
          try {
439
            ReflectionUtils.makeAccessible(getter);
4✔
440
            return getter.invoke(instance.get());
8✔
441
          }
442
          catch (Exception ex) {
1✔
443
            throw new IllegalStateException("Unable to get value for property " + this.name, ex);
8✔
444
          }
445
        };
446
      }
447
      else if (field != null) {
3✔
448
        return () -> ReflectionUtils.getField(field, instance.get());
10✔
449
      }
450
      return null;
2✔
451
    }
452

453
    boolean isSettable() {
454
      return this.setter != null || (
9✔
455
              field != null && Modifier.isPublic(field.getModifiers())
5✔
456
                      && !Modifier.isFinal(field.getModifiers())
6✔
457
      );
458
    }
459

460
    void setValue(Supplier<?> instance, Object value) {
461
      if (setter != null) {
3✔
462
        try {
463
          ReflectionUtils.makeAccessible(setter);
4✔
464
          setter.invoke(instance.get(), value);
12✔
465
        }
466
        catch (Exception ex) {
1✔
467
          throw new IllegalStateException("Unable to set value for property " + this.name, ex);
8✔
468
        }
1✔
469
      }
470
      else if (field != null) {
3!
471
        ReflectionUtils.setField(field, instance.get(), value);
6✔
472
      }
473
    }
1✔
474

475
  }
476

477
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc