├── .java-version ├── .git-blame-ignore-revs ├── .gitattributes ├── gradle.properties ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── publish.sh ├── .gitignore ├── webflux-annotated-data-binder-spring-boot-starter ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── spring │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── java │ │ └── com │ │ └── mattbertolini │ │ └── spring │ │ └── web │ │ └── reactive │ │ └── bind │ │ └── autoconfigure │ │ └── WebFluxBinderAutoConfiguration.java └── build.gradle.kts ├── webmvc-annotated-data-binder-spring-boot-starter ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── spring │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── java │ │ │ └── com │ │ │ └── mattbertolini │ │ │ └── spring │ │ │ └── web │ │ │ └── servlet │ │ │ └── mvc │ │ │ └── bind │ │ │ └── autoconfigure │ │ │ └── WebMvcBinderAutoConfiguration.java │ └── test │ │ └── java │ │ └── com │ │ └── mattbertolini │ │ └── spring │ │ └── web │ │ └── servlet │ │ └── mvc │ │ └── bind │ │ └── autoconfigure │ │ └── WebMvcBinderAutoConfigurationTest.java └── build.gradle.kts ├── .github └── workflows │ ├── gradle-wrapper-validation.yml │ └── build.yml ├── settings.gradle.kts ├── LICENSE_HEADER.txt ├── .editorconfig ├── spring-annotated-data-binder-core ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── mattbertolini │ │ │ └── spring │ │ │ └── web │ │ │ └── bind │ │ │ ├── package-info.java │ │ │ ├── support │ │ │ ├── package-info.java │ │ │ └── MapValueResolver.java │ │ │ ├── resolver │ │ │ ├── package-info.java │ │ │ ├── RequestPropertyResolverBase.java │ │ │ └── AbstractNamedRequestPropertyResolver.java │ │ │ ├── annotation │ │ │ ├── package-info.java │ │ │ ├── SessionParameter.java │ │ │ ├── PathParameter.java │ │ │ ├── RequestBean.java │ │ │ ├── CookieParameter.java │ │ │ ├── RequestBody.java │ │ │ ├── HeaderParameter.java │ │ │ ├── BeanParameter.java │ │ │ ├── RequestContext.java │ │ │ ├── FormParameter.java │ │ │ └── RequestParameter.java │ │ │ ├── introspect │ │ │ ├── package-info.java │ │ │ ├── RequestBeanIntrospectionException.java │ │ │ ├── ResolvedPropertyData.java │ │ │ ├── CircularReferenceException.java │ │ │ ├── CachedAnnotatedRequestBeanIntrospector.java │ │ │ └── AnnotatedRequestBeanIntrospector.java │ │ │ ├── RequestPropertyBindingException.java │ │ │ ├── PropertyResolutionException.java │ │ │ └── AbstractPropertyResolverRegistry.java │ └── test │ │ └── java │ │ └── com │ │ └── mattbertolini │ │ └── spring │ │ └── web │ │ └── bind │ │ ├── introspect │ │ └── scan │ │ │ ├── IgnoredBean.java │ │ │ ├── ScannedBean.java │ │ │ └── subbackage │ │ │ └── SubpackageBean.java │ │ ├── support │ │ └── MapValueResolverTest.java │ │ └── resolver │ │ └── AbstractNamedRequestPropertyResolverTest.java └── build.gradle.kts ├── spring-webflux-annotated-data-binder ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── mattbertolini │ │ │ └── spring │ │ │ └── web │ │ │ └── reactive │ │ │ └── bind │ │ │ ├── package-info.java │ │ │ ├── config │ │ │ └── package-info.java │ │ │ ├── resolver │ │ │ ├── package-info.java │ │ │ ├── RequestPropertyResolver.java │ │ │ ├── SessionParameterRequestPropertyResolver.java │ │ │ ├── PathParameterMapRequestPropertyResolver.java │ │ │ ├── HeaderParameterRequestPropertyResolver.java │ │ │ ├── FormParameterMapRequestPropertyResolver.java │ │ │ ├── HeaderParameterMapRequestPropertyResolver.java │ │ │ ├── RequestParameterMapRequestPropertyResolver.java │ │ │ ├── RequestParameterRequestPropertyResolver.java │ │ │ ├── CookieParameterRequestPropertyResolver.java │ │ │ ├── PathParameterRequestPropertyResolver.java │ │ │ ├── FormParameterRequestPropertyResolver.java │ │ │ └── RequestBodyRequestPropertyResolver.java │ │ │ └── PropertyResolverRegistry.java │ └── test │ │ └── java │ │ └── com │ │ └── mattbertolini │ │ └── spring │ │ └── web │ │ └── reactive │ │ └── bind │ │ ├── MockWebExchangeDataBinder.java │ │ ├── PropertyResolverRegistryTest.java │ │ └── MockBindingContext.java └── build.gradle.kts ├── spring-webmvc-annotated-data-binder ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── mattbertolini │ │ │ └── spring │ │ │ └── web │ │ │ └── servlet │ │ │ └── mvc │ │ │ └── bind │ │ │ ├── package-info.java │ │ │ ├── config │ │ │ └── package-info.java │ │ │ ├── resolver │ │ │ ├── package-info.java │ │ │ ├── RequestPropertyResolver.java │ │ │ ├── FormParameterMapRequestPropertyResolver.java │ │ │ ├── FormParameterRequestPropertyResolver.java │ │ │ ├── SessionParameterRequestPropertyResolver.java │ │ │ ├── HeaderParameterRequestPropertyResolver.java │ │ │ ├── PathParameterMapRequestPropertyResolver.java │ │ │ ├── PathParameterRequestPropertyResolver.java │ │ │ ├── CookieParameterRequestPropertyResolver.java │ │ │ ├── RequestBodyRequestPropertyResolver.java │ │ │ ├── RequestParameterRequestPropertyResolver.java │ │ │ └── HeaderParameterMapRequestPropertyResolver.java │ │ │ └── PropertyResolverRegistry.java │ └── test │ │ └── java │ │ └── com │ │ └── mattbertolini │ │ └── spring │ │ └── web │ │ └── servlet │ │ └── mvc │ │ └── bind │ │ ├── resolver │ │ └── ExceptionThrowingMockMultipartHttpServletRequest.java │ │ ├── PropertyResolverRegistryTest.java │ │ ├── MockWebDataBinder.java │ │ └── MockWebDataBinderFactory.java └── build.gradle.kts ├── docs ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── mattbertolini │ │ │ │ └── spring │ │ │ │ └── web │ │ │ │ └── bind │ │ │ │ └── docs │ │ │ │ ├── ExampleService.java │ │ │ │ ├── NestedBean.java │ │ │ │ ├── ExampleController.java │ │ │ │ ├── webflux │ │ │ │ └── ExampleWebFluxContext.java │ │ │ │ └── webmvc │ │ │ │ └── ExampleMvcContext.java │ │ └── resources │ │ │ └── com │ │ │ └── mattbertolini │ │ │ └── spring │ │ │ └── web │ │ │ └── bind │ │ │ └── docs │ │ │ └── webmvc │ │ │ └── example-context.xml │ └── test │ │ └── java │ │ └── com │ │ └── mattbertolini │ │ └── spring │ │ └── web │ │ └── bind │ │ └── docs │ │ ├── webflux │ │ └── WebFluxDocsJavaConfigIntegrationTest.java │ │ └── webmvc │ │ ├── WebMvcDocsJavaConfigIntegrationTest.java │ │ └── WebMvcDocsXmlConfigIntegrationTest.java └── build.gradle.kts ├── integration-tests ├── src │ └── test │ │ └── java │ │ └── com │ │ └── mattbertolini │ │ └── spring │ │ ├── test │ │ └── web │ │ │ └── bind │ │ │ ├── records │ │ │ ├── FormParameterRecord.java │ │ │ ├── PathParameterRecord.java │ │ │ ├── CookieParameterRecord.java │ │ │ ├── HeaderParameterRecord.java │ │ │ ├── RequestParameterRecord.java │ │ │ ├── SessionParameterRecord.java │ │ │ ├── RequestContextRecord.java │ │ │ └── RequestBodyRecord.java │ │ │ ├── JsonBody.java │ │ │ ├── CookieParameterBean.java │ │ │ ├── SessionParameterBean.java │ │ │ ├── DirectFieldAccessBean.java │ │ │ ├── PathParameterBean.java │ │ │ ├── DirectFieldAccessController.java │ │ │ ├── CookieParameterController.java │ │ │ └── SessionParameterController.java │ │ └── web │ │ └── reactive │ │ └── test │ │ ├── SessionFilter.java │ │ └── SessionMutator.java └── build.gradle.kts ├── RELEASE_NOTES.md └── gradlew.bat /.java-version: -------------------------------------------------------------------------------- 1 | 17 2 | 3 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Update license year 2025 2 | a236fc3e5f121c109edcdd113f63618a5492dd5b -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.bat text eol=crlf 4 | *.cmd text eol=crlf 5 | 6 | *.jar binary 7 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | org.gradle.caching=true 3 | org.gradle.configuration-cache=true 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbertolini/spring-annotated-web-data-binder/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Disabling parallel builds as it doesn't work when publishing to Maven Central 4 | ./gradlew --no-parallel --no-configuration-cache build publish -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle/ 3 | 4 | # Ignore IntelliJ files 5 | .idea/ 6 | 7 | # Ignore Gradle build output directory 8 | build/ 9 | !buildSrc/src/**/build 10 | -------------------------------------------------------------------------------- /webflux-annotated-data-binder-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | com.mattbertolini.spring.web.reactive.bind.autoconfigure.WebFluxBinderAutoConfiguration 2 | -------------------------------------------------------------------------------- /webmvc-annotated-data-binder-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | com.mattbertolini.spring.web.servlet.mvc.bind.autoconfigure.WebMvcBinderAutoConfiguration 2 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.yml: -------------------------------------------------------------------------------- 1 | name: "Validate Gradle Wrapper" 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | validation: 6 | name: "Validation" 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: gradle/actions/wrapper-validation@v3 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "spring-annotated-web-data-binder" 2 | 3 | dependencyResolutionManagement { 4 | repositories { 5 | mavenCentral() 6 | } 7 | } 8 | 9 | include(":spring-annotated-data-binder-core") 10 | include(":spring-webmvc-annotated-data-binder") 11 | include(":spring-webflux-annotated-data-binder") 12 | include(":integration-tests") 13 | include(":docs") 14 | include(":webmvc-annotated-data-binder-spring-boot-starter") 15 | include(":webflux-annotated-data-binder-spring-boot-starter") 16 | -------------------------------------------------------------------------------- /LICENSE_HEADER.txt: -------------------------------------------------------------------------------- 1 | Copyright ${year} the original author or authors. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | 7 | [*.{java,gradle,gradle.kts}] 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | 12 | [*.java] 13 | ij_java_class_count_to_use_import_on_demand = 999 14 | ij_java_names_count_to_use_import_on_demand = 999 15 | 16 | [*.{bat,cmd}] 17 | end_of_line = crlf 18 | 19 | [*.adoc] 20 | indent_size = 2 21 | indent_style = space 22 | insert_final_newline = true 23 | trim_trailing_whitespace = true 24 | 25 | [*.md] 26 | indent_size = 4 27 | indent_style = space 28 | trim_trailing_whitespace = false 29 | 30 | [*.{yml,yaml}] 31 | indent_style = space 32 | indent_size = 2 33 | -------------------------------------------------------------------------------- /webmvc-annotated-data-binder-spring-boot-starter/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.mattbertolini.buildlogic.java-library") 3 | id("com.mattbertolini.buildlogic.maven-central-publish") 4 | } 5 | 6 | dependencies { 7 | api(project(":spring-webmvc-annotated-data-binder")) 8 | api(libs.springBootStarter) 9 | 10 | testImplementation(libs.junitJupiterApi) 11 | testImplementation(libs.assertJCore) 12 | testImplementation(libs.springTest) 13 | testImplementation(libs.springBootTest) 14 | } 15 | 16 | tasks.named("jar").configure { 17 | manifest { 18 | attributes( 19 | "Automatic-Module-Name" to "com.mattbertolini.spring.web.mvc.bind.autoconfigure" 20 | ) 21 | } 22 | } 23 | 24 | mavenCentralPublish { 25 | name.set("Spring MVC Annotated Data Binder Spring Boot Starter") 26 | description.set("Spring Boot starter for Spring MVC Annotated Java Bean data binder") 27 | } 28 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NonNullApi 17 | @NonNullFields 18 | package com.mattbertolini.spring.web.bind; 19 | 20 | import org.springframework.lang.NonNullApi; 21 | import org.springframework.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/support/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NonNullApi 17 | @NonNullFields 18 | package com.mattbertolini.spring.web.bind.support; 19 | 20 | import org.springframework.lang.NonNullApi; 21 | import org.springframework.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/resolver/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NonNullApi 17 | @NonNullFields 18 | package com.mattbertolini.spring.web.bind.resolver; 19 | 20 | import org.springframework.lang.NonNullApi; 21 | import org.springframework.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NonNullApi 17 | @NonNullFields 18 | package com.mattbertolini.spring.web.reactive.bind; 19 | 20 | import org.springframework.lang.NonNullApi; 21 | import org.springframework.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/annotation/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NonNullApi 17 | @NonNullFields 18 | package com.mattbertolini.spring.web.bind.annotation; 19 | 20 | import org.springframework.lang.NonNullApi; 21 | import org.springframework.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/introspect/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NonNullApi 17 | @NonNullFields 18 | package com.mattbertolini.spring.web.bind.introspect; 19 | 20 | import org.springframework.lang.NonNullApi; 21 | import org.springframework.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NonNullApi 17 | @NonNullFields 18 | package com.mattbertolini.spring.web.servlet.mvc.bind; 19 | 20 | import org.springframework.lang.NonNullApi; 21 | import org.springframework.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/config/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NonNullApi 17 | @NonNullFields 18 | package com.mattbertolini.spring.web.reactive.bind.config; 19 | 20 | import org.springframework.lang.NonNullApi; 21 | import org.springframework.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /docs/src/main/java/com/mattbertolini/spring/web/bind/docs/ExampleService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.docs; 17 | 18 | public class ExampleService { 19 | @SuppressWarnings("unused") 20 | public String doSomethingWith(CustomRequestBean customRequestBean) { 21 | return ""; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NonNullApi 17 | @NonNullFields 18 | package com.mattbertolini.spring.web.reactive.bind.resolver; 19 | 20 | import org.springframework.lang.NonNullApi; 21 | import org.springframework.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/config/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NonNullApi 17 | @NonNullFields 18 | package com.mattbertolini.spring.web.servlet.mvc.bind.config; 19 | 20 | import org.springframework.lang.NonNullApi; 21 | import org.springframework.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NonNullApi 17 | @NonNullFields 18 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 19 | 20 | import org.springframework.lang.NonNullApi; 21 | import org.springframework.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/records/FormParameterRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind.records; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.FormParameter; 19 | 20 | public record FormParameterRecord( 21 | @FormParameter("annotated_field") String annotated 22 | ) {} 23 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/records/PathParameterRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind.records; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.PathParameter; 19 | 20 | public record PathParameterRecord( 21 | @PathParameter("annotated_field") String annotated 22 | ) {} 23 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/records/CookieParameterRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind.records; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.CookieParameter; 19 | 20 | public record CookieParameterRecord( 21 | @CookieParameter("annotated_field") String annotated 22 | ) {} 23 | -------------------------------------------------------------------------------- /webflux-annotated-data-binder-spring-boot-starter/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.mattbertolini.buildlogic.java-library") 3 | id("com.mattbertolini.buildlogic.maven-central-publish") 4 | } 5 | 6 | dependencies { 7 | api(project(":spring-webflux-annotated-data-binder")) 8 | api(libs.springBootStarter) 9 | 10 | testImplementation(libs.junitJupiterApi) 11 | testImplementation(libs.assertJCore) 12 | testImplementation(libs.springBootTest) 13 | testImplementation(libs.jakartaWebsocketClientApi) 14 | testImplementation(libs.jakartaWebsocketApi) 15 | } 16 | 17 | tasks.named("jar").configure { 18 | manifest { 19 | attributes( 20 | "Automatic-Module-Name" to "com.mattbertolini.spring.web.reactive.bind.autoconfigure" 21 | ) 22 | } 23 | } 24 | 25 | mavenCentralPublish { 26 | name.set("Spring WebFlux Annotated Data Binder Spring Boot Starter") 27 | description.set("Spring Boot starter for Spring WebFlux Annotated Java Bean data binder") 28 | } 29 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/records/HeaderParameterRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind.records; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.HeaderParameter; 19 | 20 | public record HeaderParameterRecord( 21 | @HeaderParameter("x-annotated-field") String annotated 22 | ) {} 23 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/records/RequestParameterRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind.records; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.RequestParameter; 19 | 20 | public record RequestParameterRecord( 21 | @RequestParameter("annotated_field") String annotated 22 | ) {} 23 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/records/SessionParameterRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind.records; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.SessionParameter; 19 | 20 | public record SessionParameterRecord( 21 | @SessionParameter("annotated_field") String annotated 22 | ) {} 23 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/records/RequestContextRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind.records; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.RequestContext; 19 | 20 | import java.util.TimeZone; 21 | 22 | public record RequestContextRecord( 23 | @RequestContext TimeZone timeZone 24 | ) {} 25 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/RequestPropertyBindingException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind; 17 | 18 | public class RequestPropertyBindingException extends RuntimeException { 19 | public RequestPropertyBindingException(String message, Throwable cause) { 20 | super(message, cause); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /integration-tests/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.mattbertolini.buildlogic.java-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":spring-webmvc-annotated-data-binder")) 7 | implementation(project(":spring-webflux-annotated-data-binder")) 8 | implementation(libs.jakartaServletApi) 9 | implementation(libs.hibernateValidator) 10 | implementation(libs.glassfishJakartaEl) // Needed by Hibernate Validator 11 | implementation(libs.jacksonDatabind) 12 | implementation(libs.jakartaWebsocketApi) 13 | implementation(libs.jakartaWebsocketClientApi) 14 | compileOnly(libs.findbugsJsr305) 15 | 16 | testImplementation(libs.junitJupiterApi) 17 | testImplementation(libs.assertJCore) 18 | testImplementation(libs.springTest) 19 | testCompileOnly(libs.hamcrest) // Needed for Spring mock MVC matchers 20 | testCompileOnly(libs.findbugsJsr305) 21 | } 22 | 23 | tasks.named("jacocoTestReport").configure { 24 | reports { 25 | html.required.set(false) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/records/RequestBodyRecord.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind.records; 17 | 18 | import com.mattbertolini.spring.test.web.bind.JsonBody; 19 | import com.mattbertolini.spring.web.bind.annotation.RequestBody; 20 | 21 | public record RequestBodyRecord( 22 | @RequestBody JsonBody jsonBody 23 | ) {} 24 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/introspect/RequestBeanIntrospectionException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.introspect; 17 | 18 | public class RequestBeanIntrospectionException extends RuntimeException { 19 | public RequestBeanIntrospectionException(String message, Throwable cause) { 20 | super(message, cause); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/introspect/ResolvedPropertyData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.introspect; 17 | 18 | import com.mattbertolini.spring.web.bind.resolver.RequestPropertyResolverBase; 19 | 20 | public record ResolvedPropertyData( 21 | String propertyName, 22 | BindingProperty bindingProperty, 23 | RequestPropertyResolverBase resolver) {} 24 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/introspect/CircularReferenceException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.introspect; 17 | 18 | /** 19 | * Exception thrown when the introspector encounters a circular reference. 20 | */ 21 | public class CircularReferenceException extends RuntimeException { 22 | public CircularReferenceException(String message) { 23 | super(message); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/PropertyResolutionException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind; 17 | 18 | /** 19 | * Exception thrown when a known exception is thrown during property resolution. 20 | */ 21 | public class PropertyResolutionException extends RuntimeException { 22 | public PropertyResolutionException(String message, Throwable cause) { 23 | super(message, cause); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/PropertyResolverRegistry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind; 17 | 18 | import com.mattbertolini.spring.web.bind.AbstractPropertyResolverRegistry; 19 | import com.mattbertolini.spring.web.reactive.bind.resolver.RequestPropertyResolver; 20 | 21 | public class PropertyResolverRegistry extends AbstractPropertyResolverRegistry { 22 | } 23 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/PropertyResolverRegistry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind; 17 | 18 | import com.mattbertolini.spring.web.bind.AbstractPropertyResolverRegistry; 19 | import com.mattbertolini.spring.web.servlet.mvc.bind.resolver.RequestPropertyResolver; 20 | 21 | public class PropertyResolverRegistry extends AbstractPropertyResolverRegistry { 22 | } 23 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/RequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.resolver.RequestPropertyResolverBase; 19 | import org.springframework.web.context.request.NativeWebRequest; 20 | 21 | public interface RequestPropertyResolver extends RequestPropertyResolverBase { 22 | } 23 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.mattbertolini.buildlogic.java-library") 3 | id("com.mattbertolini.buildlogic.maven-central-publish") 4 | } 5 | 6 | dependencies { 7 | api(libs.springContext) 8 | api(libs.springBeans) 9 | api(libs.springWeb) 10 | compileOnly(libs.findbugsJsr305) // To Prevent warnings on missing enum constants 11 | compileOnly(libs.jakartaServletApi) // So Javadoc doesn't give warnings about missing links 12 | 13 | testImplementation(libs.junitJupiterApi) 14 | testImplementation(libs.assertJCore) 15 | testImplementation(libs.mockitoCore) 16 | testImplementation(libs.springTest) 17 | testImplementation(libs.equalsVerifier) 18 | testCompileOnly(libs.findbugsJsr305) 19 | } 20 | 21 | tasks.named("jar").configure { 22 | manifest { 23 | attributes( 24 | "Automatic-Module-Name" to "com.mattbertolini.spring.web.bind" 25 | ) 26 | } 27 | } 28 | 29 | mavenCentralPublish { 30 | name.set("Spring Annotated Data Binder Core") 31 | description.set("Core module for Spring annotated web data binder") 32 | } 33 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.mattbertolini.buildlogic.java-library") 3 | id("com.mattbertolini.buildlogic.maven-central-publish") 4 | } 5 | 6 | dependencies { 7 | api(project(":spring-annotated-data-binder-core")) 8 | api(libs.springWebflux) 9 | compileOnly(libs.findbugsJsr305) // To Prevent warnings on missing enum constants 10 | 11 | testImplementation(libs.junitJupiterApi) 12 | testImplementation(libs.assertJCore) 13 | testImplementation(libs.mockitoCore) 14 | testImplementation(libs.springTest) 15 | testImplementation(libs.jakartaValidationApi) // Used to test validation annotations 16 | testCompileOnly(libs.findbugsJsr305) // To Prevent warnings on missing enum constants 17 | } 18 | 19 | tasks.named("jar").configure { 20 | manifest { 21 | attributes( 22 | "Automatic-Module-Name" to "com.mattbertolini.spring.web.reactive.bind" 23 | ) 24 | } 25 | } 26 | 27 | mavenCentralPublish { 28 | name.set("Spring WebFlux Annotated Data Binder") 29 | description.set("Annotated Java Bean data binder for Spring WebFlux") 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | jdk: 18 | - version: 17 19 | tasks: build jacocoTestReport testCodeCoverageReport 20 | - version: 21 21 | tasks: build jacocoTestReport testCodeCoverageReport sonar 22 | name: JDK ${{ matrix.jdk.version }} 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | with: 27 | # This is needed for Sonar to do analysis 28 | fetch-depth: 0 29 | 30 | - name: Setup Java JDK 31 | uses: actions/setup-java@v4 32 | with: 33 | java-version: ${{ matrix.jdk.version }} 34 | distribution: 'temurin' 35 | 36 | - name: Setup Gradle 37 | uses: gradle/actions/setup-gradle@v3 38 | 39 | - name: Run Gradle 40 | env: 41 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 42 | run: ./gradlew -Dorg.gradle.welcome=never ${{ matrix.jdk.tasks }} 43 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.mattbertolini.buildlogic.java-library") 3 | id("com.mattbertolini.buildlogic.maven-central-publish") 4 | } 5 | 6 | dependencies { 7 | api(project(":spring-annotated-data-binder-core")) 8 | api(libs.springWebmvc) 9 | implementation(libs.jakartaServletApi) 10 | compileOnly(libs.findbugsJsr305) // To Prevent warnings on missing enum constants 11 | 12 | testImplementation(libs.junitJupiterApi) 13 | testImplementation(libs.assertJCore) 14 | testImplementation(libs.mockitoCore) 15 | testImplementation(libs.springTest) 16 | testImplementation(libs.jakartaValidationApi) // Used to test validation annotations 17 | testCompileOnly(libs.findbugsJsr305) // To Prevent warnings on missing enum constants 18 | } 19 | 20 | tasks.named("jar").configure { 21 | manifest { 22 | attributes( 23 | "Automatic-Module-Name" to "com.mattbertolini.spring.web.servlet.mvc.bind" 24 | ) 25 | } 26 | } 27 | 28 | mavenCentralPublish { 29 | name.set("Spring MVC Annotated Data Binder") 30 | description.set("Annotated Java Bean data binder for Spring MVC") 31 | } 32 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/test/java/com/mattbertolini/spring/web/bind/introspect/scan/IgnoredBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.introspect.scan; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.RequestParameter; 19 | import org.springframework.lang.Nullable; 20 | 21 | public class IgnoredBean { 22 | @RequestParameter("property") 23 | @Nullable 24 | private String property; 25 | 26 | @Nullable 27 | public String getProperty() { 28 | return property; 29 | } 30 | 31 | public void setProperty(String property) { 32 | this.property = property; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/src/main/java/com/mattbertolini/spring/web/bind/docs/NestedBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.docs; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.RequestParameter; 19 | import org.springframework.lang.Nullable; 20 | 21 | public class NestedBean { 22 | @Nullable 23 | @RequestParameter("nested_request_param") 24 | private String nestedRequestParameter; 25 | 26 | @Nullable 27 | public String getNestedRequestParameter() { 28 | return nestedRequestParameter; 29 | } 30 | 31 | public void setNestedRequestParameter(String nestedRequestParameter) { 32 | this.nestedRequestParameter = nestedRequestParameter; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/test/java/com/mattbertolini/spring/web/bind/introspect/scan/ScannedBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.introspect.scan; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.RequestBean; 19 | import com.mattbertolini.spring.web.bind.annotation.RequestParameter; 20 | import org.springframework.lang.Nullable; 21 | 22 | @RequestBean 23 | public class ScannedBean { 24 | @RequestParameter("property") 25 | @Nullable 26 | private String property; 27 | 28 | @Nullable 29 | public String getProperty() { 30 | return property; 31 | } 32 | 33 | public void setProperty(String property) { 34 | this.property = property; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/JsonBody.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind; 17 | 18 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 19 | import com.fasterxml.jackson.annotation.JsonInclude; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import org.springframework.lang.Nullable; 22 | 23 | @JsonIgnoreProperties(ignoreUnknown = true) 24 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 25 | public class JsonBody { 26 | @Nullable 27 | @JsonProperty("json_property") 28 | private String property; 29 | 30 | @Nullable 31 | public String getProperty() { 32 | return property; 33 | } 34 | 35 | public void setProperty(String property) { 36 | this.property = property; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/test/java/com/mattbertolini/spring/web/bind/introspect/scan/subbackage/SubpackageBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.introspect.scan.subbackage; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.FormParameter; 19 | import com.mattbertolini.spring.web.bind.annotation.RequestBean; 20 | import org.springframework.lang.Nullable; 21 | 22 | @RequestBean 23 | public class SubpackageBean { 24 | @FormParameter("subpackage_property") 25 | @Nullable 26 | private String subpackageProperty; 27 | 28 | @Nullable 29 | public String getSubpackageProperty() { 30 | return subpackageProperty; 31 | } 32 | 33 | public void setSubpackageProperty(String subpackageProperty) { 34 | this.subpackageProperty = subpackageProperty; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/resolver/RequestPropertyResolverBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 19 | import org.springframework.lang.Nullable; 20 | 21 | /** 22 | * This interface should be considered an internal interface and should not be implemented by external users. Instead, 23 | * use one of the sub-interfaces that are bound to a concrete request type. 24 | * 25 | * @param The request type to use with the resolver. 26 | * @param The response type to use. 27 | */ 28 | public interface RequestPropertyResolverBase { 29 | boolean supports(BindingProperty bindingProperty); 30 | 31 | @Nullable 32 | R resolve(BindingProperty bindingProperty, T request); 33 | } 34 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/resolver/AbstractNamedRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 19 | import org.springframework.lang.Nullable; 20 | 21 | public abstract class AbstractNamedRequestPropertyResolver implements RequestPropertyResolverBase { 22 | protected abstract String getName(BindingProperty bindingProperty); 23 | 24 | @Override 25 | @Nullable 26 | public final R resolve(BindingProperty bindingProperty, T request) { 27 | String name = getName(bindingProperty); 28 | return resolveWithName(bindingProperty, name, request); 29 | } 30 | 31 | @Nullable 32 | protected abstract R resolveWithName(BindingProperty bindingProperty, String name, T request); 33 | } 34 | -------------------------------------------------------------------------------- /docs/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.asciidoctor.gradle.jvm.AsciidoctorTask 2 | 3 | plugins { 4 | id("com.mattbertolini.buildlogic.java-conventions") 5 | alias(libs.plugins.asciidoctorConvert) 6 | } 7 | 8 | configurations { 9 | register("asciidoctorExt") 10 | } 11 | 12 | dependencies { 13 | implementation(project(":spring-webmvc-annotated-data-binder")) 14 | implementation(project(":spring-webflux-annotated-data-binder")) 15 | implementation(libs.jakartaServletApi) // Version defined in Spring BOM file 16 | compileOnly(libs.findbugsJsr305) 17 | 18 | add("asciidoctorExt", libs.springAsciidoctorExtBlockSwitch) 19 | 20 | testImplementation(libs.junitJupiterApi) 21 | testImplementation(libs.assertJCore) 22 | testImplementation(libs.mockitoCore) 23 | testImplementation(libs.springTest) 24 | testImplementation(libs.jakartaWebsocketApi) 25 | testImplementation(libs.jakartaWebsocketClientApi) 26 | testCompileOnly(libs.hamcrest) // Needed for Spring mock MVC matchers 27 | testCompileOnly(libs.findbugsJsr305) 28 | } 29 | 30 | tasks.named("asciidoctor").configure { 31 | attributes(mapOf( 32 | "sourceDir" to project.sourceSets["main"].allJava.srcDirs.first(), 33 | "resourcesDir" to project.sourceSets["main"].resources.srcDirs.first(), 34 | "source-highlighter" to "coderay" 35 | )) 36 | configurations("asciidoctorExt") 37 | } 38 | 39 | tasks.named("jacocoTestReport").configure { 40 | reports { 41 | html.required.set(false) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/RequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 19 | import com.mattbertolini.spring.web.bind.resolver.RequestPropertyResolverBase; 20 | import org.springframework.lang.NonNull; 21 | import org.springframework.web.server.ServerWebExchange; 22 | import reactor.core.publisher.Mono; 23 | 24 | /** 25 | * The main property resolver interface for Spring WebFlux-based property resolvers. 26 | */ 27 | public interface RequestPropertyResolver extends RequestPropertyResolverBase> { 28 | @Override 29 | @NonNull // Explicitly setting NonNull as we are overriding a Nullable parent method 30 | Mono resolve(BindingProperty bindingProperty, ServerWebExchange request); 31 | } 32 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/test/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/ExceptionThrowingMockMultipartHttpServletRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 17 | 18 | import jakarta.servlet.ServletException; 19 | import jakarta.servlet.http.Part; 20 | import org.springframework.mock.web.MockMultipartHttpServletRequest; 21 | import org.springframework.web.multipart.MultipartFile; 22 | 23 | import java.io.IOException; 24 | import java.util.Collection; 25 | 26 | public class ExceptionThrowingMockMultipartHttpServletRequest extends MockMultipartHttpServletRequest { 27 | @Override 28 | public MultipartFile getFile(String name) { 29 | throw new RuntimeException("Failure in getFile"); 30 | } 31 | 32 | @Override 33 | public Collection getParts() throws IOException, ServletException { 34 | throw new ServletException("Failure in getParts"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/FormParameterMapRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.FormParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.lang.NonNull; 21 | import org.springframework.util.StringUtils; 22 | 23 | import java.util.Map; 24 | 25 | public class FormParameterMapRequestPropertyResolver extends RequestParameterMapRequestPropertyResolver { 26 | @Override 27 | public boolean supports(@NonNull BindingProperty bindingProperty) { 28 | FormParameter annotation = bindingProperty.getAnnotation(FormParameter.class); 29 | return annotation != null && !StringUtils.hasText(annotation.value()) && 30 | Map.class.isAssignableFrom(bindingProperty.getType()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/src/main/java/com/mattbertolini/spring/web/bind/docs/ExampleController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.docs; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.BeanParameter; 19 | import org.springframework.http.MediaType; 20 | import org.springframework.web.bind.annotation.PostMapping; 21 | import org.springframework.web.bind.annotation.RestController; 22 | 23 | // tag::controllerBody[] 24 | @RestController 25 | public class ExampleController { 26 | private final ExampleService exampleService; 27 | 28 | public ExampleController(ExampleService exampleService) { 29 | this.exampleService = exampleService; 30 | } 31 | 32 | @PostMapping(value = "/example/{pathParam}", produces = MediaType.TEXT_PLAIN_VALUE) 33 | public String handleRequest(@BeanParameter CustomRequestBean customRequestBean) { 34 | return exampleService.doSomethingWith(customRequestBean); 35 | } 36 | } 37 | // end::controllerBody[] 38 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/introspect/CachedAnnotatedRequestBeanIntrospector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.introspect; 17 | 18 | import java.util.Map; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | import java.util.concurrent.ConcurrentMap; 21 | 22 | public class CachedAnnotatedRequestBeanIntrospector implements AnnotatedRequestBeanIntrospector { 23 | private final AnnotatedRequestBeanIntrospector delegate; 24 | private final ConcurrentMap, Map> cache; 25 | 26 | public CachedAnnotatedRequestBeanIntrospector(AnnotatedRequestBeanIntrospector delegate) { 27 | this.delegate = delegate; 28 | cache = new ConcurrentHashMap<>(); 29 | } 30 | 31 | @Override 32 | public Map getResolverMapFor(Class targetType) { 33 | return cache.computeIfAbsent(targetType, delegate::getResolverMapFor); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/web/reactive/test/SessionFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.test; 17 | 18 | import org.springframework.lang.NonNull; 19 | import org.springframework.web.server.ServerWebExchange; 20 | import org.springframework.web.server.WebFilter; 21 | import org.springframework.web.server.WebFilterChain; 22 | import reactor.core.publisher.Mono; 23 | 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | 27 | class SessionFilter implements WebFilter { 28 | 29 | private final Map attributes = new HashMap<>(); 30 | 31 | public SessionFilter(Map attributes) { 32 | this.attributes .putAll(attributes); 33 | } 34 | 35 | @Override 36 | @NonNull 37 | public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { 38 | return exchange.getSession() 39 | .doOnNext(webSession -> webSession.getAttributes().putAll(attributes)) 40 | .then(chain.filter(exchange)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/src/main/java/com/mattbertolini/spring/web/bind/docs/webflux/ExampleWebFluxContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.docs.webflux; 17 | 18 | import com.mattbertolini.spring.web.bind.docs.ExampleController; 19 | import com.mattbertolini.spring.web.bind.docs.ExampleService; 20 | import com.mattbertolini.spring.web.reactive.bind.config.BinderConfiguration; 21 | import org.springframework.context.annotation.Bean; 22 | import org.springframework.context.annotation.Configuration; 23 | import org.springframework.web.reactive.config.EnableWebFlux; 24 | 25 | @Configuration 26 | @EnableWebFlux 27 | public class ExampleWebFluxContext { 28 | @Bean 29 | public BinderConfiguration binderConfiguration() { 30 | return new BinderConfiguration(); 31 | } 32 | 33 | @Bean 34 | public ExampleController exampleController(ExampleService exampleService) { 35 | return new ExampleController(exampleService); 36 | } 37 | 38 | @Bean 39 | public ExampleService exampleService() { 40 | return new ExampleService(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/support/MapValueResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.support; 17 | 18 | import org.springframework.lang.Nullable; 19 | import org.springframework.validation.DataBinder; 20 | 21 | import java.util.Collections; 22 | import java.util.Map; 23 | import java.util.Set; 24 | 25 | /** 26 | * Implementation of the {@link DataBinder.ValueResolver} to assist in mapping binding data for constructor binding. 27 | * 28 | * @param values The Map of values to pass to the binder 29 | */ 30 | public record MapValueResolver(Map values) implements DataBinder.ValueResolver { 31 | @Override 32 | @Nullable 33 | public Object resolveValue(String name, Class type) { 34 | return values.get(name); 35 | } 36 | 37 | @Override 38 | public Set getNames() { 39 | return Set.copyOf(values.keySet()); 40 | } 41 | 42 | @Override 43 | public Map values() { 44 | return Collections.unmodifiableMap(values); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/annotation/SessionParameter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.annotation; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | /** 25 | *

Annotation for fetching session attributes.

26 | * 27 | *

This annotation can be used on fields: 28 | *

{@code
29 |  *     @SessionParameter("attributeName")
30 |  *     private String attributeName;
31 |  * }
32 | * or on getter/setter methods: 33 | *
{@code
34 |  *     @SessionParameter("attributeName")
35 |  *     public void setAttributeName(String attributeName) {
36 |  *         this.attributeName = attributeName;
37 |  *     }
38 |  * }
39 | *

40 | */ 41 | @Target({ElementType.FIELD, ElementType.METHOD}) 42 | @Retention(RetentionPolicy.RUNTIME) 43 | @Documented 44 | public @interface SessionParameter { 45 | String value(); 46 | } 47 | -------------------------------------------------------------------------------- /docs/src/main/java/com/mattbertolini/spring/web/bind/docs/webmvc/ExampleMvcContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.docs.webmvc; 17 | 18 | import com.mattbertolini.spring.web.bind.docs.ExampleController; 19 | import com.mattbertolini.spring.web.bind.docs.ExampleService; 20 | import com.mattbertolini.spring.web.servlet.mvc.bind.config.BinderConfiguration; 21 | import org.springframework.context.annotation.Bean; 22 | import org.springframework.context.annotation.Configuration; 23 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 24 | 25 | @Configuration 26 | @EnableWebMvc 27 | public class ExampleMvcContext { 28 | // tag::javaMvcConfiguration[] 29 | @Bean 30 | public BinderConfiguration binderConfiguration() { 31 | return new BinderConfiguration(); 32 | } 33 | // end::javaMvcConfiguration[] 34 | 35 | @Bean 36 | public ExampleController exampleController(ExampleService exampleService) { 37 | return new ExampleController(exampleService); 38 | } 39 | 40 | @Bean 41 | public ExampleService exampleService() { 42 | return new ExampleService(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/FormParameterRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.FormParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.util.StringUtils; 21 | 22 | import java.util.Objects; 23 | 24 | public class FormParameterRequestPropertyResolver extends RequestParameterRequestPropertyResolver { 25 | @Override 26 | public boolean supports(BindingProperty bindingProperty) { 27 | FormParameter annotation = bindingProperty.getAnnotation(FormParameter.class); 28 | return annotation != null && StringUtils.hasText(annotation.value()); 29 | } 30 | 31 | @Override 32 | protected String getName(BindingProperty bindingProperty) { 33 | FormParameter annotation = bindingProperty.getAnnotation(FormParameter.class); 34 | Objects.requireNonNull(annotation, "No FormParameter annotation found on type"); 35 | return annotation.value(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/src/main/resources/com/mattbertolini/spring/web/bind/docs/webmvc/example-context.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/web/reactive/test/SessionMutator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.test; 17 | 18 | import org.springframework.http.client.reactive.ClientHttpConnector; 19 | import org.springframework.lang.NonNull; 20 | import org.springframework.test.web.reactive.server.WebTestClient; 21 | import org.springframework.test.web.reactive.server.WebTestClientConfigurer; 22 | import org.springframework.web.server.adapter.WebHttpHandlerBuilder; 23 | 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | 27 | class SessionMutator implements WebTestClientConfigurer { 28 | private final Map attributes = new HashMap<>(); 29 | 30 | public static SessionMutator session() { 31 | return new SessionMutator(); 32 | } 33 | 34 | public SessionMutator attribute(String name, String value) { 35 | attributes.put(name, value); 36 | return this; 37 | } 38 | 39 | @Override 40 | public void afterConfigurerAdded(@NonNull WebTestClient.Builder builder, WebHttpHandlerBuilder httpHandlerBuilder, ClientHttpConnector connector) { 41 | httpHandlerBuilder.filter(new SessionFilter(attributes)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/SessionParameterRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.SessionParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.lang.Nullable; 21 | import org.springframework.web.context.request.NativeWebRequest; 22 | import org.springframework.web.context.request.RequestAttributes; 23 | 24 | import java.util.Objects; 25 | 26 | public class SessionParameterRequestPropertyResolver implements RequestPropertyResolver { 27 | @Override 28 | public boolean supports(BindingProperty bindingProperty) { 29 | return bindingProperty.hasAnnotation(SessionParameter.class); 30 | } 31 | 32 | @Override 33 | @Nullable 34 | public Object resolve(BindingProperty bindingProperty, NativeWebRequest request) { 35 | SessionParameter annotation = bindingProperty.getAnnotation(SessionParameter.class); 36 | Objects.requireNonNull(annotation, "No SessionParameter annotation found on type"); 37 | return request.getAttribute(annotation.value(), RequestAttributes.SCOPE_SESSION); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/test/java/com/mattbertolini/spring/web/bind/support/MapValueResolverTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.support; 17 | 18 | import org.junit.jupiter.api.Test; 19 | 20 | import java.util.Map; 21 | 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | 24 | class MapValueResolverTest { 25 | @Test 26 | void resolveValueReturnsMapValueFromKey() { 27 | Map map = Map.of("key1", "value1", "key2", "value2"); 28 | MapValueResolver valueResolver = new MapValueResolver(map); 29 | assertThat(valueResolver.resolveValue("key1", String.class)).isEqualTo("value1"); 30 | } 31 | 32 | @Test 33 | void getNamesReturnsMapKeys() { 34 | Map map = Map.of("key1", "value1", "key2", "value2"); 35 | MapValueResolver valueResolver = new MapValueResolver(map); 36 | assertThat(valueResolver.getNames()).contains("key1", "key2"); 37 | } 38 | 39 | @Test 40 | void valuesAccessorIsUnmodifiable() { 41 | Map map = Map.of("key1", "value1", "key2", "value2"); 42 | MapValueResolver valueResolver = new MapValueResolver(map); 43 | assertThat(valueResolver.values()) 44 | .isUnmodifiable() 45 | .isEqualTo(map); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/annotation/PathParameter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.annotation; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | /** 25 | *

Annotation for fetching Spring path variable values.

26 | * 27 | *

This annotation can be used on fields: 28 | *

{@code
29 |  *     @PathParameter("pathVar")
30 |  *     private String pathVariable;
31 |  * }
32 | * or on getter/setter methods: 33 | *
{@code
34 |  *     @PathParameter("pathVar")
35 |  *     public void setPathVariable(String pathVariable) {
36 |  *         this.pathVariable = pathVariable;
37 |  *     }
38 |  * }
39 | *

40 | * 41 | *

Setting this annotation without a value on a {@link java.util.Map Map} binds all path variables to a Map. 42 | *

{@code
43 |  *     @PathParameter
44 |  *     private Map pathVariables;
45 |  * }
46 | *

47 | */ 48 | @Target({ElementType.FIELD, ElementType.METHOD}) 49 | @Retention(RetentionPolicy.RUNTIME) 50 | @Documented 51 | public @interface PathParameter { 52 | String value() default ""; 53 | } 54 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/SessionParameterRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.SessionParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.lang.NonNull; 21 | import org.springframework.web.server.ServerWebExchange; 22 | import reactor.core.publisher.Mono; 23 | 24 | import java.util.Objects; 25 | 26 | public class SessionParameterRequestPropertyResolver implements RequestPropertyResolver { 27 | @Override 28 | public boolean supports(BindingProperty bindingProperty) { 29 | return bindingProperty.hasAnnotation(SessionParameter.class); 30 | } 31 | 32 | @NonNull 33 | @Override 34 | public Mono resolve(BindingProperty bindingProperty, ServerWebExchange exchange) { 35 | SessionParameter annotation = bindingProperty.getAnnotation(SessionParameter.class); 36 | Objects.requireNonNull(annotation, "No SessionParameter annotation found on type"); 37 | return exchange.getSession() 38 | .filter(session -> session.getAttribute(annotation.value()) != null) 39 | .mapNotNull(session -> session.getAttribute(annotation.value())); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/annotation/RequestBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.annotation; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | /** 25 | *

Marker annotation that enables introspection at startup when combined with the 26 | * {@link com.mattbertolini.spring.web.bind.introspect.ClassPathScanningAnnotatedRequestBeanIntrospector}. Any Java 27 | * bean with this annotation found in a package scanned by the introspector will have it's resolved property data 28 | * pre-loaded and cached so it can be retrieved at request time without the need for additional introspection. This is 29 | * a performance improvement so introspection is not done at request time. 30 | *

{@code
31 |  *     @RequestBean
32 |  *     public class ExampleRequestBean {
33 |  *         @RequestParameter("some_parameter")
34 |  *         private String someParameter;
35 |  *         
36 |  *         // Getters/Setters
37 |  *     }
38 |  * }
39 | *

40 | * @see com.mattbertolini.spring.web.bind.introspect.ClassPathScanningAnnotatedRequestBeanIntrospector 41 | */ 42 | @Target({ElementType.TYPE}) 43 | @Retention(RetentionPolicy.RUNTIME) 44 | @Documented 45 | public @interface RequestBean {} 46 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/HeaderParameterRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.HeaderParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.lang.Nullable; 21 | import org.springframework.util.StringUtils; 22 | import org.springframework.web.context.request.NativeWebRequest; 23 | 24 | import java.util.Objects; 25 | 26 | /** 27 | * Resolve HTTP header values 28 | * 29 | * @see NativeWebRequest#getHeaderValues(String) 30 | */ 31 | public class HeaderParameterRequestPropertyResolver implements RequestPropertyResolver { 32 | 33 | @Override 34 | public boolean supports(BindingProperty bindingProperty) { 35 | HeaderParameter annotation = bindingProperty.getAnnotation(HeaderParameter.class); 36 | return annotation != null && StringUtils.hasText(annotation.value()); 37 | } 38 | 39 | @Override 40 | @Nullable 41 | public Object resolve(BindingProperty bindingProperty, NativeWebRequest request) { 42 | HeaderParameter annotation = bindingProperty.getAnnotation(HeaderParameter.class); 43 | Objects.requireNonNull(annotation, "No HeaderParameter annotation found on type"); 44 | return request.getHeaderValues(annotation.value()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/PathParameterMapRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.PathParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.lang.NonNull; 21 | import org.springframework.util.StringUtils; 22 | import org.springframework.web.reactive.HandlerMapping; 23 | import org.springframework.web.server.ServerWebExchange; 24 | import reactor.core.publisher.Mono; 25 | 26 | import java.util.Collections; 27 | import java.util.Map; 28 | 29 | public class PathParameterMapRequestPropertyResolver implements RequestPropertyResolver { 30 | @Override 31 | public boolean supports(@NonNull BindingProperty bindingProperty) { 32 | PathParameter annotation = bindingProperty.getAnnotation(PathParameter.class); 33 | return annotation != null && !StringUtils.hasText(annotation.value()) && 34 | Map.class.isAssignableFrom(bindingProperty.getType()); 35 | } 36 | 37 | @Override 38 | @NonNull 39 | public Mono resolve(@NonNull BindingProperty bindingProperty, @NonNull ServerWebExchange exchange) { 40 | Map pathVariables = exchange.getAttributeOrDefault(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Collections.emptyMap()); 41 | return Mono.just(pathVariables); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/HeaderParameterRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.HeaderParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.http.HttpHeaders; 21 | import org.springframework.lang.NonNull; 22 | import org.springframework.util.StringUtils; 23 | import org.springframework.web.server.ServerWebExchange; 24 | import reactor.core.publisher.Mono; 25 | 26 | import java.util.Objects; 27 | 28 | public class HeaderParameterRequestPropertyResolver implements RequestPropertyResolver { 29 | @Override 30 | public boolean supports(@NonNull BindingProperty bindingProperty) { 31 | HeaderParameter annotation = bindingProperty.getAnnotation(HeaderParameter.class); 32 | return annotation != null && StringUtils.hasText(annotation.value()); 33 | } 34 | 35 | @Override 36 | @NonNull 37 | public Mono resolve(BindingProperty bindingProperty, ServerWebExchange request) { 38 | HttpHeaders headers = request.getRequest().getHeaders(); 39 | HeaderParameter annotation = bindingProperty.getAnnotation(HeaderParameter.class); 40 | Objects.requireNonNull(annotation, "No HeaderParameter annotation found on type"); 41 | return Mono.justOrEmpty(headers.get(annotation.value())); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/FormParameterMapRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.FormParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.lang.NonNull; 21 | import org.springframework.util.MultiValueMap; 22 | import org.springframework.util.StringUtils; 23 | import org.springframework.web.server.ServerWebExchange; 24 | import reactor.core.publisher.Mono; 25 | 26 | import java.util.Map; 27 | import java.util.function.Function; 28 | 29 | public class FormParameterMapRequestPropertyResolver implements RequestPropertyResolver { 30 | @Override 31 | public boolean supports(BindingProperty bindingProperty) { 32 | FormParameter annotation = bindingProperty.getAnnotation(FormParameter.class); 33 | return annotation != null && !StringUtils.hasText(annotation.value()) && 34 | Map.class.isAssignableFrom(bindingProperty.getType()); 35 | } 36 | 37 | @Override 38 | @NonNull 39 | public Mono resolve(BindingProperty bindingProperty, ServerWebExchange exchange) { 40 | if (MultiValueMap.class.isAssignableFrom(bindingProperty.getType())) { 41 | return exchange.getFormData().map(Function.identity()); 42 | } 43 | return exchange.getFormData().map(MultiValueMap::toSingleValueMap); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/HeaderParameterMapRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.HeaderParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.lang.NonNull; 21 | import org.springframework.util.MultiValueMap; 22 | import org.springframework.util.StringUtils; 23 | import org.springframework.web.server.ServerWebExchange; 24 | import reactor.core.publisher.Mono; 25 | 26 | import java.util.Map; 27 | 28 | public class HeaderParameterMapRequestPropertyResolver implements RequestPropertyResolver { 29 | @Override 30 | public boolean supports(BindingProperty bindingProperty) { 31 | HeaderParameter annotation = bindingProperty.getAnnotation(HeaderParameter.class); 32 | return annotation != null && !StringUtils.hasText(annotation.value()) && 33 | Map.class.isAssignableFrom(bindingProperty.getType()); 34 | } 35 | 36 | @Override 37 | @NonNull 38 | public Mono resolve(BindingProperty bindingProperty, ServerWebExchange exchange) { 39 | // HttpHeaders class extends from MultiValueMap 40 | if (MultiValueMap.class.isAssignableFrom(bindingProperty.getType())) { 41 | return Mono.just(exchange.getRequest().getHeaders()); 42 | } 43 | return Mono.just(exchange.getRequest().getHeaders().toSingleValueMap()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/RequestParameterMapRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.RequestParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.lang.NonNull; 21 | import org.springframework.util.MultiValueMap; 22 | import org.springframework.util.StringUtils; 23 | import org.springframework.web.server.ServerWebExchange; 24 | import reactor.core.publisher.Mono; 25 | 26 | import java.util.Map; 27 | 28 | public class RequestParameterMapRequestPropertyResolver implements RequestPropertyResolver { 29 | @Override 30 | public boolean supports(@NonNull BindingProperty bindingProperty) { 31 | RequestParameter annotation = bindingProperty.getAnnotation(RequestParameter.class); 32 | return annotation != null && !StringUtils.hasText(annotation.value()) && 33 | Map.class.isAssignableFrom(bindingProperty.getType()); 34 | } 35 | 36 | @Override 37 | @NonNull 38 | public Mono resolve(@NonNull BindingProperty bindingProperty, @NonNull ServerWebExchange request) { 39 | MultiValueMap queryParams = request.getRequest().getQueryParams(); 40 | if (MultiValueMap.class.isAssignableFrom(bindingProperty.getType())) { 41 | return Mono.just(queryParams); 42 | } 43 | return Mono.just(queryParams.toSingleValueMap()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## 1.0.0-SNAPSHOT 4 | Released N/A 5 | 6 | - Breaking change 7 | - Updated to support Spring 6.x and Spring Boot 3.x. This brings with it a minimum Java version of 17. With the Spring 8 | upgrade comes a switch to the Jakara EE Servlet API instead of Java EE. 9 | - Update build to Gradle 8.9 10 | - Added support for ErrorProne and NullAway for compile time checks of nullability. 11 | 12 | ## 0.6.0 13 | Released 2023-12-01 14 | 15 | - Restructured Gradle build files to more convention based approach. 16 | - Started using type-safe dependency notation in build files. 17 | - Fix an issue where servlet api version was not being set correctly in published artifacts in Maven Central ([#11](https://github.com/mattbertolini/spring-annotated-web-data-binder/issues/11)). 18 | - Upgrade Spring Framework to 5.3.28. 19 | - Upgrade Spring Boot to 2.7.13 20 | - Update build to Gradle 8.5 21 | 22 | ## 0.5.0 23 | Released 2022-05-22 24 | 25 | - Add preliminary support for binding to Java `record` types. 26 | - JDK 17 is now required to build this project. The production artifacts are still compiled to Java 1.8 bytecode. 27 | - Upgrade to Spring Framework 5.3.13 28 | - Upgrade to Spring Boot 2.4.13 29 | - Upgraded to equals verifier 3.10 30 | - Moved the Gradle build from Groovy DSL to Kotlin DSL 31 | 32 | ## 0.4.0 33 | Released 2021-06-20 34 | 35 | - New Spring Boot starter modules for both Web MVC and WebFlux implementations. Allows quick and easy use of the data 36 | binder in Spring Boot applications with no configuration. 37 | - Add basic support for Multipart request data in WebFlux. 38 | - Minimum supported Spring version: 5.3.8 39 | - Minimum supported Spring Boot version: 2.4.7 40 | 41 | ## 0.3.0 42 | Released 2021-03-24 43 | 44 | - Add support for binding `MultipartFile` and Servlet API `Part` objects using `@FormParamter` and `@RequestParamter` 45 | annotations. `MultipartFile` and `Part` are only for Spring MVC and do not work in Spring WebFlux. 46 | 47 | ## 0.2.0 48 | Released 2020-09-02 49 | 50 | - Add support for binding request payloads using the `@RequestBody` annotation. 51 | 52 | ## 0.1.0 53 | Released 2020-04-29 54 | 55 | - Initial release 56 | - Minimum supported Spring version: 5.2.6 -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/RequestParameterRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.RequestParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.lang.NonNull; 21 | import org.springframework.util.MultiValueMap; 22 | import org.springframework.util.StringUtils; 23 | import org.springframework.web.server.ServerWebExchange; 24 | import reactor.core.publisher.Mono; 25 | 26 | import java.util.Objects; 27 | 28 | public class RequestParameterRequestPropertyResolver implements RequestPropertyResolver { 29 | @Override 30 | public boolean supports(@NonNull BindingProperty bindingProperty) { 31 | RequestParameter annotation = bindingProperty.getAnnotation(RequestParameter.class); 32 | return annotation != null && StringUtils.hasText(annotation.value()); 33 | } 34 | 35 | @NonNull 36 | @Override 37 | public Mono resolve(@NonNull BindingProperty bindingProperty, @NonNull ServerWebExchange serverWebExchange) { 38 | RequestParameter annotation = bindingProperty.getAnnotation(RequestParameter.class); 39 | Objects.requireNonNull(annotation, "No RequestParameter annotation found on type"); 40 | MultiValueMap queryParams = serverWebExchange.getRequest().getQueryParams(); 41 | return Mono.justOrEmpty(queryParams.get(annotation.value())); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/src/test/java/com/mattbertolini/spring/web/bind/docs/webflux/WebFluxDocsJavaConfigIntegrationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.docs.webflux; 17 | 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | import org.springframework.http.MediaType; 21 | import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; 22 | import org.springframework.test.web.reactive.server.WebTestClient; 23 | import org.springframework.web.context.WebApplicationContext; 24 | 25 | import static org.springframework.web.reactive.function.BodyInserters.fromFormData; 26 | 27 | @SpringJUnitWebConfig(classes = {ExampleWebFluxContext.class}) 28 | class WebFluxDocsJavaConfigIntegrationTest { 29 | private WebTestClient webTestClient; 30 | 31 | @BeforeEach 32 | void setUp(WebApplicationContext webApplicationContext) { 33 | webTestClient = WebTestClient.bindToApplicationContext(webApplicationContext).build(); 34 | } 35 | 36 | @Test 37 | void makesRequestAndBindsData() { 38 | webTestClient.post() 39 | .uri("/example/42?different_name=different_value&nested_request_param=nested") 40 | .accept(MediaType.TEXT_PLAIN) 41 | .contentType(MediaType.APPLICATION_FORM_URLENCODED) 42 | .body(fromFormData("form_data_", "form_value")) 43 | .header("Accept-Language", "en-US") 44 | .header("X-Custom-Header", "A_Header_Value") 45 | .cookie("cookie_value", "some_cookie_value") 46 | .exchange().expectStatus().isOk(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/annotation/CookieParameter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.annotation; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | /** 25 | *

Annotation for fetching HTTP cookie data.

26 | * 27 | *

This annotation can be used on fields: 28 | *

{@code
29 |  *     @CookieParameter("cookie_name")
30 |  *     private String cookieValue;
31 |  * }
32 | * or on getter/setter methods: 33 | *
{@code
34 |  *     @CookieParameter("cookie_name")
35 |  *     public void setCookieValue(String cookieValue) {
36 |  *         this.cookieValue = cookieValue;
37 |  *     }
38 |  * }
39 | *

40 | * 41 | *

If you need access to all attributes of a cookie (e.g. expiration date, domain, etc) you can bind to cookie 42 | * objects. In Spring MVC you and bind directly to a {@link jakarta.servlet.http.Cookie}: 43 | *

{@code
44 |  *     @CookieParameter("cookie_name")
45 |  *     private Cookie cookie;
46 |  * }
47 | * In Spring WebFlux you can bind directly to a {@link org.springframework.http.HttpCookie}: 48 | *
{@code
49 |  *     @CookieParameter("cookie_name")
50 |  *     private HttpCookie cookie;
51 |  * }
52 | *

53 | */ 54 | @Target({ElementType.FIELD, ElementType.METHOD}) 55 | @Retention(RetentionPolicy.RUNTIME) 56 | @Documented 57 | public @interface CookieParameter { 58 | String value(); 59 | } 60 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/annotation/RequestBody.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.annotation; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | /** 25 | *

Annotation for fetching the HTTP request body.

26 | * 27 | *

This annotation leverages the same {@link org.springframework.http.converter.HttpMessageConverter 28 | * HttpMessageConverters} in Spring MVC and {@link org.springframework.http.codec.HttpMessageReader HttpMessageReaders} 29 | * in Spring WebFlux to convert an HTTP request body into a Java representation.

30 | * 31 | *

This annotation can be used on fields: 32 | *

{@code
33 |  *     @RequestBody
34 |  *     private JsonBody requestBody;
35 |  * }
36 | * or on getter/setter methods of the property: 37 | *
{@code
38 |  *     @RequestBody
39 |  *     public void setRequestBody(JsonBody requestBody) {
40 |  *         this.requestBody = requestBody;
41 |  *     }
42 |  * }
43 | *

44 | * 45 | *

This annotation can only be used once per request and cannot be combined with the Spring 46 | * {@link org.springframework.web.bind.annotation.RequestBody RequestBody} annotation. This is because the request body 47 | * InputStream can only be read once per request.

48 | */ 49 | @Target({ElementType.FIELD, ElementType.METHOD}) 50 | @Retention(RetentionPolicy.RUNTIME) 51 | @Documented 52 | public @interface RequestBody {} 53 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/CookieParameterRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.CookieParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.http.HttpCookie; 21 | import org.springframework.lang.NonNull; 22 | import org.springframework.util.MultiValueMap; 23 | import org.springframework.web.server.ServerWebExchange; 24 | import reactor.core.publisher.Mono; 25 | 26 | import java.util.Objects; 27 | 28 | public class CookieParameterRequestPropertyResolver implements RequestPropertyResolver { 29 | @Override 30 | public boolean supports(BindingProperty bindingProperty) { 31 | return bindingProperty.hasAnnotation(CookieParameter.class); 32 | } 33 | 34 | @NonNull 35 | @Override 36 | public Mono resolve(BindingProperty bindingProperty, ServerWebExchange exchange) { 37 | MultiValueMap cookies = exchange.getRequest().getCookies(); 38 | CookieParameter annotation = bindingProperty.getAnnotation(CookieParameter.class); 39 | Objects.requireNonNull(annotation, "No CookieParameter annotation found on type"); 40 | HttpCookie cookie = cookies.getFirst(annotation.value()); 41 | if (HttpCookie.class.isAssignableFrom(bindingProperty.getType())) { 42 | return Mono.justOrEmpty(cookie); 43 | } 44 | return cookie != null ? Mono.justOrEmpty(cookie.getValue()) : Mono.empty(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/PathParameterRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.PathParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.lang.NonNull; 21 | import org.springframework.util.StringUtils; 22 | import org.springframework.web.reactive.HandlerMapping; 23 | import org.springframework.web.server.ServerWebExchange; 24 | import reactor.core.publisher.Mono; 25 | 26 | import java.util.Collections; 27 | import java.util.Map; 28 | import java.util.Objects; 29 | 30 | public class PathParameterRequestPropertyResolver implements RequestPropertyResolver { 31 | @Override 32 | public boolean supports(@NonNull BindingProperty bindingProperty) { 33 | PathParameter annotation = bindingProperty.getAnnotation(PathParameter.class); 34 | return annotation != null && StringUtils.hasText(annotation.value()); 35 | } 36 | 37 | @NonNull 38 | @Override 39 | public Mono resolve(@NonNull BindingProperty bindingProperty, @NonNull ServerWebExchange exchange) { 40 | PathParameter annotation = bindingProperty.getAnnotation(PathParameter.class); 41 | Objects.requireNonNull(annotation, "No PathParameter annotation found on type"); 42 | Map pathVariables = exchange.getAttributeOrDefault(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Collections.emptyMap()); 43 | return Mono.justOrEmpty(pathVariables.get(annotation.value())); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/PathParameterMapRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.PathParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.lang.NonNull; 21 | import org.springframework.util.StringUtils; 22 | import org.springframework.web.context.request.NativeWebRequest; 23 | import org.springframework.web.context.request.RequestAttributes; 24 | import org.springframework.web.servlet.HandlerMapping; 25 | 26 | import java.util.Collections; 27 | import java.util.LinkedHashMap; 28 | import java.util.Map; 29 | 30 | public class PathParameterMapRequestPropertyResolver implements RequestPropertyResolver { 31 | @Override 32 | public boolean supports(@NonNull BindingProperty bindingProperty) { 33 | PathParameter annotation = bindingProperty.getAnnotation(PathParameter.class); 34 | return annotation != null && !StringUtils.hasText(annotation.value()) && 35 | Map.class.isAssignableFrom(bindingProperty.getType()); 36 | } 37 | 38 | @SuppressWarnings("unchecked") 39 | @Override 40 | public Object resolve(@NonNull BindingProperty bindingProperty, @NonNull NativeWebRequest request) { 41 | Map uriTemplateVariables = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); 42 | if (uriTemplateVariables == null) { 43 | return Collections.emptyMap(); 44 | } 45 | 46 | return new LinkedHashMap<>(uriTemplateVariables); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/annotation/HeaderParameter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.annotation; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | /** 25 | *

Annotation for fetching HTTP header values.

26 | * 27 | *

This annotation can be used on fields: 28 | *

{@code
29 |  *     @HeaderParameter("X-Header-Name")
30 |  *     private String headerValue;
31 |  * }
32 | * or on getter/setter methods of the property: 33 | *
{@code
34 |  *     @HeaderParameter("X-Header-Name")
35 |  *     public void setHeaderValue(String headerValue) {
36 |  *         this.headerValue = headerValue;
37 |  *     }
38 |  * }
39 | *

40 | * 41 | *

Setting this annotation without a value on a {@link java.util.Map} or 42 | * {@link org.springframework.util.MultiValueMap} binds all of the headers to a map. 43 | *

{@code
44 |  *     @HeaderParameter
45 |  *     private MultiValueMap headers;
46 |  *
47 |  *     // Map of first values only
48 |  *     @HeaderParameter
49 |  *     private Map firstValueHeaders;
50 |  * }
51 | *

52 | * 53 | *

This can also be done with a {@link org.springframework.http.HttpHeaders} object as well. 54 | *

{@code
55 |  *     @HeaderParameter
56 |  *     private HttpHeaders httpHeaders;
57 |  * }
58 | *

59 | */ 60 | @Target({ElementType.FIELD, ElementType.METHOD}) 61 | @Retention(RetentionPolicy.RUNTIME) 62 | @Documented 63 | public @interface HeaderParameter { 64 | String value() default ""; 65 | } 66 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/PathParameterRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.PathParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.lang.Nullable; 21 | import org.springframework.util.StringUtils; 22 | import org.springframework.web.context.request.NativeWebRequest; 23 | import org.springframework.web.context.request.RequestAttributes; 24 | import org.springframework.web.servlet.HandlerMapping; 25 | 26 | import java.util.Map; 27 | import java.util.Objects; 28 | 29 | public class PathParameterRequestPropertyResolver implements RequestPropertyResolver { 30 | 31 | @Override 32 | public boolean supports(BindingProperty bindingProperty) { 33 | PathParameter annotation = bindingProperty.getAnnotation(PathParameter.class); 34 | return annotation != null && StringUtils.hasText(annotation.value()); 35 | } 36 | 37 | @SuppressWarnings("unchecked") 38 | @Override 39 | @Nullable 40 | public Object resolve(BindingProperty bindingProperty, NativeWebRequest request) { 41 | PathParameter annotation = bindingProperty.getAnnotation(PathParameter.class); 42 | Objects.requireNonNull(annotation, "No PathParameter annotation found on type"); 43 | 44 | Map uriTemplateVariables = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); 45 | if (uriTemplateVariables == null) { 46 | return null; 47 | } 48 | return uriTemplateVariables.get(annotation.value()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/CookieParameterRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.CookieParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import jakarta.servlet.http.Cookie; 21 | import jakarta.servlet.http.HttpServletRequest; 22 | import org.springframework.lang.Nullable; 23 | import org.springframework.util.Assert; 24 | import org.springframework.web.context.request.NativeWebRequest; 25 | import org.springframework.web.util.WebUtils; 26 | 27 | import java.util.Objects; 28 | 29 | public class CookieParameterRequestPropertyResolver implements RequestPropertyResolver { 30 | @Override 31 | public boolean supports(BindingProperty bindingProperty) { 32 | return bindingProperty.hasAnnotation(CookieParameter.class); 33 | } 34 | 35 | @Override 36 | @Nullable 37 | public Object resolve(BindingProperty bindingProperty, NativeWebRequest request) { 38 | HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); 39 | Assert.state(servletRequest != null, "A HttpServletRequest is required for this resolver and none found."); 40 | CookieParameter annotation = bindingProperty.getAnnotation(CookieParameter.class); 41 | Objects.requireNonNull(annotation, "No CookieParameter annotation found on type"); 42 | Cookie cookie = WebUtils.getCookie(servletRequest, annotation.value()); 43 | if (cookie == null) { 44 | return null; 45 | } 46 | if (Cookie.class == bindingProperty.getObjectType()) { 47 | return cookie; 48 | } 49 | return cookie.getValue(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/introspect/AnnotatedRequestBeanIntrospector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.introspect; 17 | 18 | import java.util.Collection; 19 | import java.util.Collections; 20 | import java.util.Map; 21 | 22 | public interface AnnotatedRequestBeanIntrospector { 23 | 24 | /** 25 | * Creates a map of resolved property data for the given target class. This method traverses the object graph for 26 | * the given type recursively. Circular references are not allowed as they will cause stack overflow errors. 27 | * 28 | * @param targetType The class or type to get property resolver data for. Required. 29 | * @return A map of resolved property data. This map is never null but may be empty. 30 | * @throws CircularReferenceException If a circular reference is found while traversing the object graph. 31 | */ 32 | Map getResolverMapFor(Class targetType); 33 | 34 | /** 35 | * Creates a list of resolved property data for the given target class. This method traverses the object graph for 36 | * the given type recursively. Circular references are not allowed as they will cause stack overflow errors. 37 | * 38 | * @param targetType The class or type to get property resolver data for. Required. 39 | * @return A list of resolved property data. This list is never null but may be empty. 40 | * @throws CircularReferenceException If a circular reference is found while traversing the object graph. 41 | */ 42 | default Collection getResolversFor(Class targetType) { 43 | Map propertyData = getResolverMapFor(targetType); 44 | return Collections.unmodifiableCollection(propertyData.values()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/annotation/BeanParameter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.annotation; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | /** 25 | *

This annotation has two purposes. The first is to mark controller method arguments for binding using this library. 26 | *

{@code
27 |  *     @GetMapping(value = "/example", produces = MediaType.TEXT_PLAIN_VALUE)
28 |  *     public String handleRequest(@BeanParameter requestBean) {
29 |  *         return someService.doSomethingWith(requestBean);
30 |  *     }
31 |  * }
32 | * The second is to mark a Java bean property as a nested object that should also be scanned for additional request 33 | * annotations. 34 | *
{@code
35 |  *     public class OuterBean {
36 |  *         @RequestParameter("some_parameter")
37 |  *         private String someParameter;
38 |  *
39 |  *         @BeanParameter
40 |  *         private NestedBean nestedBean;
41 |  *
42 |  *         // Getters/Setters
43 |  *     }
44 |  *
45 |  *     public class NestedBean {
46 |  *         @RequestParameter("another_parameter")
47 |  *         private String anotherParameter;
48 |  *
49 |  *         // Getters/Setters
50 |  *     }
51 |  * }
52 | * Note: It's important that no circular dependencies are created using this annotation. The 53 | * introspection process will fail if a circular reference is found. This is done to prevent stack overflow errors at 54 | * runtime.

55 | */ 56 | @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) 57 | @Retention(RetentionPolicy.RUNTIME) 58 | @Documented 59 | public @interface BeanParameter {} 60 | -------------------------------------------------------------------------------- /docs/src/test/java/com/mattbertolini/spring/web/bind/docs/webmvc/WebMvcDocsJavaConfigIntegrationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.docs.webmvc; 17 | 18 | import jakarta.servlet.http.Cookie; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | import org.springframework.http.MediaType; 22 | import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; 23 | import org.springframework.test.web.servlet.MockMvc; 24 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 25 | import org.springframework.web.context.WebApplicationContext; 26 | 27 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 28 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 29 | 30 | @SpringJUnitWebConfig(classes = {ExampleMvcContext.class}) 31 | class WebMvcDocsJavaConfigIntegrationTest { 32 | 33 | private MockMvc mockMvc; 34 | 35 | @BeforeEach 36 | void setUp(WebApplicationContext webApplicationContext) { 37 | mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 38 | } 39 | 40 | @Test 41 | void makesRequestAndBindsData() throws Exception { 42 | mockMvc.perform(post("/example/42") 43 | .accept(MediaType.TEXT_PLAIN) 44 | .contentType(MediaType.APPLICATION_FORM_URLENCODED) 45 | .content("form_data=form_value") 46 | .header("Accept-Language", "en-US") 47 | .header("X-Custom-Header", "A_Header_Value") 48 | .cookie(new Cookie("cookie_value", "some_cookie_value")) 49 | .queryParam("different_name", "different_value") 50 | .queryParam("nested_request_param", "nested") 51 | .sessionAttr("sessionAttribute", "sessionValue")) 52 | .andExpect(status().isOk()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/annotation/RequestContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.annotation; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | /** 25 | *

Annotation for binding special objects found in the Spring request context. Binds the following: 26 | *

    27 | *
  • {@link java.util.Locale} - The Spring-resolved locale based on the {@code Accept-Language} HTTP header.
  • 28 | *
  • {@link java.util.TimeZone} - The Spring-resolved time zone.
  • 29 | *
  • {@link java.time.ZoneId} - The Spring-resolved time zone as a Java 8+ Zone ID.
  • 30 | *
  • {@link org.springframework.http.HttpMethod} - The HTTP request method.
  • 31 | *
32 | * In Spring MVC the following additional objects are available: 33 | *
    34 | *
  • {@link org.springframework.web.context.request.WebRequest} - The Spring WebRequest.
  • 35 | *
  • {@link jakarta.servlet.http.HttpServletRequest} - The underlying Servlet request.
  • 36 | *
  • {@link jakarta.servlet.http.HttpSession} - The Servlet session object.
  • 37 | *
38 | * In Spring WebFlux the following additional objects are available: 39 | *
    40 | *
  • {@link org.springframework.web.server.ServerWebExchange} - The Spring reactive web exchange.
  • 41 | *
  • {@link org.springframework.http.server.reactive.ServerHttpRequest} - The Spring reactive HTTP request.
  • 42 | *
  • {@link org.springframework.web.server.WebSession} - The Spring session object.
  • 43 | *
44 | *

45 | */ 46 | @Target({ElementType.FIELD, ElementType.METHOD}) 47 | @Retention(RetentionPolicy.RUNTIME) 48 | @Documented 49 | public @interface RequestContext {} 50 | -------------------------------------------------------------------------------- /docs/src/test/java/com/mattbertolini/spring/web/bind/docs/webmvc/WebMvcDocsXmlConfigIntegrationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.docs.webmvc; 17 | 18 | import jakarta.servlet.http.Cookie; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | import org.springframework.http.MediaType; 22 | import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; 23 | import org.springframework.test.web.servlet.MockMvc; 24 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 25 | import org.springframework.web.context.WebApplicationContext; 26 | 27 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 28 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 29 | 30 | @SpringJUnitWebConfig(locations = {"/com/mattbertolini/spring/web/bind/docs/webmvc/example-context.xml"}) 31 | class WebMvcDocsXmlConfigIntegrationTest { 32 | 33 | private MockMvc mockMvc; 34 | 35 | @BeforeEach 36 | void setUp(WebApplicationContext webApplicationContext) { 37 | mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); 38 | } 39 | 40 | @Test 41 | void makesRequestAndBindsData() throws Exception { 42 | mockMvc.perform(post("/example/42") 43 | .accept(MediaType.TEXT_PLAIN) 44 | .contentType(MediaType.APPLICATION_FORM_URLENCODED) 45 | .content("form_data=form_value") 46 | .header("Accept-Language", "en-US") 47 | .header("X-Custom-Header", "A_Header_Value") 48 | .cookie(new Cookie("cookie_value", "some_cookie_value")) 49 | .queryParam("different_name", "different_value") 50 | .queryParam("nested_request_param", "nested") 51 | .sessionAttr("sessionAttribute", "sessionValue")) 52 | .andExpect(status().isOk()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/annotation/FormParameter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.annotation; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | /** 25 | *

Annotation for fetching POSTed form data of type {@code application/x-www-form-urlencoded}.

26 | * 27 | *

In Spring WebFlux this annotation is used for fetching form data from the request. In Spring MVC this annotation 28 | * fetches both query parameter and form data. This is due to a limitation in the Java Servlet framework where both 29 | * query and form data are merged into one. This means that this annotation and the {@link RequestParameter} annotation 30 | * are essentially the same in Spring MVC.

31 | * 32 | *

This annotation can be used on fields: 33 | *

{@code
34 |  *     @FormParameter("form_param")
35 |  *     private String formParam;
36 |  * }
37 | * or on getter/setter methods of the property: 38 | *
{@code
39 |  *     @FormParameter("form_param")
40 |  *     public void setFormParam(String formParam) {
41 |  *          this.formParam = formParam;
42 |  *     }
43 |  * }
44 | *

45 | * 46 | *

Setting this annotation without a value on a {@link java.util.Map Map} or 47 | * {@link org.springframework.util.MultiValueMap MultiValueMap} binds all of the form data to a map. 48 | *

{@code
49 |  *     @FormParameter
50 |  *     private MultiValueMap formParameters;
51 |  *
52 |  *     // Map of the first values only
53 |  *     @FormParameter
54 |  *     private Map firstParamValues;
55 |  * }
56 | *

57 | */ 58 | @Target({ElementType.FIELD, ElementType.METHOD}) 59 | @Retention(RetentionPolicy.RUNTIME) 60 | @Documented 61 | public @interface FormParameter { 62 | String value() default ""; 63 | } 64 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/annotation/RequestParameter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.annotation; 17 | 18 | import java.lang.annotation.Documented; 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | /** 25 | *

Annotation for fetching HTTP request parameters.

26 | * 27 | *

In Spring WebFlux this annotation is used for fetching query parameter data from the request. In Spring MVC this 28 | * annotation fetches both query parameter and form data. This is due to a limitation in the Java Servlet framework 29 | * where both query and form data are merged into one. This means that this annotation and the {@link FormParameter} 30 | * annotation are essentially the same in Spring MVC.

31 | * 32 | *

This annotation can be used on fields: 33 | *

{@code
34 |  *     @RequestParameter("query_param")
35 |  *     private String queryParam;
36 |  * }
37 | * or on getter/setter methods of the property: 38 | *
{@code
39 |  *     @RequestParameter("query_param")
40 |  *     public void setQueryParam(String queryParam) {
41 |  *          this.queryParam = queryParam;
42 |  *     }
43 |  * }
44 | *

45 | * 46 | *

Setting this annotation without a value on a {@link java.util.Map Map} or 47 | * {@link org.springframework.util.MultiValueMap MultiValueMap} binds all of the query parameters to a map. 48 | *

{@code
49 |  *     @RequestParameter
50 |  *     private MultiValueMap queryParameters;
51 |  *
52 |  *     // Map of the first values only
53 |  *     @RequestParameter
54 |  *     private Map firstParamValues;
55 |  * }
56 | *

57 | */ 58 | @Target({ElementType.FIELD, ElementType.METHOD}) 59 | @Retention(RetentionPolicy.RUNTIME) 60 | @Documented 61 | public @interface RequestParameter { 62 | String value() default ""; 63 | } 64 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/RequestBodyRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.PropertyResolutionException; 19 | import com.mattbertolini.spring.web.bind.annotation.RequestBody; 20 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 21 | import org.springframework.http.converter.HttpMessageConverter; 22 | import org.springframework.lang.NonNull; 23 | import org.springframework.util.Assert; 24 | import org.springframework.web.context.request.NativeWebRequest; 25 | import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor; 26 | 27 | import java.util.List; 28 | 29 | public class RequestBodyRequestPropertyResolver implements RequestPropertyResolver { 30 | private final RequestResponseBodyMethodProcessor processor; 31 | 32 | public RequestBodyRequestPropertyResolver(List> messageConverters) { 33 | this(new RequestResponseBodyMethodProcessor(messageConverters)); 34 | } 35 | 36 | /** 37 | * Visible for testing purposes only. 38 | */ 39 | RequestBodyRequestPropertyResolver(@NonNull RequestResponseBodyMethodProcessor processor) { 40 | this.processor = processor; 41 | } 42 | 43 | @Override 44 | public boolean supports(@NonNull BindingProperty bindingProperty) { 45 | return bindingProperty.hasAnnotation(RequestBody.class); 46 | } 47 | 48 | @Override 49 | public Object resolve(@NonNull BindingProperty bindingProperty, @NonNull NativeWebRequest request) { 50 | RequestBody annotation = bindingProperty.getAnnotation(RequestBody.class); 51 | Assert.state(annotation != null, "No RequestBody annotation found on type"); 52 | try { 53 | return processor.resolveArgument(bindingProperty.getMethodParameter(), null, request, null); 54 | } catch (Exception e) { 55 | throw new PropertyResolutionException("Error resolving request body.", e); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/CookieParameterBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.BeanParameter; 19 | import com.mattbertolini.spring.web.bind.annotation.CookieParameter; 20 | import jakarta.validation.constraints.NotEmpty; 21 | import org.springframework.lang.Nullable; 22 | 23 | public class CookieParameterBean { 24 | @Nullable 25 | @CookieParameter("annotated_field") 26 | private String annotatedField; 27 | 28 | @Nullable 29 | private String annotatedSetter; 30 | 31 | @Nullable 32 | private String annotatedGetter; 33 | 34 | @Nullable 35 | @NotEmpty 36 | @CookieParameter("validated") 37 | private String validated; 38 | 39 | @Nullable 40 | @BeanParameter 41 | private NestedBean nestedBean; 42 | 43 | @Nullable 44 | public String getAnnotatedField() { 45 | return annotatedField; 46 | } 47 | 48 | public void setAnnotatedField(String annotatedField) { 49 | this.annotatedField = annotatedField; 50 | } 51 | 52 | @Nullable 53 | public String getAnnotatedSetter() { 54 | return annotatedSetter; 55 | } 56 | 57 | @CookieParameter("annotated_setter") 58 | public void setAnnotatedSetter(String annotatedSetter) { 59 | this.annotatedSetter = annotatedSetter; 60 | } 61 | 62 | @Nullable 63 | @CookieParameter("annotated_getter") 64 | public String getAnnotatedGetter() { 65 | return annotatedGetter; 66 | } 67 | 68 | public void setAnnotatedGetter(String annotatedGetter) { 69 | this.annotatedGetter = annotatedGetter; 70 | } 71 | 72 | @Nullable 73 | public String getValidated() { 74 | return validated; 75 | } 76 | 77 | public void setValidated(String validated) { 78 | this.validated = validated; 79 | } 80 | 81 | @Nullable 82 | public NestedBean getNestedBean() { 83 | return nestedBean; 84 | } 85 | 86 | public void setNestedBean(NestedBean nestedBean) { 87 | this.nestedBean = nestedBean; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/SessionParameterBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.BeanParameter; 19 | import com.mattbertolini.spring.web.bind.annotation.SessionParameter; 20 | import jakarta.validation.constraints.NotEmpty; 21 | import org.springframework.lang.Nullable; 22 | 23 | public class SessionParameterBean { 24 | @Nullable 25 | @SessionParameter("annotated_field") 26 | private String annotatedField; 27 | 28 | @Nullable 29 | private String annotatedSetter; 30 | 31 | @Nullable 32 | private String annotatedGetter; 33 | 34 | @Nullable 35 | public String getAnnotatedField() { 36 | return annotatedField; 37 | } 38 | 39 | @Nullable 40 | @NotEmpty 41 | @SessionParameter("validated") 42 | private String validated; 43 | 44 | @Nullable 45 | @BeanParameter 46 | private NestedBean nestedBean; 47 | 48 | public void setAnnotatedField(String annotatedField) { 49 | this.annotatedField = annotatedField; 50 | } 51 | 52 | @Nullable 53 | public String getAnnotatedSetter() { 54 | return annotatedSetter; 55 | } 56 | 57 | @SessionParameter("annotated_setter") 58 | public void setAnnotatedSetter(String annotatedSetter) { 59 | this.annotatedSetter = annotatedSetter; 60 | } 61 | 62 | @Nullable 63 | @SessionParameter("annotated_getter") 64 | public String getAnnotatedGetter() { 65 | return annotatedGetter; 66 | } 67 | 68 | public void setAnnotatedGetter(String annotatedGetter) { 69 | this.annotatedGetter = annotatedGetter; 70 | } 71 | 72 | @Nullable 73 | public String getValidated() { 74 | return validated; 75 | } 76 | 77 | public void setValidated(String validated) { 78 | this.validated = validated; 79 | } 80 | 81 | @Nullable 82 | public NestedBean getNestedBean() { 83 | return nestedBean; 84 | } 85 | 86 | public void setNestedBean(NestedBean nestedBean) { 87 | this.nestedBean = nestedBean; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/FormParameterRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.FormParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.http.codec.multipart.FormFieldPart; 21 | import org.springframework.http.codec.multipart.Part; 22 | import org.springframework.lang.NonNull; 23 | import org.springframework.util.StringUtils; 24 | import org.springframework.web.server.ServerWebExchange; 25 | import reactor.core.publisher.Mono; 26 | 27 | import java.util.List; 28 | import java.util.Objects; 29 | 30 | public class FormParameterRequestPropertyResolver implements RequestPropertyResolver { 31 | @Override 32 | public boolean supports(BindingProperty bindingProperty) { 33 | FormParameter annotation = bindingProperty.getAnnotation(FormParameter.class); 34 | return annotation != null && StringUtils.hasText(annotation.value()); 35 | } 36 | 37 | @NonNull 38 | @Override 39 | public Mono resolve(BindingProperty bindingProperty, ServerWebExchange exchange) { 40 | FormParameter annotation = bindingProperty.getAnnotation(FormParameter.class); 41 | Objects.requireNonNull(annotation, "No FormParameter annotation found on type"); 42 | return exchange.getMultipartData() 43 | .filter(multipartData -> multipartData.getFirst(annotation.value()) != null) 44 | .map(multipartData -> multipartData.get(annotation.value())) 45 | .map(this::getPartValues) 46 | .switchIfEmpty(exchange.getFormData() 47 | .filter(formData -> formData.getFirst(annotation.value()) != null) 48 | .map(formData -> formData.get(annotation.value()))); 49 | } 50 | 51 | @NonNull 52 | private Object getPartValues(@NonNull List parts) { 53 | List values = parts.stream() 54 | .map(value -> value instanceof FormFieldPart formFieldPart ? formFieldPart.value() : value) 55 | .toList(); 56 | return values.size() == 1 ? values.get(0) : values; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/test/java/com/mattbertolini/spring/web/servlet/mvc/bind/PropertyResolverRegistryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind; 17 | 18 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 19 | import com.mattbertolini.spring.web.servlet.mvc.bind.resolver.RequestPropertyResolver; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.Test; 22 | import org.springframework.lang.Nullable; 23 | import org.springframework.web.context.request.NativeWebRequest; 24 | 25 | import java.util.Collections; 26 | 27 | import static org.assertj.core.api.Assertions.assertThat; 28 | 29 | class PropertyResolverRegistryTest { 30 | 31 | private PropertyResolverRegistry registry; 32 | 33 | @BeforeEach 34 | void setUp() { 35 | registry = new PropertyResolverRegistry(); 36 | } 37 | 38 | @Test 39 | void addsResolversFromSet() { 40 | assertThat(registry.getPropertyResolvers()).isEmpty(); 41 | registry.addResolvers(Collections.singleton(new FakeResolver())); 42 | assertThat(registry.getPropertyResolvers()).hasSize(1); 43 | } 44 | 45 | @Test 46 | void addsSingleResolver() { 47 | assertThat(registry.getPropertyResolvers()).isEmpty(); 48 | registry.addResolver(new FakeResolver()); 49 | assertThat(registry.getPropertyResolvers()).hasSize(1); 50 | } 51 | 52 | @Test 53 | void addsResolversFromRegistry() { 54 | PropertyResolverRegistry anotherRegistry = new PropertyResolverRegistry(); 55 | anotherRegistry.addResolver(new FakeResolver()); 56 | assertThat(registry.getPropertyResolvers()).isEmpty(); 57 | registry.addResolvers(anotherRegistry); 58 | assertThat(registry.getPropertyResolvers()).hasSize(1); 59 | } 60 | 61 | private static class FakeResolver implements RequestPropertyResolver { 62 | 63 | @Override 64 | public boolean supports(BindingProperty bindingProperty) { 65 | return false; 66 | } 67 | 68 | @Override 69 | @Nullable 70 | public Object resolve(BindingProperty bindingProperty, NativeWebRequest request) { 71 | return null; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/test/java/com/mattbertolini/spring/web/bind/resolver/AbstractNamedRequestPropertyResolverTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 19 | import org.junit.jupiter.api.Test; 20 | import org.springframework.lang.NonNull; 21 | import org.springframework.lang.Nullable; 22 | 23 | import java.beans.PropertyDescriptor; 24 | 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | 27 | class AbstractNamedRequestPropertyResolverTest { 28 | @SuppressWarnings("unchecked") 29 | @Test 30 | void resolveUsesResolveWithNameMethod() throws Exception { 31 | TestingResolver resolver = new TestingResolver("expected"); 32 | BindingProperty bindingProperty = BindingProperty.forPropertyDescriptor(new PropertyDescriptor("property", TestingBean.class)); 33 | Object actual = resolver.resolve(bindingProperty, new Object()); 34 | assertThat(actual).isEqualTo("expected"); 35 | } 36 | 37 | @SuppressWarnings("rawtypes") 38 | private static class TestingResolver extends AbstractNamedRequestPropertyResolver { 39 | 40 | private final String name; 41 | 42 | public TestingResolver(String name) { 43 | this.name = name; 44 | } 45 | 46 | @Override 47 | @NonNull 48 | protected String getName(@NonNull BindingProperty bindingProperty) { 49 | return name; 50 | } 51 | 52 | @Override 53 | protected Object resolveWithName(@NonNull BindingProperty bindingProperty, String name, @NonNull Object request) { 54 | return name; 55 | } 56 | 57 | @Override 58 | public boolean supports(@NonNull BindingProperty bindingProperty) { 59 | return true; 60 | } 61 | } 62 | 63 | @SuppressWarnings("unused") 64 | private static class TestingBean { 65 | @Nullable 66 | private String property; 67 | 68 | @Nullable 69 | public String getProperty() { 70 | return property; 71 | } 72 | 73 | public void setProperty(String property) { 74 | this.property = property; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/reactive/bind/resolver/RequestBodyRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.RequestBody; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.core.MethodParameter; 21 | import org.springframework.core.ReactiveAdapterRegistry; 22 | import org.springframework.http.codec.HttpMessageReader; 23 | import org.springframework.lang.NonNull; 24 | import org.springframework.util.Assert; 25 | import org.springframework.web.reactive.BindingContext; 26 | import org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver; 27 | import org.springframework.web.server.ServerWebExchange; 28 | import reactor.core.publisher.Mono; 29 | 30 | import java.util.List; 31 | 32 | public class RequestBodyRequestPropertyResolver extends AbstractMessageReaderArgumentResolver implements RequestPropertyResolver { 33 | public RequestBodyRequestPropertyResolver(List> readers, ReactiveAdapterRegistry registry) { 34 | super(readers, registry); 35 | } 36 | 37 | @Override 38 | public boolean supportsParameter(@NonNull MethodParameter parameter) { 39 | return false; 40 | } 41 | 42 | @Override 43 | public boolean supports(@NonNull BindingProperty bindingProperty) { 44 | return bindingProperty.hasAnnotation(RequestBody.class); 45 | } 46 | 47 | @NonNull 48 | @Override 49 | public Mono resolve(@NonNull BindingProperty bindingProperty, @NonNull ServerWebExchange request) { 50 | RequestBody annotation = bindingProperty.getAnnotation(RequestBody.class); 51 | Assert.state(annotation != null, "No RequestBody annotation found on type"); 52 | return resolveArgument(bindingProperty.getMethodParameter(), new BindingContext(), request); 53 | } 54 | 55 | @NonNull 56 | @Override 57 | public Mono resolveArgument(@NonNull MethodParameter parameter, @NonNull BindingContext bindingContext, @NonNull ServerWebExchange exchange) { 58 | return readBody(parameter, false, bindingContext, exchange); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/test/java/com/mattbertolini/spring/web/reactive/bind/MockWebExchangeDataBinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind; 17 | 18 | import org.springframework.beans.MutablePropertyValues; 19 | import org.springframework.beans.PropertyValues; 20 | import org.springframework.lang.Nullable; 21 | import org.springframework.validation.BindingResult; 22 | import org.springframework.web.bind.support.WebExchangeDataBinder; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Arrays; 26 | import java.util.List; 27 | 28 | public class MockWebExchangeDataBinder extends WebExchangeDataBinder { 29 | private boolean bindInvoked = false; 30 | private boolean validateInvoked = true; 31 | private PropertyValues pvs; 32 | private List validationHints; 33 | @Nullable 34 | private BindingResult bindingResult; 35 | 36 | public MockWebExchangeDataBinder(@Nullable Object target) { 37 | super(target); 38 | pvs = new MutablePropertyValues(); 39 | validationHints = new ArrayList<>(); 40 | } 41 | 42 | @Override 43 | public void bind(PropertyValues pvs) { 44 | this.pvs = pvs; 45 | bindInvoked = true; 46 | } 47 | 48 | @Override 49 | public void validate() { 50 | validateInvoked = true; 51 | } 52 | 53 | @Override 54 | public void validate(Object... validationHints) { 55 | this.validationHints = Arrays.asList(validationHints); 56 | validateInvoked = true; 57 | } 58 | 59 | @Override 60 | public BindingResult getBindingResult() { 61 | if (bindingResult == null) { 62 | return super.getBindingResult(); 63 | } 64 | return bindingResult; 65 | } 66 | 67 | public void setBindingResult(BindingResult bindingResult) { 68 | this.bindingResult = bindingResult; 69 | } 70 | 71 | public boolean isBindInvoked() { 72 | return bindInvoked; 73 | } 74 | 75 | public boolean isValidateInvoked() { 76 | return validateInvoked; 77 | } 78 | 79 | public List getValidationHints() { 80 | return validationHints; 81 | } 82 | 83 | public PropertyValues getPropertyValues() { 84 | return pvs; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/test/java/com/mattbertolini/spring/web/reactive/bind/PropertyResolverRegistryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind; 17 | 18 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 19 | import com.mattbertolini.spring.web.reactive.bind.resolver.RequestPropertyResolver; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.Test; 22 | import org.springframework.lang.NonNull; 23 | import org.springframework.web.server.ServerWebExchange; 24 | import reactor.core.publisher.Mono; 25 | 26 | import java.util.Collections; 27 | 28 | import static org.assertj.core.api.Assertions.assertThat; 29 | 30 | class PropertyResolverRegistryTest { 31 | 32 | private PropertyResolverRegistry registry; 33 | 34 | @BeforeEach 35 | void setUp() { 36 | registry = new PropertyResolverRegistry(); 37 | } 38 | 39 | @Test 40 | void addsResolversFromSet() { 41 | assertThat(registry.getPropertyResolvers()).isEmpty(); 42 | registry.addResolvers(Collections.singleton(new FakeResolver())); 43 | assertThat(registry.getPropertyResolvers()).hasSize(1); 44 | } 45 | 46 | @Test 47 | void addsSingleResolver() { 48 | assertThat(registry.getPropertyResolvers()).isEmpty(); 49 | registry.addResolver(new FakeResolver()); 50 | assertThat(registry.getPropertyResolvers()).hasSize(1); 51 | } 52 | 53 | @Test 54 | void addsResolversFromRegistry() { 55 | PropertyResolverRegistry anotherRegistry = new PropertyResolverRegistry(); 56 | anotherRegistry.addResolver(new FakeResolver()); 57 | assertThat(registry.getPropertyResolvers()).isEmpty(); 58 | registry.addResolvers(anotherRegistry); 59 | assertThat(registry.getPropertyResolvers()).hasSize(1); 60 | } 61 | 62 | private static class FakeResolver implements RequestPropertyResolver { 63 | 64 | @Override 65 | public boolean supports(@NonNull BindingProperty bindingProperty) { 66 | return false; 67 | } 68 | 69 | @NonNull 70 | @Override 71 | public Mono resolve(@NonNull BindingProperty bindingProperty, @NonNull ServerWebExchange exchange) { 72 | return Mono.empty(); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /spring-annotated-data-binder-core/src/main/java/com/mattbertolini/spring/web/bind/AbstractPropertyResolverRegistry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.bind; 17 | 18 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 19 | import com.mattbertolini.spring.web.bind.resolver.RequestPropertyResolverBase; 20 | import org.springframework.lang.Nullable; 21 | 22 | import java.util.Collections; 23 | import java.util.LinkedHashSet; 24 | import java.util.Set; 25 | 26 | /** 27 | * Do not extend directly from this class. Extend from the two subclasses that are specific to Spring MVC or Spring 28 | * WebFlux. 29 | * @param The resolver type 30 | */ 31 | public abstract class AbstractPropertyResolverRegistry> { 32 | private final Set propertyResolvers; 33 | 34 | protected AbstractPropertyResolverRegistry() { 35 | propertyResolvers = new LinkedHashSet<>(); 36 | } 37 | 38 | @Nullable 39 | public T findResolverFor(BindingProperty bindingProperty) { 40 | for (T resolver : propertyResolvers) { 41 | if (resolver.supports(bindingProperty)) { 42 | return resolver; 43 | } 44 | } 45 | return null; 46 | } 47 | 48 | /** 49 | * Add the given resolver to this registry. 50 | * 51 | * @param resolver The resolver to add. 52 | */ 53 | public void addResolver(T resolver) { 54 | propertyResolvers.add(resolver); 55 | } 56 | 57 | /** 58 | * Add all the resolvers in the given set to this registry. 59 | * 60 | * @param resolvers The set of resolvers to add. 61 | */ 62 | public void addResolvers(Set resolvers) { 63 | propertyResolvers.addAll(resolvers); 64 | } 65 | 66 | /** 67 | * Add all the resolvers in the given registry to this registry. 68 | * 69 | * @param registry The registry to add resolvers from. 70 | */ 71 | public void addResolvers(AbstractPropertyResolverRegistry registry) { 72 | addResolvers(registry.getPropertyResolvers()); 73 | } 74 | 75 | /** 76 | * Returns an unmodifiable collection of the resolvers. 77 | */ 78 | public Set getPropertyResolvers() { 79 | return Collections.unmodifiableSet(propertyResolvers); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/DirectFieldAccessBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.CookieParameter; 19 | import com.mattbertolini.spring.web.bind.annotation.FormParameter; 20 | import com.mattbertolini.spring.web.bind.annotation.HeaderParameter; 21 | import com.mattbertolini.spring.web.bind.annotation.PathParameter; 22 | import com.mattbertolini.spring.web.bind.annotation.RequestBody; 23 | import com.mattbertolini.spring.web.bind.annotation.RequestParameter; 24 | import com.mattbertolini.spring.web.bind.annotation.SessionParameter; 25 | import org.springframework.lang.Nullable; 26 | 27 | @SuppressWarnings("unused") 28 | public class DirectFieldAccessBean { 29 | @Nullable 30 | @CookieParameter("cookie_parameter") 31 | private String cookieParameter; 32 | 33 | @Nullable 34 | @FormParameter("form_parameter") 35 | private String formParameter; 36 | 37 | @Nullable 38 | @HeaderParameter("header_parameter") 39 | private String headerParameter; 40 | 41 | @Nullable 42 | @PathParameter("path_parameter") 43 | private String pathParameter; 44 | 45 | @Nullable 46 | @RequestParameter("request_parameter") 47 | private String requestParameter; 48 | 49 | @Nullable 50 | @SessionParameter("session_parameter") 51 | private String sessionParameter; 52 | 53 | @Nullable 54 | public String getCookieParameter() { 55 | return cookieParameter; 56 | } 57 | 58 | @Nullable 59 | public String getFormParameter() { 60 | return formParameter; 61 | } 62 | 63 | @Nullable 64 | public String getHeaderParameter() { 65 | return headerParameter; 66 | } 67 | 68 | @Nullable 69 | public String getPathParameter() { 70 | return pathParameter; 71 | } 72 | 73 | @Nullable 74 | public String getRequestParameter() { 75 | return requestParameter; 76 | } 77 | 78 | @Nullable 79 | public String getSessionParameter() { 80 | return sessionParameter; 81 | } 82 | 83 | public static class RequestBodyBean { 84 | @Nullable 85 | @RequestBody 86 | private JsonBody requestBody; 87 | 88 | @Nullable 89 | public JsonBody getRequestBody() { 90 | return requestBody; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/test/java/com/mattbertolini/spring/web/servlet/mvc/bind/MockWebDataBinder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind; 17 | 18 | import org.springframework.beans.MutablePropertyValues; 19 | import org.springframework.beans.PropertyValues; 20 | import org.springframework.lang.Nullable; 21 | import org.springframework.validation.BindingResult; 22 | import org.springframework.web.bind.WebDataBinder; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Arrays; 26 | import java.util.List; 27 | 28 | public class MockWebDataBinder extends WebDataBinder { 29 | private boolean bindInvoked = false; 30 | private boolean validateInvoked = true; 31 | private PropertyValues pvs; 32 | private List validationHints; 33 | @Nullable 34 | private BindingResult bindingResult; 35 | 36 | public MockWebDataBinder(@Nullable Object target) { 37 | super(target); 38 | pvs = new MutablePropertyValues(); 39 | validationHints = new ArrayList<>(); 40 | } 41 | 42 | public MockWebDataBinder(@Nullable Object target, String objectName) { 43 | super(target, objectName); 44 | pvs = new MutablePropertyValues(); 45 | validationHints = new ArrayList<>(); 46 | } 47 | 48 | @Override 49 | public void bind(PropertyValues pvs) { 50 | this.pvs = pvs; 51 | bindInvoked = true; 52 | } 53 | 54 | @Override 55 | public void validate() { 56 | validateInvoked = true; 57 | } 58 | 59 | @Override 60 | public void validate(Object... validationHints) { 61 | this.validationHints = Arrays.asList(validationHints); 62 | validateInvoked = true; 63 | } 64 | 65 | @Override 66 | public BindingResult getBindingResult() { 67 | if (bindingResult == null) { 68 | return super.getBindingResult(); 69 | } 70 | return bindingResult; 71 | } 72 | 73 | public void setBindingResult(BindingResult bindingResult) { 74 | this.bindingResult = bindingResult; 75 | } 76 | 77 | public boolean isBindInvoked() { 78 | return bindInvoked; 79 | } 80 | 81 | public boolean isValidateInvoked() { 82 | return validateInvoked; 83 | } 84 | 85 | public List getValidationHints() { 86 | return validationHints; 87 | } 88 | 89 | public PropertyValues getPropertyValues() { 90 | return pvs; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | spring = "6.1.10" # Used in java-conventions.gradle.kts 3 | springBoot = "3.3.1" # Used in java-conventions.gradle.kts 4 | junit = "5.9.3" # Used in java-conventions.gradle.kts 5 | jacoco = "0.8.10" # Used in java-conventions.gradle.kts 6 | errorProne = "2.29.0" 7 | nullAway = "0.11.0" 8 | 9 | [libraries] 10 | jakartaServletApi = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.0.0" } 11 | jakartaValidationApi = { module = "jakarta.validation:jakarta.validation-api", version = "3.1.0" } 12 | jakartaWebsocketApi = { module = "jakarta.websocket:jakarta.websocket-api", version = "2.2.0" } 13 | jakartaWebsocketClientApi = { module = "jakarta.websocket:jakarta.websocket-client-api", version = "2.2.0" } 14 | 15 | findbugsJsr305 = { module = "com.google.code.findbugs:jsr305", version = "3.0.2" } 16 | 17 | springBeans = { module = "org.springframework:spring-beans", version.ref = "spring" } 18 | springContext = { module = "org.springframework:spring-context", version.ref = "spring" } 19 | springTest = { module = "org.springframework:spring-test", version.ref = "spring" } 20 | springWeb = { module = "org.springframework:spring-web", version.ref = "spring" } 21 | springWebflux = { module = "org.springframework:spring-webflux", version.ref = "spring" } 22 | springWebmvc = { module = "org.springframework:spring-webmvc", version.ref = "spring" } 23 | 24 | springBootStarter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springBoot" } 25 | springBootTest = { module = "org.springframework.boot:spring-boot-test", version.ref = "springBoot" } 26 | 27 | springAsciidoctorExtBlockSwitch = { module = "io.spring.asciidoctor:spring-asciidoctor-extensions-block-switch", version = "0.6.1" } 28 | 29 | glassfishJakartaEl = { module = "org.glassfish:jakarta.el", version = "4.0.2" } # Needed by Hibernate Validator 30 | hibernateValidator = { module = "org.hibernate.validator:hibernate-validator", version = "8.0.1.Final" } 31 | jacksonDatabind = { module = "com.fasterxml.jackson.core:jackson-databind", version = "2.17.2" } 32 | 33 | junitJupiterApi = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } 34 | assertJCore = { module = "org.assertj:assertj-core", version = "3.24.2" } 35 | mockitoCore = { module = "org.mockito:mockito-core", version = "5.3.1" } 36 | equalsVerifier = { module = "nl.jqno.equalsverifier:equalsverifier", version = "3.14.2" } 37 | hamcrest = { module = "org.hamcrest:hamcrest", version = "2.2" } 38 | 39 | errorProneCore = { module = "com.google.errorprone:error_prone_core", version.ref = "errorProne" } 40 | errorProneAnnotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "errorProne"} 41 | nullAway = { module = "com.uber.nullaway:nullaway", version.ref = "nullAway" } 42 | nullAwayAnnotations = { module = "com.uber.nullaway:nullaway-annotations", version.ref = "nullAway" } 43 | 44 | nmcpPlugin = { module = "com.gradleup.nmcp:nmcp", version = "1.0.2" } 45 | 46 | [plugins] 47 | asciidoctorConvert = { id = "org.asciidoctor.jvm.convert", version = "3.3.2" } 48 | sonarqube = { id = "org.sonarqube", version = "5.0.0.4638" } -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/RequestParameterRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.PropertyResolutionException; 19 | import com.mattbertolini.spring.web.bind.annotation.RequestParameter; 20 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 21 | import com.mattbertolini.spring.web.bind.resolver.AbstractNamedRequestPropertyResolver; 22 | import jakarta.servlet.http.HttpServletRequest; 23 | import org.springframework.lang.Nullable; 24 | import org.springframework.util.StringUtils; 25 | import org.springframework.web.context.request.NativeWebRequest; 26 | import org.springframework.web.multipart.support.MultipartResolutionDelegate; 27 | 28 | import java.util.Objects; 29 | 30 | public class RequestParameterRequestPropertyResolver extends AbstractNamedRequestPropertyResolver 31 | implements RequestPropertyResolver { 32 | 33 | @Override 34 | public boolean supports(BindingProperty bindingProperty) { 35 | RequestParameter annotation = bindingProperty.getAnnotation(RequestParameter.class); 36 | return annotation != null && StringUtils.hasText(annotation.value()); 37 | } 38 | 39 | @Override 40 | @Nullable 41 | protected Object resolveWithName(BindingProperty bindingProperty, String name, NativeWebRequest request) { 42 | HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); 43 | if (servletRequest != null) { 44 | try { 45 | Object value = MultipartResolutionDelegate.resolveMultipartArgument(name, bindingProperty.getMethodParameter(), servletRequest); 46 | if (MultipartResolutionDelegate.UNRESOLVABLE != value) { 47 | return value; 48 | } 49 | } catch (Exception e) { 50 | throw new PropertyResolutionException("Exception resolving multipart argument", e); 51 | } 52 | } 53 | 54 | return request.getParameterValues(name); 55 | } 56 | 57 | @Override 58 | protected String getName(BindingProperty bindingProperty) { 59 | RequestParameter annotation = bindingProperty.getAnnotation(RequestParameter.class); 60 | Objects.requireNonNull(annotation, "No RequestParameter annotation found on type"); 61 | return annotation.value(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/test/java/com/mattbertolini/spring/web/servlet/mvc/bind/MockWebDataBinderFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind; 17 | 18 | import com.uber.nullaway.annotations.Initializer; 19 | import org.springframework.core.ResolvableType; 20 | import org.springframework.format.support.FormattingConversionServiceFactoryBean; 21 | import org.springframework.lang.Nullable; 22 | import org.springframework.validation.BindingResult; 23 | import org.springframework.web.bind.WebDataBinder; 24 | import org.springframework.web.bind.support.WebDataBinderFactory; 25 | import org.springframework.web.context.request.NativeWebRequest; 26 | 27 | public class MockWebDataBinderFactory implements WebDataBinderFactory { 28 | private MockWebDataBinder binder; 29 | @Nullable 30 | private BindingResult bindingResult; 31 | 32 | @Initializer 33 | @Override 34 | public WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName, ResolvableType targetType) throws Exception { 35 | binder = new MockWebDataBinder(target, objectName); 36 | if (target == null) { 37 | binder.setTargetType(targetType); 38 | } 39 | 40 | if (bindingResult != null) { 41 | binder.setBindingResult(bindingResult); 42 | } 43 | 44 | FormattingConversionServiceFactoryBean conversionServiceFactoryBean = new FormattingConversionServiceFactoryBean(); 45 | conversionServiceFactoryBean.afterPropertiesSet(); 46 | binder.setConversionService(conversionServiceFactoryBean.getObject()); 47 | 48 | return binder; 49 | } 50 | 51 | @Initializer 52 | @Override 53 | public WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception { 54 | binder = new MockWebDataBinder(target, objectName); 55 | 56 | if (bindingResult != null) { 57 | binder.setBindingResult(bindingResult); 58 | } 59 | 60 | FormattingConversionServiceFactoryBean conversionServiceFactoryBean = new FormattingConversionServiceFactoryBean(); 61 | conversionServiceFactoryBean.afterPropertiesSet(); 62 | binder.setConversionService(conversionServiceFactoryBean.getObject()); 63 | 64 | return binder; 65 | } 66 | 67 | public void setBindingResult(BindingResult bindingResult) { 68 | this.bindingResult = bindingResult; 69 | } 70 | 71 | public MockWebDataBinder getBinder() { 72 | return binder; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/PathParameterBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.BeanParameter; 19 | import com.mattbertolini.spring.web.bind.annotation.PathParameter; 20 | import jakarta.validation.constraints.NotEmpty; 21 | import org.springframework.lang.Nullable; 22 | 23 | import java.util.Map; 24 | 25 | public class PathParameterBean { 26 | @Nullable 27 | @PathParameter("annotated_field") 28 | private String annotatedField; 29 | 30 | @Nullable 31 | private String annotatedSetter; 32 | 33 | @Nullable 34 | private String annotatedGetter; 35 | 36 | @Nullable 37 | @PathParameter 38 | private Map simpleMap; 39 | 40 | @Nullable 41 | @NotEmpty 42 | @PathParameter("validated") 43 | private String validated; 44 | 45 | @Nullable 46 | @BeanParameter 47 | private NestedBean nestedBean; 48 | 49 | @Nullable 50 | public String getAnnotatedField() { 51 | return annotatedField; 52 | } 53 | 54 | public void setAnnotatedField(String annotatedField) { 55 | this.annotatedField = annotatedField; 56 | } 57 | 58 | @Nullable 59 | public String getAnnotatedSetter() { 60 | return annotatedSetter; 61 | } 62 | 63 | @PathParameter("annotated_setter") 64 | public void setAnnotatedSetter(String annotatedSetter) { 65 | this.annotatedSetter = annotatedSetter; 66 | } 67 | 68 | @Nullable 69 | @PathParameter("annotated_getter") 70 | public String getAnnotatedGetter() { 71 | return annotatedGetter; 72 | } 73 | 74 | public void setAnnotatedGetter(String annotatedGetter) { 75 | this.annotatedGetter = annotatedGetter; 76 | } 77 | 78 | @Nullable 79 | public Map getSimpleMap() { 80 | return simpleMap; 81 | } 82 | 83 | public void setSimpleMap(Map simpleMap) { 84 | this.simpleMap = simpleMap; 85 | } 86 | 87 | @Nullable 88 | public String getValidated() { 89 | return validated; 90 | } 91 | 92 | public void setValidated(String validated) { 93 | this.validated = validated; 94 | } 95 | 96 | @Nullable 97 | public NestedBean getNestedBean() { 98 | return nestedBean; 99 | } 100 | 101 | public void setNestedBean(NestedBean nestedBean) { 102 | this.nestedBean = nestedBean; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /spring-webmvc-annotated-data-binder/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/resolver/HeaderParameterMapRequestPropertyResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.resolver; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.HeaderParameter; 19 | import com.mattbertolini.spring.web.bind.introspect.BindingProperty; 20 | import org.springframework.http.HttpHeaders; 21 | import org.springframework.lang.NonNull; 22 | import org.springframework.util.LinkedMultiValueMap; 23 | import org.springframework.util.MultiValueMap; 24 | import org.springframework.util.StringUtils; 25 | import org.springframework.web.context.request.NativeWebRequest; 26 | 27 | import java.util.Iterator; 28 | import java.util.LinkedHashMap; 29 | import java.util.Map; 30 | 31 | public class HeaderParameterMapRequestPropertyResolver implements RequestPropertyResolver { 32 | @Override 33 | public boolean supports(@NonNull BindingProperty bindingProperty) { 34 | HeaderParameter annotation = bindingProperty.getAnnotation(HeaderParameter.class); 35 | return annotation != null && !StringUtils.hasText(annotation.value()) && 36 | Map.class.isAssignableFrom(bindingProperty.getType()); 37 | } 38 | 39 | @Override 40 | public Object resolve(@NonNull BindingProperty bindingProperty, @NonNull NativeWebRequest request) { 41 | if (MultiValueMap.class.isAssignableFrom(bindingProperty.getType())) { 42 | MultiValueMap retMap; 43 | if (HttpHeaders.class.isAssignableFrom(bindingProperty.getType())) { 44 | retMap = new HttpHeaders(); 45 | } else { 46 | retMap = new LinkedMultiValueMap<>(); 47 | } 48 | for (Iterator iterator = request.getHeaderNames(); iterator.hasNext();) { 49 | String headerName = iterator.next(); 50 | String[] headerValues = request.getHeaderValues(headerName); 51 | if (headerValues != null) { 52 | for (String headerValue : headerValues) { 53 | retMap.add(headerName, headerValue); 54 | } 55 | } 56 | } 57 | return retMap; 58 | } 59 | 60 | Map retMap = new LinkedHashMap<>(); 61 | for (Iterator iterator = request.getHeaderNames(); iterator.hasNext();) { 62 | String headerName = iterator.next(); 63 | String headerValue = request.getHeader(headerName); 64 | retMap.put(headerName, headerValue); 65 | } 66 | return retMap; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/DirectFieldAccessController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind; 17 | 18 | import com.mattbertolini.spring.web.bind.annotation.BeanParameter; 19 | import org.springframework.http.MediaType; 20 | import org.springframework.lang.Nullable; 21 | import org.springframework.web.bind.annotation.GetMapping; 22 | import org.springframework.web.bind.annotation.PostMapping; 23 | import org.springframework.web.bind.annotation.RestController; 24 | 25 | import java.util.Objects; 26 | 27 | @RestController 28 | public class DirectFieldAccessController { 29 | @Nullable 30 | @GetMapping(value = "/cookieParameter", produces = MediaType.TEXT_PLAIN_VALUE) 31 | public String cookieParameter(@BeanParameter DirectFieldAccessBean directFieldAccessBean) { 32 | return directFieldAccessBean.getCookieParameter(); 33 | } 34 | 35 | @Nullable 36 | @PostMapping(value = "/formParameter", produces = MediaType.TEXT_PLAIN_VALUE) 37 | public String formParameter(@BeanParameter DirectFieldAccessBean directFieldAccessBean) { 38 | return directFieldAccessBean.getFormParameter(); 39 | } 40 | 41 | @Nullable 42 | @GetMapping(value = "/headerParameter", produces = MediaType.TEXT_PLAIN_VALUE) 43 | public String headerParameter(@BeanParameter DirectFieldAccessBean directFieldAccessBean) { 44 | return directFieldAccessBean.getHeaderParameter(); 45 | } 46 | 47 | @Nullable 48 | @SuppressWarnings("MVCPathVariableInspection") 49 | @GetMapping(value = "/pathParameter/{path_parameter}", produces = MediaType.TEXT_PLAIN_VALUE) 50 | public String pathParameter(@BeanParameter DirectFieldAccessBean directFieldAccessBean) { 51 | return directFieldAccessBean.getPathParameter(); 52 | } 53 | 54 | @Nullable 55 | @GetMapping(value = "/requestParameter", produces = MediaType.TEXT_PLAIN_VALUE) 56 | public String requestParameter(@BeanParameter DirectFieldAccessBean directFieldAccessBean) { 57 | return directFieldAccessBean.getRequestParameter(); 58 | } 59 | 60 | @Nullable 61 | @GetMapping(value = "/sessionParameter", produces = MediaType.TEXT_PLAIN_VALUE) 62 | public String sessionParameter(@BeanParameter DirectFieldAccessBean directFieldAccessBean) { 63 | return directFieldAccessBean.getSessionParameter(); 64 | } 65 | 66 | @Nullable 67 | @PostMapping(value = "/requestBody", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.TEXT_PLAIN_VALUE) 68 | public String requestBody(@BeanParameter DirectFieldAccessBean.RequestBodyBean directFieldAccessBean) { 69 | return Objects.requireNonNull(directFieldAccessBean.getRequestBody()).getProperty(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /webflux-annotated-data-binder-spring-boot-starter/src/main/java/com/mattbertolini/spring/web/reactive/bind/autoconfigure/WebFluxBinderAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind.autoconfigure; 17 | 18 | import com.mattbertolini.spring.web.reactive.bind.PropertyResolverRegistry; 19 | import com.mattbertolini.spring.web.reactive.bind.config.BinderConfiguration; 20 | import com.mattbertolini.spring.web.reactive.bind.resolver.RequestPropertyResolver; 21 | import org.springframework.beans.factory.BeanFactory; 22 | import org.springframework.beans.factory.ObjectProvider; 23 | import org.springframework.beans.factory.config.BeanDefinition; 24 | import org.springframework.boot.autoconfigure.AutoConfiguration; 25 | import org.springframework.boot.autoconfigure.AutoConfigurationPackages; 26 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 27 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 28 | import org.springframework.context.annotation.Bean; 29 | import org.springframework.context.annotation.Role; 30 | 31 | import java.util.ArrayList; 32 | import java.util.LinkedHashSet; 33 | import java.util.List; 34 | import java.util.Set; 35 | 36 | @AutoConfiguration 37 | @Role(BeanDefinition.ROLE_INFRASTRUCTURE) 38 | @ConditionalOnMissingBean(BinderConfiguration.class) 39 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) 40 | public class WebFluxBinderAutoConfiguration { 41 | private final List packagesToScan = new ArrayList<>(); 42 | private final Set customResolvers = new LinkedHashSet<>(); 43 | private final Set propertyResolverRegistries = new LinkedHashSet<>(); 44 | 45 | public WebFluxBinderAutoConfiguration(BeanFactory beanFactory, 46 | ObjectProvider> customResolvers, 47 | ObjectProvider> propertyResolverRegistries) { 48 | if (AutoConfigurationPackages.has(beanFactory)) { 49 | packagesToScan.addAll(AutoConfigurationPackages.get(beanFactory)); 50 | } 51 | customResolvers.ifAvailable(this.customResolvers::addAll); 52 | propertyResolverRegistries.ifAvailable(this.propertyResolverRegistries::addAll); 53 | } 54 | 55 | @Bean 56 | public BinderConfiguration binderConfiguration() { 57 | BinderConfiguration binderConfiguration = new BinderConfiguration(); 58 | packagesToScan.forEach(binderConfiguration::addPackageToScan); 59 | binderConfiguration.addResolvers(customResolvers); 60 | propertyResolverRegistries.forEach(binderConfiguration::addResolvers); 61 | return binderConfiguration; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /webmvc-annotated-data-binder-spring-boot-starter/src/main/java/com/mattbertolini/spring/web/servlet/mvc/bind/autoconfigure/WebMvcBinderAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.autoconfigure; 17 | 18 | import com.mattbertolini.spring.web.servlet.mvc.bind.PropertyResolverRegistry; 19 | import com.mattbertolini.spring.web.servlet.mvc.bind.config.BinderConfiguration; 20 | import com.mattbertolini.spring.web.servlet.mvc.bind.resolver.RequestPropertyResolver; 21 | import org.springframework.beans.factory.BeanFactory; 22 | import org.springframework.beans.factory.ObjectProvider; 23 | import org.springframework.beans.factory.config.BeanDefinition; 24 | import org.springframework.boot.autoconfigure.AutoConfiguration; 25 | import org.springframework.boot.autoconfigure.AutoConfigurationPackages; 26 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 27 | import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 28 | import org.springframework.context.annotation.Bean; 29 | import org.springframework.context.annotation.Role; 30 | 31 | import java.util.ArrayList; 32 | import java.util.LinkedHashSet; 33 | import java.util.List; 34 | import java.util.Set; 35 | 36 | @AutoConfiguration 37 | @Role(BeanDefinition.ROLE_INFRASTRUCTURE) 38 | @ConditionalOnMissingBean(BinderConfiguration.class) 39 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) 40 | public class WebMvcBinderAutoConfiguration { 41 | private final List packagesToScan = new ArrayList<>(); 42 | private final Set customResolvers = new LinkedHashSet<>(); 43 | private final Set propertyResolverRegistries = new LinkedHashSet<>(); 44 | 45 | public WebMvcBinderAutoConfiguration(BeanFactory beanFactory, 46 | ObjectProvider> customResolvers, 47 | ObjectProvider> propertyResolverRegistries) { 48 | if (AutoConfigurationPackages.has(beanFactory)) { 49 | packagesToScan.addAll(AutoConfigurationPackages.get(beanFactory)); 50 | } 51 | customResolvers.ifAvailable(this.customResolvers::addAll); 52 | propertyResolverRegistries.ifAvailable(this.propertyResolverRegistries::addAll); 53 | } 54 | 55 | @Bean 56 | public BinderConfiguration binderConfiguration() { 57 | BinderConfiguration binderConfiguration = new BinderConfiguration(); 58 | packagesToScan.forEach(binderConfiguration::addPackageToScan); 59 | binderConfiguration.addResolvers(customResolvers); 60 | propertyResolverRegistries.forEach(binderConfiguration::addResolvers); 61 | return binderConfiguration; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /spring-webflux-annotated-data-binder/src/test/java/com/mattbertolini/spring/web/reactive/bind/MockBindingContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.reactive.bind; 17 | 18 | import com.uber.nullaway.annotations.Initializer; 19 | import org.springframework.core.ResolvableType; 20 | import org.springframework.format.support.FormattingConversionServiceFactoryBean; 21 | import org.springframework.lang.Nullable; 22 | import org.springframework.ui.Model; 23 | import org.springframework.validation.BindingResult; 24 | import org.springframework.validation.support.BindingAwareConcurrentModel; 25 | import org.springframework.web.bind.support.WebExchangeDataBinder; 26 | import org.springframework.web.reactive.BindingContext; 27 | import org.springframework.web.server.ServerWebExchange; 28 | 29 | public class MockBindingContext extends BindingContext { 30 | private final BindingAwareConcurrentModel model = new BindingAwareConcurrentModel(); 31 | private MockWebExchangeDataBinder dataBinder; 32 | @Nullable 33 | private BindingResult bindingResult; 34 | 35 | @Override 36 | @Initializer 37 | public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, @Nullable Object target, String name) { 38 | dataBinder = new MockWebExchangeDataBinder(target); 39 | 40 | if (bindingResult != null) { 41 | dataBinder.setBindingResult(bindingResult); 42 | } 43 | 44 | FormattingConversionServiceFactoryBean conversionServiceFactoryBean = new FormattingConversionServiceFactoryBean(); 45 | conversionServiceFactoryBean.afterPropertiesSet(); 46 | dataBinder.setConversionService(conversionServiceFactoryBean.getObject()); 47 | 48 | return dataBinder; 49 | } 50 | 51 | @Override 52 | @Initializer 53 | public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, @Nullable Object target, String name, ResolvableType targetType) { 54 | dataBinder = new MockWebExchangeDataBinder(target); 55 | 56 | if (target == null) { 57 | dataBinder.setTargetType(targetType); 58 | } 59 | 60 | if (bindingResult != null) { 61 | dataBinder.setBindingResult(bindingResult); 62 | } 63 | 64 | FormattingConversionServiceFactoryBean conversionServiceFactoryBean = new FormattingConversionServiceFactoryBean(); 65 | conversionServiceFactoryBean.afterPropertiesSet(); 66 | dataBinder.setConversionService(conversionServiceFactoryBean.getObject()); 67 | 68 | return dataBinder; 69 | } 70 | 71 | @Override 72 | public Model getModel() { 73 | return model; 74 | } 75 | 76 | public MockWebExchangeDataBinder getDataBinder() { 77 | return dataBinder; 78 | } 79 | 80 | public void setBindingResult(BindingResult bindingResult) { 81 | this.bindingResult = bindingResult; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/CookieParameterController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind; 17 | 18 | import com.mattbertolini.spring.test.web.bind.records.CookieParameterRecord; 19 | import com.mattbertolini.spring.web.bind.annotation.BeanParameter; 20 | import jakarta.validation.Valid; 21 | import org.springframework.http.MediaType; 22 | import org.springframework.lang.Nullable; 23 | import org.springframework.validation.BindingResult; 24 | import org.springframework.web.bind.annotation.GetMapping; 25 | import org.springframework.web.bind.annotation.RestController; 26 | 27 | import java.util.Objects; 28 | 29 | @RestController 30 | public class CookieParameterController { 31 | @Nullable 32 | @GetMapping(value = "/annotatedField", produces = MediaType.TEXT_PLAIN_VALUE) 33 | public String annotatedField(@BeanParameter CookieParameterBean cookieParameterBean) { 34 | return cookieParameterBean.getAnnotatedField(); 35 | } 36 | 37 | @Nullable 38 | @GetMapping(value = "/annotatedSetter", produces = MediaType.TEXT_PLAIN_VALUE) 39 | public String annotatedSetter(@BeanParameter CookieParameterBean cookieParameterBean) { 40 | return cookieParameterBean.getAnnotatedSetter(); 41 | } 42 | 43 | @Nullable 44 | @GetMapping(value = "/annotatedGetter", produces = MediaType.TEXT_PLAIN_VALUE) 45 | public String annotatedGetter(@BeanParameter CookieParameterBean cookieParameterBean) { 46 | return cookieParameterBean.getAnnotatedGetter(); 47 | } 48 | 49 | @GetMapping(value = "/bindingResult", produces = MediaType.TEXT_PLAIN_VALUE) 50 | public String bindingResult(@BeanParameter CookieParameterBean cookieParameterBean, BindingResult bindingResult) { 51 | return Integer.toString(bindingResult.getErrorCount()); 52 | } 53 | 54 | @Nullable 55 | @GetMapping(value = "/validated", produces = MediaType.TEXT_PLAIN_VALUE) 56 | public String validated(@Valid @BeanParameter CookieParameterBean cookieParameterBean) { 57 | return cookieParameterBean.getValidated(); 58 | } 59 | 60 | @GetMapping(value = "/validatedWithBindingResult", produces = MediaType.TEXT_PLAIN_VALUE) 61 | public String validatedWithBindingResult(@Valid @BeanParameter CookieParameterBean cookieParameterBean, BindingResult bindingResult) { 62 | if (bindingResult.hasErrors()) { 63 | return "notValid"; 64 | } 65 | return "valid"; 66 | } 67 | 68 | @Nullable 69 | @GetMapping(value = "/nested", produces = MediaType.TEXT_PLAIN_VALUE) 70 | public String nestedBean(@BeanParameter CookieParameterBean cookieParameterBean) { 71 | return Objects.requireNonNull(cookieParameterBean.getNestedBean()).getCookieValue(); 72 | } 73 | 74 | @GetMapping(value = "/record", produces = MediaType.TEXT_PLAIN_VALUE) 75 | public String javaRecord(@BeanParameter CookieParameterRecord cookieParameterRecord) { 76 | return cookieParameterRecord.annotated(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /integration-tests/src/test/java/com/mattbertolini/spring/test/web/bind/SessionParameterController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.test.web.bind; 17 | 18 | import com.mattbertolini.spring.test.web.bind.records.SessionParameterRecord; 19 | import com.mattbertolini.spring.web.bind.annotation.BeanParameter; 20 | import jakarta.validation.Valid; 21 | import org.springframework.http.MediaType; 22 | import org.springframework.lang.Nullable; 23 | import org.springframework.validation.BindingResult; 24 | import org.springframework.web.bind.annotation.GetMapping; 25 | import org.springframework.web.bind.annotation.RestController; 26 | 27 | import java.util.Objects; 28 | 29 | @RestController 30 | public class SessionParameterController { 31 | @Nullable 32 | @GetMapping(value = "/annotatedField", produces = MediaType.TEXT_PLAIN_VALUE) 33 | public String annotatedField(@BeanParameter SessionParameterBean sessionParameterBean) { 34 | return sessionParameterBean.getAnnotatedField(); 35 | } 36 | 37 | @Nullable 38 | @GetMapping(value = "/annotatedSetter", produces = MediaType.TEXT_PLAIN_VALUE) 39 | public String annotatedSetter(@BeanParameter SessionParameterBean sessionParameterBean) { 40 | return sessionParameterBean.getAnnotatedSetter(); 41 | } 42 | 43 | @Nullable 44 | @GetMapping(value = "/annotatedGetter", produces = MediaType.TEXT_PLAIN_VALUE) 45 | public String annotatedGetter(@BeanParameter SessionParameterBean sessionParameterBean) { 46 | return sessionParameterBean.getAnnotatedGetter(); 47 | } 48 | 49 | @GetMapping(value = "/bindingResult", produces = MediaType.TEXT_PLAIN_VALUE) 50 | public String bindingResult(@BeanParameter SessionParameterBean sessionParameterBean, BindingResult bindingResult) { 51 | return Integer.toString(bindingResult.getErrorCount()); 52 | } 53 | 54 | @Nullable 55 | @GetMapping(value = "/validated", produces = MediaType.TEXT_PLAIN_VALUE) 56 | public String validated(@Valid @BeanParameter SessionParameterBean sessionParameterBean) { 57 | return sessionParameterBean.getValidated(); 58 | } 59 | 60 | @GetMapping(value = "/validatedWithBindingResult", produces = MediaType.TEXT_PLAIN_VALUE) 61 | public String validatedWithBindingResult(@Valid @BeanParameter SessionParameterBean sessionParameterBean, BindingResult bindingResult) { 62 | if (bindingResult.hasErrors()) { 63 | return "notValid"; 64 | } 65 | return "valid"; 66 | } 67 | 68 | @Nullable 69 | @GetMapping(value = "/nested", produces = MediaType.TEXT_PLAIN_VALUE) 70 | public String nestedBean(@BeanParameter SessionParameterBean sessionParameterBean) { 71 | return Objects.requireNonNull(sessionParameterBean.getNestedBean()).getSessionAttribute(); 72 | } 73 | 74 | @GetMapping(value = "/record", produces = MediaType.TEXT_PLAIN_VALUE) 75 | public String javaRecord(@BeanParameter SessionParameterRecord sessionParameterRecord) { 76 | return sessionParameterRecord.annotated(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /webmvc-annotated-data-binder-spring-boot-starter/src/test/java/com/mattbertolini/spring/web/servlet/mvc/bind/autoconfigure/WebMvcBinderAutoConfigurationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.mattbertolini.spring.web.servlet.mvc.bind.autoconfigure; 17 | 18 | import com.mattbertolini.spring.web.servlet.mvc.bind.config.BinderConfiguration; 19 | import org.junit.jupiter.api.Test; 20 | import org.springframework.boot.autoconfigure.AutoConfigurations; 21 | import org.springframework.boot.autoconfigure.SpringBootApplication; 22 | import org.springframework.boot.test.context.runner.WebApplicationContextRunner; 23 | import org.springframework.context.annotation.Bean; 24 | import org.springframework.context.annotation.ComponentScan; 25 | import org.springframework.context.annotation.FilterType; 26 | 27 | import static org.assertj.core.api.Assertions.assertThat; 28 | 29 | class WebMvcBinderAutoConfigurationTest { 30 | private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() 31 | .withConfiguration(AutoConfigurations.of(WebMvcBinderAutoConfiguration.class)); 32 | 33 | @Test 34 | void autoConfiguredWithPackagesToScan() { 35 | contextRunner.withUserConfiguration(EmptyConfig.class).run(context -> { 36 | assertThat(context).hasSingleBean(BinderConfiguration.class); 37 | BinderConfiguration binderConfiguration = context.getBean(BinderConfiguration.class); 38 | assertThat(binderConfiguration.getPackagesToScan()).hasSize(1) 39 | .contains(EmptyConfig.class.getPackage().getName()); 40 | }); 41 | } 42 | 43 | @Test 44 | void noPackagesToScanWhenAutoConfigurationNotEnabled() { 45 | contextRunner.run(context -> { 46 | assertThat(context).hasSingleBean(BinderConfiguration.class); 47 | BinderConfiguration binderConfiguration = context.getBean(BinderConfiguration.class); 48 | assertThat(binderConfiguration.getPackagesToScan()).isEmpty(); 49 | }); 50 | } 51 | 52 | @Test 53 | void overridesAutoConfigurationWhenBeanIsDefined() { 54 | contextRunner.withUserConfiguration(OverrideBeanDefinition.class).run(context -> { 55 | assertThat(context).hasSingleBean(BinderConfiguration.class); 56 | assertThat(context).getBean("customBinderConfig") 57 | .isEqualTo(context.getBean(BinderConfiguration.class)); 58 | BinderConfiguration binderConfiguration = context.getBean(BinderConfiguration.class); 59 | assertThat(binderConfiguration.getPackagesToScan()) 60 | .containsOnly("com.mattbertolini.override"); 61 | }); 62 | } 63 | 64 | @SpringBootApplication(proxyBeanMethods = false) 65 | @ComponentScan(excludeFilters = { 66 | @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = OverrideBeanDefinition.class) 67 | }) 68 | private static class EmptyConfig {} 69 | 70 | @SpringBootApplication(proxyBeanMethods = false) 71 | private static class OverrideBeanDefinition { 72 | @Bean 73 | public BinderConfiguration customBinderConfig() { 74 | return new BinderConfiguration().addPackageToScan("com.mattbertolini.override"); 75 | } 76 | } 77 | } 78 | --------------------------------------------------------------------------------