Skip to content

Commit cc4593f

Browse files
committed
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
1 parent c99b7c1 commit cc4593f

File tree

3 files changed

+71
-13
lines changed

3 files changed

+71
-13
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.io.OutputStreamWriter;
2222
import java.io.Writer;
2323
import java.nio.charset.Charset;
24+
import java.util.HashMap;
2425
import java.util.Locale;
2526
import java.util.Map;
2627
import java.util.Optional;
@@ -47,6 +48,7 @@
4748
import org.springframework.util.Assert;
4849
import org.springframework.util.MimeType;
4950
import org.springframework.web.reactive.result.view.AbstractUrlBasedView;
51+
import org.springframework.web.reactive.result.view.RequestContext;
5052
import org.springframework.web.server.ServerWebExchange;
5153

5254
/**
@@ -64,16 +66,30 @@
6466
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
6567
*
6668
* @author Rossen Stoyanchev
69+
* @author Sam Brannen
6770
* @since 5.0
6871
*/
6972
public class FreeMarkerView extends AbstractUrlBasedView {
7073

74+
/**
75+
* Attribute name of the {@link RequestContext} instance in the template model,
76+
* available to Spring's macros &mdash; for example, for creating
77+
* {@link org.springframework.web.reactive.result.view.BindStatus BindStatus}
78+
* objects.
79+
* @since 5.2
80+
* @see #setExposeSpringMacroHelpers(boolean)
81+
*/
82+
public static final String SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE = "springMacroRequestContext";
83+
84+
7185
@Nullable
7286
private Configuration configuration;
7387

7488
@Nullable
7589
private String encoding;
7690

91+
private boolean exposeSpringMacroHelpers = true;
92+
7793

7894
/**
7995
* Set the FreeMarker Configuration to be used by this view.
@@ -124,6 +140,19 @@ protected String getEncoding() {
124140
return this.encoding;
125141
}
126142

143+
/**
144+
* Set whether to expose a {@link RequestContext} for use by Spring's macro
145+
* library, under the name {@value #SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE}.
146+
* <p>Default is {@code true}.
147+
* <p>Needed for Spring's FreeMarker default macros. Note that this is
148+
* <i>not</i> required for templates that use HTML forms <i>unless</i> you
149+
* wish to take advantage of the Spring helper macros.
150+
* @see #SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE
151+
*/
152+
public void setExposeSpringMacroHelpers(boolean exposeSpringMacroHelpers) {
153+
this.exposeSpringMacroHelpers = exposeSpringMacroHelpers;
154+
}
155+
127156

128157
@Override
129158
public void afterPropertiesSet() throws Exception {
@@ -180,6 +209,34 @@ public boolean checkResourceExists(Locale locale) throws Exception {
180209
}
181210
}
182211

212+
/**
213+
* Prepare the model to use for rendering by potentially exposing a
214+
* {@link RequestContext} for use in Spring FreeMarker macros and then
215+
* delegating to the inherited implementation of this method.
216+
* @since 5.2
217+
* @see #setExposeSpringMacroHelpers(boolean)
218+
* @see org.springframework.web.reactive.result.view.AbstractView#getModelAttributes(Map, ServerWebExchange)
219+
*/
220+
@Override
221+
protected Mono<Map<String, Object>> getModelAttributes(Map<String, ?> model,
222+
ServerWebExchange exchange) {
223+
224+
if (this.exposeSpringMacroHelpers) {
225+
if (model.containsKey(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE)) {
226+
throw new IllegalStateException(
227+
"Cannot expose bind macro helper '" + SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE +
228+
"' because of an existing model object of the same name");
229+
}
230+
// Make a defensive copy of the model.
231+
Map<String, Object> attributes = new HashMap<>(model);
232+
// Expose RequestContext instance for Spring macros.
233+
attributes.put(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE, new RequestContext(
234+
exchange, attributes, obtainApplicationContext(), getRequestDataValueProcessor()));
235+
return super.getModelAttributes(attributes, exchange);
236+
}
237+
return super.getModelAttributes(model, exchange);
238+
}
239+
183240
@Override
184241
protected Mono<Void> renderInternal(Map<String, Object> renderAttributes,
185242
@Nullable MediaType contentType, ServerWebExchange exchange) {

spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerMacroTests.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import org.springframework.ui.ModelMap;
4141
import org.springframework.util.FileCopyUtils;
4242
import org.springframework.util.StringUtils;
43-
import org.springframework.web.context.support.StaticWebApplicationContext;
4443
import org.springframework.web.reactive.result.view.BindStatus;
4544
import org.springframework.web.reactive.result.view.DummyMacroRequestContext;
4645
import org.springframework.web.reactive.result.view.RequestContext;
@@ -59,23 +58,25 @@
5958
*/
6059
public class FreeMarkerMacroTests {
6160

62-
private static final String SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE = "springMacroRequestContext";
63-
6461
private static final String TEMPLATE_FILE = "test-macro.ftl";
6562

6663
private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path"));
6764

65+
private final GenericApplicationContext applicationContext = new GenericApplicationContext();
66+
6867
private Configuration freeMarkerConfig;
6968

7069
@Before
7170
public void setUp() throws Exception {
71+
this.applicationContext.refresh();
72+
7273
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
7374
configurer.setTemplateLoaderPaths("classpath:/", "file://" + System.getProperty("java.io.tmpdir"));
7475
this.freeMarkerConfig = configurer.createConfiguration();
7576
}
7677

7778
@Test
78-
public void exposeRequestContextAsModelAttribute() throws Exception {
79+
public void springMacroRequestContextIsAutomaticallyExposedAsModelAttribute() throws Exception {
7980
storeTemplateInTempDir("<@spring.bind \"testBean.name\"/>\nHi ${spring.status.value}");
8081

8182
FreeMarkerView view = new FreeMarkerView() {
@@ -94,11 +95,7 @@ protected Mono<Void> renderInternal(Map<String, Object> renderAttributes,
9495
}
9596
};
9697

97-
StaticWebApplicationContext wac = new StaticWebApplicationContext();
98-
wac.refresh();
99-
100-
view.setApplicationContext(wac);
101-
view.setRequestContextAttribute(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE);
98+
view.setApplicationContext(this.applicationContext);
10299
view.setBeanName("myView");
103100
view.setUrl("tmp.ftl");
104101
view.setConfiguration(this.freeMarkerConfig);
@@ -314,19 +311,21 @@ private List<String> getMacroOutput(String name) throws Exception {
314311

315312
ModelMap model = new ExtendedModelMap();
316313
DummyMacroRequestContext rc = new DummyMacroRequestContext(this.exchange, model,
317-
new GenericApplicationContext());
314+
this.applicationContext);
318315
rc.setMessageMap(msgMap);
319316
rc.setContextPath("/springtest");
320317

321318
model.put("command", darren);
322-
model.put("springMacroRequestContext", rc);
319+
model.put(FreeMarkerView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE, rc);
323320
model.put("msgArgs", new Object[] { "World" });
324321
model.put("nameOptionMap", names);
325322
model.put("options", names.values());
326323

327324
FreeMarkerView view = new FreeMarkerView();
325+
view.setApplicationContext(this.applicationContext);
328326
view.setBeanName("myView");
329327
view.setUrl("tmp.ftl");
328+
view.setExposeSpringMacroHelpers(false);
330329
view.setConfiguration(freeMarkerConfig);
331330

332331
view.render(model, null, this.exchange).subscribe();

spring-webflux/src/test/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646

4747
/**
4848
* @author Rossen Stoyanchev
49+
* @author Sam Brannen
4950
*/
5051
public class FreeMarkerViewTests {
5152

@@ -56,14 +57,13 @@ public class FreeMarkerViewTests {
5657
private final MockServerWebExchange exchange =
5758
MockServerWebExchange.from(MockServerHttpRequest.get("/path"));
5859

59-
private GenericApplicationContext context;
60+
private final GenericApplicationContext context = new GenericApplicationContext();
6061

6162
private Configuration freeMarkerConfig;
6263

6364

6465
@Before
6566
public void setup() throws Exception {
66-
this.context = new GenericApplicationContext();
6767
this.context.refresh();
6868

6969
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
@@ -104,6 +104,7 @@ public void checkResourceExists() throws Exception {
104104
@Test
105105
public void render() {
106106
FreeMarkerView view = new FreeMarkerView();
107+
view.setApplicationContext(this.context);
107108
view.setConfiguration(this.freeMarkerConfig);
108109
view.setUrl("test.ftl");
109110

@@ -126,6 +127,7 @@ public void subscribeWithoutDemand() {
126127
new AcceptHeaderLocaleContextResolver());
127128

128129
FreeMarkerView view = new FreeMarkerView();
130+
view.setApplicationContext(this.context);
129131
view.setConfiguration(this.freeMarkerConfig);
130132
view.setUrl("test.ftl");
131133

0 commit comments

Comments
 (0)