• 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

84.84
today-context/src/main/java/infra/validation/beanvalidation/MethodValidationAdapter.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.lang.reflect.Method;
23
import java.util.ArrayList;
24
import java.util.Collections;
25
import java.util.Comparator;
26
import java.util.Iterator;
27
import java.util.LinkedHashMap;
28
import java.util.LinkedHashSet;
29
import java.util.List;
30
import java.util.Map;
31
import java.util.Optional;
32
import java.util.Set;
33
import java.util.function.Function;
34
import java.util.function.Supplier;
35

36
import infra.aop.framework.AopProxyUtils;
37
import infra.aop.support.AopUtils;
38
import infra.context.MessageSourceResolvable;
39
import infra.context.support.DefaultMessageSourceResolvable;
40
import infra.core.BridgeMethodResolver;
41
import infra.core.Conventions;
42
import infra.core.DefaultParameterNameDiscoverer;
43
import infra.core.GenericTypeResolver;
44
import infra.core.MethodParameter;
45
import infra.core.ParameterNameDiscoverer;
46
import infra.core.annotation.AnnotationUtils;
47
import infra.lang.Assert;
48
import infra.validation.BeanPropertyBindingResult;
49
import infra.validation.BindingResult;
50
import infra.validation.DefaultMessageCodesResolver;
51
import infra.validation.Errors;
52
import infra.validation.MessageCodesResolver;
53
import infra.validation.annotation.Validated;
54
import infra.validation.method.MethodValidationResult;
55
import infra.validation.method.MethodValidator;
56
import infra.validation.method.ParameterErrors;
57
import infra.validation.method.ParameterValidationResult;
58
import jakarta.validation.ConstraintViolation;
59
import jakarta.validation.ElementKind;
60
import jakarta.validation.Path;
61
import jakarta.validation.Validation;
62
import jakarta.validation.Validator;
63
import jakarta.validation.ValidatorFactory;
64
import jakarta.validation.executable.ExecutableValidator;
65
import jakarta.validation.metadata.ConstraintDescriptor;
66

67
/**
68
 * {@link MethodValidator} that uses a Bean Validation
69
 * {@link jakarta.validation.Validator} for validation, and adapts
70
 * {@link ConstraintViolation}s to {@link MethodValidationResult}.
71
 *
72
 * @author Rossen Stoyanchev
73
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
74
 * @since 4.0 2023/6/15 22:23
75
 */
76
public class MethodValidationAdapter implements MethodValidator {
77

78
  private static final MethodValidationResult emptyValidationResult = MethodValidationResult.emptyResult();
2✔
79

80
  private static final ObjectNameResolver defaultObjectNameResolver = new DefaultObjectNameResolver();
4✔
81

82
  private static final Comparator<ParameterValidationResult> resultComparator = new ResultComparator();
5✔
83

84
  private final Validator validator;
85

86
  private final InfraValidatorAdapter validatorAdapter;
87

88
  private MessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver();
5✔
89

90
  private ParameterNameDiscoverer parameterNameDiscoverer = ParameterNameDiscoverer.getSharedInstance();
3✔
91

92
  private ObjectNameResolver objectNameResolver = defaultObjectNameResolver;
3✔
93

94
  /**
95
   * Create an instance using a default JSR-303 validator underneath.
96
   */
97
  public MethodValidationAdapter() {
98
    this(Validation.buildDefaultValidatorFactory());
3✔
99
  }
1✔
100

101
  /**
102
   * Create an instance using the given JSR-303 ValidatorFactory.
103
   *
104
   * @param validatorFactory the JSR-303 ValidatorFactory to use
105
   */
106
  public MethodValidationAdapter(ValidatorFactory validatorFactory) {
107
    this(validatorFactory::getValidator);
7✔
108
  }
1✔
109

110
  /**
111
   * Create an instance using the given JSR-303 Validator.
112
   *
113
   * @param validator the JSR-303 Validator to use
114
   */
115
  public MethodValidationAdapter(Validator validator) {
2✔
116
    Assert.notNull(validator, "Validator is required");
3✔
117
    this.validator = validator;
3✔
118
    this.validatorAdapter = new InfraValidatorAdapter(validator);
6✔
119
  }
1✔
120

121
  /**
122
   * Create an instance for the supplied (potentially lazily initialized) Validator.
123
   *
124
   * @param validator a Supplier for the Validator to use
125
   */
126
  public MethodValidationAdapter(Supplier<Validator> validator) {
127
    this(new SuppliedValidator(validator));
6✔
128
  }
1✔
129

130
  /**
131
   * Set the strategy to use to determine message codes for violations.
132
   * <p>Default is a DefaultMessageCodesResolver.
133
   */
134
  public void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) {
135
    this.messageCodesResolver = messageCodesResolver;
×
136
  }
