• 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

80.68
today-context/src/main/java/infra/validation/beanvalidation/InfraValidatorAdapter.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.validation.beanvalidation;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.io.Serial;
23
import java.io.Serializable;
24
import java.util.ArrayList;
25
import java.util.LinkedHashSet;
26
import java.util.Map;
27
import java.util.Set;
28
import java.util.TreeMap;
29

30
import infra.beans.NotReadablePropertyException;
31
import infra.context.MessageSourceResolvable;
32
import infra.context.support.DefaultMessageSourceResolvable;
33
import infra.lang.Assert;
34
import infra.util.ClassUtils;
35
import infra.util.StringUtils;
36
import infra.validation.BindingResult;
37
import infra.validation.DefaultBindingErrorProcessor;
38
import infra.validation.Errors;
39
import infra.validation.FieldError;
40
import infra.validation.MessageCodesResolver;
41
import infra.validation.ObjectError;
42
import infra.validation.SmartValidator;
43
import jakarta.validation.ConstraintViolation;
44
import jakarta.validation.ElementKind;
45
import jakarta.validation.Path;
46
import jakarta.validation.ValidationException;
47
import jakarta.validation.Validator;
48
import jakarta.validation.executable.ExecutableValidator;
49
import jakarta.validation.metadata.BeanDescriptor;
50
import jakarta.validation.metadata.ConstraintDescriptor;
51

52
/**
53
 * Adapter that takes a JSR-303 {@code javax.validator.Validator} and
54
 * exposes it as a Framework {@link infra.validation.Validator}
55
 * while also exposing the original JSR-303 Validator interface itself.
56
 *
57
 * <p>Can be used as a programmatic wrapper. Also serves as base class for
58
 * {@link CustomValidatorBean} and {@link LocalValidatorFactoryBean},
59
 * and as the primary implementation of the {@link SmartValidator} interface.
60
 *
61
 * <p>this adapter is fully compatible with
62
 * Bean Validation 1.1 as well as 2.0.
63
 *
64
 * @author Juergen Hoeller
65
 * @author Sam Brannen
66
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
67
 * @see SmartValidator
68
 * @see CustomValidatorBean
69
 * @see LocalValidatorFactoryBean
70
 * @since 4.0
71
 */
