Skip to content

Commit

Permalink
Support flash attributes on ResponseEntity redirect
Browse files Browse the repository at this point in the history
Issue: SPR-15176
  • Loading branch information
rstoyanchev committed Feb 21, 2017
1 parent ba97432 commit 5ea5f81
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 35 deletions.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
Expand All @@ -23,6 +23,8 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
Expand All @@ -34,14 +36,18 @@
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.support.RequestContextUtils;

/**
* Resolves {@link HttpEntity} and {@link RequestEntity} method argument values
Expand Down Expand Up @@ -197,6 +203,12 @@ public void handleReturnValue(Object returnValue, MethodParameter returnType,
return;
}
}
else if (returnStatus / 100 == 3) {
String location = outputHeaders.getFirst("location");
if (location != null) {
saveFlashAttributes(mavContainer, webRequest, location);
}
}
}

// Try even with null body. ResponseBodyAdvice could get involved.
Expand Down Expand Up @@ -241,6 +253,20 @@ private boolean isResourceNotModified(ServletServerHttpRequest inputMessage, Ser
return servletWebRequest.checkNotModified(etag, lastModifiedTimestamp);
}

private void saveFlashAttributes(ModelAndViewContainer mav, NativeWebRequest request, String location) {
mav.setRedirectModelScenario(true);
ModelMap model = mav.getModel();
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
if (!CollectionUtils.isEmpty(flashAttributes)) {
HttpServletRequest req = request.getNativeRequest(HttpServletRequest.class);
HttpServletResponse res = request.getNativeRequest(HttpServletResponse.class);
RequestContextUtils.getOutputFlashMap(req).putAll(flashAttributes);
RequestContextUtils.saveOutputFlashMap(location, req, res);
}
}
}

@Override
protected Class<?> getReturnValueType(Object returnValue, MethodParameter returnType) {
if (returnValue != null) {
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
Expand All @@ -22,11 +22,14 @@
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.TimeZoneAwareLocaleContext;
import org.springframework.ui.context.Theme;
import org.springframework.ui.context.ThemeSource;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
Expand All @@ -36,6 +39,8 @@
import org.springframework.web.servlet.LocaleContextResolver;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.ThemeResolver;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

/**
* Utility class for easy access to request-specific state which has been
Expand Down Expand Up @@ -209,9 +214,8 @@ public static Theme getTheme(HttpServletRequest request) {
}

/**
* Return a read-only {@link Map} with "input" flash attributes saved on a
* previous request.
* @param request the current request
* Return read-only "input" flash attributes from request before redirect.
* @param request current request
* @return a read-only Map, or {@code null} if not found
* @see FlashMap
*/
Expand All @@ -221,23 +225,52 @@ public static Theme getTheme(HttpServletRequest request) {
}

/**
* Return the "output" FlashMap with attributes to save for a subsequent request.
* @param request the current request
* @return a {@link FlashMap} instance (never {@code null} within a DispatcherServlet request)
* @see FlashMap
* Return "output" FlashMap to save attributes for request after redirect.
* @param request current request
* @return a {@link FlashMap} instance, never {@code null} within a
* {@code DispatcherServlet}-handled request
*/
public static FlashMap getOutputFlashMap(HttpServletRequest request) {
return (FlashMap) request.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE);
}

/**
* Return the FlashMapManager instance to save flash attributes with
* before a redirect.
* Return the {@code FlashMapManager} instance to save flash attributes.
* <p>As of 5.0 the convenience method {@link #saveOutputFlashMap} may be
* used to save the "output" FlashMap.
* @param request the current request
* @return a {@link FlashMapManager} instance (never {@code null} within a DispatcherServlet request)
* @return a {@link FlashMapManager} instance, never {@code null} within a
* {@code DispatcherServlet}-handled request
*/
public static FlashMapManager getFlashMapManager(HttpServletRequest request) {
return (FlashMapManager) request.getAttribute(DispatcherServlet.FLASH_MAP_MANAGER_ATTRIBUTE);
}