×
137

138
  /**
139
   * Return the {@link #setMessageCodesResolver(MessageCodesResolver) configured}
140
   * {@code MessageCodesResolver}.
141
   */
142
  public MessageCodesResolver getMessageCodesResolver() {
143
    return this.messageCodesResolver;
×
144
  }
145

146
  /**
147
   * Set the {@code ParameterNameDiscoverer} to discover method parameter names
148
   * with to create error codes for {@link MessageSourceResolvable}. Used only
149
   * when {@link MethodParameter}s are not passed into
150
   * {@link #validateArguments} or {@link #validateReturnValue}.
151
   * <p>Default is {@link DefaultParameterNameDiscoverer}.
152
   */
153
  public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
154
    this.parameterNameDiscoverer = parameterNameDiscoverer;
×
155
  }
×
156

157
  /**
158
   * Return the {@link #setParameterNameDiscoverer configured}
159
   * {@code ParameterNameDiscoverer}.
160
   */
161
  public ParameterNameDiscoverer getParameterNameDiscoverer() {
162
    return this.parameterNameDiscoverer;
×
163
  }
164

165
  /**
166
   * Configure a resolver to determine the name of an {@code @Valid} method
167
   * parameter to use for its {@link BindingResult}. This allows aligning with
168
   * a higher level programming model such as to resolve the name of an
169
   * {@code @ModelAttribute} method parameter in Infra MVC.
170
   * <p>By default, the object name is resolved through:
171
   * <ul>
172
   * <li>{@link MethodParameter#getParameterName()} for input parameters
173
   * <li>{@link Conventions#getVariableNameForReturnType(Method, Class, Object)}
174
   * for a return type
175
   * </ul>
176
   * If a name cannot be determined, e.g. a return value with insufficient
177
   * type information, then it defaults to one of:
178
   * <ul>
179
   * <li>{@code "{methodName}.arg{index}"} for input parameters
180
   * <li>{@code "{methodName}.returnValue"} for a return type
181
   * </ul>
182
   */
183
  public void setObjectNameResolver(ObjectNameResolver nameResolver) {
184
    this.objectNameResolver = nameResolver;
3✔
185
  }
1✔
186

187
  /**
188
   * Return the {@link InfraValidatorAdapter} configured for use.
189
   *
190
   * @since 5.0
191
   */
192
  public InfraValidatorAdapter getValidatorAdapter() {
193
    return validatorAdapter;
3✔
194
  }
195

196
  /**
197
   * {@inheritDoc}.
198
   * <p>Default are the validation groups as specified in the {@link Validated}
199
   * annotation on the method, or on the containing target class of the method,
200
   * or for an AOP proxy without a target (with all behavior in advisors), also
201
   * check on proxied interfaces.
202
   */
203
  @Override
204
  public Class<?>[] determineValidationGroups(Object target, Method method) {
205
    Validated validatedAnn = AnnotationUtils.findAnnotation(method, Validated.class);
5✔
206
    if (validatedAnn == null) {
2✔
207
      if (AopUtils.isAopProxy(target)) {
3✔
208
        for (Class<?> type : AopProxyUtils.proxiedUserInterfaces(target)) {
16!
209
          validatedAnn = AnnotationUtils.findAnnotation(type, Validated.class);
5✔
210
          if (validatedAnn != null) {
2!
211
            break;
1✔
212
          }
213
        }
214
      }
215
      else {
216
        validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class);
6✔
217
      }
218
    }
219
    return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
8✔
220
  }
221

222
  @Override
