diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index 13c47eab1e..dbf71b14cf 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -60,7 +60,6 @@ import org.springframework.web.servlet.mvc.method.annotation.support.ServletRequ import org.springframework.web.servlet.mvc.method.annotation.support.ServletResponseMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.support.ServletWebArgumentResolverAdapter; import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodReturnValueHandler; -import org.springframework.web.servlet.mvc.method.support.ResponseContext; /** * An {@link AbstractHandlerMethodExceptionResolver} that supports using {@link ExceptionHandler}-annotated methods @@ -214,10 +213,10 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, - Exception ex) { + Exception exception) { if (handlerMethod != null) { ExceptionMethodMapping mapping = getExceptionMethodMapping(handlerMethod); - Method method = mapping.getMethod(ex); + Method method = mapping.getMethod(exception); if (method != null) { Object handler = handlerMethod.getBean(); @@ -232,9 +231,8 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce } ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - ResponseContext responseContext = new ResponseContext(webRequest, mavContainer); - exceptionHandler.invokeAndHandle(webRequest, mavContainer, ex, responseContext); + exceptionHandler.invokeAndHandle(webRequest, mavContainer, exception); if (!mavContainer.isResolveView()) { return new ModelAndView(); diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index ddc1594467..6bc2152b4f 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -40,6 +40,7 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter; +import org.springframework.ui.ModelMap; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.validation.DataBinder; import org.springframework.web.bind.WebDataBinder; @@ -84,6 +85,7 @@ import org.springframework.web.servlet.mvc.method.annotation.support.DefaultMeth import org.springframework.web.servlet.mvc.method.annotation.support.HttpEntityMethodProcessor; import org.springframework.web.servlet.mvc.method.annotation.support.ModelAndViewMethodReturnValueHandler; import org.springframework.web.servlet.mvc.method.annotation.support.PathVariableMethodArgumentResolver; +import org.springframework.web.servlet.mvc.method.annotation.support.RedirectModelMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.support.RequestPartMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.support.RequestResponseBodyMethodProcessor; import org.springframework.web.servlet.mvc.method.annotation.support.ServletCookieValueMethodArgumentResolver; @@ -91,7 +93,8 @@ import org.springframework.web.servlet.mvc.method.annotation.support.ServletMode import org.springframework.web.servlet.mvc.method.annotation.support.ServletRequestMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.support.ServletResponseMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.support.ViewMethodReturnValueHandler; -import org.springframework.web.servlet.mvc.method.support.ResponseContext; +import org.springframework.web.servlet.mvc.support.RedirectModel; +import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.util.WebUtils; /** @@ -366,6 +369,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i argumentResolvers.addResolver(new ServletRequestMethodArgumentResolver()); argumentResolvers.addResolver(new ServletResponseMethodArgumentResolver()); argumentResolvers.addResolver(new HttpEntityMethodProcessor(messageConverters)); + argumentResolvers.addResolver(new RedirectModelMethodArgumentResolver()); argumentResolvers.addResolver(new ModelMethodProcessor()); argumentResolvers.addResolver(new ErrorsMethodArgumentResolver()); @@ -512,27 +516,32 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod); - FlashMap flashMap = (FlashMap) request.getAttribute(FlashMapManager.PREVIOUS_FLASH_MAP_ATTRIBUTE); + FlashMap previousFlashMap = (FlashMap) request.getAttribute(FlashMapManager.PREVIOUS_FLASH_MAP_ATTRIBUTE); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); - mavContainer.addAllAttributes(flashMap); + mavContainer.addAllAttributes(previousFlashMap); modelFactory.initModel(webRequest, mavContainer, requestMappingMethod); SessionStatus sessionStatus = new SimpleSessionStatus(); - ResponseContext responseContext = new ResponseContext(webRequest, mavContainer); - requestMappingMethod.invokeAndHandle(webRequest, mavContainer, sessionStatus, responseContext); + requestMappingMethod.invokeAndHandle(webRequest, mavContainer, sessionStatus); modelFactory.updateModel(webRequest, mavContainer, sessionStatus); if (!mavContainer.isResolveView()) { return null; } else { - ModelAndView mav = new ModelAndView().addAllObjects(mavContainer.getModel()); + ModelMap model = mavContainer.getModel(); + ModelAndView mav = new ModelAndView().addAllObjects(model); mav.setViewName(mavContainer.getViewName()); if (!mavContainer.isViewReference()) { mav.setView((View) mavContainer.getView()); } + if (model instanceof RedirectModel) { + RedirectModel redirectModel = (RedirectModel) model; + FlashMap currentFlashMap = RequestContextUtils.getFlashMap(request); + currentFlashMap.putAll(redirectModel.getFlashAttributes()); + } return mav; } } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RedirectModelMethodArgumentResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RedirectModelMethodArgumentResolver.java new file mode 100644 index 0000000000..5c353b794f --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/RedirectModelMethodArgumentResolver.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2011 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.servlet.mvc.method.annotation.support; + +import org.springframework.core.MethodParameter; +import org.springframework.ui.Model; +import org.springframework.ui.ModelMap; +import org.springframework.validation.DataBinder; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.annotation.support.ModelMethodProcessor; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.mvc.support.RedirectModel; + +/** + * Resolves {@link RedirectModel} method arguments. + * + *

This resolver must be listed ahead of the {@link ModelMethodProcessor}, + * which also resolves arguments of type {@link Model}. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class RedirectModelMethodArgumentResolver implements HandlerMethodArgumentResolver { + + public boolean supportsParameter(MethodParameter parameter) { + return RedirectModel.class.equals(parameter.getParameterType()); + } + + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + DataBinder dataBinder = binderFactory.createBinder(webRequest, null, null); + ModelMap implicitModel = mavContainer.getModel(); + RedirectModel redirectModel = new RedirectModel(dataBinder, implicitModel); + mavContainer.setRedirectModel(redirectModel); + return redirectModel; + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandler.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandler.java index e634d08961..bd90fe91a9 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandler.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandler.java @@ -16,8 +16,6 @@ package org.springframework.web.servlet.mvc.method.annotation.support; -import java.lang.reflect.Method; - import org.springframework.core.MethodParameter; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ResponseBody; @@ -26,18 +24,20 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.RequestToViewNameTranslator; import org.springframework.web.servlet.View; -import org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator; /** - * Handles return values that are of type {@code void}, {@code String} (i.e. logical view name), or {@link View}. + * Handles return values that are of type {@code void}, {@code String} (i.e. + * logical view name), or {@link View}. * - *

A {@code null} return value, either due to a void return type or as the actual value returned from a - * method is left unhandled, leaving it to the configured {@link RequestToViewNameTranslator} to resolve the - * request to an actual view name. By default it is the {@link DefaultRequestToViewNameTranslator}. + *

A {@code null} return value, either due to a void return type or as the + * actual value returned from a method is left unhandled, leaving it to the + * configured {@link RequestToViewNameTranslator} to resolve the request to + * an actual view name. * - *

Since a {@link String} return value may handled in different ways, especially in combination with method - * annotations such as @{@link ModelAttribute} and @{@link ResponseBody}, this handler should be ordered after - * return value handlers that support method annotations. + *

Since a {@link String} return value may be handled in combination with + * method annotations such as @{@link ModelAttribute} or @{@link ResponseBody}, + * this handler should be ordered after return value handlers that support + * method annotations. * * @author Rossen Stoyanchev * @since 3.1 @@ -57,17 +57,44 @@ public class ViewMethodReturnValueHandler implements HandlerMethodReturnValueHan return; } if (returnValue instanceof String) { - mavContainer.setViewName((String) returnValue); + String viewName = (String) returnValue; + mavContainer.setViewName(viewName); + if (isRedirectViewName(viewName)) { + mavContainer.useRedirectModel(); + } } else if (returnValue instanceof View){ - mavContainer.setView(returnValue); + View view = (View) returnValue; + mavContainer.setView(view); + if (isRedirectView(view)) { + mavContainer.useRedirectModel(); + } } else { // should not happen - Method method = returnType.getMethod(); - String returnTypeName = returnType.getParameterType().getName(); - throw new UnsupportedOperationException("Unknown return type: " + returnTypeName + " in method: " + method); + throw new UnsupportedOperationException("Unknown return type: " + + returnType.getParameterType().getName() + " in method: " + returnType.getMethod()); } } + /** + * Whether the given view name is a redirect view reference. + * @param viewName the view name to check, never {@code null} + * @return "true" if the given view name is recognized as a redirect view + * reference; "false" otherwise. + */ + protected boolean isRedirectViewName(String viewName) { + return viewName.startsWith("redirect:"); + } + + /** + * Whether the given View instance is a redirect view. + * @param view a view instance, never {@code null} + * @return "true" if the given view is recognized as a redirect View; + * "false" otherwise. + */ + protected boolean isRedirectView(View view) { + return "RedirectView".equals(view.getClass().getSimpleName()); + } + } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/support/RedirectResponse.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/support/RedirectResponse.java deleted file mode 100644 index 4ff908bd89..0000000000 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/support/RedirectResponse.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2002-2011 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.servlet.mvc.method.support; - -import java.util.Collection; - -import javax.servlet.http.HttpServletRequest; - -import org.springframework.core.Conventions; -import org.springframework.util.Assert; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.ModelAndViewContainer; -import org.springframework.web.servlet.FlashMap; -import org.springframework.web.servlet.support.RequestContextUtils; - -/** - * Provides annotated controller methods with convenience methods for setting - * up response with a view name that will result in a redirect. - * - *

An instance of this class is obtained via {@link ResponseContext#redirect}. - * - * @author Keith Donald - * @author Rossen Stoyanchev - * - * @since 3.1 - */ -public class RedirectResponse { - - private final NativeWebRequest webRequest; - - private final ModelAndViewContainer mavContainer; - - RedirectResponse(NativeWebRequest webRequest, ModelAndViewContainer mavContainer) { - this.webRequest = webRequest; - this.mavContainer = mavContainer; - } - - /** - * Add a URI template variable to use to expand the URI template into a URL. - *

Note: URI template variables from the current - * request are automatically used when expanding the redirect URI template. - * They don't need to be added explicitly here. - */ - public RedirectResponse uriVariable(String name, Object value) { - this.mavContainer.addAttribute(name, value); - return this; - } - - /** - * Add a URI template variable to use to expand the URI template into a URL. - * The name of the variable is selected using a - * {@link org.springframework.core.Conventions#getVariableName generated name}. - *

Note: URI template variables from the current - * request are automatically used when expanding the redirect URI template. - * They don't need to be added explicitly here. - */ - public RedirectResponse uriVariable(Object value) { - this.mavContainer.addAttribute(value); - return this; - } - - /** - * Add a query parameter to append to the redirect URL. - */ - public RedirectResponse queryParam(String name, Object value) { - this.mavContainer.addAttribute(name, value); - return this; - } - - /** - * Add a query parameter to append to the redirect URL. - * The name of the parameter is selected using a - * {@link org.springframework.core.Conventions#getVariableName generated name}. - */ - public RedirectResponse queryParam(Object value) { - this.mavContainer.addAttribute(value); - return this; - } - - /** - * Add a flash attribute to save and make available in the model of the - * target controller method after the redirect. - */ - public RedirectResponse flashAttribute(String name, Object value) { - getFlashMap().put(name, value); - return this; - } - - /** - * Add a flash attribute to save and make available in the model of the - * target controller method after the redirect. - * The name of the attribute is selected using a - * {@link org.springframework.core.Conventions#getVariableName generated name}. - */ - public RedirectResponse flashAttribute(Object attributeValue) { - Assert.notNull(attributeValue, "Model object must not be null"); - if (attributeValue instanceof Collection && ((Collection) attributeValue).isEmpty()) { - return this; - } - return flashAttribute(Conventions.getVariableName(attributeValue), attributeValue); - } - - private FlashMap getFlashMap() { - HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); - return RequestContextUtils.getFlashMap(servletRequest); - } - -} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/support/ResponseContext.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/support/ResponseContext.java deleted file mode 100644 index ff2ba35f31..0000000000 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/support/ResponseContext.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2002-2011 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.servlet.mvc.method.support; - -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.ModelAndViewContainer; - -/** - * Provides annotated controllers with convenience methods for setting up view - * resolution. - * - * @author Keith Donald - * @author Rossen Stoyanchev - * - * @since 3.1 - */ -public class ResponseContext { - - private final NativeWebRequest webRequest; - - private final ModelAndViewContainer mavContainer; - - public ResponseContext(NativeWebRequest webRequest, ModelAndViewContainer mavContainer) { - this.webRequest = webRequest; - this.mavContainer = mavContainer; - } - - /** - * Set up view resolution based on the given view name and the implicit model - * of the current request. - */ - public ViewResponse view(String viewName) { - this.mavContainer.setViewName(viewName); - return new ViewResponse(this.mavContainer); - } - - /** - * Set up view resolution for a redirect. This method clears the implicit - * model. Use convenience methods on the returned {@link RedirectResponse} - * instance to add URI variables, query parameters, and flash attributes - * as necessary. - * @param redirectUri a URI template either relative to the current URL or - * absolute; do not prefix with "redirect:" - */ - public RedirectResponse redirect(String redirectUri) { - if (!redirectUri.startsWith("redirect:")) { - redirectUri = "redirect:" + redirectUri; - } - this.mavContainer.getModel().clear(); - this.mavContainer.setViewName(redirectUri); - return new RedirectResponse(this.webRequest, this.mavContainer); - } - -} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/support/ViewResponse.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/support/ViewResponse.java deleted file mode 100644 index 140abd6c66..0000000000 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/support/ViewResponse.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2002-2011 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.servlet.mvc.method.support; - -import org.springframework.web.method.support.ModelAndViewContainer; - -/** - * Provides annotated controller methods with convenience methods for setting - * up a response with a view name that does not have the "redirect:" prefix . - * - *

An instance of this class is obtained via {@link ResponseContext#view}. - * - * @author Keith Donald - * @author Rossen Stoyanchev - * - * @since 3.1 - */ -public class ViewResponse { - - private final ModelAndViewContainer mavContainer; - - ViewResponse(ModelAndViewContainer mavContainer) { - this.mavContainer = mavContainer; - } - - public ViewResponse attribute(String attributeName, Object attributeValue) { - this.mavContainer.addAttribute(attributeName, attributeValue); - return this; - } - - public ViewResponse attribute(Object attributeValue) { - this.mavContainer.addAttribute(attributeValue); - return this; - } - -} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/RedirectModel.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/RedirectModel.java new file mode 100644 index 0000000000..209e959191 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/support/RedirectModel.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2011 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.servlet.mvc.support; + +import java.beans.PropertyEditor; +import java.util.Collection; +import java.util.Map; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.format.Formatter; +import org.springframework.ui.ExtendedModelMap; +import org.springframework.ui.Model; +import org.springframework.ui.ModelMap; +import org.springframework.util.Assert; +import org.springframework.validation.DataBinder; + +/** + * A {@link Model} implementation that controllers can use when they wish to + * redirect. For a redirect a controller needs to use an empty model and + * only add those attributes that will be used in the redirect URL -- + * either embedded as URI template variables or appended as query parameters. + * To be used in the URL such attributes need to be formatted as String + * values. Alternatively a controller may choose to keep attributes in + * flash storage instead for the duration of the redirect. + * + *

A RedirectModel serves the above needs as follows: + *

+ * + *

Note that a RedirectModel will not be used unless the controller decides + * to redirect. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +@SuppressWarnings("serial") +public class RedirectModel extends ExtendedModelMap { + + private final DataBinder dataBinder; + + private final ModelMap flashAttributes = new ModelMap(); + + private final ModelMap implicitModel; + + /** + * Create a new instance without a DataBinder. Attribute values will be + * formatted via {@link #toString()}. + */ + public RedirectModel() { + this(null, null); + } + + /** + * Create a new instance providing a DataBinder to use for formatting + * attribute values. + * @param dataBinder a DataBinder for converting attribute values to String. + * @param implicitModel the implicit model for the current request; + * used in conjunction with {@link #addModelAttributes(String...)} + * to copy attributes from the implicit model to the redirect model. + */ + public RedirectModel(DataBinder dataBinder, ModelMap implicitModel) { + this.dataBinder = dataBinder; + this.implicitModel = implicitModel; + } + + /** + * Return the attributes candidate for flash storage. + */ + public Map getFlashAttributes() { + return flashAttributes; + } + + /** + * Add an attribute. Before being added the attribute value is formatted as + * a String in preparation for use in the redirect URL. If the attribute + * value is null it is not be added to the model. + * @param attributeName the attribute name; never null + * @param attributeValue the attribute value; skipped if null + */ + public RedirectModel addAttribute(String attributeName, Object attributeValue) { + if (attributeValue != null) { + super.addAttribute(attributeName, formatValue(attributeValue)); + } + return this; + } + + private String formatValue(Object value) { + return (dataBinder != null) ? dataBinder.convertIfNecessary(value, String.class) : value.toString(); + } + + /** + * Add an attribute using a + * {@link org.springframework.core.Conventions#getVariableName generated name}. + * Before being added the attribute value is formatted as a String. + * @param attributeValue the attribute value; never null + */ + public RedirectModel addAttribute(Object attributeValue) { + super.addAttribute(attributeValue); + return this; + } + + /** + * Copy all attributes in the supplied Collection into this + * Model using attribute name generation for each element. + * @see #addAttribute(Object) + */ + public RedirectModel addAllAttributes(Collection attributeValues) { + super.addAllAttributes(attributeValues); + return this; + } + + /** + * Copy all supplied attributes into this redirect model. + * @see #addAttribute(String, Object) + */ + public RedirectModel addAllAttributes(Map attributes) { + if (attributes != null) { + for (String key : attributes.keySet()) { + addAttribute(key, attributes.get(key)); + } + } + return this; + } + + /** + * Copy all supplied attributes into this redirect model with with existing + * attributes of the same name taking precedence (i.e. not getting replaced). + * @see #addAttribute(String, Object) + */ + public RedirectModel mergeAttributes(Map attributes) { + if (attributes != null) { + for (String key : attributes.keySet()) { + if (!containsKey(key)) { + addAttribute(key, attributes.get(key)); + } + } + } + return this; + } + + /** + * Copy the attributes specified by name from the "implicit" model of the + * current request to this redirect model instance. + * @param attributeNames the names of attributes present in the implicit model. + * attribute names are required to be present; if an attribute is present + * but is null, it is skipped + * @see #addAttribute(String, Object) + */ + public RedirectModel addModelAttributes(String... attributeNames) { + Assert.notNull(this.implicitModel, "The implicit model has not been set."); + for (String name : attributeNames) { + Assert.isTrue(this.implicitModel.containsAttribute(name), name + " not found in implicit model"); + Object value = this.implicitModel.get(name); + addAttribute(name, value); + } + return this; + } + + /** + * Add the given attribute as a candidate for flash storage. + * @param attributeName the flash attribute name; never null + * @param attributeValue the flash attribute value; may be null + */ + public RedirectModel addFlashAttribute(String attributeName, Object attributeValue) { + this.flashAttributes.addAttribute(attributeName, attributeValue); + return this; + } + + /** + * Add the given attribute value as a candidate for flash storage using a + * {@link org.springframework.core.Conventions#getVariableName generated name}. + * @param attributeValue the flash attribute value; never null + */ + public RedirectModel addFlashAttribute(Object attributeValue) { + this.flashAttributes.addAttribute(attributeValue); + return this; + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index e1de3dfa8b..266934a81b 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -138,7 +138,7 @@ import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.mvc.annotation.ModelAndViewResolver; import org.springframework.web.servlet.mvc.method.annotation.support.ServletWebArgumentResolverAdapter; -import org.springframework.web.servlet.mvc.method.support.ResponseContext; +import org.springframework.web.servlet.mvc.support.RedirectModel; import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.servlet.view.InternalResourceViewResolver; @@ -2816,13 +2816,14 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl } @RequestMapping(value = "/messages", method = RequestMethod.POST) - public void sendMessage(TestBean testBean, BindingResult result, ResponseContext responseContext) { + public String sendMessage(TestBean testBean, BindingResult result, RedirectModel redirectModel) { if (result.hasErrors()) { - responseContext.view("messages/new"); + return "messages/new"; } else { - responseContext.redirect("/messages/{id}").uriVariable("id", "1").queryParam("name", "value") - .flashAttribute("successMessage", "yay!"); + redirectModel.addAttribute("id", "1").addAttribute("name", "value"); + redirectModel.addFlashAttribute("successMessage", "yay!"); + return "redirect:/messages/{id}"; } } } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandlerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandlerTests.java index f760024a1a..e6e77e808b 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandlerTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/support/ViewMethodReturnValueHandlerTests.java @@ -29,7 +29,9 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.View; +import org.springframework.web.servlet.mvc.support.RedirectModel; import org.springframework.web.servlet.view.InternalResourceView; +import org.springframework.web.servlet.view.RedirectView; /** * Test fixture with {@link DefaultMethodReturnValueHandler}. @@ -61,15 +63,38 @@ public class ViewMethodReturnValueHandlerTests { public void returnView() throws Exception { InternalResourceView view = new InternalResourceView("testView"); handler.handleReturnValue(view, createMethodParam("view"), mavContainer, webRequest); + assertSame(view, mavContainer.getView()); } + + @Test + public void returnViewRedirect() throws Exception { + RedirectView redirectView = new RedirectView("testView"); + RedirectModel redirectModel = new RedirectModel(); + mavContainer.setRedirectModel(redirectModel); + handler.handleReturnValue(redirectView, createMethodParam("view"), mavContainer, webRequest); + + assertSame(redirectView, mavContainer.getView()); + assertSame("Should have switched to the RedirectModel", redirectModel, mavContainer.getModel()); + } @Test public void returnViewName() throws Exception { handler.handleReturnValue("testView", createMethodParam("viewName"), mavContainer, webRequest); + assertEquals("testView", mavContainer.getViewName()); } + @Test + public void returnViewNameRedirect() throws Exception { + RedirectModel redirectModel = new RedirectModel(); + mavContainer.setRedirectModel(redirectModel); + handler.handleReturnValue("redirect:testView", createMethodParam("viewName"), mavContainer, webRequest); + + assertEquals("redirect:testView", mavContainer.getViewName()); + assertSame("Should have switched to the RedirectModel", redirectModel, mavContainer.getModel()); + } + private MethodParameter createMethodParam(String methodName) throws Exception { Method method = getClass().getDeclaredMethod(methodName); return new MethodParameter(method, -1); diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/support/RedirectModelTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/support/RedirectModelTests.java new file mode 100644 index 0000000000..06b8e363ad --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/support/RedirectModelTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2011 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.servlet.mvc.support; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.TestBean; +import org.springframework.core.convert.converter.Converter; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.ui.ModelMap; +import org.springframework.validation.DataBinder; + +/** + * + * Test fixture for RedirectModel tests. + * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class RedirectModelTests { + + private RedirectModel redirectModel; + + private FormattingConversionService conversionService; + + private ModelMap implicitModel; + + @Before + public void setup() { + this.conversionService = new DefaultFormattingConversionService(); + DataBinder dataBinder = new DataBinder(null); + dataBinder.setConversionService(conversionService); + + this.implicitModel = new ModelMap(); + this.redirectModel = new RedirectModel(dataBinder, implicitModel); + } + + @Test + public void addAttributePrimitiveType() { + this.redirectModel.addAttribute("speed", 65); + assertEquals("65", this.redirectModel.get("speed")); + } + + @Test + public void addAttributeCustomType() { + String attrName = "person"; + this.redirectModel.addAttribute(attrName, new TestBean("Fred")); + + assertEquals("ConversionService should have invoked toString()", "Fred", this.redirectModel.get(attrName)); + + this.conversionService.addConverter(new TestBeanConverter()); + this.redirectModel.addAttribute(attrName, new TestBean("Fred")); + + assertEquals("Type converter should have been used", "[Fred]", this.redirectModel.get(attrName)); + } + + @Test + public void addAttributeToString() { + String attrName = "person"; + RedirectModel model = new RedirectModel(); + model.addAttribute(attrName, new TestBean("Fred")); + + assertEquals("toString() should have been used", "Fred", model.get(attrName)); + } + + @Test + public void addAttributeValue() { + this.redirectModel.addAttribute(new TestBean("Fred")); + + assertEquals("Fred", this.redirectModel.get("testBean")); + } + + @Test + public void addAllAttributesList() { + this.redirectModel.addAllAttributes(Arrays.asList(new TestBean("Fred"), new Integer(5))); + + assertEquals("Fred", this.redirectModel.get("testBean")); + assertEquals("5", this.redirectModel.get("integer")); + } + + @Test + public void addAttributesMap() { + Map map = new HashMap(); + map.put("person", new TestBean("Fred")); + map.put("age", 33); + this.redirectModel.addAllAttributes(map); + + assertEquals("Fred", this.redirectModel.get("person")); + assertEquals("33", this.redirectModel.get("age")); + } + + @Test + public void mergeAttributes() { + Map map = new HashMap(); + map.put("person", new TestBean("Fred")); + map.put("age", 33); + + this.redirectModel.addAttribute("person", new TestBean("Ralph")); + this.redirectModel.mergeAttributes(map); + + assertEquals("Ralph", this.redirectModel.get("person")); + assertEquals("33", this.redirectModel.get("age")); + } + + @Test + public void addModelAttributes() { + this.implicitModel.addAttribute("person", new TestBean("Ralph")); + this.implicitModel.addAttribute("age", 33); + + this.redirectModel.addModelAttributes("person", "age"); + + assertEquals("Ralph", this.redirectModel.get("person")); + assertEquals("33", this.redirectModel.get("age")); + } + + @Test(expected=IllegalArgumentException.class) + public void addModelAttributesInvalidName() { + this.redirectModel.addModelAttributes("person"); + } + + public static class TestBeanConverter implements Converter { + + public String convert(TestBean source) { + return "[" + source.getName() + "]"; + } + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java index b96c654998..19fc71d3cf 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java +++ b/org.springframework.web/src/main/java/org/springframework/web/method/annotation/SessionAttributesHandler.java @@ -24,7 +24,6 @@ import java.util.Map; import java.util.Set; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.support.SessionAttributeStore; @@ -35,11 +34,12 @@ import org.springframework.web.context.request.WebRequest; * Manages handler-specific session attributes declared via @{@link SessionAttributes}. * Actual storage is performed through an instance of {@link SessionAttributeStore}. * - *

A typical scenario begins with a controller adding attributes to the {@link Model}. - * At the end of the request, model attributes are checked to see if any of them match - * the names and types declared via @{@link SessionAttributes}. Matching model - * attributes are "promoted" to the session and remain there until the controller - * calls {@link SessionStatus#setComplete()} to indicate the session attributes are + *

A typical scenario begins with a controller adding attributes to the + * {@link org.springframework.ui.Model Model}. At the end of the request, model + * attributes are checked to see if any of them match the names and types declared + * via @{@link SessionAttributes}. Matching model attributes are "promoted" to + * the session and remain there until the controller calls + * {@link SessionStatus#setComplete()} to indicate the session attributes are * no longer needed and can be removed. * * @author Rossen Stoyanchev diff --git a/org.springframework.web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java b/org.springframework.web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java index 999fb73d7c..c1ce67059e 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java +++ b/org.springframework.web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java @@ -35,9 +35,13 @@ public class ModelAndViewContainer { private Object view; private boolean resolveView = true; - - private final ModelMap model = new BindingAwareModelMap(); + private final ModelMap model = new BindingAwareModelMap(); + + private ModelMap redirectModel; + + private boolean useRedirectModel = false; + /** * Create a new instance. */ @@ -104,10 +108,35 @@ public class ModelAndViewContainer { } /** - * Return the underlying {@code ModelMap} instance, never {@code null}. + * Return the internal model currently in use. Normally this is the + * implicit model created in the constructor of this class. + * However, when a redirect model is provided via {@link #setRedirectModel} + * and then enabled for use via {@link #useRedirectModel()}, this method + * returns the redirect model instead. + * + * @return the internal model currently in use, never {@code null}. */ public ModelMap getModel() { - return this.model; + return this.useRedirectModel ? this.redirectModel : this.model; + } + + /** + * Provide a model that may be used in case of a redirect. + * If {@link #useRedirectModel()} is called at any time after this method is + * called, the ModelAndViewContainer will switch to using the redirect model + * for all operations. Until then, the redirect model is not used. + */ + public void setRedirectModel(ModelMap redirectModel) { + this.redirectModel = redirectModel; + } + + /** + * Make a switch internally from using the implicit model to using the + * redirect model provided earlier via {@link #setRedirectModel(ModelMap)}. + * If the redirect model was never set, this method has no effect. + */ + public void useRedirectModel() { + this.useRedirectModel = this.redirectModel != null; } /** @@ -115,7 +144,7 @@ public class ModelAndViewContainer { * @see ModelMap#addAttribute(String, Object) */ public ModelAndViewContainer addAttribute(String name, Object value) { - this.model.addAttribute(name, value); + getModel().addAttribute(name, value); return this; } @@ -124,7 +153,7 @@ public class ModelAndViewContainer { * @see Model#addAttribute(Object) */ public ModelAndViewContainer addAttribute(Object value) { - this.model.addAttribute(value); + getModel().addAttribute(value); return this; } @@ -133,7 +162,7 @@ public class ModelAndViewContainer { * @see ModelMap#addAllAttributes(Map) */ public ModelAndViewContainer addAllAttributes(Map attributes) { - this.model.addAllAttributes(attributes); + getModel().addAllAttributes(attributes); return this; } @@ -143,7 +172,7 @@ public class ModelAndViewContainer { * @see ModelMap#mergeAttributes(Map) */ public ModelAndViewContainer mergeAttributes(Map attributes) { - this.model.mergeAttributes(attributes); + getModel().mergeAttributes(attributes); return this; } @@ -152,7 +181,7 @@ public class ModelAndViewContainer { * @see ModelMap#containsAttribute(String) */ public boolean containsAttribute(String name) { - return this.model.containsAttribute(name); + return getModel().containsAttribute(name); } - + } diff --git a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java index fa18de22c9..f535cc6377 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java +++ b/org.springframework.web/src/test/java/org/springframework/web/method/annotation/InitBinderDataBinderFactoryTests.java @@ -74,7 +74,7 @@ public class InitBinderDataBinderFactoryTests { @Test public void createBinderWithGlobalInitialization() throws Exception { ConversionService conversionService = new DefaultFormattingConversionService(); - bindingInitializer.setConversionService(conversionService ); + bindingInitializer.setConversionService(conversionService); WebDataBinderFactory factory = createBinderFactory("initBinder", WebDataBinder.class); WebDataBinder dataBinder = factory.createBinder(webRequest, null, null);