• 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

89.64
today-context/src/main/java/infra/context/properties/bind/ValueObjectBinder.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.lang.annotation.Annotation;
23
import java.lang.reflect.Array;
24
import java.lang.reflect.Constructor;
25
import java.lang.reflect.Executable;
26
import java.lang.reflect.Method;
27
import java.lang.reflect.Modifier;
28
import java.lang.reflect.Parameter;
29
import java.util.ArrayList;
30
import java.util.Collection;
31
import java.util.Collections;
32
import java.util.EnumMap;
33
import java.util.List;
34
import java.util.Map;
35
import java.util.Optional;
36
import java.util.concurrent.ConcurrentHashMap;
37
import java.util.function.Consumer;
38

39
import infra.beans.BeanUtils;
40
import infra.context.properties.source.ConfigurationPropertyName;
41
import infra.core.MethodParameter;
42
import infra.core.ParameterNameDiscoverer;
43
import infra.core.ResolvableType;
44
import infra.core.annotation.MergedAnnotation;
45
import infra.core.annotation.MergedAnnotations;
46
import infra.core.conversion.ConversionException;
47
import infra.lang.NullValue;
48
import infra.logging.LogMessage;
49
import infra.logging.Logger;
50
import infra.logging.LoggerFactory;
51
import infra.util.CollectionUtils;
52

53
/**
54
 * {@link DataObjectBinder} for immutable value objects.
55
 *
56
 * @author Madhura Bhave
57
 * @author Stephane Nicoll
58
 * @author Phillip Webb
59
 * @author Scott Frederick
60
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
61
 * @since 4.0
62
 */
