• 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

87.5
today-context/src/main/java/infra/context/properties/bind/validation/ValidationBindHandler.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.validation;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.io.Serial;
23
import java.util.LinkedHashMap;
24
import java.util.LinkedHashSet;
25
import java.util.Map;
26
import java.util.Set;
27
import java.util.stream.Collectors;
28

29
import infra.beans.NotReadablePropertyException;
30
import infra.context.properties.bind.AbstractBindHandler;
31
import infra.context.properties.bind.BindContext;
32
import infra.context.properties.bind.BindHandler;
33
import infra.context.properties.bind.Bindable;
34
import infra.context.properties.bind.DataObjectPropertyName;
35
import infra.context.properties.source.ConfigurationProperty;
36
import infra.context.properties.source.ConfigurationPropertyName;
37
import infra.core.ResolvableType;
38
import infra.lang.Assert;
39
import infra.util.ObjectUtils;
40
import infra.validation.AbstractBindingResult;
41
import infra.validation.BeanPropertyBindingResult;
42
import infra.validation.Validator;
43

44
/**
45
 * {@link BindHandler} to apply {@link Validator Validators} to bound results.
46
 *
47
 * @author Phillip Webb
48
 * @author Madhura Bhave
49
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
50
 * @since 4.0
51
 */
52
public class ValidationBindHandler extends AbstractBindHandler {
53

54
  private final Validator[] validators;
55

56
  private final Map<ConfigurationPropertyName, ResolvableType> boundTypes = new LinkedHashMap<>();
10✔
57

58
  private final Map<ConfigurationPropertyName, Object> boundResults = new LinkedHashMap<>();
10✔
59

60
  private final Set<ConfigurationProperty> boundProperties = new LinkedHashSet<>();
10✔
61

62
  @Nullable
63
  private BindValidationException exception;
64

65
  public ValidationBindHandler(Validator... validators) {
2✔
66
    this.validators = validators;
3✔
67
  }
1✔
68

69
  public ValidationBindHandler(BindHandler parent, Validator... validators) {
70
    super(parent);
3✔
71
    this.validators = validators;
3✔
72
  }
1✔
73

74
  @Nullable
75
  @Override
76
  public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target, BindContext context) {
77
    this.boundTypes.put(name, target.getType());
7✔
78
    return super.onStart(name, target, context);
6✔
79
  }
80

81
  @Nullable
82
  @Override
83
  public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
84
    this.boundResults.put(name, result);
6✔
85
    if (context.getConfigurationProperty() != null) {
3✔
86
      this.boundProperties.add(context.getConfigurationProperty());
6✔
87
    }
88
    return super.onSuccess(name, target, context, result);
7✔
89
  }
90

91
  @Nullable
92
  @Override
93
  public Object onFailure(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Exception error)
94
          throws Exception {
95
    Object result = super.onFailure(name, target, context, error);
7✔
96
    if (result != null) {
2✔
97
      clear();
2✔
98
      this.boundResults.put(name, result);
6✔
99
    }
100
    validate(name, target, context, result);
6✔
101
    return result;
2✔
102
  }
103

104
  private void clear() {
105
    this.boundTypes.clear();
3✔
106
    this.boundResults.clear();
3✔
107
    this.boundProperties.clear();
3✔
108
    this.exception = null;
3✔
109
  }
1✔
110

111
  @Override
112
  public void onFinish(ConfigurationPropertyName name, Bindable<?> target, BindContext context, @Nullable Object result)
113
          throws Exception {
114
    validate(name, target, context, result);
6✔
115
    super.onFinish(name, target, context, result);
6✔
116
  }
1✔
117

118
  @SuppressWarnings("NullAway")
119
  private void validate(ConfigurationPropertyName name, Bindable<?> target, BindContext context, @Nullable Object result) {
120
    if (this.exception == null) {
3✔
121
      Object validationTarget = getValidationTarget(target, context, result);
6✔
122
      Class<?> validationType = target.getBoxedType().resolve();
4✔
123
      if (validationTarget != null) {
2✔
124
        Assert.state(validationType != null, "'validationType' is required");
6!
125
        validateAndPush(name, validationTarget, validationType);
5✔
126
      }
127
    }
128
    if (context.getDepth() == 0 && this.exception != null) {
6✔
129
      throw this.exception;
3✔
130
    }
131
  }
1✔
132

133
  @Nullable
134
  private Object getValidationTarget(Bindable<?> target, BindContext context, @Nullable Object result) {
135
    if (result != null) {
2✔
136
      return result;
2✔
137
    }
138
    if (context.getDepth() == 0 && target.getValue() != null) {
6✔
139
      return target.getValue().get();
4✔
140
    }
141
    return null;
2✔
142
  }
143

