/* * Copyright 2002-2017 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.method.annotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Method; import org.junit.Before; import org.junit.Test; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.tests.sample.beans.TestBean; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.bind.support.WebRequestDataBinder; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.WebRequest; import org.springframework.web.method.support.ModelAndViewContainer; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import static org.junit.Assert.*; import static org.mockito.BDDMockito.*; /** * Test fixture with {@link ModelAttributeMethodProcessor}. * * @author Rossen Stoyanchev */ public class ModelAttributeMethodProcessorTests { private NativeWebRequest request; private ModelAndViewContainer container; private ModelAttributeMethodProcessor processor; private MethodParameter paramNamedValidModelAttr; private MethodParameter paramErrors; private MethodParameter paramInt; private MethodParameter paramModelAttr; private MethodParameter paramBindingDisabledAttr; private MethodParameter paramNonSimpleType; private MethodParameter returnParamNamedModelAttr; private MethodParameter returnParamNonSimpleType; @Before public void setup() throws Exception { this.request = new ServletWebRequest(new MockHttpServletRequest()); this.container = new ModelAndViewContainer(); this.processor = new ModelAttributeMethodProcessor(false); Method method = ModelAttributeHandler.class.getDeclaredMethod("modelAttribute", TestBean.class, Errors.class, int.class, TestBean.class, TestBean.class, TestBean.class); this.paramNamedValidModelAttr = new SynthesizingMethodParameter(method, 0); this.paramErrors = new SynthesizingMethodParameter(method, 1); this.paramInt = new SynthesizingMethodParameter(method, 2); this.paramModelAttr = new SynthesizingMethodParameter(method, 3); this.paramBindingDisabledAttr = new SynthesizingMethodParameter(method, 4); this.paramNonSimpleType = new SynthesizingMethodParameter(method, 5); method = getClass().getDeclaredMethod("annotatedReturnValue"); this.returnParamNamedModelAttr = new MethodParameter(method, -1); method = getClass().getDeclaredMethod("notAnnotatedReturnValue"); this.returnParamNonSimpleType = new MethodParameter(method, -1); } @Test public void supportedParameters() throws Exception { assertTrue(this.processor.supportsParameter(this.paramNamedValidModelAttr)); assertTrue(this.processor.supportsParameter(this.paramModelAttr)); assertFalse(this.processor.supportsParameter(this.paramErrors)); assertFalse(this.processor.supportsParameter(this.paramInt)); assertFalse(this.processor.supportsParameter(this.paramNonSimpleType)); } @Test public void supportedParametersInDefaultResolutionMode() throws Exception { processor = new ModelAttributeMethodProcessor(true); // Only non-simple types, even if not annotated assertTrue(this.processor.supportsParameter(this.paramNamedValidModelAttr)); assertTrue(this.processor.supportsParameter(this.paramErrors)); assertTrue(this.processor.supportsParameter(this.paramModelAttr)); assertTrue(this.processor.supportsParameter(this.paramNonSimpleType)); assertFalse(this.processor.supportsParameter(this.paramInt)); } @Test public void supportedReturnTypes() throws Exception { processor = new ModelAttributeMethodProcessor(false); assertTrue(this.processor.supportsReturnType(returnParamNamedModelAttr)); assertFalse(this.processor.supportsReturnType(returnParamNonSimpleType)); } @Test public void supportedReturnTypesInDefaultResolutionMode() throws Exception { processor = new ModelAttributeMethodProcessor(true); assertTrue(this.processor.supportsReturnType(returnParamNamedModelAttr)); assertTrue(this.processor.supportsReturnType(returnParamNonSimpleType)); } @Test public void bindExceptionRequired() throws Exception { assertTrue(this.processor.isBindExceptionRequired(null, this.paramNonSimpleType)); assertFalse(this.processor.isBindExceptionRequired(null, this.paramNamedValidModelAttr)); } @Test public void resolveArgumentFromModel() throws Exception { testGetAttributeFromModel("attrName", this.paramNamedValidModelAttr); testGetAttributeFromModel("testBean", this.paramModelAttr); testGetAttributeFromModel("testBean", this.paramNonSimpleType); } @Test public void resolveArgumentViaDefaultConstructor() throws Exception { WebDataBinder dataBinder = new WebRequestDataBinder(null); WebDataBinderFactory factory = mock(WebDataBinderFactory.class); given(factory.createBinder(any(), notNull(), eq("attrName"))).willReturn(dataBinder); this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory); verify(factory).createBinder(any(), notNull(), eq("attrName")); } @Test public void resolveArgumentValidation() throws Exception { String name = "attrName"; Object target = new TestBean(); this.container.addAttribute(name, target); StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); WebDataBinderFactory factory = mock(WebDataBinderFactory.class); given(factory.createBinder(this.request, target, name)).willReturn(dataBinder); this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory); assertTrue(dataBinder.isBindInvoked()); assertTrue(dataBinder.isValidateInvoked()); } @Test public void resolveArgumentBindingDisabledPreviously() throws Exception { String name = "attrName"; Object target = new TestBean(); this.container.addAttribute(name, target); // Declare binding disabled (e.g. via @ModelAttribute method) this.container.setBindingDisabled(name); StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); WebDataBinderFactory factory = mock(WebDataBinderFactory.class); given(factory.createBinder(this.request, target, name)).willReturn(dataBinder); this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory); assertFalse(dataBinder.isBindInvoked()); assertTrue(dataBinder.isValidateInvoked()); } @Test public void resolveArgumentBindingDisabled() throws Exception { String name = "noBindAttr"; Object target = new TestBean(); this.container.addAttribute(name, target); StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); WebDataBinderFactory factory = mock(WebDataBinderFactory.class); given(factory.createBinder(this.request, target, name)).willReturn(dataBinder); this.processor.resolveArgument(this.paramBindingDisabledAttr, this.container, this.request, factory); assertFalse(dataBinder.isBindInvoked()); assertTrue(dataBinder.isValidateInvoked()); } @Test(expected = BindException.class) public void resolveArgumentBindException() throws Exception { String name = "testBean"; Object target = new TestBean(); this.container.getModel().addAttribute(target); StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name); dataBinder.getBindingResult().reject("error"); WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class); given(binderFactory.createBinder(this.request, target, name)).willReturn(dataBinder); this.processor.resolveArgument(this.paramNonSimpleType, this.container, this.request, binderFactory); verify(binderFactory).createBinder(this.request, target, name); } @Test // SPR-9378 public void resolveArgumentOrdering() throws Exception { String name = "testBean"; Object testBean = new TestBean(name); this.container.addAttribute(name, testBean); this.container.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, testBean); Object anotherTestBean = new TestBean(); this.container.addAttribute("anotherTestBean", anotherTestBean); StubRequestDataBinder dataBinder = new StubRequestDataBinder(testBean, name); WebDataBinderFactory binderFactory = mock(WebDataBinderFactory.class); given(binderFactory.createBinder(this.request, testBean, name)).willReturn(dataBinder); this.processor.resolveArgument(this.paramModelAttr, this.container, this.request, binderFactory); Object[] values = this.container.getModel().values().toArray(); assertSame("Resolved attribute should be updated to be last", testBean, values[1]); assertSame("BindingResult of resolved attr should be last", dataBinder.getBindingResult(), values[2]); } @Test public void handleAnnotatedReturnValue() throws Exception { this.processor.handleReturnValue("expected", this.returnParamNamedModelAttr, this.container, this.request); assertEquals("expected", this.container.getModel().get("modelAttrName")); } @Test public void handleNotAnnotatedReturnValue() throws Exception { TestBean testBean = new TestBean("expected"); this.processor.handleReturnValue(testBean, this.returnParamNonSimpleType, this.container, this.request); assertSame(testBean, this.container.getModel().get("testBean")); } private void testGetAttributeFromModel(String expectedAttrName, MethodParameter param) throws Exception { Object target = new TestBean(); this.container.addAttribute(expectedAttrName, target); WebDataBinder dataBinder = new WebRequestDataBinder(target); WebDataBinderFactory factory = mock(WebDataBinderFactory.class); given(factory.createBinder(this.request, target, expectedAttrName)).willReturn(dataBinder); this.processor.resolveArgument(param, this.container, this.request, factory); verify(factory).createBinder(this.request, target, expectedAttrName); } private static class StubRequestDataBinder extends WebRequestDataBinder { private boolean bindInvoked; private boolean validateInvoked; public StubRequestDataBinder(Object target, String objectName) { super(target, objectName); } public boolean isBindInvoked() { return bindInvoked; } public boolean isValidateInvoked() { return validateInvoked; } @Override public void bind(WebRequest request) { bindInvoked = true; } @Override public void validate() { validateInvoked = true; } @Override public void validate(Object... validationHints) { validateInvoked = true; } } @Target({METHOD, FIELD, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) public @interface Valid { } @SessionAttributes(types=TestBean.class) private static class ModelAttributeHandler { @SuppressWarnings("unused") public void modelAttribute( @ModelAttribute("attrName") @Valid TestBean annotatedAttr, Errors errors, int intArg, @ModelAttribute TestBean defaultNameAttr, @ModelAttribute(name="noBindAttr", binding=false) @Valid TestBean noBindAttr, TestBean notAnnotatedAttr) { } } @ModelAttribute("modelAttrName") @SuppressWarnings("unused") private String annotatedReturnValue() { return null; } @SuppressWarnings("unused") private TestBean notAnnotatedReturnValue() { return null; } }