From f440fb8bafc1277c8a6f85851179d1a7985a83dc Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 23 Sep 2021 16:13:20 +0200 Subject: [PATCH] Unit tests for record binding See gh-27437 --- .../jdbc/core/AbstractRowMapperTests.java | 2 +- .../jdbc/core/DataClassRowMapperTests.java | 24 +++ ...nnotationControllerHandlerMethodTests.java | 181 ++++++++++-------- 3 files changed, 128 insertions(+), 79 deletions(-) diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java index 601bbdfd7a1d..633e86c4c281 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -89,7 +89,7 @@ protected void verifyPerson(ConstructorPerson person) { verifyPersonViaBeanWrapper(person); } - private void verifyPersonViaBeanWrapper(Object person) { + protected void verifyPersonViaBeanWrapper(Object person) { BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(person); assertThat(bw.getPropertyValue("name")).isEqualTo("Bubba"); assertThat(bw.getPropertyValue("age")).isEqualTo(22L); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java index 48b0f7f03134..b9fb7ad44df4 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -79,4 +79,28 @@ public void testStaticQueryWithDataClassAndSetters() throws Exception { mock.verifyClosed(); } + @Test + public void testStaticQueryWithDataRecord() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new DataClassRowMapper<>(RecordPerson.class)); + assertThat(result.size()).isEqualTo(1); + verifyPerson(result.get(0)); + + mock.verifyClosed(); + } + + protected void verifyPerson(RecordPerson person) { + assertThat(person.name()).isEqualTo("Bubba"); + assertThat(person.age()).isEqualTo(22L); + assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56")); + verifyPersonViaBeanWrapper(person); + } + + + static record RecordPerson(String name, long age, Date birth_date, BigDecimal balance) { + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index 8f07c7071f8e..46f4d2577311 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -2202,6 +2202,19 @@ void dataClassBindingWithLocalDate(boolean usePathPatterns) throws Exception { assertThat(response.getContentAsString()).isEqualTo("2010-01-01"); } + @PathPatternsParameterizedTest + void dataRecordBinding(boolean usePathPatterns) throws Exception { + initDispatcherServlet(DataRecordController.class, usePathPatterns); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bind"); + request.addParameter("param1", "value1"); + request.addParameter("param2", "true"); + request.addParameter("param3", "3"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertThat(response.getContentAsString()).isEqualTo("value1-true-3"); + } + @Test void routerFunction() throws ServletException, IOException { GenericWebApplicationContext wac = new GenericWebApplicationContext(); @@ -2397,7 +2410,7 @@ public void nonEmptyParameterListHandler(HttpServletResponse response) { @Controller @RequestMapping("/myPage") @SessionAttributes(names = { "object1", "object2" }) - public static class MySessionAttributesController { + static class MySessionAttributesController { @RequestMapping(method = RequestMethod.GET) public String get(Model model) { @@ -2417,7 +2430,7 @@ public String post(@ModelAttribute("object1") Object object1) { @RequestMapping("/myPage") @SessionAttributes({"object1", "object2"}) @Controller - public interface MySessionAttributesControllerIfc { + interface MySessionAttributesControllerIfc { @RequestMapping(method = RequestMethod.GET) String get(Model model); @@ -2426,7 +2439,7 @@ public interface MySessionAttributesControllerIfc { String post(@ModelAttribute("object1") Object object1); } - public static class MySessionAttributesControllerImpl implements MySessionAttributesControllerIfc { + static class MySessionAttributesControllerImpl implements MySessionAttributesControllerIfc { @Override public String get(Model model) { @@ -2444,7 +2457,7 @@ public String post(@ModelAttribute("object1") Object object1) { @RequestMapping("/myPage") @SessionAttributes({"object1", "object2"}) - public interface MyParameterizedControllerIfc { + interface MyParameterizedControllerIfc { @ModelAttribute("testBeanList") List getTestBeans(); @@ -2453,14 +2466,14 @@ public interface MyParameterizedControllerIfc { String get(Model model); } - public interface MyEditableParameterizedControllerIfc extends MyParameterizedControllerIfc { + interface MyEditableParameterizedControllerIfc extends MyParameterizedControllerIfc { @RequestMapping(method = RequestMethod.POST) String post(@ModelAttribute("object1") T object); } @Controller - public static class MyParameterizedControllerImpl implements MyEditableParameterizedControllerIfc { + static class MyParameterizedControllerImpl implements MyEditableParameterizedControllerIfc { @Override public List getTestBeans() { @@ -2485,7 +2498,7 @@ public String post(TestBean object) { } @Controller - public static class MyParameterizedControllerImplWithOverriddenMappings + static class MyParameterizedControllerImplWithOverriddenMappings implements MyEditableParameterizedControllerIfc { @Override @@ -2514,7 +2527,7 @@ public String post(@ModelAttribute("object1") TestBean object1) { } @Controller - public static class MyFormController { + static class MyFormController { @ModelAttribute("testBeanList") public List getTestBeans() { @@ -2536,7 +2549,7 @@ public String myHandle(@ModelAttribute("myCommand") TestBean tb, BindingResult e } } - public static class ValidTestBean extends TestBean { + static class ValidTestBean extends TestBean { @NotNull private String validCountry; @@ -2551,7 +2564,7 @@ public String getValidCountry() { } @Controller - public static class MyModelFormController { + static class MyModelFormController { @ModelAttribute public List getTestBeans() { @@ -2572,7 +2585,7 @@ public String myHandle(@ModelAttribute("myCommand") TestBean tb, BindingResult e } @Controller - public static class LateBindingFormController { + static class LateBindingFormController { @ModelAttribute("testBeanList") public List getTestBeans(@ModelAttribute(name="myCommand", binding=false) TestBean tb) { @@ -2915,7 +2928,7 @@ public void render(@Nullable Map model, HttpServletRequest request, HttpServletR } } - public static class ModelExposingViewResolver implements ViewResolver { + static class ModelExposingViewResolver implements ViewResolver { @Override public View resolveViewName(String viewName, Locale locale) { @@ -2926,7 +2939,7 @@ public View resolveViewName(String viewName, Locale locale) { } } - public static class ParentController { + static class ParentController { @RequestMapping(method = RequestMethod.GET) public void doGet(HttpServletRequest req, HttpServletResponse resp) { @@ -2935,7 +2948,7 @@ public void doGet(HttpServletRequest req, HttpServletResponse resp) { @Controller @RequestMapping("/child/test") - public static class ChildController extends ParentController { + static class ChildController extends ParentController { @RequestMapping(method = RequestMethod.GET) public void doGet(HttpServletRequest req, HttpServletResponse resp, @RequestParam("childId") String id) { @@ -2969,7 +2982,7 @@ public void get(Writer writer) throws IOException { } @MyControllerAnnotation - public static class CustomAnnotationController { + static class CustomAnnotationController { @RequestMapping("/myPath.do") public void myHandle() { @@ -2977,7 +2990,7 @@ public void myHandle() { } @Controller - public static class RequiredParamController { + static class RequiredParamController { @RequestMapping("/myPath.do") public void myHandle(@RequestParam(value = "id", required = true) int id, @@ -2986,7 +2999,7 @@ public void myHandle(@RequestParam(value = "id", required = true) int id, } @Controller - public static class OptionalParamController { + static class OptionalParamController { @RequestMapping("/myPath.do") public void myHandle(@RequestParam(required = false) String id, @@ -2998,7 +3011,7 @@ public void myHandle(@RequestParam(required = false) String id, } @Controller - public static class DefaultValueParamController { + static class DefaultValueParamController { @RequestMapping("/myPath.do") public void myHandle(@RequestParam(value = "id", defaultValue = "foo") String id, @@ -3010,7 +3023,7 @@ public void myHandle(@RequestParam(value = "id", defaultValue = "foo") String id } @Controller - public static class DefaultExpressionValueParamController { + static class DefaultExpressionValueParamController { @RequestMapping("/myPath.do") public void myHandle(@RequestParam(value = "id", defaultValue = "${myKey}") String id, @@ -3022,7 +3035,7 @@ public void myHandle(@RequestParam(value = "id", defaultValue = "${myKey}") Stri } @Controller - public static class NestedSetController { + static class NestedSetController { @RequestMapping("/myPath.do") public void myHandle(GenericBean gb, HttpServletResponse response) throws Exception { @@ -3031,7 +3044,7 @@ public void myHandle(GenericBean gb, HttpServletResponse response) throws Exc } } - public static class TestBeanConverter implements Converter { + static class TestBeanConverter implements Converter { @Override public ITestBean convert(String source) { @@ -3040,14 +3053,14 @@ public ITestBean convert(String source) { } @Controller - public static class PathVariableWithCustomConverterController { + static class PathVariableWithCustomConverterController { @RequestMapping("/myPath/{id}") public void myHandle(@PathVariable("id") ITestBean bean) throws Exception { } } - public static class AnnotatedExceptionRaisingConverter implements Converter { + static class AnnotatedExceptionRaisingConverter implements Converter { @Override public ITestBean convert(String source) { @@ -3061,7 +3074,7 @@ private static class NotFoundException extends RuntimeException { } @Controller - public static class MethodNotAllowedController { + static class MethodNotAllowedController { @RequestMapping(value = "/myPath.do", method = RequestMethod.DELETE) public void delete() { @@ -3093,7 +3106,7 @@ public void get() { } @Controller - public static class PathOrderingController { + static class PathOrderingController { @RequestMapping(value = {"/dir/myPath1.do", "/*/*.do"}) public void method1(Writer writer) throws IOException { @@ -3107,7 +3120,7 @@ public void method2(Writer writer) throws IOException { } @Controller - public static class RequestResponseBodyController { + static class RequestResponseBodyController { @RequestMapping(value = "/something", method = RequestMethod.PUT) @ResponseBody @@ -3123,7 +3136,7 @@ public String handlePartialUpdate(@RequestBody String content) throws IOExceptio } @Controller - public static class RequestResponseBodyProducesController { + static class RequestResponseBodyProducesController { @RequestMapping(value = "/something", method = RequestMethod.PUT, produces = "text/plain") @ResponseBody @@ -3133,7 +3146,7 @@ public String handle(@RequestBody String body) throws IOException { } @Controller - public static class ResponseBodyVoidController { + static class ResponseBodyVoidController { @RequestMapping("/something") @ResponseBody @@ -3142,7 +3155,7 @@ public void handle() throws IOException { } @Controller - public static class RequestBodyArgMismatchController { + static class RequestBodyArgMismatchController { @RequestMapping(value = "/something", method = RequestMethod.PUT) public void handle(@RequestBody A a) throws IOException { @@ -3150,14 +3163,14 @@ public void handle(@RequestBody A a) throws IOException { } @XmlRootElement - public static class A { + static class A { } @XmlRootElement - public static class B { + static class B { } - public static class NotReadableMessageConverter implements HttpMessageConverter { + static class NotReadableMessageConverter implements HttpMessageConverter { @Override public boolean canRead(Class clazz, @Nullable MediaType mediaType) { @@ -3185,7 +3198,7 @@ public void write(Object o, @Nullable MediaType contentType, HttpOutputMessage o } } - public static class SimpleMessageConverter implements HttpMessageConverter { + static class SimpleMessageConverter implements HttpMessageConverter { private final List supportedMediaTypes; @@ -3223,7 +3236,7 @@ public void write(Object o, @Nullable MediaType contentType, HttpOutputMessage o } @Controller - public static class ContentTypeHeadersController { + static class ContentTypeHeadersController { @RequestMapping(value = "/something", headers = "content-type=application/pdf") public void handlePdf(Writer writer) throws IOException { @@ -3237,7 +3250,7 @@ public void handleHtml(Writer writer) throws IOException { } @Controller - public static class ConsumesController { + static class ConsumesController { @RequestMapping(value = "/something", consumes = "application/pdf") public void handlePdf(Writer writer) throws IOException { @@ -3251,7 +3264,7 @@ public void handleHtml(Writer writer) throws IOException { } @Controller - public static class NegatedContentTypeHeadersController { + static class NegatedContentTypeHeadersController { @RequestMapping(value = "/something", headers = "content-type=application/pdf") public void handlePdf(Writer writer) throws IOException { @@ -3266,7 +3279,7 @@ public void handleNonPdf(Writer writer) throws IOException { } @Controller - public static class AcceptHeadersController { + static class AcceptHeadersController { @RequestMapping(value = "/something", headers = "accept=text/html") public void handleHtml(Writer writer) throws IOException { @@ -3280,7 +3293,7 @@ public void handleXml(Writer writer) throws IOException { } @Controller - public static class ProducesController { + static class ProducesController { @GetMapping(path = "/something", produces = "text/html") public void handleHtml(Writer writer) throws IOException { @@ -3304,7 +3317,7 @@ public ResponseEntity> handle(IllegalArgumentException ex) { } @Controller - public static class ResponseStatusController { + static class ResponseStatusController { @RequestMapping("/something") @ResponseStatus(code = HttpStatus.CREATED, reason = "It's alive!") @@ -3314,7 +3327,7 @@ public void handle(Writer writer) throws IOException { } @Controller - public static class ModelAndViewResolverController { + static class ModelAndViewResolverController { @RequestMapping("/") public MySpecialArg handle() { @@ -3322,7 +3335,7 @@ public MySpecialArg handle() { } } - public static class MyModelAndViewResolver implements ModelAndViewResolver { + static class MyModelAndViewResolver implements ModelAndViewResolver { @Override public ModelAndView resolveModelAndView(Method handlerMethod, Class handlerType, Object returnValue, @@ -3376,7 +3389,7 @@ public void patternMatch(Writer writer) throws IOException { @Controller @RequestMapping("/test*") - public static class BindingCookieValueController { + static class BindingCookieValueController { @InitBinder public void initBinder(WebDataBinder binder) { @@ -3393,16 +3406,16 @@ public void handle(@CookieValue("date") Date date, Writer writer) throws IOExcep } } - public interface TestController { + interface TestController { ModelAndView method(T object); } - public static class MyEntity { + static class MyEntity { } @Controller - public static class TestControllerImpl implements TestController { + static class TestControllerImpl implements TestController { @Override @RequestMapping("/method") @@ -3413,7 +3426,7 @@ public ModelAndView method(MyEntity object) { @RestController @RequestMapping(path = ApiConstants.ARTICLES_PATH) - public static class ArticleController implements ApiConstants, ResourceEndpoint { + static class ArticleController implements ApiConstants, ResourceEndpoint { @Override @GetMapping(params = "page") @@ -3435,14 +3448,14 @@ interface ApiConstants { String ARTICLES_PATH = API_V1 + "/articles"; } - public interface ResourceEndpoint> { + interface ResourceEndpoint> { Collection find(String pageable, P predicate) throws IOException; List find(boolean sort, P predicate) throws IOException; } - public static abstract class Entity { + static abstract class Entity { public UUID id; @@ -3451,7 +3464,7 @@ public static abstract class Entity { public Instant createdDate; } - public static class Article extends Entity { + static class Article extends Entity { public String slug; @@ -3460,7 +3473,7 @@ public static class Article extends Entity { public String content; } - public static abstract class EntityPredicate { + static abstract class EntityPredicate { public String createdBy; @@ -3475,7 +3488,7 @@ public boolean accept(E entity) { } } - public static class ArticlePredicate extends EntityPredicate
{ + static class ArticlePredicate extends EntityPredicate
{ public String query; @@ -3486,7 +3499,7 @@ public boolean accept(Article entity) { } @Controller - public static class RequestParamMapController { + static class RequestParamMapController { @RequestMapping("/map") public void map(@RequestParam Map params, Writer writer) throws IOException { @@ -3521,7 +3534,7 @@ public void multiValueMap(@RequestParam MultiValueMap params, Wr } @Controller - public static class RequestHeaderMapController { + static class RequestHeaderMapController { @RequestMapping("/map") public void map(@RequestHeader Map headers, Writer writer) throws IOException { @@ -3564,14 +3577,14 @@ public void httpHeaders(@RequestHeader HttpHeaders headers, Writer writer) throw } @Controller - public interface IMyController { + interface IMyController { @RequestMapping("/handle") void handle(Writer writer, @RequestParam(value="p", required=false) String param) throws IOException; } @Controller - public static class IMyControllerImpl implements IMyController { + static class IMyControllerImpl implements IMyController { @Override public void handle(Writer writer, @RequestParam(value="p", required=false) String param) throws IOException { @@ -3579,14 +3592,14 @@ public void handle(Writer writer, @RequestParam(value="p", required=false) Strin } } - public static abstract class MyAbstractController { + static abstract class MyAbstractController { @RequestMapping("/handle") public abstract void handle(Writer writer) throws IOException; } @Controller - public static class MyAbstractControllerImpl extends MyAbstractController { + static class MyAbstractControllerImpl extends MyAbstractController { @Override public void handle(Writer writer) throws IOException { @@ -3595,7 +3608,7 @@ public void handle(Writer writer) throws IOException { } @Controller - public static class TrailingSlashController { + static class TrailingSlashController { @RequestMapping(value = "/", method = RequestMethod.GET) public void root(Writer writer) throws IOException { @@ -3609,7 +3622,7 @@ public void templatePath(Writer writer) throws IOException { } @Controller - public static class ResponseEntityController { + static class ResponseEntityController { @PostMapping("/foo") public ResponseEntity foo(HttpEntity requestEntity) throws Exception { @@ -3666,7 +3679,7 @@ public void setName(String name) { } @Controller - public static class CustomMapEditorController { + static class CustomMapEditorController { @InitBinder public void initBinder(WebDataBinder binder) { @@ -3681,7 +3694,7 @@ public void handle(@RequestParam("map") Map map, Writer writer) throws IOExcepti } } - public static class CustomMapEditor extends PropertyEditorSupport { + static class CustomMapEditor extends PropertyEditorSupport { @Override public void setAsText(String text) throws IllegalArgumentException { @@ -3695,7 +3708,7 @@ public void setAsText(String text) throws IllegalArgumentException { } @Controller - public static class MultipartController { + static class MultipartController { @InitBinder public void initBinder(WebDataBinder binder) { @@ -3716,7 +3729,7 @@ public void processMultipart(@RequestParam("content") String[] content, HttpServ } @Controller - public static class CsvController { + static class CsvController { @RequestMapping("/singleInteger") public void processCsv(@RequestParam("content") Integer content, HttpServletResponse response) throws IOException { @@ -3840,7 +3853,7 @@ public HttpHeaders createNoHeader() { } @RestController - public static class TextRestController { + static class TextRestController { @RequestMapping(path = "/a1", method = RequestMethod.GET) public String a1(@RequestBody String body) { @@ -3864,7 +3877,7 @@ public String a4(@RequestBody String body) { } @Controller - public static class ModelAndViewController { + static class ModelAndViewController { @RequestMapping("/path") public ModelAndView methodWithHttpStatus(MyEntity object) { @@ -3886,7 +3899,7 @@ private static class TestException extends Exception { } } - public static class DataClass { + static class DataClass { @NotNull private final String param1; @@ -3921,7 +3934,7 @@ public int getParam3() { } @RestController - public static class DataClassController { + static class DataClassController { @RequestMapping("/bind") public String handle(DataClass data) { @@ -3930,7 +3943,7 @@ public String handle(DataClass data) { } @RestController - public static class PathVariableDataClassController { + static class PathVariableDataClassController { @RequestMapping("/bind/{param2}") public String handle(DataClass data) { @@ -3939,7 +3952,7 @@ public String handle(DataClass data) { } @RestController - public static class ValidatedDataClassController { + static class ValidatedDataClassController { @InitBinder public void initBinder(WebDataBinder binder) { @@ -3960,7 +3973,7 @@ public BindStatusView handle(@Valid DataClass data, BindingResult result) { } } - public static class BindStatusView extends AbstractView { + static class BindStatusView extends AbstractView { private final String content; @@ -3980,7 +3993,7 @@ protected void renderMergedOutputModel( } } - public static class MultipartFileDataClass { + static class MultipartFileDataClass { @NotNull public final MultipartFile param1; @@ -4003,7 +4016,7 @@ public void setParam3(int param3) { } @RestController - public static class MultipartFileDataClassController { + static class MultipartFileDataClassController { @RequestMapping("/bind") public String handle(MultipartFileDataClass data) throws IOException { @@ -4012,7 +4025,7 @@ public String handle(MultipartFileDataClass data) throws IOException { } } - public static class ServletPartDataClass { + static class ServletPartDataClass { @NotNull public final Part param1; @@ -4035,7 +4048,7 @@ public void setParam3(int param3) { } @RestController - public static class ServletPartDataClassController { + static class ServletPartDataClassController { @RequestMapping("/bind") public String handle(ServletPartDataClass data) throws IOException { @@ -4045,7 +4058,7 @@ public String handle(ServletPartDataClass data) throws IOException { } @RestController - public static class NullableDataClassController { + static class NullableDataClassController { @RequestMapping("/bind") public String handle(@Nullable DataClass data, BindingResult result) { @@ -4060,7 +4073,7 @@ public String handle(@Nullable DataClass data, BindingResult result) { } @RestController - public static class OptionalDataClassController { + static class OptionalDataClassController { @RequestMapping("/bind") public String handle(Optional optionalData, BindingResult result) { @@ -4074,7 +4087,7 @@ public String handle(Optional optionalData, BindingResult result) { } } - public static class DateClass { + static class DateClass { @DateTimeFormat(pattern = "yyyy-MM-dd") public LocalDate date; @@ -4085,7 +4098,7 @@ public DateClass(LocalDate date) { } @RestController - public static class DateClassController { + static class DateClassController { @InitBinder public void initBinder(WebDataBinder binder) { @@ -4107,4 +4120,16 @@ public String handle(DateClass data, BindingResult result) { } } + static record DataRecord(String param1, boolean param2, int param3) { + } + + @RestController + static class DataRecordController { + + @RequestMapping("/bind") + public String handle(DataRecord data) { + return data.param1 + "-" + data.param2 + "-" + data.param3; + } + } + }