• 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

88.7
today-context/src/main/java/infra/validation/AbstractBindingResult.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;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.beans.PropertyEditor;
23
import java.io.Serial;
24
import java.io.Serializable;
25
import java.util.ArrayList;
26
import java.util.Collections;
27
import java.util.HashMap;
28
import java.util.HashSet;
29
import java.util.LinkedHashMap;
30
import java.util.List;
31
import java.util.Map;
32

33
import infra.beans.PropertyEditorRegistry;
34
import infra.lang.Assert;
35
import infra.util.ObjectUtils;
36
import infra.util.StringUtils;
37

38
/**
39
 * Abstract implementation of the {@link BindingResult} interface and
40
 * its super-interface {@link Errors}. Encapsulates common management of
41
 * {@link ObjectError ObjectErrors} and {@link FieldError FieldErrors}.
42
 *
43
 * @author Juergen Hoeller
44
 * @author Rob Harrop
45
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
46
 * @see Errors
47
 * @since 4.0
48
 */
49
public abstract class AbstractBindingResult extends AbstractErrors implements BindingResult, Serializable {
50

51
  @Serial
52
  private static final long serialVersionUID = 1L;
53

54
  private final String objectName;
55

56
  private MessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver();
5✔
57

58
  private final ArrayList<ObjectError> errors = new ArrayList<>();
5✔
59
  private final HashSet<String> suppressedFields = new HashSet<>();
5✔
60
  private final HashMap<String, Object> fieldValues = new HashMap<>();
5✔
61
  private final HashMap<String, Class<?>> fieldTypes = new HashMap<>();
5✔
62

63
  /**
64
   * Create a new AbstractBindingResult instance.
65
   *
66
   * @param objectName the name of the target object
67
   * @see DefaultMessageCodesResolver
68
   */
69
  protected AbstractBindingResult(String objectName) {
2✔
70
    this.objectName = objectName;
3✔
71
  }
1✔
72

73
  /**
74
   * Set the strategy to use for resolving errors into message codes.
75
   * Default is DefaultMessageCodesResolver.
76
   *
77
   * @see DefaultMessageCodesResolver
78
   */
79
  public void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) {
80
    Assert.notNull(messageCodesResolver, "MessageCodesResolver is required");
3✔
81
    this.messageCodesResolver = messageCodesResolver;
3✔
82
  }
1✔
83

84
  /**
85
   * Return the strategy to use for resolving errors into message codes.
86
   */
87
  public MessageCodesResolver getMessageCodesResolver() {
88
    return this.messageCodesResolver;
3✔
89
  }
90

91
  //---------------------------------------------------------------------
92
  // Implementation of the Errors interface
93
  //---------------------------------------------------------------------
94

95
  @Override
96
  public String getObjectName() {
97
    return this.objectName;
3✔
98
  }
99

100
  @Override
101
  public void reject(String errorCode, Object @Nullable [] errorArgs, @Nullable String defaultMessage) {
102
    addError(new ObjectError(getObjectName(), resolveMessageCodes(errorCode), errorArgs, defaultMessage));
12✔
103
  }
1✔
104

105
  @Override
106
  public void rejectValue(@Nullable String field, String errorCode,
107
          Object @Nullable [] errorArgs, @Nullable String defaultMessage) {
108

109
    if (StringUtils.isEmpty(getNestedPath()) && StringUtils.isEmpty(field)) {
7!
110
      // We're at the top of the nested object hierarchy,
111
      // so the present level is not a field but rather the top object.
112
      // The best we can do is register a global error here...
113
      reject(errorCode, errorArgs, defaultMessage);
×
114
      return;
×
115
    }
116

117
    String fixedField = fixedField(field);
4✔
118
    Object newVal = getActualFieldValue(fixedField);
4✔
119
    FieldError fe = new FieldError(getObjectName(), fixedField, newVal, false,
10✔
120
            resolveMessageCodes(errorCode, field), errorArgs, defaultMessage);
5✔
121
    addError(fe);
3✔
122
  }
1✔
123

124
  @Override
125
  public void addAllErrors(Errors errors) {
126
    if (!errors.getObjectName().equals(getObjectName())) {
6!
127
      throw new IllegalArgumentException("Errors object needs to have same object name");
×
128
    }
129
    this.errors.addAll(errors.getAllErrors());
6✔
130
  }
1✔
131

132
  @Override
133
  public boolean hasErrors() {
134
    return !this.errors.isEmpty();
8✔
135
  }
136

137
  @Override
138
  public int getErrorCount() {
139
    return this.errors.size();
4✔
140
  }
141

142
  @Override
143
  public List<ObjectError> getAllErrors() {
144
    return Collections.unmodifiableList(this.errors);
4✔
145
  }
146

147
  @Override