/**
* Convenience method that retrieves the {@link #getOutputFlashMap "output"
* FlashMap}, updates it with the path and query params of the target URL,
* and then saves it using the {@link #getFlashMapManager FlashMapManager}.
*
* @param location the target URL for the redirect
* @param request the current request
* @param response the current response
* @since 5.0
*/
public static void saveOutputFlashMap(String location, HttpServletRequest request,
HttpServletResponse response) {

FlashMap flashMap = getOutputFlashMap(request);
if (CollectionUtils.isEmpty(flashMap)) {
return;
}

UriComponents uriComponents = UriComponentsBuilder.fromUriString(location).build();
flashMap.setTargetRequestPath(uriComponents.getPath());
flashMap.addTargetRequestParams(uriComponents.getQueryParams());

FlashMapManager manager = getFlashMapManager(request);
Assert.state(manager != null, "No FlashMapManager. Is this a DispatcherServlet handled request?");
manager.saveOutputFlashMap(flashMap, request, response);
}

}
Expand Up @@ -33,18 +33,14 @@

import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.FlashMapManager;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.SmartView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.support.RequestDataValueProcessor;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.WebUtils;
Expand Down Expand Up @@ -305,18 +301,10 @@ protected void renderMergedOutputModel(Map<String, Object> model, HttpServletReq
String targetUrl = createTargetUrl(model, request);
targetUrl = updateTargetUrl(targetUrl, model, request, response);

FlashMap flashMap = RequestContextUtils.getOutputFlashMap(request);
if (!CollectionUtils.isEmpty(flashMap)) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(targetUrl).build();
flashMap.setTargetRequestPath(uriComponents.getPath());
flashMap.addTargetRequestParams(uriComponents.getQueryParams());
FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request);
if (flashMapManager == null) {
throw new IllegalStateException("FlashMapManager not found despite output FlashMap having been set");
}
flashMapManager.saveOutputFlashMap(flashMap, request, response);
}
// Save flash attributes
RequestContextUtils.saveOutputFlashMap(targetUrl, request, response);

// Redirect
sendRedirect(request, response, targetUrl, this.http10Compatible);
}

Expand Down
Expand Up @@ -109,9 +109,11 @@
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down Expand Up @@ -1569,6 +1571,32 @@ public void redirectAttribute() throws Exception {
assertTrue(RequestContextUtils.getOutputFlashMap(request).isEmpty());
}

@Test // SPR-15176
public void flashAttributesWithResponseEntity() throws Exception {
initServletWithControllers(RedirectAttributesController.class);

MockHttpServletRequest request = new MockHttpServletRequest("POST", "/messages-response-entity");
MockHttpServletResponse response = new MockHttpServletResponse();
HttpSession session = request.getSession();

getServlet().service(request, response);

assertEquals(302, response.getStatus());
assertEquals("/messages/1?name=value", response.getRedirectedUrl());
assertEquals("yay!", RequestContextUtils.getOutputFlashMap(request).get("successMessage"));

// GET after POST
request = new MockHttpServletRequest("GET", "/messages/1");
request.setQueryString("name=value");
request.setSession(session);
response = new MockHttpServletResponse();
getServlet().service(request, response);

assertEquals(200, response.getStatus());
assertEquals("Got: yay!", response.getContentAsString());
assertTrue(RequestContextUtils.getOutputFlashMap(request).isEmpty());
}

@Test
public void prototypeController() throws Exception {
initServlet(new ApplicationContextInitializer<GenericWebApplicationContext>() {
Expand Down Expand Up @@ -3215,21 +3243,26 @@ public void initBinder(WebDataBinder dataBinder) {
dataBinder.setRequiredFields("name");
}

@RequestMapping(value = "/messages/{id}", method = RequestMethod.GET)
@GetMapping("/messages/{id}")
public void message(ModelMap model, Writer writer) throws IOException {
writer.write("Got: " + model.get("successMessage"));
}

@RequestMapping(value = "/messages", method = RequestMethod.POST)
public String sendMessage(TestBean testBean, BindingResult result, RedirectAttributes redirectAttrs) {
@PostMapping("/messages")
public String sendMessage(TestBean testBean, BindingResult result, RedirectAttributes attributes) {
if (result.hasErrors()) {
return "messages/new";
}
else {
redirectAttrs.addAttribute("id", "1").addAttribute("name", "value")
.addFlashAttribute("successMessage", "yay!");
return "redirect:/messages/{id}";
}
attributes.addAttribute("id", "1").addAttribute("name", "value");
attributes.addFlashAttribute("successMessage", "yay!");
return "redirect:/messages/{id}";
}

@PostMapping("/messages-response-entity")
public ResponseEntity<Void> sendMessage(RedirectAttributes attributes) {
attributes.addFlashAttribute("successMessage", "yay!");
URI location = URI.create("/messages/1?name=value");
return ResponseEntity.status(HttpStatus.FOUND).location(location).build();
}
}

Expand Down

0 comments on commit 5ea5f81

Please sign in to comment.