72
public class InfraValidatorAdapter implements SmartValidator, jakarta.validation.Validator {
73

74
  private static final Set<String> internalAnnotationAttributes = Set.of("message", "groups", "payload");
6✔
75

76
  @Nullable
77
  Validator targetValidator;
78

79
  /**
80
   * Create a new ContextValidatorAdapter for the given JSR-303 Validator.
81
   *
82
   * @param targetValidator the JSR-303 Validator to wrap
83
   */
84
  public InfraValidatorAdapter(jakarta.validation.Validator targetValidator) {
2✔
85
    Assert.notNull(targetValidator, "Target Validator is required");
3✔
86
    this.targetValidator = targetValidator;
3✔
87
  }
1✔
88

89
  InfraValidatorAdapter() {
2✔
90
  }
1✔
91

92
  void setTargetValidator(jakarta.validation.Validator targetValidator) {
93
    this.targetValidator = targetValidator;
3✔
94
  }
1✔
95

96
  //---------------------------------------------------------------------
97
  // Implementation of Framework Validator interface
98
  //---------------------------------------------------------------------
99

100
  @Override
101
  public boolean supports(Class<?> clazz) {
102
    return (this.targetValidator != null);
6!
103
  }
104

105
  @Override
106
  public void validate(Object target, Errors errors) {
107
    if (this.targetValidator != null) {
3!
108
      processConstraintViolations(this.targetValidator.validate(target), errors);
9✔
109
    }
110
  }
1✔
111

112
  @Override
113
  public void validate(Object target, Errors errors, Object... validationHints) {
114
    if (this.targetValidator != null) {
×
115
      processConstraintViolations(
×
116
              this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
×
117
    }
118
  }
×
119

120
  @SuppressWarnings({ "unchecked", "rawtypes" })
121
  @Override
122
  public void validateValue(Class<?> targetType, String fieldName,
123
          @Nullable Object value, Errors errors, Object... validationHints) {
124

125
    if (this.targetValidator != null) {
3!
126
      processConstraintViolations(this.targetValidator.validateValue(
11✔
127
              (Class) targetType, fieldName, value, asValidationGroups(validationHints)), errors);
1✔
128
    }
129
  }
1✔
130

131
  /**
132
   * Turn the specified validation hints into JSR-303 validation groups.
133
   */
134
  private Class<?>[] asValidationGroups(Object... validationHints) {
135
    Set<Class<?>> groups = new LinkedHashSet<>(4);
5✔
136
    for (Object hint : validationHints) {
10!
137
      if (hint instanceof Class<?> clazz) {
×
138
        groups.add(clazz);
×
139
      }
140
    }
141
    return ClassUtils.toClassArray(groups);
3✔
142
  }
143

144
  /**
145
   * Process the given JSR-303 ConstraintViolations, adding corresponding errors to
146
   * the provided Framework {@link Errors} object.
147
   *
148
   * @param violations the JSR-303 ConstraintViolation results
149
   * @param errors the Framework errors object to register to
150
   */
151
  protected void processConstraintViolations(Set<ConstraintViolation<Object>> violations, Errors errors) {
152
    for (ConstraintViolation<Object> violation : violations) {
10✔
153
      String field = determineField(violation);
4✔
154
      FieldError fieldError = errors.getFieldError(field);
4✔
155
      if (fieldError == null || !fieldError.isBindingFailure()) {
5!
156
        try {
157
          ConstraintDescriptor<?> cd = violation.getConstraintDescriptor();
3✔
158
          String errorCode = determineErrorCode(cd);
4✔
159
          Object[] errorArgs = getArgumentsForConstraint(errors.getObjectName(), field, cd);
7✔
160
          if (errors instanceof BindingResult bindingResult) {
6!
161
            // Can do custom FieldError registration with invalid value from ConstraintViolation,
162
            // as necessary for Hibernate Validator compatibility (non-indexed set path in field)
163
            String nestedField = bindingResult.getNestedPath() + field;
5✔
164
            if (nestedField.isEmpty()) {
3✔
165
              String[] errorCodes = bindingResult.resolveMessageCodes(errorCode);
4✔
166
              ObjectError error = new ViolationObjectError(
3✔
167
                      errors.getObjectName(), errorCodes, errorArgs, violation, this);
7✔
168
              bindingResult.addError(error);
3✔
169
            }
1✔
170
            else {
171
              Object rejectedValue = getRejectedValue(field, violation, bindingResult);
6✔
172
              String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field);
5✔
173
              FieldError error = new ViolationFieldError(errors.getObjectName(), nestedField,
12✔
174
                      rejectedValue, errorCodes, errorArgs, violation, this);
175
              bindingResult.addError(error);
3✔
176
            }
177
          }
1✔
178
          else {
179
            // Got no BindingResult - can only do standard rejectValue call
180
            // with automatic extraction of the current field value
181
            errors.rejectValue(field, errorCode, errorArgs, violation.getMessage());
×
182
          }
183
        }
184
        catch (NotReadablePropertyException ex) {
×
185
          throw new IllegalStateException("JSR-303 validated property '" + field +
×
186
                  "' does not have a corresponding accessor for Framework data binding - " +
187
                  "check your DataBinder's configuration (bean property versus direct field access)", ex);
188
        }
1✔
189
      }
190
    }
1✔
191
  }
1✔
192

193
  /**
194
   * Determine a field for the given constraint violation.
195
   * <p>The default implementation returns the stringified property path.
196
   *
197
   * @param violation the current JSR-303 ConstraintViolation
198
   * @return the Framework-reported field (for use with {@link Errors})
199
   * @see ConstraintViolation#getPropertyPath()
200
   * @see FieldError#getField()
201
   */
202
  protected String determineField(ConstraintViolation<Object> violation) {
203
    Path path = violation.getPropertyPath();
3✔
204
    StringBuilder sb = new StringBuilder();
4✔
205
    boolean first = true;
2✔
206
    for (Path.Node node : path) {
10✔
207
      if (node.isInIterable() && !first) {
5✔
208
        sb.append('[');
4✔
209
        Object index = node.getIndex();
3✔
210
        if (index == null) {
2✔
211
          index = node.getKey();
3✔
212
        }
213
        if (index != null) {
2✔
214
          sb.append(index);
4✔
215
        }
216
        sb.append(']');
4✔
217
      }
218
      String name = node.getName();
3✔
219
      if (name != null && node.getKind() == ElementKind.PROPERTY && !name.startsWith("<")) {
10!
220
        if (!first) {
2✔
221
          sb.append('.');
4✔
222
        }
223
        first = false;
2✔
224
        sb.append(name);
4✔
225
      }
226
    }
1✔
227
    return sb.toString();
3✔
228
  }
229

230
  /**
231
   * Determine a Framework-reported error code for the given constraint descriptor.
232
   * <p>The default implementation returns the simple class name of the descriptor's
233
   * annotation type. Note that the configured
234
   * {@link MessageCodesResolver} will automatically
235
   * generate error code variations which include the object name and the field name.
236
   *
237
   * @param descriptor the JSR-303 ConstraintDescriptor for the current violation
238
   * @return a corresponding error code (for use with {@link Errors})
239
   * @see ConstraintDescriptor#getAnnotation()
240
   * @see MessageCodesResolver
241
   */
242
  protected String determineErrorCode(ConstraintDescriptor<?> descriptor) {
243
    return descriptor.getAnnotation().annotationType().getSimpleName();
5✔
244
  }
245

246
  /**
247
   * Return FieldError arguments for a validation error on the given field.
248
   * Invoked for each violated constraint.
249
   * <p>The default implementation returns a first argument indicating the field name
250
   * (see {@link #getResolvableField}). Afterwards, it adds all actual constraint
251
   * annotation attributes (i.e. excluding "message", "groups" and "payload") in
252
   * alphabetical order of their attribute names.
253
   * <p>Can be overridden to e.g. add further attributes from the constraint descriptor.
254
   *
255
   * @param objectName the name of the target object
256
   * @param field the field that caused the binding error
257
   * @param descriptor the JSR-303 constraint descriptor
258
   * @return the Object array that represents the FieldError arguments
259
   * @see FieldError#getArguments
260
   * @see DefaultMessageSourceResolvable
261
   * @see DefaultBindingErrorProcessor#getArgumentsForBindError
262
   */
263
  protected Object[] getArgumentsForConstraint(
264
          String objectName, String field, ConstraintDescriptor<?> descriptor) {
265
    ArrayList<Object> arguments = new ArrayList<>();
4✔
266
    arguments.add(getResolvableField(objectName, field));
7✔
267
    // Using a TreeMap for alphabetical ordering of attribute names
268
    TreeMap<String, Object> attributesToExpose = new TreeMap<>();
4✔
269

270
    for (Map.Entry<String, Object> entry : descriptor.getAttributes().entrySet()) {
12✔
271
      String attributeName = entry.getKey();
4✔
272
      Object attributeValue = entry.getValue();
3✔
273
      if (!internalAnnotationAttributes.contains(attributeName)) {
4✔
274
        if (attributeValue instanceof String str) {
6✔
275
          attributeValue = new ResolvableAttribute(str);
5✔
276
        }
277
        attributesToExpose.put(attributeName, attributeValue);
5✔
278
      }
279
    }
1✔
280

281
    arguments.addAll(attributesToExpose.values());
5✔
282
    return arguments.toArray();
3✔
283
  }
284

285
  /**
286
   * Build a resolvable wrapper for the specified field, allowing to resolve the field's
287
   * name in a {@code MessageSource}.
288
   * <p>The default implementation returns a first argument indicating the field:
289
   * of type {@code DefaultMessageSourceResolvable}, with "objectName.field" and "field"
290
   * as codes, and with the plain field name as default message.
291
   *
292
   * @param objectName the name of the target object
293
   * @param field the field that caused the binding error
294
   * @return a corresponding {@code MessageSourceResolvable} for the specified field
295
   * @see #getArgumentsForConstraint
296
   */
297
  protected MessageSourceResolvable getResolvableField(String objectName, String field) {
298
    String[] codes = StringUtils.hasText(field)
3✔
299
            ? new String[] { objectName + Errors.NESTED_PATH_SEPARATOR + field, field }
13✔
300
            : new String[] { objectName };
7✔
301
    return new DefaultMessageSourceResolvable(codes, field);
6✔
302
  }
303

304
  /**
305
   * Extract the rejected value behind the given constraint violation,
306
   * for exposure through the Framework errors representation.
307
   *
308
   * @param field the field that caused the binding error
309
   * @param violation the corresponding JSR-303 ConstraintViolation
310
   * @param bindingResult a Framework BindingResult for the backing object
311
   * which contains the current field's value
312
   * @return the invalid value to expose as part of the field error
313
   * @see ConstraintViolation#getInvalidValue()
314
   * @see FieldError#getRejectedValue()
315
   */
316
  @Nullable
317
  protected Object getRejectedValue(
318
          String field, ConstraintViolation<Object> violation, BindingResult bindingResult) {
319
    Object invalidValue = violation.getInvalidValue();
3✔
320
    if (!field.isEmpty() && !field.contains("[]") &&
9!
321
            (invalidValue == violation.getLeafBean() || field.contains("[") || field.contains("."))) {
10✔
322
      // Possibly a bean constraint with property path: retrieve the actual property value.
323
      // However, explicitly avoid this for "address[]" style paths that we can't handle.
324
      invalidValue = bindingResult.getRawFieldValue(field);
4✔
325
    }
326
    return invalidValue;
2✔
327
  }
328

329
  /**
330
   * Indicate whether this violation's interpolated message has remaining
331
   * placeholders and therefore requires {@link java.text.MessageFormat}
332
   * to be applied to it. Called for a Bean Validation defined message
333
   * (coming out {@code ValidationMessages.properties}) when rendered
334
   * as the default message in Framework's MessageSource.
335
   * <p>The default implementation considers a Framework-style "{0}" placeholder
336
   * for the field name as an indication for {@link java.text.MessageFormat}.
337
   * Any other placeholder or escape syntax occurrences are typically a
338
   * mismatch, coming out of regex pattern values or the like. Note that
339
   * standard Bean Validation does not support "{0}" style placeholders at all;
340
   * this is a feature typically used in Framework MessageSource resource bundles.
341
   *
342
   * @param violation the Bean Validation constraint violation, including
343
   * BV-defined interpolation of named attribute references in its message
344
   * @return {@code true} if {@code java.text.MessageFormat} is to be applied,
345
   * or {@code false} if the violation's message should be used as-is
346
   * @see #getArgumentsForConstraint
347
   */
348
  protected boolean requiresMessageFormat(ConstraintViolation<?> violation) {
349
    return containsInfraStylePlaceholder(violation.getMessage());
4✔
350
  }
351

352
  private static boolean containsInfraStylePlaceholder(@Nullable String message) {
353
    return message != null && message.contains("{0}");
8!
354
  }
355

356
  //---------------------------------------------------------------------
357
  // Implementation of JSR-303 Validator interface
358
  //---------------------------------------------------------------------
359

360
  @Override
361
  public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
362
    return targetValidator().validate(object, groups);
6✔
363
  }
