Skip to content

Commit ac429a4

Browse files
committed
Restore fallback to request attributes in FreeMarker template model
Closes gh-29787
1 parent 12d4dc1 commit ac429a4

File tree

3 files changed

+76
-14
lines changed

3 files changed

+76
-14
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractTemplateView.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -66,14 +66,16 @@ public abstract class AbstractTemplateView extends AbstractUrlBasedView {
6666
/**
6767
* Set whether all request attributes should be added to the
6868
* model prior to merging with the template. Default is "false".
69+
* <p>Note that some templates may make request attributes visible
70+
* on their own, e.g. FreeMarker, without exposure in the MVC model.
6971
*/
7072
public void setExposeRequestAttributes(boolean exposeRequestAttributes) {
7173
this.exposeRequestAttributes = exposeRequestAttributes;
7274
}
7375

7476
/**
7577
* Set whether HttpServletRequest attributes are allowed to override (hide)
76-
* controller generated model attributes of the same name. Default is "false",
78+
* controller generated model attributes of the same name. Default is "false"
7779
* which causes an exception to be thrown if request attributes of the same
7880
* name as model attributes are found.
7981
*/
@@ -122,11 +124,11 @@ protected final void renderMergedOutputModel(
122124
String attribute = en.nextElement();
123125
if (model.containsKey(attribute) && !this.allowRequestOverride) {
124126
throw new ServletException("Cannot expose request attribute '" + attribute +
125-
"' because of an existing model object of the same name");
127+
"' because of an existing model object of the same name");
126128
}
127129
Object attributeValue = request.getAttribute(attribute);
128130
if (logger.isDebugEnabled()) {
129-
exposed = exposed != null ? exposed : new LinkedHashMap<>();
131+
exposed = (exposed != null ? exposed : new LinkedHashMap<>());
130132
exposed.put(attribute, attributeValue);
131133
}
132134
model.put(attribute, attributeValue);
@@ -144,11 +146,11 @@ protected final void renderMergedOutputModel(
144146
String attribute = en.nextElement();
145147
if (model.containsKey(attribute) && !this.allowSessionOverride) {
146148
throw new ServletException("Cannot expose session attribute '" + attribute +
147-
"' because of an existing model object of the same name");
149+
"' because of an existing model object of the same name");
148150
}
149151
Object attributeValue = session.getAttribute(attribute);
150152
if (logger.isDebugEnabled()) {
151-
exposed = exposed != null ? exposed : new LinkedHashMap<>();
153+
exposed = (exposed != null ? exposed : new LinkedHashMap<>());
152154
exposed.put(attribute, attributeValue);
153155
}
154156
model.put(attribute, attributeValue);

spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,8 @@
2828
import freemarker.template.SimpleHash;
2929
import freemarker.template.Template;
3030
import freemarker.template.TemplateException;
31+
import freemarker.template.TemplateModel;
32+
import freemarker.template.TemplateModelException;
3133
import jakarta.servlet.ServletContext;
3234
import jakarta.servlet.http.HttpServletRequest;
3335
import jakarta.servlet.http.HttpServletResponse;
@@ -59,6 +61,9 @@
5961
* of this approach.
6062
*
6163
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
64+
* As of Spring Framework 6.0, FreeMarker templates are rendered in a minimal
65+
* fashion without JSP support, just exposing request attributes in addition
66+
* to the MVC-provided model map for alignment with common Servlet resources.
6267
*
6368
* @author Darren Davison
6469
* @author Juergen Hoeller
@@ -235,9 +240,6 @@ protected void exposeHelpers(Map<String, Object> model, HttpServletRequest reque
235240
* bean property, retrieved via {@code getTemplate}. It delegates to the
236241
* {@code processTemplate} method to merge the template instance with
237242
* the given template model.
238-
* <p>Adds the standard Freemarker hash models to the model: request parameters,
239-
* request, session and application (ServletContext), as well as the JSP tag
240-
* library hash model.
241243
* <p>Can be overridden to customize the behavior, for example to render
242244
* multiple templates into a single view.
243245
* @param model the model to use for rendering
@@ -256,7 +258,7 @@ protected void doRender(Map<String, Object> model, HttpServletRequest request,
256258

257259
// Expose model to JSP tags (as request attributes).
258260
exposeModelAsRequestAttributes(model, request);
259-
// Expose all standard FreeMarker hash models.
261+
// Expose FreeMarker hash model.
260262
SimpleHash fmModel = buildTemplateModel(model, request, response);
261263

262264
// Grab the locale-specific version of the template.
@@ -266,7 +268,8 @@ protected void doRender(Map<String, Object> model, HttpServletRequest request,
266268

267269
/**
268270
* Build a FreeMarker template model for the given model Map.
269-
* <p>The default implementation builds a {@link SimpleHash}.
271+
* <p>The default implementation builds a {@link SimpleHash} for the
272+
* given MVC model with an additional fallback to request attributes.
270273
* @param model the model to use for rendering
271274
* @param request current HTTP request
272275
* @param response current servlet response
@@ -275,7 +278,7 @@ protected void doRender(Map<String, Object> model, HttpServletRequest request,
275278
protected SimpleHash buildTemplateModel(Map<String, Object> model, HttpServletRequest request,
276279
HttpServletResponse response) {
277280

278-
SimpleHash fmModel = new SimpleHash(getObjectWrapper());
281+
SimpleHash fmModel = new RequestHashModel(getObjectWrapper(), request);
279282
fmModel.putAll(model);
280283
return fmModel;
281284
}
@@ -329,4 +332,34 @@ protected void processTemplate(Template template, SimpleHash model, HttpServletR
329332
template.process(model, response.getWriter());
330333
}
331334

335+
336+
/**
337+
* Extension of FreeMarker {@link SimpleHash}, adding a fallback to request attributes.
338+
* Similar to the formerly used {@link freemarker.ext.servlet.AllHttpScopesHashModel},
339+
* just limited to common request attribute exposure.
340+
*/
341+
@SuppressWarnings("serial")
342+
private static class RequestHashModel extends SimpleHash {
343+
344+
private final HttpServletRequest request;
345+
346+
public RequestHashModel(ObjectWrapper wrapper, HttpServletRequest request) {
347+
super(wrapper);
348+
this.request = request;
349+
}
350+
351+
@Override
352+
public TemplateModel get(String key) throws TemplateModelException {
353+
TemplateModel model = super.get(key);
354+
if (model != null) {
355+
return model;
356+
}
357+
Object obj = this.request.getAttribute(key);
358+
if (obj != null) {
359+
return wrap(obj);
360+
}
361+
return wrap(null);
362+
}
363+
}
364+
332365
}

spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewTests.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,33 @@ public void keepExistingContentType() throws Exception {
142142
assertThat(response.getContentType()).isEqualTo("myContentType");
143143
}
144144

145+
@Test
146+
public void requestAttributeVisible() throws Exception {
147+
FreeMarkerView fv = new FreeMarkerView();
148+
149+
WebApplicationContext wac = mock();
150+
MockServletContext sc = new MockServletContext();
151+
152+
Map<String, FreeMarkerConfig> configs = new HashMap<>();
153+
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
154+
configurer.setConfiguration(new TestConfiguration());
155+
configs.put("configurer", configurer);
156+
given(wac.getBeansOfType(FreeMarkerConfig.class, true, false)).willReturn(configs);
157+
given(wac.getServletContext()).willReturn(sc);
158+
159+
fv.setUrl("templateName");
160+
fv.setApplicationContext(wac);
161+
162+
MockHttpServletRequest request = new MockHttpServletRequest();
163+
request.addPreferredLocale(Locale.US);
164+
request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac);
165+
request.setAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE, new AcceptHeaderLocaleResolver());
166+
HttpServletResponse response = new MockHttpServletResponse();
167+
168+
request.setAttribute("myattr", "myvalue");
169+
fv.render(null, request, response);
170+
}
171+
145172
@Test
146173
public void freeMarkerViewResolver() throws Exception {
147174
MockServletContext sc = new MockServletContext();
@@ -189,7 +216,7 @@ public void process(Object model, Writer writer) throws TemplateException, IOExc
189216
assertThat(locale).isEqualTo(Locale.US);
190217
assertThat(model instanceof SimpleHash).isTrue();
191218
SimpleHash fmModel = (SimpleHash) model;
192-
assertThat(fmModel.get("myattr").toString()).isEqualTo("myvalue");
219+
assertThat(String.valueOf(fmModel.get("myattr"))).isEqualTo("myvalue");
193220
}
194221
};
195222
}

0 commit comments

Comments
 (0)