223
  public final MethodValidationResult validateArguments(Object target, Method method,
224
          MethodParameter @Nullable [] parameters, @Nullable Object[] arguments, Class<?>[] groups) {
225

226
    Set<ConstraintViolation<Object>> violations =
5✔
227
            invokeValidatorForArguments(target, method, arguments, groups);
2✔
228

229
    if (violations.isEmpty()) {
3✔
230
      return emptyValidationResult;
2✔
231
    }
232

233
    return adaptViolations(target, method, violations,
12✔
234
            i -> parameters != null ? parameters[i] : initMethodParameter(method, i),
8!
235
            i -> arguments[i]);
5✔
236
  }
237

238
  /**
239
   * Invoke the validator, and return the resulting violations.
240
   */
241
  public final Set<ConstraintViolation<Object>> invokeValidatorForArguments(
242
          Object target, Method method, @Nullable Object[] arguments, Class<?>[] groups) {
243

244
    ExecutableValidator execVal = validator.forExecutables();
4✔
245
    Set<ConstraintViolation<Object>> violations;
246
    try {
247
      violations = execVal.validateParameters(target, method, arguments, groups);
7✔
248
    }
249
    catch (IllegalArgumentException ex) {
×
250
      // Let's try to find the bridged method on the implementation class...
251
      Method bridgedMethod = BridgeMethodResolver.getMostSpecificMethod(method, target.getClass());
×
252
      violations = execVal.validateParameters(target, bridgedMethod, arguments, groups);
×
253
    }
1✔
254
    return violations;
2✔
255
  }
256

257
  @Override
258
  public final MethodValidationResult validateReturnValue(Object target, Method method,
259
          @Nullable MethodParameter returnType, @Nullable Object returnValue, Class<?>[] groups) {
260

261
    Set<ConstraintViolation<Object>> violations =
5✔
262
            invokeValidatorForReturnValue(target, method, returnValue, groups);
2✔
263

264
    if (violations.isEmpty()) {
3✔
265
      return emptyValidationResult;
2✔
266
    }
267

268
    return adaptViolations(target, method, violations,
12✔
269
            i -> returnType != null ? returnType : initMethodParameter(method, -1),
7!
270
            i -> returnValue);
2✔
271
  }
272

273
  /**
274
   * Invoke the validator, and return the resulting violations.
275
   */
276
  public final Set<ConstraintViolation<Object>> invokeValidatorForReturnValue(
277
          Object target, Method method, @Nullable Object returnValue, Class<?>[] groups) {
278

279
    ExecutableValidator execVal = validator.forExecutables();
4✔
280
    return execVal.validateReturnValue(target, method, returnValue, groups);
7✔
281
  }
282