364

365
  @Override
366
  public <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups) {
367
    return targetValidator().validateProperty(object, propertyName, groups);
×
368
  }
369

370
  @Override
371
  public <T> Set<ConstraintViolation<T>> validateValue(
372
          Class<T> beanType, String propertyName, Object value, Class<?>... groups) {
373

374
    return targetValidator().validateValue(beanType, propertyName, value, groups);
×
375
  }
376

377
  @Override
378
  public BeanDescriptor getConstraintsForClass(Class<?> clazz) {
379
    return targetValidator().getConstraintsForClass(clazz);
×
380
  }
381

382
  @Override
383
  @SuppressWarnings("unchecked")
384
  public <T> T unwrap(@Nullable Class<T> type) {
385
    Validator targetValidator = targetValidator();
3✔
386
    try {
387
      return type != null ? targetValidator.unwrap(type) : (T) targetValidator;
7!
388
    }
389
    catch (ValidationException ex) {
1✔
390
      // Ignore if just being asked for plain JSR-303 Validator
391
      if (jakarta.validation.Validator.class == type) {
3!
392
        return (T) targetValidator;
×
393
      }
394
      throw ex;
2✔
395
    }
396
  }
397

398
  @Override
399
  public ExecutableValidator forExecutables() {
400
    return targetValidator().forExecutables();
4✔
401
  }