63
class ValueObjectBinder implements DataObjectBinder {
64

65
  private static final Logger logger = LoggerFactory.getLogger(ValueObjectBinder.class);
4✔
66

67
  private final BindConstructorProvider constructorProvider;
68

69
  ValueObjectBinder(BindConstructorProvider constructorProvider) {
2✔
70
    this.constructorProvider = constructorProvider;
3✔
71
  }
1✔
72

73
  @SuppressWarnings("NullAway")
74
  @Nullable
75
  @Override
76
  public <T> T bind(ConfigurationPropertyName name, Bindable<T> target,
77
          Binder.Context context, DataObjectPropertyBinder propertyBinder) {
78

79
    ValueObject<T> valueObject = ValueObject.get(target, context, this.constructorProvider, Discoverer.LENIENT);
7✔
80
    if (valueObject == null) {
2✔
81
      return null;
2✔
82
    }
83
    context.pushConstructorBoundTypes(target.getType().resolve());
5✔
84
    List<ConstructorParameter> parameters = valueObject.getConstructorParameters();
3✔
85
    ArrayList<Object> args = new ArrayList<>(parameters.size());
6✔
86
    boolean bound = false;
2✔
87
    for (ConstructorParameter parameter : parameters) {
10✔
88
      Object arg = parameter.bind(propertyBinder);
4✔
89
      bound = bound || arg != null;
8✔
90
      arg = (arg != null) ? arg : getDefaultValue(context, parameter);
9✔
91
      args.add(arg);
4✔
92
    }
1✔
93
    context.clearConfigurationProperty();
2✔
94
    context.popConstructorBoundTypes();
2✔
95
    return bound ? valueObject.instantiate(args) : null;
8✔
96
  }
97

98
  @Nullable
99
  @Override
100
  public <T> T create(Bindable<T> target, Binder.Context context) {
101
    ValueObject<T> valueObject = ValueObject.get(target, context, this.constructorProvider, Discoverer.LENIENT);
7✔
102
    if (valueObject == null) {
2✔
103
      return null;
2✔
104
    }
105
    List<ConstructorParameter> parameters = valueObject.getConstructorParameters();
3✔
106
    ArrayList<Object> args = new ArrayList<>(parameters.size());
6✔
107
    for (ConstructorParameter parameter : parameters) {
10✔
108
      args.add(getDefaultValue(context, parameter));
7✔
109
    }
1✔
110
    return valueObject.instantiate(args);
4✔
111
  }
112

113
  @Override
114
  public <T> void onUnableToCreateInstance(Bindable<T> target, Binder.Context context, RuntimeException exception) {
115
    try {
116
      ValueObject.get(target, context, this.constructorProvider, Discoverer.STRICT);
×
117
    }
118
    catch (Exception ex) {
×
119
      exception.addSuppressed(ex);
×
120
    }
×
121
  }
×
122

123
  @Nullable
124
  private <T> T getDefaultValue(Binder.Context context, ConstructorParameter parameter) {
125
    ResolvableType type = parameter.type;
3✔
126
    Annotation[] annotations = parameter.annotations;
3✔
127
    for (Annotation annotation : annotations) {
16✔
128
      if (annotation instanceof DefaultValue defaultValueAnnotation) {
6✔
129
        String[] defaultValue = defaultValueAnnotation.value();
3✔
130
        if (defaultValue.length == 0) {
3✔
131
          return getNewDefaultValueInstanceIfPossible(context, type);
5✔
132
        }
133
        return convertDefaultValue(context.getConverter(), defaultValue, type, annotations);
8✔
134
      }
135
    }
136
    return null;
2✔
137
  }
138

139
  @Nullable
140
  private <T> T convertDefaultValue(BindConverter converter,
141
          String[] defaultValue, ResolvableType type, Annotation[] annotations) {
142
    try {
143
      return converter.convert(defaultValue, type, annotations);
6✔
144
    }
145
    catch (ConversionException ex) {
1✔
146
      // Try again in case ArrayToObjectConverter is not in play
147
      if (defaultValue.length == 1) {
4!
148
        return converter.convert(defaultValue[0], type, annotations);
8✔
149
      }
150
      throw ex;
×
151
    }
152
  }
153

154
  @Nullable
155
  @SuppressWarnings("unchecked")
156
  private <T> T getNewDefaultValueInstanceIfPossible(Binder.Context context, ResolvableType type) {
157
    Class<T> resolved = (Class<T>) type.resolve();
3✔
158
    if (!(resolved == null || isEmptyDefaultValueAllowed(resolved))) {
6!
159
      throw new IllegalStateException("Parameter of type %s must have a non-empty default value.".formatted(type));
12✔
160
    }
161
    if (resolved != null) {
2!
162
      if (Optional.class == resolved) {
3✔
163
        return (T) Optional.empty();
2✔
164
      }
165
      if (Collection.class.isAssignableFrom(resolved)) {
4✔
166
        return (T) CollectionUtils.createCollection(resolved, 0);
4✔
167
      }
168
      if (EnumMap.class.isAssignableFrom(resolved)) {
4✔
169
        Class<?> keyType = type.asMap().resolveGeneric(0);
10✔
170
        return (T) CollectionUtils.createMap(resolved, keyType, 0);
5✔
171
      }
172
      if (Map.class.isAssignableFrom(resolved)) {
4✔
173
        return (T) CollectionUtils.createMap(resolved, 0);
4✔
174
      }
175
      if (resolved.isArray()) {
3✔
176
        return (T) Array.newInstance(resolved.getComponentType(), 0);
5✔
177
      }
178
    }
179
    T instance = create(Bindable.of(type), context);
6✔
180
    if (instance != null) {
2✔
181
      return instance;
2✔
182
    }
183
    return resolved != null ? BeanUtils.newInstance(resolved) : null;
6!
184
  }
185

186
  private boolean isEmptyDefaultValueAllowed(Class<?> type) {
187
    return (Optional.class == type || isAggregate(type))
9✔
188
            || !(type.isPrimitive() || type.isEnum() || type.getName().startsWith("java.lang"));
13✔
189
  }
190

191
  private boolean isAggregate(Class<?> type) {
192
    return type.isArray() || Map.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type);
15✔
193
  }
