diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandler.java index 11dfd8fcdd..5968894287 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandler.java @@ -18,8 +18,12 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.util.Collection; +import jakarta.servlet.http.HttpServletResponse; + import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.PatternMatchUtils; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -96,6 +100,13 @@ public class ModelAndViewMethodReturnValueHandler implements HandlerMethodReturn } if (returnValue instanceof FragmentsRendering rendering) { + mavContainer.setStatus(rendering.status()); + HttpHeaders headers = rendering.headers(); + if (!headers.isEmpty()) { + HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); + Assert.state(response != null, "No HttpServletResponse"); + headers.forEach((name, values) -> values.forEach(value -> response.addHeader(name, value))); + } mavContainer.setView(rendering); return; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultFragmentsRendering.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultFragmentsRendering.java index b7c8bf5942..a17adc76ee 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultFragmentsRendering.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultFragmentsRendering.java @@ -29,6 +29,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponseWrapper; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.servlet.ModelAndView; @@ -44,14 +46,34 @@ import org.springframework.web.servlet.ViewResolver; */ final class DefaultFragmentsRendering implements FragmentsRendering { + @Nullable + private final HttpStatusCode status; + + private final HttpHeaders headers; + private final Collection modelAndViews; - DefaultFragmentsRendering(Collection modelAndViews) { - this.modelAndViews = new ArrayList<>(modelAndViews); + DefaultFragmentsRendering( + @Nullable HttpStatusCode status, HttpHeaders headers, Collection fragments) { + + this.status = status; + this.headers = headers; + this.modelAndViews = new ArrayList<>(fragments); } + @Nullable + @Override + public HttpStatusCode status() { + return this.status; + } + + @Override + public HttpHeaders headers() { + return this.headers; + } + @Override public boolean isRedirectView() { return false; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultFragmentsRenderingBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultFragmentsRenderingBuilder.java index afaa777027..8eb8b1b672 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultFragmentsRenderingBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultFragmentsRenderingBuilder.java @@ -17,9 +17,14 @@ package org.springframework.web.servlet.view; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Map; +import java.util.function.Consumer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.lang.Nullable; import org.springframework.web.servlet.ModelAndView; /** @@ -31,9 +36,40 @@ import org.springframework.web.servlet.ModelAndView; */ final class DefaultFragmentsRenderingBuilder implements FragmentsRendering.Builder { + @Nullable + private HttpStatusCode status; + + @Nullable + private HttpHeaders headers; + private final Collection fragments = new ArrayList<>(); + @Override + public FragmentsRendering.Builder status(HttpStatusCode status) { + this.status = status; + return this; + } + + @Override + public FragmentsRendering.Builder header(String headerName, String... headerValues) { + initHeaders().put(headerName, Arrays.asList(headerValues)); + return this; + } + + @Override + public FragmentsRendering.Builder headers(Consumer headersConsumer) { + headersConsumer.accept(initHeaders()); + return this; + } + + private HttpHeaders initHeaders() { + if (this.headers == null) { + this.headers = new HttpHeaders(); + } + return this.headers; + } + @Override public DefaultFragmentsRenderingBuilder fragment(String viewName, Map model) { return fragment(new ModelAndView(viewName, model)); @@ -58,7 +94,8 @@ final class DefaultFragmentsRenderingBuilder implements FragmentsRendering.Build @Override public FragmentsRendering build() { - return new DefaultFragmentsRendering(this.fragments); + return new DefaultFragmentsRendering( + this.status, (this.headers != null ? this.headers : HttpHeaders.EMPTY), this.fragments); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/FragmentsRendering.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/FragmentsRendering.java index 9affca28aa..743a1867d0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/FragmentsRendering.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/FragmentsRendering.java @@ -18,7 +18,11 @@ package org.springframework.web.servlet.view; import java.util.Collection; import java.util.Map; +import java.util.function.Consumer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.lang.Nullable; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.SmartView; @@ -34,6 +38,17 @@ import org.springframework.web.servlet.SmartView; */ public interface FragmentsRendering extends SmartView { + /** + * Return the HTTP status to set the response to. + */ + @Nullable + HttpStatusCode status(); + + /** + * Return headers to add to the response. + */ + HttpHeaders headers(); + /** * Create a builder for {@link FragmentsRendering}, adding a fragment with @@ -73,6 +88,29 @@ public interface FragmentsRendering extends SmartView { */ interface Builder { + /** + * Specify the status to use for the response. + * @param status the status to set + * @return this builder + */ + Builder status(HttpStatusCode status); + + /** + * Add the given, single header value under the given name. + * @param headerName the header name + * @param headerValues the header value(s) + * @return this builder + */ + Builder header(String headerName, String... headerValues); + + /** + * Provides access to every header declared so far with the possibility + * to add, replace, or remove values. + * @param headersConsumer the consumer to provide access to + * @return this builder + */ + Builder headers(Consumer headersConsumer); + /** * Add a fragment with a view name and a model. * @param viewName the name of the view for the fragment diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandlerTests.java index 269f76d47d..0b89f6200b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandlerTests.java @@ -33,6 +33,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; import org.springframework.web.servlet.view.FragmentsRendering; import org.springframework.web.servlet.view.RedirectView; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import static org.assertj.core.api.Assertions.assertThat; @@ -56,7 +57,7 @@ class ModelAndViewMethodReturnValueHandlerTests { void setup() throws Exception { this.handler = new ModelAndViewMethodReturnValueHandler(); this.mavContainer = new ModelAndViewContainer(); - this.webRequest = new ServletWebRequest(new MockHttpServletRequest()); + this.webRequest = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse()); this.returnParamModelAndView = getReturnValueParam("modelAndView"); } @@ -90,10 +91,13 @@ class ModelAndViewMethodReturnValueHandlerTests { @Test void handleFragmentsRendering() throws Exception { - FragmentsRendering rendering = FragmentsRendering.with("viewName").build(); + FragmentsRendering rendering = FragmentsRendering.with("viewName") + .header("headerName", "headerValue") + .build(); handler.handleReturnValue(rendering, returnParamModelAndView, mavContainer, webRequest); assertThat(mavContainer.getView()).isInstanceOf(SmartView.class); + assertThat(this.webRequest.getResponse().getHeader("headerName")).isEqualTo("headerValue"); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultFragmentsRenderingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultFragmentsRenderingTests.java index 3d50e270e3..250f895877 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultFragmentsRenderingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultFragmentsRenderingTests.java @@ -17,7 +17,6 @@ package org.springframework.web.servlet.view; import java.util.Collections; -import java.util.List; import java.util.Locale; import java.util.Map; @@ -28,7 +27,6 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ResourceBundleMessageSource; -import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.script.ScriptTemplateConfigurer; import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; @@ -59,9 +57,9 @@ public class DefaultFragmentsRenderingTests { MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); - DefaultFragmentsRendering view = new DefaultFragmentsRendering(List.of( - new ModelAndView("fragment1", Map.of("foo", "Foo")), - new ModelAndView("fragment2", Map.of("bar", "Bar")))); + FragmentsRendering view = FragmentsRendering.with("fragment1", Map.of("foo", "Foo")) + .fragment("fragment2", Map.of("bar", "Bar")) + .build(); view.resolveNestedViews(viewResolver, Locale.ENGLISH); view.render(Collections.emptyMap(), request, response);