From af5eb31f415f104916893abecc4525849592b3ec Mon Sep 17 00:00:00 2001 From: Jimmy Praet Date: Thu, 26 Sep 2024 15:02:50 +0200 Subject: [PATCH] Retrieve bean validation parameter names from Spring MVC / JAX-RS annotations (#105) Fixes #96 --- .../github/belgif/rest/problem/Frontend.java | 10 +-- .../belgif/rest/problem/FrontendImpl.java | 20 +++--- .../github/belgif/rest/problem/Frontend.java | 10 +-- .../belgif/rest/problem/FrontendImpl.java | 20 +++--- .../rest/problem/ControllerInterface.java | 4 +- .../rest/problem/FrontendController.java | 26 +++---- .../rest/problem/ControllerInterface.java | 4 +- .../rest/problem/FrontendController.java | 26 +++---- .../jaxrs/JaxRsParameterNameProvider.java | 68 +++++++++++++++++++ .../main/resources/META-INF/validation.xml | 9 +++ .../jaxrs/JaxRsParameterNameProviderTest.java | 65 ++++++++++++++++++ belgif-rest-problem-spring-boot-2/pom.xml | 4 ++ .../AnnotationParameterNameDiscoverer.java | 67 ++++++++++++++++++ .../spring/ProblemValidatorConfiguration.java | 38 +++++++++++ ...AnnotationParameterNameDiscovererTest.java | 66 ++++++++++++++++++ .../ProblemValidatorConfigurationTest.java | 22 ++++++ src/main/asciidoc/index.adoc | 11 +++ 17 files changed, 410 insertions(+), 60 deletions(-) create mode 100644 belgif-rest-problem-java-ee/src/main/java/io/github/belgif/rest/problem/jaxrs/JaxRsParameterNameProvider.java create mode 100644 belgif-rest-problem-java-ee/src/main/resources/META-INF/validation.xml create mode 100644 belgif-rest-problem-java-ee/src/test/java/io/github/belgif/rest/problem/jaxrs/JaxRsParameterNameProviderTest.java create mode 100644 belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/AnnotationParameterNameDiscoverer.java create mode 100644 belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/ProblemValidatorConfiguration.java create mode 100644 belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/AnnotationParameterNameDiscovererTest.java create mode 100644 belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/ProblemValidatorConfigurationTest.java diff --git a/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/Frontend.java b/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/Frontend.java index 466c9e58..99623784 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/Frontend.java +++ b/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/Frontend.java @@ -56,20 +56,20 @@ public interface Frontend { @GET @Path("/beanValidation/queryParameter") - Response beanValidationQueryParameter(@QueryParam("param") @NotNull @Positive Integer param, - @QueryParam("other") @Size(max = 5) String other); + Response beanValidationQueryParameter(@QueryParam("param") @NotNull @Positive Integer p, + @QueryParam("other") @Size(max = 5) String o); @GET @Path("/beanValidation/headerParameter") - Response beanValidationHeaderParameter(@HeaderParam("param") @NotNull @Positive Integer param); + Response beanValidationHeaderParameter(@HeaderParam("param") @NotNull @Positive Integer p); @GET @Path("/beanValidation/pathParameter/inherited/{param}") - Response beanValidationPathParameterInherited(@PathParam("param") @NotNull @Positive Integer param); + Response beanValidationPathParameterInherited(@PathParam("param") @NotNull @Positive Integer p); @GET @Path("/beanValidation/pathParameter/overridden/{param}") - Response beanValidationPathParameterOverridden(@PathParam("param") @NotNull @Positive Integer param); + Response beanValidationPathParameterOverridden(@PathParam("param") @NotNull @Positive Integer p); @POST @Path("/beanValidation/body") diff --git a/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java b/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java index c0ae33c9..19483f75 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java +++ b/belgif-rest-problem-it/belgif-rest-problem-jakarta-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java @@ -190,31 +190,31 @@ public Response unmappedFromBackend(@QueryParam("client") Client client) { } @Override - public Response beanValidationQueryParameter(Integer param, String other) { - return Response.ok("param: " + param + ", other: " + other).build(); + public Response beanValidationQueryParameter(Integer p, String o) { + return Response.ok("param: " + p + ", other: " + o).build(); } @Override - public Response beanValidationHeaderParameter(Integer param) { - return Response.ok("param: " + param).build(); + public Response beanValidationHeaderParameter(Integer p) { + return Response.ok("param: " + p).build(); } @GET @Path("/beanValidation/pathParameter/class/{param}") - public Response beanValidationPathParameter(@PathParam("param") @NotNull @Positive Integer param) { - return Response.ok("param: " + param).build(); + public Response beanValidationPathParameter(@PathParam("param") @NotNull @Positive Integer p) { + return Response.ok("param: " + p).build(); } @Override - public Response beanValidationPathParameterInherited(Integer param) { - return Response.ok("param: " + param).build(); + public Response beanValidationPathParameterInherited(Integer p) { + return Response.ok("param: " + p).build(); } @Override @GET @Path("/beanValidation/pathParameter/overridden") - public Response beanValidationPathParameterOverridden(@QueryParam("param") @NotNull @Positive Integer param) { - return Response.ok("param: " + param).build(); + public Response beanValidationPathParameterOverridden(@QueryParam("param") @NotNull @Positive Integer p) { + return Response.ok("param: " + p).build(); } @Override diff --git a/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/Frontend.java b/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/Frontend.java index b7bc0a95..f13a7a22 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/Frontend.java +++ b/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/Frontend.java @@ -56,20 +56,20 @@ public interface Frontend { @GET @Path("/beanValidation/queryParameter") - Response beanValidationQueryParameter(@QueryParam("param") @NotNull @Positive Integer param, - @QueryParam("other") @Size(max = 5) String other); + Response beanValidationQueryParameter(@QueryParam("param") @NotNull @Positive Integer p, + @QueryParam("other") @Size(max = 5) String o); @GET @Path("/beanValidation/headerParameter") - Response beanValidationHeaderParameter(@HeaderParam("param") @NotNull @Positive Integer param); + Response beanValidationHeaderParameter(@HeaderParam("param") @NotNull @Positive Integer p); @GET @Path("/beanValidation/pathParameter/inherited/{param}") - Response beanValidationPathParameterInherited(@PathParam("param") @NotNull @Positive Integer param); + Response beanValidationPathParameterInherited(@PathParam("param") @NotNull @Positive Integer p); @GET @Path("/beanValidation/pathParameter/overridden/{param}") - Response beanValidationPathParameterOverridden(@PathParam("param") @NotNull @Positive Integer param); + Response beanValidationPathParameterOverridden(@PathParam("param") @NotNull @Positive Integer p); @POST @Path("/beanValidation/body") diff --git a/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java b/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java index 7acc9d6b..31a343ca 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java +++ b/belgif-rest-problem-it/belgif-rest-problem-java-ee-it/src/main/java/io/github/belgif/rest/problem/FrontendImpl.java @@ -189,31 +189,31 @@ public Response unmappedFromBackend(@QueryParam("client") Client client) { } @Override - public Response beanValidationQueryParameter(Integer param, String other) { - return Response.ok("param: " + param + ", other: " + other).build(); + public Response beanValidationQueryParameter(Integer p, String o) { + return Response.ok("param: " + p + ", other: " + o).build(); } @Override - public Response beanValidationHeaderParameter(Integer param) { - return Response.ok("header: " + param).build(); + public Response beanValidationHeaderParameter(Integer p) { + return Response.ok("header: " + p).build(); } @GET @Path("/beanValidation/pathParameter/class/{param}") - public Response beanValidationPathParameter(@PathParam("param") @NotNull @Positive Integer param) { - return Response.ok("param: " + param).build(); + public Response beanValidationPathParameter(@PathParam("param") @NotNull @Positive Integer p) { + return Response.ok("param: " + p).build(); } @Override - public Response beanValidationPathParameterInherited(Integer param) { - return Response.ok("param: " + param).build(); + public Response beanValidationPathParameterInherited(Integer p) { + return Response.ok("param: " + p).build(); } @Override @GET @Path("/beanValidation/pathParameter/overridden") - public Response beanValidationPathParameterOverridden(@QueryParam("param") @NotNull @Positive Integer param) { - return Response.ok("param: " + param).build(); + public Response beanValidationPathParameterOverridden(@QueryParam("param") @NotNull @Positive Integer p) { + return Response.ok("param: " + p).build(); } @Override diff --git a/belgif-rest-problem-it/belgif-rest-problem-spring-boot-2-it/src/main/java/io/github/belgif/rest/problem/ControllerInterface.java b/belgif-rest-problem-it/belgif-rest-problem-spring-boot-2-it/src/main/java/io/github/belgif/rest/problem/ControllerInterface.java index e088cfa5..4f616835 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-spring-boot-2-it/src/main/java/io/github/belgif/rest/problem/ControllerInterface.java +++ b/belgif-rest-problem-it/belgif-rest-problem-spring-boot-2-it/src/main/java/io/github/belgif/rest/problem/ControllerInterface.java @@ -15,10 +15,10 @@ public interface ControllerInterface { @GetMapping("/beanValidation/pathParameter/inherited/{param}") ResponseEntity beanValidationPathParameterInherited( - @PathVariable("param") @Positive @NotNull Integer param); + @PathVariable("param") @Positive @NotNull Integer p); @GetMapping("/beanValidation/pathParameter/overridden/{param}") ResponseEntity beanValidationPathParameterOverridden( - @PathVariable("param") @Positive @NotNull Integer param); + @PathVariable("param") @Positive @NotNull Integer p); } diff --git a/belgif-rest-problem-it/belgif-rest-problem-spring-boot-2-it/src/main/java/io/github/belgif/rest/problem/FrontendController.java b/belgif-rest-problem-it/belgif-rest-problem-spring-boot-2-it/src/main/java/io/github/belgif/rest/problem/FrontendController.java index 43bc6166..ff9e980c 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-spring-boot-2-it/src/main/java/io/github/belgif/rest/problem/FrontendController.java +++ b/belgif-rest-problem-it/belgif-rest-problem-spring-boot-2-it/src/main/java/io/github/belgif/rest/problem/FrontendController.java @@ -150,32 +150,32 @@ public void unmappedFromBackend(@RequestParam("client") Client client) { @GetMapping("/beanValidation/queryParameter") public ResponseEntity beanValidationQueryParameter( - @RequestParam("param") @Positive @NotNull Integer param, - @RequestParam("other") @Size(max = 5) String other) { - return ResponseEntity.ok("param: " + param + ", other: " + other); + @RequestParam("param") @Positive @NotNull Integer p, + @RequestParam @Size(max = 5) String other) { + return ResponseEntity.ok("param: " + p + ", other: " + other); } @GetMapping("/beanValidation/headerParameter") public ResponseEntity beanValidationHeaderParameter( - @RequestHeader("param") @Positive @NotNull Integer param) { - return ResponseEntity.ok("param: " + param); + @RequestHeader("param") @Positive @NotNull Integer p) { + return ResponseEntity.ok("param: " + p); } @GetMapping("/beanValidation/pathParameter/class/{param}") public ResponseEntity beanValidationPathParameter( - @PathVariable("param") @Positive @NotNull Integer param) { - return ResponseEntity.ok("param: " + param); + @PathVariable("param") @Positive @NotNull Integer p) { + return ResponseEntity.ok("param: " + p); } @Override - public ResponseEntity beanValidationPathParameterInherited(Integer param) { - return ResponseEntity.ok("param: " + param); + public ResponseEntity beanValidationPathParameterInherited(Integer p) { + return ResponseEntity.ok("param: " + p); } @Override @GetMapping("/beanValidation/pathParameter/overridden") - public ResponseEntity beanValidationPathParameterOverridden(@RequestParam Integer param) { - return ResponseEntity.ok("param: " + param); + public ResponseEntity beanValidationPathParameterOverridden(@RequestParam("param") Integer p) { + return ResponseEntity.ok("param: " + p); } @PostMapping("/beanValidation/body") @@ -194,8 +194,8 @@ public ResponseEntity beanValidationBodyInheritance(@Valid @RequestBody } @PostMapping("/beanValidation/queryParameter/nested") - public ResponseEntity beanValidationQueryParameterNested(@Valid Model param) { - return ResponseEntity.ok("param: " + param); + public ResponseEntity beanValidationQueryParameterNested(@Valid Model p) { + return ResponseEntity.ok("param: " + p); } } diff --git a/belgif-rest-problem-it/belgif-rest-problem-spring-boot-3-it/src/main/java/io/github/belgif/rest/problem/ControllerInterface.java b/belgif-rest-problem-it/belgif-rest-problem-spring-boot-3-it/src/main/java/io/github/belgif/rest/problem/ControllerInterface.java index a125b039..3d39fd72 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-spring-boot-3-it/src/main/java/io/github/belgif/rest/problem/ControllerInterface.java +++ b/belgif-rest-problem-it/belgif-rest-problem-spring-boot-3-it/src/main/java/io/github/belgif/rest/problem/ControllerInterface.java @@ -15,10 +15,10 @@ public interface ControllerInterface { @GetMapping("/beanValidation/pathParameter/inherited/{param}") ResponseEntity beanValidationPathParameterInherited( - @PathVariable("param") @Positive @NotNull Integer param); + @PathVariable("param") @Positive @NotNull Integer p); @GetMapping("/beanValidation/pathParameter/overridden/{param}") ResponseEntity beanValidationPathParameterOverridden( - @PathVariable("param") @Positive @NotNull Integer param); + @PathVariable("param") @Positive @NotNull Integer p); } diff --git a/belgif-rest-problem-it/belgif-rest-problem-spring-boot-3-it/src/main/java/io/github/belgif/rest/problem/FrontendController.java b/belgif-rest-problem-it/belgif-rest-problem-spring-boot-3-it/src/main/java/io/github/belgif/rest/problem/FrontendController.java index 39b916e5..2c1bf450 100644 --- a/belgif-rest-problem-it/belgif-rest-problem-spring-boot-3-it/src/main/java/io/github/belgif/rest/problem/FrontendController.java +++ b/belgif-rest-problem-it/belgif-rest-problem-spring-boot-3-it/src/main/java/io/github/belgif/rest/problem/FrontendController.java @@ -170,32 +170,32 @@ public void unmappedFromBackend(@RequestParam("client") Client client) { @GetMapping("/beanValidation/queryParameter") public ResponseEntity beanValidationQueryParameter( - @RequestParam("param") @Positive @NotNull Integer param, - @RequestParam("other") @Size(max = 5) String other) { - return ResponseEntity.ok("param: " + param + ", other: " + other); + @RequestParam("param") @Positive @NotNull Integer p, + @RequestParam @Size(max = 5) String other) { + return ResponseEntity.ok("param: " + p + ", other: " + other); } @GetMapping("/beanValidation/headerParameter") public ResponseEntity beanValidationHeaderParameter( - @RequestHeader("param") @Positive @NotNull Integer param) { - return ResponseEntity.ok("param: " + param); + @RequestHeader("param") @Positive @NotNull Integer p) { + return ResponseEntity.ok("param: " + p); } @GetMapping("/beanValidation/pathParameter/class/{param}") public ResponseEntity beanValidationPathParameter( - @PathVariable("param") @Positive @NotNull Integer param) { - return ResponseEntity.ok("param: " + param); + @PathVariable("param") @Positive @NotNull Integer p) { + return ResponseEntity.ok("param: " + p); } @Override - public ResponseEntity beanValidationPathParameterInherited(Integer param) { - return ResponseEntity.ok("param: " + param); + public ResponseEntity beanValidationPathParameterInherited(Integer p) { + return ResponseEntity.ok("param: " + p); } @Override @GetMapping("/beanValidation/pathParameter/overridden") - public ResponseEntity beanValidationPathParameterOverridden(@RequestParam Integer param) { - return ResponseEntity.ok("param: " + param); + public ResponseEntity beanValidationPathParameterOverridden(@RequestParam("param") Integer p) { + return ResponseEntity.ok("param: " + p); } @PostMapping("/beanValidation/body") @@ -214,8 +214,8 @@ public ResponseEntity beanValidationBodyInheritance(@Valid @RequestBody } @PostMapping("/beanValidation/queryParameter/nested") - public ResponseEntity beanValidationQueryParameterNested(@Valid Model param) { - return ResponseEntity.ok("param: " + param); + public ResponseEntity beanValidationQueryParameterNested(@Valid Model p) { + return ResponseEntity.ok("param: " + p); } } diff --git a/belgif-rest-problem-java-ee/src/main/java/io/github/belgif/rest/problem/jaxrs/JaxRsParameterNameProvider.java b/belgif-rest-problem-java-ee/src/main/java/io/github/belgif/rest/problem/jaxrs/JaxRsParameterNameProvider.java new file mode 100644 index 00000000..62d9babf --- /dev/null +++ b/belgif-rest-problem-java-ee/src/main/java/io/github/belgif/rest/problem/jaxrs/JaxRsParameterNameProvider.java @@ -0,0 +1,68 @@ +package io.github.belgif.rest.problem.jaxrs; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import javax.validation.ParameterNameProvider; +import javax.ws.rs.CookieParam; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.MatrixParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import javax.ws.rs.ext.Provider; + +/** + * A ParameterNameProvider that retrieves the parameter name from JAX-RS annotations (if present). + * + * @see ParameterNameProvider + */ +@Provider +public class JaxRsParameterNameProvider implements ParameterNameProvider { + + private static final ConcurrentHashMap> PARAMETER_NAME_CACHE = new ConcurrentHashMap<>(); + + @Override + public List getParameterNames(Constructor constructor) { + return PARAMETER_NAME_CACHE.computeIfAbsent(constructor, this::getParameterNames); + } + + @Override + public List getParameterNames(Method method) { + return PARAMETER_NAME_CACHE.computeIfAbsent(method, this::getParameterNames); + } + + private List getParameterNames(Executable executable) { + return Arrays.stream(executable.getParameters()) + .map(this::getParameterName) + .collect(Collectors.toList()); + } + + private String getParameterName(Parameter parameter) { + Annotation[] annotations = parameter.getAnnotations(); + for (Annotation annotation : annotations) { + if (annotation instanceof QueryParam) { + return ((QueryParam) annotation).value(); + } else if (annotation instanceof PathParam) { + return ((PathParam) annotation).value(); + } else if (annotation instanceof HeaderParam) { + return ((HeaderParam) annotation).value(); + } else if (annotation instanceof CookieParam) { + return ((CookieParam) annotation).value(); + } else if (annotation instanceof FormParam) { + return ((FormParam) annotation).value(); + } else if (annotation instanceof MatrixParam) { + return ((MatrixParam) annotation).value(); + } + } + return parameter.getName(); + } + +} diff --git a/belgif-rest-problem-java-ee/src/main/resources/META-INF/validation.xml b/belgif-rest-problem-java-ee/src/main/resources/META-INF/validation.xml new file mode 100644 index 00000000..80fe353d --- /dev/null +++ b/belgif-rest-problem-java-ee/src/main/resources/META-INF/validation.xml @@ -0,0 +1,9 @@ + + + + io.github.belgif.rest.problem.jaxrs.JaxRsParameterNameProvider + + diff --git a/belgif-rest-problem-java-ee/src/test/java/io/github/belgif/rest/problem/jaxrs/JaxRsParameterNameProviderTest.java b/belgif-rest-problem-java-ee/src/test/java/io/github/belgif/rest/problem/jaxrs/JaxRsParameterNameProviderTest.java new file mode 100644 index 00000000..8c19e93b --- /dev/null +++ b/belgif-rest-problem-java-ee/src/test/java/io/github/belgif/rest/problem/jaxrs/JaxRsParameterNameProviderTest.java @@ -0,0 +1,65 @@ +package io.github.belgif.rest.problem.jaxrs; + +import static org.assertj.core.api.Assertions.*; + +import javax.ws.rs.CookieParam; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.MatrixParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class JaxRsParameterNameProviderTest { + + private final JaxRsParameterNameProvider provider = new JaxRsParameterNameProvider(); + + @Test + void getParameterNameConstructor() throws Exception { + assertThat(provider.getParameterNames(Tested.class.getDeclaredConstructor(String.class, Long.class))) + .containsExactly("name", "other"); + } + + @Test + void getParameterNamesMethod() throws Exception { + assertThat(provider.getParameterNames(Tested.class.getDeclaredMethod("plainMethod", String.class, Long.class))) + .containsExactly("name", "other"); + } + + @ParameterizedTest + @ValueSource(strings = { "queryParam", "pathParam", "headerParam", "cookieParam", "formParam", "matrixParam" }) + void getParameterNamesMethodWithJaxRsAnnotation(String method) throws Exception { + assertThat(provider.getParameterNames(Tested.class.getDeclaredMethod(method, String.class))) + .containsExactly("name"); + } + + static class Tested { + Tested(String name, Long other) { + } + + void plainMethod(String name, Long other) { + } + + void queryParam(@QueryParam("name") String x) { + } + + void pathParam(@PathParam("name") String x) { + } + + void headerParam(@HeaderParam("name") String x) { + } + + void cookieParam(@CookieParam("name") String x) { + } + + void formParam(@FormParam("name") String x) { + } + + void matrixParam(@MatrixParam("name") String x) { + } + } + +} diff --git a/belgif-rest-problem-spring-boot-2/pom.xml b/belgif-rest-problem-spring-boot-2/pom.xml index 7825cb29..7c265adf 100644 --- a/belgif-rest-problem-spring-boot-2/pom.xml +++ b/belgif-rest-problem-spring-boot-2/pom.xml @@ -71,6 +71,10 @@ assertj-core test + + jakarta.validation + jakarta.validation-api + diff --git a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/AnnotationParameterNameDiscoverer.java b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/AnnotationParameterNameDiscoverer.java new file mode 100644 index 00000000..8542bf06 --- /dev/null +++ b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/AnnotationParameterNameDiscoverer.java @@ -0,0 +1,67 @@ +package io.github.belgif.rest.problem.spring; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * ParameterNameDiscoverer that retrieves the parameter name from Spring MVC annotations (if present). + */ +public class AnnotationParameterNameDiscoverer implements ParameterNameDiscoverer { + + private final ConcurrentHashMap parameterNameCache = new ConcurrentHashMap<>(); + + @Override + public String[] getParameterNames(Constructor constructor) { + return parameterNameCache.computeIfAbsent(constructor, this::getParameterNames); + } + + @Override + public String[] getParameterNames(Method method) { + return parameterNameCache.computeIfAbsent(method, this::getParameterNames); + } + + private String[] getParameterNames(Executable executable) { + return Arrays.stream(executable.getParameters()) + .map(this::getParameterName) + .toArray(String[]::new); + } + + private String getParameterName(Parameter parameter) { + String parameterName = getParameterNameFromAnnotations(parameter); + if (parameterName == null || parameterName.isEmpty()) { + parameterName = parameter.getName(); + } + return parameterName; + } + + private String getParameterNameFromAnnotations(Parameter parameter) { + Annotation[] annotations = parameter.getAnnotations(); + for (Annotation annotation : annotations) { + if (annotation instanceof RequestParam) { + return ((RequestParam) annotation).value(); + } else if (annotation instanceof PathVariable) { + return ((PathVariable) annotation).value(); + } else if (annotation instanceof RequestHeader) { + return ((RequestHeader) annotation).value(); + } else if (annotation instanceof CookieValue) { + return ((CookieValue) annotation).value(); + } else if (annotation instanceof MatrixVariable) { + return ((MatrixVariable) annotation).value(); + } + } + return null; + } + +} diff --git a/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/ProblemValidatorConfiguration.java b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/ProblemValidatorConfiguration.java new file mode 100644 index 00000000..ca872c5e --- /dev/null +++ b/belgif-rest-problem-spring/src/main/java/io/github/belgif/rest/problem/spring/ProblemValidatorConfiguration.java @@ -0,0 +1,38 @@ +package io.github.belgif.rest.problem.spring; + +import javax.validation.ConstraintViolationException; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.validation.MessageInterpolatorFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +/** + * Registers a LocalValidatorFactoryBean with the AnnotationParameterNameDiscoverer. + * + * @see LocalValidatorFactoryBean + * @see AnnotationParameterNameDiscoverer + * @see org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration + */ +@Configuration +public class ProblemValidatorConfiguration { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @ConditionalOnMissingBean(Validator.class) + @ConditionalOnClass(ConstraintViolationException.class) + public LocalValidatorFactoryBean defaultValidator(ApplicationContext applicationContext) { + LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean(); + MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(applicationContext); + factoryBean.setMessageInterpolator(interpolatorFactory.getObject()); + factoryBean.setParameterNameDiscoverer(new AnnotationParameterNameDiscoverer()); + return factoryBean; + } + +} diff --git a/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/AnnotationParameterNameDiscovererTest.java b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/AnnotationParameterNameDiscovererTest.java new file mode 100644 index 00000000..3c120940 --- /dev/null +++ b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/AnnotationParameterNameDiscovererTest.java @@ -0,0 +1,66 @@ +package io.github.belgif.rest.problem.spring; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; + +class AnnotationParameterNameDiscovererTest { + + private final AnnotationParameterNameDiscoverer discoverer = new AnnotationParameterNameDiscoverer(); + + @Test + void getParameterNameConstructor() throws Exception { + assertThat(discoverer.getParameterNames(Tested.class.getDeclaredConstructor(String.class, Long.class))) + .containsExactly("name", "other"); + } + + @Test + void getParameterNamesMethod() throws Exception { + assertThat( + discoverer.getParameterNames(Tested.class.getDeclaredMethod("plainMethod", String.class, Long.class))) + .containsExactly("name", "other"); + } + + @ParameterizedTest + @ValueSource( + strings = { "requestParam", "pathVariable", "requestHeader", "cookieValue", "matrixVariable", "fallback" }) + void getParameterNamesMethodWithJaxRsAnnotation(String method) throws Exception { + assertThat(discoverer.getParameterNames(Tested.class.getDeclaredMethod(method, String.class))) + .containsExactly("name"); + } + + static class Tested { + Tested(String name, Long other) { + } + + void plainMethod(String name, Long other) { + } + + void requestParam(@RequestParam("name") String x) { + } + + void pathVariable(@PathVariable("name") String x) { + } + + void requestHeader(@RequestHeader("name") String x) { + } + + void cookieValue(@CookieValue("name") String x) { + } + + void matrixVariable(@MatrixVariable("name") String x) { + } + + void fallback(@RequestParam String name) { + } + + } + +} diff --git a/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/ProblemValidatorConfigurationTest.java b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/ProblemValidatorConfigurationTest.java new file mode 100644 index 00000000..e4a63e40 --- /dev/null +++ b/belgif-rest-problem-spring/src/test/java/io/github/belgif/rest/problem/spring/ProblemValidatorConfigurationTest.java @@ -0,0 +1,22 @@ +package io.github.belgif.rest.problem.spring; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.context.ApplicationContext; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +class ProblemValidatorConfigurationTest { + + private final ProblemValidatorConfiguration config = new ProblemValidatorConfiguration(); + + @Test + void defaultValidator() { + LocalValidatorFactoryBean validator = config.defaultValidator(Mockito.mock(ApplicationContext.class)); + assertThat(validator) + .extracting("parameterNameDiscoverer") + .isInstanceOf(AnnotationParameterNameDiscoverer.class); + } + +} diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 2b95dda9..4ae231bd 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -38,6 +38,11 @@ The library consists of these modules: *belgif-rest-problem-spring:* * Extract ProblemWebClientCustomizer to belgif-rest-problem-spring-boot-2 and belgif-rest-problem-spring-boot-3 to fix NoSuchMethodError compatibility issue +* Add AnnotationParameterNameDiscoverer to retrieve parameter names from Spring MVC annotations for bean validation + +*belgif-rest-problem-java-ee:* + +* Add JaxRsParameterNameProvider to retrieve parameter names from JAX-RS annotations for bean validation === Version 0.6 @@ -325,6 +330,7 @@ Its priority of "Priorities.USER + 200" allows it to be overridden if needed by * *ProblemExceptionMapper:* a JAX-RS ExceptionMapper that converts Problem exceptions to a proper application/problem+json response. * *WebApplicationExceptionMapper:* a JAX-RS ExceptionMapper that handles WebApplicationExceptions thrown by the JAX-RS runtime itself, to prevent them from being handled by the DefaultExceptionMapper. * *DefaultExceptionMapper:* a JAX-RS ExceptionMapper that converts any other uncaught exception to an HTTP 500 InternalServerErrorProblem. +* *JaxRsParameterNameProvider*: a bean validation ParameterNameProvider that retrieves parameter names from JAX-RS annotations * *JAX-RS Client integration:* ** *ProblemClientResponseFilter:* JAX-RS ClientResponseFilter that converts problem response to a ProblemWrapper exception. @@ -397,6 +403,11 @@ io.github.belgif.rest.problem.scan-additional-problem-packages=com.acme.custom * *RoutingExceptionsHandler:* an exception handler for RestControllers that converts routing related validation exceptions to HTTP 400 BadRequestProblem. * *ProblemResponseErrorHandler:* a RestTemplate error handler that converts problem responses to Problem exceptions. * *ProblemRestTemplateCustomizer:* a RestTemplateCustomizer that registers the ProblemResponseErrorHandler. +* *AnnotationParameterNameDiscoverer:* a bean validation ParameterNameDiscoverer that retrieves parameter names from Spring MVC annotations ++ +WARNING: When using a Spring MVC annotation without name (e.g. `@RequestParam String name` instead of `@RequestParam("name") String name`), the parameter name is retrieved via reflection as fallback. +For this to work, your application code must be compiled with the https://docs.oracle.com/en/java/javase/21/docs/specs/man/javac.html#option-parameters[-parameters] flag (see https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#parameters[maven-compiler-plugin]). +Otherwise, you will see parameter names like `arg0`. In general, these components make it possible to use standard java exception handling (throw and try-catch) for dealing with problems in Spring Boot REST APIs.