194

195
  /**
196
   * The value object being bound.
197
   *
198
   * @param <T> the value object type
199
   */
200
  private abstract static class ValueObject<T> {
201

202
    private final Constructor<T> constructor;
203

204
    protected ValueObject(Constructor<T> constructor) {
2✔
205
      this.constructor = constructor;
3✔
206
    }
1✔
207

208
    T instantiate(List<Object> args) {
209
      return BeanUtils.newInstance(this.constructor, args.toArray());
6✔
210
    }
211

212
    abstract List<ConstructorParameter> getConstructorParameters();
213

214
    @Nullable
215
    @SuppressWarnings("unchecked")
216
    static <T> ValueObject<T> get(Bindable<T> bindable, Binder.Context context,
217
            BindConstructorProvider constructorProvider, ParameterNameDiscoverer parameterNameDiscoverer) {
218
      Class<T> resolvedType = (Class<T>) bindable.getType().resolve();
4✔
219
      if (resolvedType == null || resolvedType.isEnum() || Modifier.isAbstract(resolvedType.getModifiers())) {
9!
220
        return null;
2✔
221
      }
222
      Map<CacheKey, Object> cache = getCache(context);
3✔
223
      CacheKey cacheKey = new CacheKey(bindable, constructorProvider, parameterNameDiscoverer);
7✔
224
      Object valueObject = cache.get(cacheKey);
4✔
225
      if (valueObject == null) {
2✔
226
        Constructor<?> bindConstructor = constructorProvider.getBindConstructor(bindable, context.isNestedConstructorBinding());
6✔
227
        if (bindConstructor != null) {
2✔
228
          valueObject = DefaultValueObject.get(bindConstructor, bindable.getType(), parameterNameDiscoverer);
6✔
229
        }
230
        cache.put(cacheKey, valueObject != null ? valueObject : NullValue.INSTANCE);
9✔
231
      }
232
      return valueObject != NullValue.INSTANCE ? (ValueObject<T>) valueObject : null;
8✔
233
    }
234

235
    @SuppressWarnings("unchecked")
236
    private static Map<CacheKey, Object> getCache(Binder.Context context) {
237
      Map<CacheKey, Object> cache = (Map<CacheKey, Object>) context.getCache().get(ValueObject.class);
6✔
238
      if (cache == null) {
2✔
239
        cache = new ConcurrentHashMap<>();
4✔
240
        context.getCache().put(ValueObject.class, cache);
6✔
241
      }
242
      return cache;
2✔
243
    }
244

245
    private record CacheKey(Bindable<?> bindable, BindConstructorProvider constructorProvider,
12✔
246
            ParameterNameDiscoverer parameterNameDiscoverer) {
247

248
    }
249

250
  }
251

252
  /**
253
   * A default {@link ValueObject} implementation that uses only standard Java
254
   * reflection calls.
255
   */