283
  private MethodValidationResult adaptViolations(Object target, Method method, Set<ConstraintViolation<Object>> violations,
284
          Function<Integer, MethodParameter> parameterFunction, Function<Integer, @Nullable Object> argumentFunction) {
285

286
    Map<Path.Node, ParamValidationResultBuilder> paramViolations = new LinkedHashMap<>();
4✔
287
    Map<Path.Node, ParamErrorsBuilder> nestedViolations = new LinkedHashMap<>();
4✔
288
    List<MessageSourceResolvable> crossParamErrors = null;
2✔
289

290
    for (ConstraintViolation<Object> violation : violations) {
10✔
291
      Iterator<Path.Node> nodes = violation.getPropertyPath().iterator();
4✔
292
      while (nodes.hasNext()) {
3!
293
        Path.Node node = nodes.next();
4✔
294

295
        MethodParameter parameter;
296
        if (node.getKind().equals(ElementKind.PARAMETER)) {
5✔
297
          int index = node.as(Path.ParameterNode.class).getParameterIndex();
6✔
298
          parameter = parameterFunction.apply(index);
6✔
299
        }
1✔
300
        else if (node.getKind().equals(ElementKind.RETURN_VALUE)) {
5✔
301
          parameter = parameterFunction.apply(-1);
7✔
302
        }
303
        else if (node.getKind().equals(ElementKind.CROSS_PARAMETER)) {
5✔
304
          crossParamErrors = (crossParamErrors != null ? crossParamErrors : new ArrayList<>());
6!
305
          crossParamErrors.add(createCrossParamError(target, method, violation));
8✔
306
          break;
1✔
307
        }
308
        else {
309
          continue;
310
        }
311

312
        Object arg = argumentFunction.apply(parameter.getParameterIndex());
6✔
313

314
        // If the arg is a container, we need the element, but the only way to extract it
315
        // is to check for and use a container index or key on the next node:
316
        // https://github.com/jakartaee/validation/issues/194
317

318
        Path.Node parameterNode = node;
2✔
319
        if (nodes.hasNext()) {
3✔
320
          node = nodes.next();
4✔
321
        }
322

323
        Object value;
324
        Object container;
325
        Integer index = node.getIndex();
3✔
326
        Object key = node.getKey();
3✔
327
        if (index != null && arg instanceof List<?> list) {
8✔
328
          value = list.get(index);
5✔
329
          container = list;
3✔
330
        }
331
        else if (index != null && arg instanceof Object[] array) {
8!
332
          value = array[index];
5✔
333
          container = array;
3✔
334
        }
335
        else if (key != null && arg instanceof Map<?, ?> map) {
8!
336
          value = map.get(key);
4✔
337
          container = map;
3✔
338
        }
339
        else if (arg instanceof Iterable<?>) {
3✔
340
          // No index or key, cannot access the specific value
341
          value = arg;
2✔
342
          container = arg;
3✔
343
        }
344
        else if (arg instanceof Optional<?> optional) {
6✔
345
          value = optional.orElse(null);
4✔
346
          container = optional;
3✔
347
        }
348
        else {
349
          value = arg;
2✔
350
          container = null;
2✔
351
        }
352

353
        if (node.getKind().equals(ElementKind.PROPERTY) || node.getKind().equals(ElementKind.BEAN)) {
10!
354
          nestedViolations.computeIfAbsent(parameterNode, k -> new ParamErrorsBuilder(parameter, value, container, index, key))
22✔
355
                  .addViolation(violation);
2✔
356
        }
357
        else {
358
          paramViolations.computeIfAbsent(parameterNode, p -> new ParamValidationResultBuilder(target, parameter, value, container, index, key))
24✔
359
                  .addViolation(violation);
1✔
360
        }
361

362
        break;
1✔
363
      }
364
    }
1✔
365

366
    List<ParameterValidationResult> resultList = new ArrayList<>();
4✔
367
    paramViolations.forEach((param, builder) -> resultList.add(builder.build()));
10✔
368
    nestedViolations.forEach((key, builder) -> resultList.add(builder.build()));
10✔
369
    resultList.sort(resultComparator);
3✔
370

371
    return MethodValidationResult.create(target, method, resultList,
5✔
372
            (crossParamErrors != null ? crossParamErrors : Collections.emptyList()));
5✔
373
  }
374

375
  private MethodParameter initMethodParameter(Method method, int index) {
376
    MethodParameter parameter = new MethodParameter(method, index);
6✔
377
    parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
4✔
378
    return parameter;
2✔
379
  }
380

381
  private MessageSourceResolvable createMessageSourceResolvable(
382
          Object target, MethodParameter parameter, ConstraintViolation<Object> violation) {
383

384
    String objectName = Conventions.getVariableName(target) + "#" + parameter.getExecutable().getName();
7✔
385
    String paramName = (parameter.getParameterName() != null ? parameter.getParameterName() : "");
8✔
386
    Class<?> parameterType = parameter.getParameterType();
3✔
387

388
    ConstraintDescriptor<?> descriptor = violation.getConstraintDescriptor();
3✔
389
    String code = descriptor.getAnnotation().annotationType().getSimpleName();
5✔
390
    String[] codes = messageCodesResolver.resolveMessageCodes(code, objectName, paramName, parameterType);
8✔
391
    Object[] arguments = validatorAdapter.getArgumentsForConstraint(objectName, paramName, descriptor);
7✔
392

393
    return new ViolationMessageSourceResolvable(codes, arguments, violation.getMessage(), violation);
9✔
394
  }
395

396
  private BindingResult createBindingResult(MethodParameter parameter, @Nullable Object argument) {
397
    String objectName = this.objectNameResolver.resolveName(parameter, argument);
6✔
398
    BeanPropertyBindingResult result = new BeanPropertyBindingResult(argument, objectName);
6✔
399
    result.setMessageCodesResolver(this.messageCodesResolver);
4✔
400
    return result;
2✔
401
  }
