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:
@@ -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 — 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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user