148
  public List<ObjectError> getGlobalErrors() {
149
    ArrayList<ObjectError> result = new ArrayList<>();
4✔
150
    for (ObjectError objectError : this.errors) {
11✔
151
      if (!(objectError instanceof FieldError)) {
3✔
152
        result.add(objectError);
4✔
153
      }
154
    }
1✔
155
    return Collections.unmodifiableList(result);
3✔
156
  }
157

158
  @Override
159
  @Nullable
160
  public ObjectError getGlobalError() {
161
    for (ObjectError objectError : this.errors) {
11✔
162
      if (!(objectError instanceof FieldError)) {
3✔
163
        return objectError;
2✔
164
      }
165
    }
1✔
166
    return null;
2✔
167
  }
168

169
  @Override
170
  public List<FieldError> getFieldErrors() {
171
    ArrayList<FieldError> result = new ArrayList<>();
4✔
172
    for (ObjectError objectError : this.errors) {
11✔
173
      if (objectError instanceof FieldError) {
3✔
174
        result.add((FieldError) objectError);
5✔
175
      }
176
    }
1✔
177
    return Collections.unmodifiableList(result);
3✔
178
  }
179

180
  @Override
181
  @Nullable
182
  public FieldError getFieldError() {
183
    for (ObjectError objectError : this.errors) {
11✔
184
      if (objectError instanceof FieldError) {
3!
185
        return (FieldError) objectError;
3✔
186
      }
187
    }
×
188
    return null;
2✔
189
  }
190

191
  @Override
192
  public List<FieldError> getFieldErrors(String field) {
193
    ArrayList<FieldError> result = new ArrayList<>();
4✔
194
    String fixedField = fixedField(field);
4✔
195
    for (ObjectError objectError : this.errors) {
11✔
196
      if (objectError instanceof FieldError && isMatchingFieldError(fixedField, (FieldError) objectError)) {
9✔
197
        result.add((FieldError) objectError);
5✔
198
      }
199
    }
1✔
200
    return Collections.unmodifiableList(result);
3✔
201
  }
202

203
  @Override
204
  @Nullable
205
  public FieldError getFieldError(String field) {
206
    String fixedField = fixedField(field);
4✔
207
    for (ObjectError objectError : this.errors) {
11✔
208
      if (objectError instanceof FieldError fieldError) {
6✔
209
        if (isMatchingFieldError(fixedField, fieldError)) {
5✔
210
          return fieldError;
2✔
211
        }
212
      }
213
    }
1✔
214
    return null;
2✔
215
  }
216

217
  @Override
218
  @Nullable
219
  public Object getFieldValue(String field) {
220
    FieldError fieldError = getFieldError(field);
4✔
221
    // Use rejected value in case of error, current field value otherwise.
222
    if (fieldError != null) {
2✔
223
      Object value = fieldError.getRejectedValue();
3✔
224
      // Do not apply formatting on binding failures like type mismatches.
225
      return fieldError.isBindingFailure() || getTarget() == null ? value : formatFieldValue(field, value);
13✔
226
    }
227
    else if (getTarget() != null) {
3✔
228
      Object value = getActualFieldValue(fixedField(field));
6✔
229
      return formatFieldValue(field, value);
5✔
230
    }
231
    else {
232
      return this.fieldValues.get(field);
5✔
233
    }
234
  }
235

236
  /**
237
   * This default implementation determines the type based on the actual
238
   * field value, if any. Subclasses should override this to determine
239
   * the type from a descriptor, even for {@code null} values.
240
   *
241
   * @see #getActualFieldValue
242
   */
243
  @Override
244
  @Nullable
245
  public Class<?> getFieldType(@Nullable String field) {
246
    if (getTarget() != null) {
3✔
247
      Object value = getActualFieldValue(fixedField(field));
6✔
248
      if (value != null) {
2!
249
        return value.getClass();
×
250
      }
251
    }
252
    return this.fieldTypes.get(field);
6✔
253
  }
254

255
  //---------------------------------------------------------------------
256
  // Implementation of BindingResult interface
257
  //---------------------------------------------------------------------
258

259
  /**
260
   * Return a model Map for the obtained state, exposing an Errors
261
   * instance as '{@link #MODEL_KEY_PREFIX MODEL_KEY_PREFIX} + objectName'
262
   * and the object itself.
263
   * <p>Note that the Map is constructed every time you're calling this method.
264
   * Adding things to the map and then re-calling this method will not work.
265
   * <p>The attributes in the model Map returned by this method are usually
266
   * included in the ModelAndView for a form view that uses Framework's bind tag,
267
   * which needs access to the Errors instance.
268
   *
269
   * @see #getObjectName
270
   * @see #MODEL_KEY_PREFIX
271
   */
272
  @Override
273
  @SuppressWarnings("NullAway")
