diff --git a/build.gradle b/build.gradle index f70ce53816f..281295022ba 100644 --- a/build.gradle +++ b/build.gradle @@ -815,6 +815,8 @@ project("spring-web-reactive") { optional "org.apache.httpcomponents:httpclient:${httpclientVersion}" optional('org.webjars:webjars-locator:0.32') testCompile("javax.validation:validation-api:${beanvalVersion}") + testCompile("org.hibernate:hibernate-validator:${hibval5Version}") + testCompile("javax.el:javax.el-api:${elApiVersion}") testCompile("org.apache.tomcat:tomcat-util:${tomcatVersion}") testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}") testCompile("org.eclipse.jetty:jetty-server:${jettyVersion}") diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index a78a7d6c8d3..081bae89b1d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -55,7 +55,10 @@ import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.util.ClassUtils; import org.springframework.validation.Errors; +import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.CompositeContentTypeResolver; @@ -255,6 +258,7 @@ public class WebReactiveConfiguration implements ApplicationContextAware { } adapter.setMessageReaders(getMessageReaders()); + adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer()); adapter.setConversionService(mvcConversionService()); adapter.setValidator(mvcValidator()); @@ -325,6 +329,18 @@ public class WebReactiveConfiguration implements ApplicationContextAware { protected void extendMessageReaders(List> messageReaders) { } + /** + * Return the {@link ConfigurableWebBindingInitializer} to use for + * initializing all {@link WebDataBinder} instances. + */ + protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() { + ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); + initializer.setConversionService(mvcConversionService()); + initializer.setValidator(mvcValidator()); + initializer.setMessageCodesResolver(getMessageCodesResolver()); + return initializer; + } + @Bean public FormattingConversionService mvcConversionService() { FormattingConversionService service = new DefaultFormattingConversionService(); @@ -378,6 +394,13 @@ public class WebReactiveConfiguration implements ApplicationContextAware { return null; } + /** + * Override this method to provide a custom {@link MessageCodesResolver}. + */ + protected MessageCodesResolver getMessageCodesResolver() { + return null; + } + @Bean public SimpleHandlerAdapter simpleHandlerAdapter() { return new SimpleHandlerAdapter(); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index 9fb743c2269..b8f86d17ee8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -41,6 +41,7 @@ import org.springframework.http.codec.HttpMessageReader; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; import org.springframework.validation.Validator; +import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; import org.springframework.web.reactive.HandlerAdapter; @@ -68,6 +69,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory private ReactiveAdapterRegistry reactiveAdapters = new ReactiveAdapterRegistry(); + private WebBindingInitializer webBindingInitializer; + private ConversionService conversionService = new DefaultFormattingConversionService(); private Validator validator; @@ -136,6 +139,21 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory return this.reactiveAdapters; } + /** + * Provide a WebBindingInitializer with "global" initialization to apply + * to every DataBinder instance. + */ + public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) { + this.webBindingInitializer = webBindingInitializer; + } + + /** + * Return the configured WebBindingInitializer, or {@code null} if none. + */ + public WebBindingInitializer getWebBindingInitializer() { + return this.webBindingInitializer; + } + /** * Configure a ConversionService for type conversion of controller method * arguments as well as for converting from different async types to diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java index 0714739e83a..083b822fdac 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/WebReactiveConfigurationTests.java @@ -49,6 +49,8 @@ import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean; +import org.springframework.web.bind.WebExchangeDataBinder; +import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.handler.AbstractHandlerMapping; import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; @@ -161,6 +163,13 @@ public class WebReactiveConfigurationTests { Validator validator = context.getBean(name, Validator.class); assertSame(validator, adapter.getValidator()); assertEquals(OptionalValidatorFactoryBean.class, validator.getClass()); + + WebBindingInitializer bindingInitializer = adapter.getWebBindingInitializer(); + assertNotNull(bindingInitializer); + WebExchangeDataBinder binder = new WebExchangeDataBinder(new Object()); + bindingInitializer.initBinder(binder); + assertSame(service, binder.getConversionService()); + assertSame(validator, binder.getValidator()); } @Test diff --git a/spring-web/src/main/java/org/springframework/web/bind/WebExchangeDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/WebExchangeDataBinder.java new file mode 100644 index 00000000000..55c188bd52f --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/WebExchangeDataBinder.java @@ -0,0 +1,166 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.bind; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.core.ResolvableType; +import org.springframework.http.MediaType; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ServerWebExchange; + +/** + * Specialized {@link org.springframework.validation.DataBinder} to perform data + * binding from URL query params or form data in the request data to Java objects. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class WebExchangeDataBinder extends WebDataBinder { + + private static final ResolvableType MULTIVALUE_MAP_TYPE = ResolvableType.forClass(MultiValueMap.class); + + + private HttpMessageReader> formReader = null; + + + /** + * Create a new instance, with default object name. + * @param target the target object to bind onto (or {@code null} if the + * binder is just used to convert a plain parameter value) + * @see #DEFAULT_OBJECT_NAME + */ + public WebExchangeDataBinder(Object target) { + super(target); + } + + /** + * Create a new instance. + * @param target the target object to bind onto (or {@code null} if the + * binder is just used to convert a plain parameter value) + * @param objectName the name of the target object + */ + public WebExchangeDataBinder(Object target, String objectName) { + super(target, objectName); + } + + + public void setFormReader(HttpMessageReader> formReader) { + this.formReader = formReader; + } + + public HttpMessageReader> getFormReader() { + return this.formReader; + } + + + /** + * Bind the URL query parameters or form data of the body of the given request + * to this binder's target. The request body is parsed if the content-type + * is "application/x-www-form-urlencoded". + * + * @param exchange the current exchange. + * @return a {@code Mono} to indicate the result + */ + public Mono bind(ServerWebExchange exchange) { + + ServerHttpRequest request = exchange.getRequest(); + Mono> queryParams = Mono.just(request.getQueryParams()); + Mono> formParams = getFormParams(exchange); + + return Mono.zip(this::mergeParams, queryParams, formParams) + .map(this::getParamsToBind) + .doOnNext(values -> values.putAll(getMultipartFiles(exchange))) + .doOnNext(values -> values.putAll(getExtraValuesToBind(exchange))) + .then(values -> { + doBind(new MutablePropertyValues(values)); + return Mono.empty(); + }); + } + + private Mono> getFormParams(ServerWebExchange exchange) { + ServerHttpRequest request = exchange.getRequest(); + MediaType contentType = request.getHeaders().getContentType(); + if (this.formReader.canRead(MULTIVALUE_MAP_TYPE, contentType)) { + return this.formReader.readMono(MULTIVALUE_MAP_TYPE, request, Collections.emptyMap()); + } + else { + return Mono.just(new LinkedMultiValueMap<>()); + } + } + + @SuppressWarnings("unchecked") + private MultiValueMap mergeParams(Object[] paramMaps) { + MultiValueMap result = new LinkedMultiValueMap<>(); + Arrays.stream(paramMaps).forEach(map -> result.putAll((MultiValueMap) map)); + return result; + } + + private Map getParamsToBind(MultiValueMap params) { + Map valuesToBind = new TreeMap<>(); + for (Map.Entry> entry : params.entrySet()) { + String name = entry.getKey(); + List values = entry.getValue(); + if (values == null || values.isEmpty()) { + // Do nothing, no values found at all. + } + else { + if (values.size() > 1) { + valuesToBind.put(name, values); + } + else { + valuesToBind.put(name, values.get(0)); + } + } + } + return valuesToBind; + } + + /** + * Bind all multipart files contained in the given request, if any (in case + * of a multipart request). + *

