Skip to content

Commit 1e64a38

Browse files
Make BindParam-aware method default
1 parent 0011371 commit 1e64a38

3 files changed

Lines changed: 114 additions & 166 deletions

File tree

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.github.problem4j.spring.web.autoconfigure;
22

3-
import io.github.problem4j.spring.web.parameter.BindParamAwareResultSupport;
43
import io.github.problem4j.spring.web.parameter.BindingResultSupport;
54
import io.github.problem4j.spring.web.parameter.DefaultBindingResultSupport;
65
import io.github.problem4j.spring.web.parameter.DefaultMethodParameterSupport;
@@ -12,9 +11,7 @@
1211
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
1312
import org.springframework.context.annotation.Bean;
1413
import org.springframework.context.annotation.Configuration;
15-
import org.springframework.core.annotation.Order;
1614
import org.springframework.validation.method.MethodValidationResult;
17-
import org.springframework.web.bind.annotation.BindParam;
1815

1916
/**
2017
* Configuration for parameter support components, such as method parameter name resolution and
@@ -52,36 +49,14 @@ MethodValidationResultSupport problemMethodValidationResultSupport(
5249
}
5350
}
5451

55-
@Order(0)
56-
@ConditionalOnClass(BindParam.class)
57-
@Configuration(proxyBeanMethods = false)
58-
static class ProblemBeanParamAwareBindingConfiguration {
59-
60-
/**
61-
* Provides a {@link BindingResultSupport} bean that handles {@code BindParam}-annotations.
62-
*
63-
* @return a new {@link BindParamAwareResultSupport}
64-
*/
65-
@ConditionalOnMissingBean(BindingResultSupport.class)
66-
@Bean
67-
BindingResultSupport problemBindingResultSupport() {
68-
return new BindParamAwareResultSupport();
69-
}
70-
}
71-
72-
@Order(1)
73-
@Configuration(proxyBeanMethods = false)
74-
static class ProblemDefaultBindingConfiguration {
75-
76-
/**
77-
* Provides a default {@link BindingResultSupport} bean that uses field names for violations.
78-
*
79-
* @return a new {@link DefaultBindingResultSupport}
80-
*/
81-
@ConditionalOnMissingBean(BindingResultSupport.class)
82-
@Bean
83-
BindingResultSupport problemBindingSupport() {
84-
return new DefaultBindingResultSupport();
85-
}
52+
/**
53+
* Provides a default {@link BindingResultSupport} bean that uses field names for violations.
54+
*
55+
* @return a new {@link DefaultBindingResultSupport}
56+
*/
57+
@ConditionalOnMissingBean(BindingResultSupport.class)
58+
@Bean
59+
BindingResultSupport problemBindingSupport() {
60+
return new DefaultBindingResultSupport();
8661
}
8762
}

problem4j-spring-web/src/main/java/io/github/problem4j/spring/web/parameter/BindParamAwareResultSupport.java

Lines changed: 0 additions & 129 deletions
This file was deleted.

problem4j-spring-web/src/main/java/io/github/problem4j/spring/web/parameter/DefaultBindingResultSupport.java

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@
22

33
import 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;
59
import java.util.ArrayList;
10+
import java.util.Arrays;
11+
import java.util.Collections;
12+
import java.util.HashMap;
613
import java.util.List;
14+
import java.util.Map;
15+
import java.util.Optional;
716
import org.springframework.validation.BindingResult;
817
import org.springframework.validation.FieldError;
918
import org.springframework.validation.ObjectError;
19+
import org.springframework.web.bind.annotation.BindParam;
1020

1121
/** Default implementation of {@link BindingResultSupport}. */
1222
public 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

Comments
 (0)