diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java index 52a0c4e3a4..f2b5cb3ef8 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.Charset; +import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -47,6 +48,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.web.reactive.result.view.AbstractUrlBasedView; +import org.springframework.web.reactive.result.view.RequestContext; import org.springframework.web.server.ServerWebExchange; /** @@ -64,16 +66,30 @@ import org.springframework.web.server.ServerWebExchange; *

Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. * * @author Rossen Stoyanchev + * @author Sam Brannen * @since 5.0 */ public class FreeMarkerView extends AbstractUrlBasedView { + /** + * Attribute name of the {@link RequestContext} instance in the template model, + * available to Spring's macros — for example, for creating + * {@link org.springframework.web.reactive.result.view.BindStatus BindStatus} + * objects. + * @since 5.2 + * @see #setExposeSpringMacroHelpers(boolean) + */ + public static final String SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE = "springMacroRequestContext"; + + @Nullable private Configuration configuration; @Nullable private String encoding; + private boolean exposeSpringMacroHelpers = true; + /** * Set the FreeMarker Configuration to be used by this view. @@ -124,6 +140,19 @@ public class FreeMarkerView extends AbstractUrlBasedView { return this.encoding; } + /** + * Set whether to expose a {@link RequestContext} for use by Spring's macro + * library, under the name {@value #SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE}. + *

Default is {@code true}. + *

Needed for Spring's FreeMarker default macros. Note that this is + * not required for templates that use HTML forms unless you + * wish to take advantage of the Spring helper macros. + * @see #SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + */ + public void setExposeSpringMacroHelpers(boolean exposeSpringMacroHelpers) { + this.exposeSpringMacroHelpers = exposeSpringMacroHelpers; + } + @Override public void afterPropertiesSet() throws Exception { @@ -180,6 +209,34 @@ public class FreeMarkerView extends AbstractUrlBasedView { } } + /** + * Prepare the model to use for rendering by potentially exposing a + * {@link RequestContext} for use in Spring FreeMarker macros and then + * delegating to the inherited implementation of this method. + * @since 5.2 + * @see #setExposeSpringMacroHelpers(boolean) + * @see org.springframework.web.reactive.result.view.AbstractView#getModelAttributes(Map, ServerWebExchange) + */ + @Override + protected Mono> getModelAttributes(Map model, + ServerWebExchange exchange) { + + if (this.exposeSpringMacroHelpers) { + if (model.containsKey(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE)) { + throw new IllegalStateException( + "Cannot expose bind macro helper '" + SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE + + "' because of an existing model object of the same name"); + } + // Make a defensive copy of the model. + Map attributes = new HashMap<>(model); + // Expose RequestContext instance for Spring macros. + attributes.put(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE, new RequestContext( + exchange, attributes, obtainApplicationContext(), getRequestDataValueProcessor())); + return super.getModelAttributes(attributes, exchange); + } + return super.getModelAttributes(model, exchange); + } + @Override protected Mono renderInternal(Map renderAttributes, @Nullable MediaType contentType, ServerWebExchange exchange) { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java index 327bd3d941..0f02c4eb00 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java @@ -40,7 +40,6 @@ import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; -import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.reactive.result.view.BindStatus; import org.springframework.web.reactive.result.view.DummyMacroRequestContext; import org.springframework.web.reactive.result.view.RequestContext; @@ -59,23 +58,25 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class FreeMarkerMacroTests { - private static final String SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE = "springMacroRequestContext"; - private static final String TEMPLATE_FILE = "test-macro.ftl"; private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path")); + private final GenericApplicationContext applicationContext = new GenericApplicationContext(); + private Configuration freeMarkerConfig; @Before public void setUp() throws Exception { + this.applicationContext.refresh(); + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); configurer.setTemplateLoaderPaths("classpath:/", "file://" + System.getProperty("java.io.tmpdir")); this.freeMarkerConfig = configurer.createConfiguration(); } @Test - public void exposeRequestContextAsModelAttribute() throws Exception { + public void springMacroRequestContextIsAutomaticallyExposedAsModelAttribute() throws Exception { storeTemplateInTempDir("<@spring.bind \"testBean.name\"/>\nHi ${spring.status.value}"); FreeMarkerView view = new FreeMarkerView() { @@ -94,11 +95,7 @@ public class FreeMarkerMacroTests { } }; - StaticWebApplicationContext wac = new StaticWebApplicationContext(); - wac.refresh(); - - view.setApplicationContext(wac); - view.setRequestContextAttribute(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE); + view.setApplicationContext(this.applicationContext); view.setBeanName("myView"); view.setUrl("tmp.ftl"); view.setConfiguration(this.freeMarkerConfig); @@ -314,19 +311,21 @@ public class FreeMarkerMacroTests { ModelMap model = new ExtendedModelMap(); DummyMacroRequestContext rc = new DummyMacroRequestContext(this.exchange, model, - new GenericApplicationContext()); + this.applicationContext); rc.setMessageMap(msgMap); rc.setContextPath("/springtest"); model.put("command", darren); - model.put("springMacroRequestContext", rc); + model.put(FreeMarkerView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE, rc); model.put("msgArgs", new Object[] { "World" }); model.put("nameOptionMap", names); model.put("options", names.values()); FreeMarkerView view = new FreeMarkerView(); + view.setApplicationContext(this.applicationContext); view.setBeanName("myView"); view.setUrl("tmp.ftl"); + view.setExposeSpringMacroHelpers(false); view.setConfiguration(freeMarkerConfig); view.render(model, null, this.exchange).subscribe(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java index e8cd4e902e..e542714f12 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java @@ -46,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException /** * @author Rossen Stoyanchev + * @author Sam Brannen */ public class FreeMarkerViewTests { @@ -56,14 +57,13 @@ public class FreeMarkerViewTests { private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path")); - private GenericApplicationContext context; + private final GenericApplicationContext context = new GenericApplicationContext(); private Configuration freeMarkerConfig; @Before public void setup() throws Exception { - this.context = new GenericApplicationContext(); this.context.refresh(); FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); @@ -104,6 +104,7 @@ public class FreeMarkerViewTests { @Test public void render() { FreeMarkerView view = new FreeMarkerView(); + view.setApplicationContext(this.context); view.setConfiguration(this.freeMarkerConfig); view.setUrl("test.ftl"); @@ -126,6 +127,7 @@ public class FreeMarkerViewTests { new AcceptHeaderLocaleContextResolver()); FreeMarkerView view = new FreeMarkerView(); + view.setApplicationContext(this.context); view.setConfiguration(this.freeMarkerConfig); view.setUrl("test.ftl");