22
33import static io .github .problem4j .spring .web .ProblemSupport .IS_NOT_VALID_ERROR ;
44
5+ import java .lang .annotation .Annotation ;
6+ import java .lang .reflect .Constructor ;
7+ import java .lang .reflect .Parameter ;
8+ import java .lang .reflect .RecordComponent ;
59import java .util .ArrayList ;
10+ import java .util .Arrays ;
11+ import java .util .Collections ;
12+ import java .util .HashMap ;
613import java .util .List ;
14+ import java .util .Map ;
15+ import java .util .Optional ;
716import org .springframework .validation .BindingResult ;
817import org .springframework .validation .FieldError ;
918import org .springframework .validation .ObjectError ;
19+ import org .springframework .web .bind .annotation .BindParam ;
1020
1121/** Default implementation of {@link BindingResultSupport}. */
1222public class DefaultBindingResultSupport implements BindingResultSupport {
@@ -29,7 +39,10 @@ public List<Violation> fetchViolations(BindingResult result) {
2939 }
3040
3141 /**
32- * Converts a {@link FieldError} from a {@link BindingResult} into a {@link Violation}. *
42+ * Converts a {@link FieldError} from a {@link BindingResult} into a {@link Violation}.
43+ *
44+ * <p>Resolves a field error into a Violation, taking into account {@link BindParam} annotations
45+ * on the target object's constructor parameters.
3346 *
3447 * <p>{@code isBindingFailure() == true} usually means that there was a failure in creation of
3548 * object from values taken out of request. Most common one is validation error or type mismatch
@@ -40,10 +53,12 @@ public List<Violation> fetchViolations(BindingResult result) {
4053 * @return a {@link Violation} representing the field error
4154 */
4255 protected Violation resolveFieldError (BindingResult bindingResult , FieldError error ) {
56+ Map <String , String > parametersMetadata = findParametersMetadata (bindingResult );
57+ String field = parametersMetadata .getOrDefault (error .getField (), error .getField ());
4358 if (error .isBindingFailure ()) {
44- return new Violation (error . getField () , IS_NOT_VALID_ERROR );
59+ return new Violation (field , IS_NOT_VALID_ERROR );
4560 } else {
46- return new Violation (error . getField () , error .getDefaultMessage ());
61+ return new Violation (field , error .getDefaultMessage ());
4762 }
4863 }
4964
@@ -61,4 +76,91 @@ protected Violation resolveFieldError(BindingResult bindingResult, FieldError er
6176 protected Violation resolveGlobalError (BindingResult bindingResult , ObjectError error ) {
6277 return new Violation (null , error .getDefaultMessage ());
6378 }
79+
80+ /**
81+ * Reads metadata mapping for the target object of a BindingResult.
82+ *
83+ * @param bindingResult the BindingResult containing the target object
84+ * @return an unmodifiable map of parameter names to their bound names, or empty map if target is
85+ * {@code null}
86+ */
87+ protected Map <String , String > findParametersMetadata (BindingResult bindingResult ) {
88+ if (bindingResult .getTarget () != null ) {
89+ Class <?> target = bindingResult .getTarget ().getClass ();
90+ return computeConstructorMetadata (target );
91+ }
92+ return Map .of ();
93+ }
94+
95+ /**
96+ * Computes constructor metadata for the given class.
97+ *
98+ * @param target the class to analyze
99+ * @return an unmodifiable map of constructor parameter names to their bound names
100+ */
101+ protected Map <String , String > computeConstructorMetadata (Class <?> target ) {
102+ return findBindingConstructor (target )
103+ .filter (c -> c .getParameters ().length > 0 )
104+ .map (this ::getConstructorParameterMetadata )
105+ .orElseGet (Map ::of );
106+ }
107+
108+ /**
109+ * Finds the constructor that most likely was used for binding for the given class.
110+ *
111+ * <p>For records, returns the canonical constructor. For non-records, returns the single declared
112+ * constructor if only one exists.
113+ *
114+ * @param target the class to inspect
115+ * @return an {@code Optional} containing the binding constructor if found
116+ */
117+ protected Optional <Constructor <?>> findBindingConstructor (Class <?> target ) {
118+ if (target .isRecord ()) {
119+ Class <?>[] mainArgs =
120+ Arrays .stream (target .getRecordComponents ())
121+ .map (RecordComponent ::getType )
122+ .toArray (i -> new Class <?>[i ]);
123+ try {
124+ return Optional .of (target .getDeclaredConstructor (mainArgs ));
125+ } catch (NoSuchMethodException e ) {
126+ return Optional .empty ();
127+ }
128+ } else {
129+ // Non records are required to have single constructor anyway, otherwise binding will fail
130+ // and this code won't be called anyway
131+ Constructor <?>[] ctors = target .getDeclaredConstructors ();
132+ if (ctors .length == 1 ) {
133+ return Optional .of (ctors [0 ]);
134+ }
135+ }
136+ return Optional .empty ();
137+ }
138+
139+ /**
140+ * Extracts parameter metadata from the given constructor.
141+ *
142+ * <p>Each constructor parameter is added with its parameter name. {@link
143+ * org.springframework.web.bind.annotation.BindParam} is taken into account if present.
144+ *
145+ * @param constructor the constructor to inspect
146+ * @return an unmodifiable map of parameter names to their bound names
147+ */
148+ protected Map <String , String > getConstructorParameterMetadata (Constructor <?> constructor ) {
149+ Annotation [][] annotations = constructor .getParameterAnnotations ();
150+ Parameter [] parameters = constructor .getParameters ();
151+
152+ Map <String , String > metadata = new HashMap <>();
153+ for (int i = 0 ; i < parameters .length ; i ++) {
154+ String rawParamName = parameters [i ].getName ();
155+ metadata .put (rawParamName , rawParamName );
156+
157+ for (Annotation annotation : annotations [i ]) {
158+ if (annotation instanceof BindParam bindParam ) {
159+ String bindParamName = bindParam .value ();
160+ metadata .put (rawParamName , bindParamName );
161+ }
162+ }
163+ }
164+ return Collections .unmodifiableMap (metadata );
165+ }
64166}
0 commit comments