Improve FreeMarker macro support in spring-webflux

This commit improves the FreeMarker macro support in spring-webflux by
automatically exposing a RequestContext under the name
"springMacroRequestContext" for use with Spring's FreeMarker macros in
spring.ftl.

This aligns with the user experience for FreeMarkerView in spring-webmvc.

Closes gh-23105
This commit is contained in:
Sam Brannen
2019-06-12 16:40:51 +03:00
parent c99b7c1c90
commit cc4593fda8
3 changed files with 71 additions and 13 deletions

View File

@@ -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;
* <p>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 &mdash; 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}.
* <p>Default is {@code true}.
* <p>Needed for Spring's FreeMarker default macros. Note that this is
* <i>not</i> required for templates that use HTML forms <i>unless</i> 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<Map<String, Object>> getModelAttributes(Map<String, ?> 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<String, Object> 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<Void> renderInternal(Map<String, Object> renderAttributes,
@Nullable MediaType contentType, ServerWebExchange exchange) {

View File

@@ -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();

View File

@@ -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");