Multipart files will only be added to the property values if they + * are not empty or if we're configured to bind empty multipart files too. + * @param exchange the current exchange + * @return Map of field name String to MultipartFile object + */ + protected Map> getMultipartFiles(ServerWebExchange exchange) { + // TODO + return Collections.emptyMap(); + } + + /** + * Extension point that subclasses can use to add extra bind values for a + * request. Invoked before {@link #doBind(MutablePropertyValues)}. + * The default implementation is empty. + * @param exchange the current exchange + */ + protected Map getExtraValuesToBind(ServerWebExchange exchange) { + return Collections.emptyMap(); + } + +} \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java b/spring-web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java index 3056ad03119..acb3a459814 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import org.springframework.validation.BindingErrorProcessor; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.context.request.WebRequest; /** * Convenient {@link WebBindingInitializer} for declarative configuration @@ -182,7 +181,7 @@ public class ConfigurableWebBindingInitializer implements WebBindingInitializer @Override - public void initBinder(WebDataBinder binder, WebRequest request) { + public void initBinder(WebDataBinder binder) { binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths); if (this.directFieldAccess) { binder.initDirectFieldAccess(); diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java b/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java index de00470231f..1df05ee1411 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,7 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory { * @throws Exception in case of invalid state or arguments */ @Override + @SuppressWarnings("deprecation") public final WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception { @@ -74,7 +75,7 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory { /** * Extension point to further initialize the created data binder instance - * (e.g. with {@code @InitBinder} methods) after "global" initializaton + * (e.g. with {@code @InitBinder} methods) after "global" initialization * via {@link WebBindingInitializer}. * @param dataBinder the data binder instance to customize * @param webRequest the current request diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebBindingInitializer.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebBindingInitializer.java index be7e0479dee..3aa7621f49b 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/WebBindingInitializer.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebBindingInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2007 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,19 +20,31 @@ import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.WebRequest; /** - * Callback interface for initializing a {@link org.springframework.web.bind.WebDataBinder} - * for performing data binding in the context of a specific web request. + * Callback interface for initializing a {@link WebDataBinder} for performing + * data binding in the context of a specific web request. * * @author Juergen Hoeller + * @author Rossen Stoyanchev * @since 2.5 */ public interface WebBindingInitializer { /** - * Initialize the given DataBinder for the given request. + * Initialize the given DataBinder. + * @param binder the DataBinder to initialize + * @since 5.0 + */ + void initBinder(WebDataBinder binder); + + /** + * Initialize the given DataBinder for the given (Servlet) request. * @param binder the DataBinder to initialize * @param request the web request that the data binding happens within + * @deprecated as of 5.0 in favor of {@link #initBinder(WebDataBinder)} */ - void initBinder(WebDataBinder binder, WebRequest request); + @Deprecated + default void initBinder(WebDataBinder binder, WebRequest request) { + initBinder(binder); + } } diff --git a/spring-web/src/test/java/org/springframework/web/bind/WebExchangeDataBinderTests.java b/spring-web/src/test/java/org/springframework/web/bind/WebExchangeDataBinderTests.java new file mode 100644 index 00000000000..09ae60b93b2 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/bind/WebExchangeDataBinderTests.java @@ -0,0 +1,214 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.bind; + +import java.beans.PropertyEditorSupport; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; +import org.springframework.tests.sample.beans.ITestBean; +import org.springframework.tests.sample.beans.TestBean; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; + +import static junit.framework.TestCase.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED; + +/** + * Unit tests for {@link WebExchangeDataBinder}. + * + * @author Rossen Stoyanchev + */ +public class WebExchangeDataBinderTests { + + private static final ResolvableType ELEMENT_TYPE = ResolvableType.forClass(MultiValueMap.class); + + + private WebExchangeDataBinder binder; + + private TestBean testBean; + + private ServerWebExchange exchange; + + @Mock + private HttpMessageReader> formReader; + + private MultiValueMap formData; + + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + this.testBean = new TestBean(); + this.binder = new WebExchangeDataBinder(this.testBean, "person"); + this.binder.registerCustomEditor(ITestBean.class, new TestBeanPropertyEditor()); + this.binder.setFormReader(this.formReader); + + MockServerHttpRequest request = new MockServerHttpRequest(); + MockServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager sessionManager = new DefaultWebSessionManager(); + this.exchange = new DefaultServerWebExchange(request, response, sessionManager); + + request.getHeaders().setContentType(APPLICATION_FORM_URLENCODED); + + this.formData = new LinkedMultiValueMap<>(); + when(this.formReader.canRead(ELEMENT_TYPE, APPLICATION_FORM_URLENCODED)).thenReturn(true); + when(this.formReader.readMono(ELEMENT_TYPE, request, Collections.emptyMap())) + .thenReturn(Mono.just(formData)); + } + + + @Test + public void testBindingWithNestedObjectCreation() throws Exception { + this.formData.add("spouse", "someValue"); + this.formData.add("spouse.name", "test"); + this.binder.bind(this.exchange).blockMillis(5000); + + assertNotNull(this.testBean.getSpouse()); + assertEquals("test", testBean.getSpouse().getName()); + } + + @Test + public void testFieldPrefixCausesFieldReset() throws Exception { + this.formData.add("_postProcessed", "visible"); + this.formData.add("postProcessed", "on"); + this.binder.bind(this.exchange).blockMillis(5000); + assertTrue(this.testBean.isPostProcessed()); + + this.formData.remove("postProcessed"); + this.binder.bind(this.exchange).blockMillis(5000); + assertFalse(this.testBean.isPostProcessed()); + } + + @Test + public void testFieldPrefixCausesFieldResetWithIgnoreUnknownFields() throws Exception { + this.binder.setIgnoreUnknownFields(false); + + this.formData.add("_postProcessed", "visible"); + this.formData.add("postProcessed", "on"); + this.binder.bind(this.exchange).blockMillis(5000); + assertTrue(this.testBean.isPostProcessed()); + + this.formData.remove("postProcessed"); + this.binder.bind(this.exchange).blockMillis(5000); + assertFalse(this.testBean.isPostProcessed()); + } + + @Test + public void testFieldDefault() throws Exception { + this.formData.add("!postProcessed", "off"); + this.formData.add("postProcessed", "on"); + this.binder.bind(this.exchange).blockMillis(5000); + assertTrue(this.testBean.isPostProcessed()); + + this.formData.remove("postProcessed"); + this.binder.bind(this.exchange).blockMillis(5000); + assertFalse(this.testBean.isPostProcessed()); + } + + @Test + public void testFieldDefaultPreemptsFieldMarker() throws Exception { + this.formData.add("!postProcessed", "on"); + this.formData.add("_postProcessed", "visible"); + this.formData.add("postProcessed", "on"); + this.binder.bind(this.exchange).blockMillis(5000); + assertTrue(this.testBean.isPostProcessed()); + + this.formData.remove("postProcessed"); + this.binder.bind(this.exchange).blockMillis(5000); + assertTrue(this.testBean.isPostProcessed()); + + this.formData.remove("!postProcessed"); + this.binder.bind(this.exchange).blockMillis(5000); + assertFalse(this.testBean.isPostProcessed()); + } + + @Test + public void testFieldDefaultNonBoolean() throws Exception { + this.formData.add("!name", "anonymous"); + this.formData.add("name", "Scott"); + this.binder.bind(this.exchange).blockMillis(5000); + assertEquals("Scott", this.testBean.getName()); + + this.formData.remove("name"); + this.binder.bind(this.exchange).blockMillis(5000); + assertEquals("anonymous", this.testBean.getName()); + } + + @Test + public void testWithCommaSeparatedStringArray() throws Exception { + this.formData.add("stringArray", "bar"); + this.formData.add("stringArray", "abc"); + this.formData.add("stringArray", "123,def"); + this.binder.bind(this.exchange).blockMillis(5000); + assertEquals("Expected all three items to be bound", 3, this.testBean.getStringArray().length); + + this.formData.remove("stringArray"); + this.formData.add("stringArray", "123,def"); + this.binder.bind(this.exchange).blockMillis(5000); + assertEquals("Expected only 1 item to be bound", 1, this.testBean.getStringArray().length); + } + + @Test + public void testBindingWithNestedObjectCreationAndWrongOrder() throws Exception { + this.formData.add("spouse.name", "test"); + this.formData.add("spouse", "someValue"); + this.binder.bind(this.exchange).blockMillis(5000); + + assertNotNull(this.testBean.getSpouse()); + assertEquals("test", this.testBean.getSpouse().getName()); + } + + @Test + public void testBindingWithQueryParams() throws Exception { + MultiValueMap queryParams = this.exchange.getRequest().getQueryParams(); + queryParams.add("spouse", "someValue"); + queryParams.add("spouse.name", "test"); + this.binder.bind(this.exchange).blockMillis(5000); + + assertNotNull(this.testBean.getSpouse()); + assertEquals("test", this.testBean.getSpouse().getName()); + } + + + + private static class TestBeanPropertyEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) { + setValue(new TestBean()); + } + } + +}