274
  public Map<String, Object> getModel() {
275
    LinkedHashMap<String, Object> model = new LinkedHashMap<>(2);
5✔
276
    // Mapping from name to target object.
277
    model.put(getObjectName(), getTarget());
7✔
278
    // Errors instance, even if no errors.
279
    model.put(MODEL_KEY_PREFIX + getObjectName(), this);
8✔
280
    return model;
2✔
281
  }
282

283
  @Override
284
  @Nullable
285
  public Object getRawFieldValue(String field) {
286
    return getTarget() != null ? getActualFieldValue(fixedField(field)) : null;
11✔
287
  }
288

289
  /**
290
   * This implementation delegates to the
291
   * {@link #getPropertyEditorRegistry() PropertyEditorRegistry}'s
292
   * editor lookup facility, if available.
293
   */
294
  @Override
295
  @Nullable
296
  public PropertyEditor findEditor(@Nullable String field, @Nullable Class<?> valueType) {
297
    PropertyEditorRegistry editorRegistry = getPropertyEditorRegistry();
3✔
298
    if (editorRegistry != null) {
2!
299
      Class<?> valueTypeToUse = valueType;
2✔
300
      if (valueTypeToUse == null) {
2!
301
        valueTypeToUse = getFieldType(field);
×
302
      }
303
      return editorRegistry.findCustomEditor(valueTypeToUse, fixedField(field));
7✔
304
    }
305
    else {
306
      return null;
×
307
    }
308
  }
309

310
  /**
311
   * This implementation returns {@code null}.
312
   */
313
  @Override
314
  @Nullable
315
  public PropertyEditorRegistry getPropertyEditorRegistry() {
316
    return null;
×
317
  }
318

319
  @Override
320
  public String[] resolveMessageCodes(String errorCode) {
321
    return getMessageCodesResolver().resolveMessageCodes(errorCode, getObjectName());
7✔
322
  }
323

324
  @Override
325
  public String[] resolveMessageCodes(String errorCode, @Nullable String field) {
326
    return getMessageCodesResolver().resolveMessageCodes(
6✔
327
            errorCode, getObjectName(), fixedField(field), getFieldType(field));
7✔
328
  }
329

330
  @Override
331
  public void addError(ObjectError error) {
332
    this.errors.add(error);
5✔
333
  }
1✔
334

335
  @SuppressWarnings("NullAway")
336
  @Override
337
  public void recordFieldValue(String field, Class<?> type, @Nullable Object value) {
338
    this.fieldTypes.put(field, type);
6✔
339
    this.fieldValues.put(field, value);
6✔
340
  }
1✔
341

342
  /**
343
   * Mark the specified disallowed field as suppressed.
344
   * <p>The data binder invokes this for each field value that was
345
   * detected to target a disallowed field.
346
   *
347
   * @see DataBinder#setAllowedFields
348
   */
349
  @Override
350
  public void recordSuppressedField(String field) {
351
    this.suppressedFields.add(field);
5✔
352
  }
1✔
353

354
  /**
355
   * Return the list of fields that were suppressed during the bind process.
356
   * <p>Can be used to determine whether any field values were targeting
357
   * disallowed fields.
358
   *
359
   * @see DataBinder#setAllowedFields
360
   */
361
  @Override
362
  public String[] getSuppressedFields() {
363
    return StringUtils.toStringArray(this.suppressedFields);
4✔
364
  }
365

366
  @Override
367
  public boolean equals(@Nullable Object other) {
368
    if (this == other) {
3✔
369
      return true;
2✔
370
    }
371
    if (!(other instanceof BindingResult otherResult)) {
7!
372
      return false;
×
373
    }
374
    return (getObjectName().equals(otherResult.getObjectName())
8!
375
            && ObjectUtils.nullSafeEquals(getTarget(), otherResult.getTarget())
6!
376
            && getAllErrors().equals(otherResult.getAllErrors()));
8✔
377
  }
378

379
  @Override
380
  public int hashCode() {
381
    return getObjectName().hashCode();
×
382
  }
383

384
  //---------------------------------------------------------------------
385
  // Template methods to be implemented/overridden by subclasses
386
  //---------------------------------------------------------------------
387

388
  /**
389
   * Return the wrapped target object.
390
   */
391
  @Override
392
  @Nullable
393
  public abstract Object getTarget();
394

395
  /**
396
   * Extract the actual field value for the given field.
397
   *
398
   * @param field the field to check
399
   * @return the current value of the field
400
   */
401
  @Nullable
402
  protected abstract Object getActualFieldValue(String field);
403

404
  /**
405
   * Format the given value for the specified field.
406
   * <p>The default implementation simply returns the field value as-is.
407
   *
408
   * @param field the field to check
409
   * @param value the value of the field (either a rejected value
410
   * other than from a binding error, or an actual field value)
411
   * @return the formatted value
412
   */
413
  @Nullable
414
  protected Object formatFieldValue(String field, @Nullable Object value) {
415
    return value;
×
416
  }
417

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