256
  private static final class DefaultValueObject<T> extends ValueObject<T> {
257

258
    private final List<ConstructorParameter> constructorParameters;
259

260
    private DefaultValueObject(Constructor<T> constructor, List<ConstructorParameter> constructorParameters) {
261
      super(constructor);
3✔
262
      this.constructorParameters = constructorParameters;
3✔
263
    }
1✔
264

265
    @Override
266
    List<ConstructorParameter> getConstructorParameters() {
267
      return this.constructorParameters;
3✔
268
    }
269

270
    @Nullable
271
    @SuppressWarnings("unchecked")
272
    static <T> ValueObject<T> get(Constructor<?> bindConstructor, ResolvableType type, ParameterNameDiscoverer parameterNameDiscoverer) {
273
      String[] names = parameterNameDiscoverer.getParameterNames(bindConstructor);
4✔
274
      if (names == null) {
2!
275
        return null;
×
276
      }
277
      List<ConstructorParameter> constructorParameters = parseConstructorParameters(bindConstructor, type, names);
5✔
278
      return new DefaultValueObject<>((Constructor<T>) bindConstructor, constructorParameters);
6✔
279
    }
280

281
    private static List<ConstructorParameter> parseConstructorParameters(Constructor<?> constructor, ResolvableType type, String[] names) {
282
      Parameter[] parameters = constructor.getParameters();
3✔
283
      List<ConstructorParameter> result = new ArrayList<>(parameters.length);
6✔
284
      for (int i = 0; i < parameters.length; i++) {
8✔
285
        String name = MergedAnnotations.from(parameters[i])
5✔
286
                .get(Name.class)
6✔
287
                .getValue(MergedAnnotation.VALUE, String.class, names[i]);
3✔
288
        ResolvableType parameterType = ResolvableType.forMethodParameter(new MethodParameter(constructor, i), type);
8✔
289
        Annotation[] annotations = parameters[i].getDeclaredAnnotations();
5✔
290
        result.add(new ConstructorParameter(name, parameterType, annotations));
9✔
291
      }
292
      return Collections.unmodifiableList(result);
3✔
293
    }
294

295
  }
296

297
  /**
298
   * A constructor parameter being bound.
299
   */
300
  private static final class ConstructorParameter {
301

302
    public final String name;
303

304
    public final ResolvableType type;
305

306
    public final Annotation[] annotations;
307

308
    private ConstructorParameter(String name, ResolvableType type, Annotation[] annotations) {
2✔
309
      this.name = DataObjectPropertyName.toDashedForm(name);
4✔
310
      this.type = type;
3✔
311
      this.annotations = annotations;
3✔
312
    }
1✔
313

314
    @Nullable
315
    public Object bind(DataObjectPropertyBinder propertyBinder) {
316
      return propertyBinder.bindProperty(this.name, Bindable.of(this.type).withAnnotations(this.annotations));
11✔
317
    }
318

319
  }
320

321
  /**
322
   * {@link ParameterNameDiscoverer} used for value data object binding.
323
   */
324
  static final class Discoverer extends ParameterNameDiscoverer {
325

326
    private static final ParameterNameDiscoverer DEFAULT_DELEGATE = ParameterNameDiscoverer.getSharedInstance();
2✔
327

328
    private static final ParameterNameDiscoverer LENIENT = new Discoverer(DEFAULT_DELEGATE, message -> {
6✔
329

330
    });
×
331

332
    private static final ParameterNameDiscoverer STRICT = new Discoverer(DEFAULT_DELEGATE, message -> {
7✔
333
      throw new IllegalStateException(message.toString());
×
334
    });
335

336
    private final ParameterNameDiscoverer delegate;
337

338
    private final Consumer<LogMessage> noParameterNamesHandler;
339

340
    private Discoverer(ParameterNameDiscoverer delegate, Consumer<LogMessage> noParameterNamesHandler) {
2✔
341
      this.delegate = delegate;
3✔
342
      this.noParameterNamesHandler = noParameterNamesHandler;
3✔
343
    }
1✔
344

345
    @Override
346
    public String @Nullable [] getParameterNames(@Nullable Executable executable) {
347
      if (executable instanceof Method) {
3!
348
        throw new UnsupportedOperationException();
×
349
      }
350
      else if (executable instanceof Constructor<?> constructor) {
6!
351
        String[] names = delegate.getParameterNames(constructor);
5✔
352
        if (names != null) {
2!
353
          return names;
2✔
354
        }
355
        LogMessage message = LogMessage.format(
×
356
                "Unable to use value object binding with constructor [{}] as parameter names cannot be discovered. "
357
                        + "Ensure that the compiler uses the '-parameters' flag", constructor);
358
        this.noParameterNamesHandler.accept(message);
×
359
        logger.debug(message);
×
360
      }
361
      return null;
×
362
    }
363

364
  }
365

366
}
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