402

403
  private MessageSourceResolvable createCrossParamError(
404
          Object target, Method method, ConstraintViolation<Object> violation) {
405
    String objectName = Conventions.getVariableName(target) + "#" + method.getName();
6✔
406

407
    ConstraintDescriptor<?> descriptor = violation.getConstraintDescriptor();
3✔
408
    String code = descriptor.getAnnotation().annotationType().getSimpleName();
5✔
409
    String[] codes = this.messageCodesResolver.resolveMessageCodes(code, objectName);
6✔
410
    Object[] arguments = this.validatorAdapter.getArgumentsForConstraint(objectName, "", descriptor);
7✔
411

412
    return new ViolationMessageSourceResolvable(codes, arguments, violation.getMessage(), violation);
9✔
413
  }
414

415
  /**
416
   * Strategy to resolve the name of an {@code @Valid} method parameter to
417
   * use for its {@link BindingResult}.
418
   */
419
  public interface ObjectNameResolver {
420

421
    /**
422
     * Determine the name for the given method argument.
423
     *
424
     * @param parameter the method parameter
425
     * @param value the argument value or return value
426
     * @return the name to use
427
     */
428
    String resolveName(MethodParameter parameter, @Nullable Object value);
429

430
  }
431

432
  /**
433
   * Builds a validation result for a value method parameter with constraints
434
   * declared directly on it.
435
   */
436
  private final class ParamValidationResultBuilder {
437

438
    private final Object target;
439

440
    private final MethodParameter parameter;
441

442
    @Nullable
443
    private final Object value;
444

445
    @Nullable
446
    private final Object container;
447

448
    @Nullable
449
    private final Integer containerIndex;
450

451
    @Nullable
452
    private final Object containerKey;
453

454
    private final List<MessageSourceResolvable> resolvableErrors = new ArrayList<>();
5✔
455

456
    public ParamValidationResultBuilder(Object target, MethodParameter parameter,
457
            @Nullable Object value, @Nullable Object container, @Nullable Integer containerIndex, @Nullable Object containerKey) {
5✔
458
      this.target = target;
3✔
459
      this.parameter = parameter;
3✔
460
      this.value = value;
3✔
461
      this.container = container;
3✔
462
      this.containerIndex = containerIndex;
3✔
463
      this.containerKey = containerKey;
3✔
464
    }
1✔
465

466
    public void addViolation(ConstraintViolation<Object> violation) {
467
      this.resolvableErrors.add(createMessageSourceResolvable(this.target, this.parameter, violation));
12✔
468
    }
1✔
469

470
    public ParameterValidationResult build() {
471
      return new ParameterValidationResult(this.parameter, this.value, this.resolvableErrors,
17✔
472
              this.container, this.containerIndex, this.containerKey, (error, sourceType) -> {
473
        Assert.isTrue(sourceType.equals(ConstraintViolation.class), "Unexpected source type");
5✔
474
        return ((ViolationMessageSourceResolvable) error).getViolation();
4✔
475
      });
476
    }
477

478
  }
479

480
  /**
481
   * Builds a validation result for an {@link jakarta.validation.Valid @Valid}
482
   * annotated bean method parameter with cascaded constraints.
483
   */
484
  private final class ParamErrorsBuilder {
485

486
    private final MethodParameter parameter;
487

488
    @Nullable
489
    private final Object bean;
490

491
    @Nullable
492
    private final Object container;
493

494
    @Nullable
495
    private final Integer containerIndex;
496

497
    @Nullable
498
    private final Object containerKey;
499

500
    private final Errors errors;
501

502
    private final Set<ConstraintViolation<Object>> violations = new LinkedHashSet<>();
5✔
503

504
    public ParamErrorsBuilder(MethodParameter param, @Nullable Object bean,
505
            @Nullable Object container, @Nullable Integer containerIndex, @Nullable Object containerKey) {
5✔
506

507
      this.parameter = param;
3✔
508
      this.bean = bean;
3✔
509
      this.container = container;
3✔
510
      this.containerIndex = containerIndex;
3✔
511
      this.containerKey = containerKey;
3✔
512
      this.errors = createBindingResult(param, this.bean);
7✔
513
    }
1✔
514

515
    public void addViolation(ConstraintViolation<Object> violation) {
516
      this.violations.add(violation);
5✔
517
    }
1✔
518

519
    public ParameterErrors build() {
520
      validatorAdapter.processConstraintViolations(this.violations, this.errors);
8✔
521
      return new ParameterErrors(
16✔
522
              this.parameter, this.bean, this.errors, this.container,
523
              this.containerIndex, this.containerKey);
524
    }
525
  }