402

403
  private Validator targetValidator() {
404
    Validator validator = targetValidator;
3✔
405
    Assert.state(validator != null, "No target Validator set");
6!
406
    return validator;
2✔
407
  }
408

409
  /**
410
   * Wrapper for a String attribute which can be resolved via a {@code MessageSource},
411
   * falling back to the original attribute as a default value otherwise.
412
   */
413
  private record ResolvableAttribute(String resolvableString) implements MessageSourceResolvable, Serializable {
6✔
414

415
    @Override
416
    public String[] getCodes() {
417
      return new String[] { this.resolvableString };
8✔
418
    }
419

420
    @Override
421
    public String getDefaultMessage() {
422
      return this.resolvableString;
3✔
423
    }
424

425
    @Override
426
    public String toString() {
427
      return this.resolvableString;
3✔
428
    }
429
  }
430

431
  /**
432
   * Subclass of {@code ObjectError} with Infra-style default message rendering.
433
   */
434
  private static class ViolationObjectError extends ObjectError implements Serializable {
435

436
    @Serial
437
    private static final long serialVersionUID = 1L;
438

439
    @Nullable
440
    private final transient InfraValidatorAdapter adapter;
441

442
    @Nullable
443
    private final transient ConstraintViolation<?> violation;
444

445
    public ViolationObjectError(String objectName, String[] codes, Object[] arguments,
446
            ConstraintViolation<?> violation, InfraValidatorAdapter adapter) {
447

448
      super(objectName, codes, arguments, violation.getMessage());
7✔
449
      this.adapter = adapter;
3✔
450
      this.violation = violation;
3✔
451
      wrap(violation);
3✔
452
    }
1✔
453

454
    @Override
455
    public boolean shouldRenderDefaultMessage() {
456
      return (this.adapter != null && this.violation != null ?
×
457
              this.adapter.requiresMessageFormat(this.violation) :
×
458
              containsInfraStylePlaceholder(getDefaultMessage()));
×
459
    }
460
  }
461

462
  /**
463
   * Subclass of {@code FieldError} with Framework-style default message rendering.
464
   */
465
  private static class ViolationFieldError extends FieldError implements Serializable {
466

467
    @Serial
468
    private static final long serialVersionUID = 1L;
469

470
    @Nullable
471
    private final transient InfraValidatorAdapter adapter;
472

473
    @Nullable
474
    private final transient ConstraintViolation<?> violation;
475

476
    public ViolationFieldError(String objectName, String field,
477
            @Nullable Object rejectedValue, String[] codes,
478
            Object[] arguments, ConstraintViolation<?> violation, InfraValidatorAdapter adapter) {
479

480
      super(objectName, field, rejectedValue, false, codes, arguments, violation.getMessage());
10✔
481
      this.adapter = adapter;
3✔
482
      this.violation = violation;
3✔
483
      wrap(violation);
3✔
484
    }
1✔
485

486
    @Override
487
    public boolean shouldRenderDefaultMessage() {
488
      return this.adapter != null && this.violation != null
7!
489
              ? this.adapter.requiresMessageFormat(this.violation)
6✔
490
              : containsInfraStylePlaceholder(getDefaultMessage());
×
491
    }
492
  }
493

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