144
  private void validateAndPush(ConfigurationPropertyName name, Object target, Class<?> type) {
145
    ValidationResult result = null;
2✔
146
    for (Validator validator : validators) {
17✔
147
      if (validator.supports(type)) {
4✔
148
        if (result == null) {
2✔
149
          result = new ValidationResult(name, target);
7✔
150
        }
151
        validator.validate(target, result);
4✔
152
      }
153
    }
154
    if (result != null && result.hasErrors()) {
5✔
155
      this.exception = new BindValidationException(result.getValidationErrors());
7✔
156
    }
157
  }
1✔
158

159
  /**
160
   * {@link AbstractBindingResult} implementation backed by the bound properties.
161
   */
162
  private class ValidationResult extends BeanPropertyBindingResult {
163

164
    @Serial
165
    private static final long serialVersionUID = 1L;
166

167
    private final ConfigurationPropertyName name;
168

169
    protected ValidationResult(ConfigurationPropertyName name, Object target) {
3✔
170
      super(target, "");
4✔
171
      this.name = name;
3✔
172
    }
1✔
173

174
    @Override
175
    public String getObjectName() {
176
      return this.name.toString();
4✔
177
    }
178

179
    @Nullable
180
    @Override
181
    public Class<?> getFieldType(@Nullable String field) {
182
      ResolvableType type = getBoundField(ValidationBindHandler.this.boundTypes, field);
8✔
183
      Class<?> resolved = (type != null) ? type.resolve() : null;
7✔
184
      if (resolved != null) {
2✔
185
        return resolved;
2✔
186
      }
187
      return super.getFieldType(field);
4✔
188
    }
189

190
    @Nullable
191
    @Override
192
    protected Object getActualFieldValue(String field) {
193
      Object boundField = getBoundField(ValidationBindHandler.this.boundResults, field);
7✔
194
      if (boundField != null) {
2✔
195
        return boundField;
2✔
196
      }
197
      try {
198
        return super.getActualFieldValue(field);
4✔
199
      }
200
      catch (Exception ex) {
×
201
        if (isPropertyNotReadable(ex)) {
×
202
          return null;
×
203
        }
204
        throw ex;
×
205
      }
206
    }
207

208
    private boolean isPropertyNotReadable(Throwable ex) {
209
      while (ex != null) {
×
210
        if (ex instanceof NotReadablePropertyException) {
×
211
          return true;
×
212
        }
213
        ex = ex.getCause();
×
214
      }
215
      return false;
×
216
    }
217

218
    @Nullable
219
    private <T> T getBoundField(Map<ConfigurationPropertyName, T> boundFields, @Nullable String field) {
220
      if (field == null) {
2!
221
        return null;
×
222
      }
223
      try {
224
        ConfigurationPropertyName name = getName(field);
4✔
225
        T bound = boundFields.get(name);
4✔
226
        if (bound != null) {
2✔
227
          return bound;
2✔
228
        }
229
        if (name.hasIndexedElement()) {
3✔
230
          for (Map.Entry<ConfigurationPropertyName, T> entry : boundFields.entrySet()) {
11!
231
            if (isFieldNameMatch(entry.getKey(), name)) {
7✔
232
              return entry.getValue();
3✔
233
            }
234
          }
1✔
235
        }
236
      }
237
      catch (Exception ignored) {
×
238
      }
1✔
239
      return null;
2✔
240
    }
241

242
    private boolean isFieldNameMatch(ConfigurationPropertyName name, ConfigurationPropertyName fieldName) {
243
      if (name.getNumberOfElements() != fieldName.getNumberOfElements()) {
5✔
244
        return false;
2✔
245
      }
246
      for (int i = 0; i < name.getNumberOfElements(); i++) {
8✔
247
        String element = name.getElement(i, ConfigurationPropertyName.Form.ORIGINAL);
5✔
248
        String fieldElement = fieldName.getElement(i, ConfigurationPropertyName.Form.ORIGINAL);
5✔
249
        if (!ObjectUtils.nullSafeEquals(element, fieldElement)) {
4✔
250
          return false;
2✔
251
        }
252
      }
253
      return true;
2✔
254
    }
255

256
    private ConfigurationPropertyName getName(String field) {
257
      return this.name.append(DataObjectPropertyName.toDashedForm(field));
6✔
258
    }
259

260
    ValidationErrors getValidationErrors() {
261
      Set<ConfigurationProperty> boundProperties = ValidationBindHandler.this.boundProperties.stream()
6✔
262
              .filter((property) -> this.name.isAncestorOf(property.getName()))
8✔
263
              .collect(Collectors.toCollection(LinkedHashSet::new));
4✔
264
      return new ValidationErrors(this.name, boundProperties, getAllErrors());
9✔
265
    }
266

267
  }
268

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