526

527
  @SuppressWarnings("serial")
528
  private static final class ViolationMessageSourceResolvable extends DefaultMessageSourceResolvable {
529

530
    private final transient ConstraintViolation<Object> violation;
531

532
    public ViolationMessageSourceResolvable(
533
            String[] codes, Object[] arguments, String defaultMessage, ConstraintViolation<Object> violation) {
534

535
      super(codes, arguments, defaultMessage);
5✔
536
      this.violation = violation;
3✔
537
    }
1✔
538

539
    public ConstraintViolation<Object> getViolation() {
540
      return this.violation;
3✔
541
    }
542
  }
543

544
  /**
545
   * Default algorithm to select an object name, as described in
546
   * {@link #setObjectNameResolver(ObjectNameResolver)}.
547
   */
548
  private static final class DefaultObjectNameResolver implements ObjectNameResolver {
549

550
    @Override
551
    public String resolveName(MethodParameter parameter, @Nullable Object value) {
552
      String objectName = null;
2✔
553
      if (parameter.getParameterIndex() != -1) {
4✔
554
        objectName = parameter.getParameterName();
4✔
555
      }
556
      else {
557
        try {
558
          Method method = parameter.getMethod();
3✔
559
          if (method != null) {
2!
560
            Class<?> containingClass = parameter.getContainingClass();
3✔
561
            Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass);
4✔
562
            objectName = Conventions.getVariableNameForReturnType(method, resolvedType, value);
5✔
563
          }
564
        }
565
        catch (IllegalArgumentException ex) {
×
566
          // insufficient type information
567
        }
1✔
568
      }
569
      if (objectName == null) {
2!
570
        int index = parameter.getParameterIndex();
×
571
        objectName = (parameter.getExecutable().getName() + (index != -1 ? ".arg" + index : ".returnValue"));
×
572
      }
573
      return objectName;
2✔
574
    }
575
  }
576

577
  /**
578
   * Comparator for validation results, sorted by method parameter index first,
579
   * also falling back on container indexes if necessary for cascaded
580
   * constraints on a List container.
581
   */
582
  private static final class ResultComparator implements Comparator<ParameterValidationResult> {
583

584
    @Override
585
    public int compare(ParameterValidationResult result1, ParameterValidationResult result2) {
586
      int index1 = result1.getMethodParameter().getParameterIndex();
4✔
587
      int index2 = result2.getMethodParameter().getParameterIndex();
4✔
588
      int i = Integer.compare(index1, index2);
4✔
589
      if (i != 0) {
2✔
590
        return i;
2✔
591
      }
592
      if (result1 instanceof ParameterErrors errors1 && result2 instanceof ParameterErrors errors2) {
12!
593
        Integer containerIndex1 = errors1.getContainerIndex();
3✔
594
        Integer containerIndex2 = errors2.getContainerIndex();
3✔
595
        if (containerIndex1 != null && containerIndex2 != null) {
4!
596
          i = Integer.compare(containerIndex1, containerIndex2);
6✔
597
          if (i != 0) {
2!
598
            return i;
2✔
599
          }
600
        }
601
        i = compareKeys(errors1, errors2);
×
602
        return i;
×
603
      }
604
      return 0;
×
605
    }
606

607
    @SuppressWarnings("unchecked")
608
    private <E> int compareKeys(ParameterErrors errors1, ParameterErrors errors2) {
609
      Object key1 = errors1.getContainerKey();
×
610
      Object key2 = errors2.getContainerKey();
×
611
      if (key1 instanceof Comparable<?> && key2 instanceof Comparable<?>) {
×
612
        return ((Comparable<E>) key1).compareTo((E) key2);
×
613
      }
614
      return 0;
×
615
    }
616
  }
617

618
}
619

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