Skip to content

Commit e4edd32

Browse files
committed
Restore Freemarker support now it supports Jakarta
Closes gh-30186
1 parent ade8128 commit e4edd32

File tree

12 files changed

+274
-88
lines changed

12 files changed

+274
-88
lines changed

spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
* <p>The simplest way to use this class is to specify a "templateLoaderPath";
6363
* FreeMarker does not need any further configuration then.
6464
*
65-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
65+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
6666
*
6767
* @author Darren Davison
6868
* @author Juergen Hoeller

spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
* <p>See the {@link FreeMarkerConfigurationFactory} base class for configuration
4646
* details.
4747
*
48-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
48+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
4949
*
5050
* @author Darren Davison
5151
* @since 03.03.2004

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
*
2525
* <p>Detected and used by {@link FreeMarkerView}.
2626
*
27-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
27+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
2828
*
2929
* @author Rossen Stoyanchev
3030
* @since 5.0

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
* &lt;@spring.bind "person.age"/&gt;
5757
* age is ${spring.status.value}</pre>
5858
*
59-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
59+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
6060
*
6161
* @author Rossen Stoyanchev
6262
* @since 5.0

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
* sets the supported media type to {@code "text/html;charset=UTF-8"} by default.
9191
* Thus, those default values are likely suitable for most applications.
9292
*
93-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
93+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
9494
*
9595
* @author Rossen Stoyanchev
9696
* @author Sam Brannen

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* <p>The view class for all views generated by this resolver can be specified
2727
* via the "viewClass" property. See {@link UrlBasedViewResolver} for details.
2828
*
29-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
29+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
3030
*
3131
* @author Rossen Stoyanchev
3232
* @since 5.0

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.servlet.view.freemarker;
1818

19+
import freemarker.ext.jakarta.jsp.TaglibFactory;
1920
import freemarker.template.Configuration;
2021

2122
/**
@@ -24,7 +25,7 @@
2425
*
2526
* <p>Detected and used by {@link FreeMarkerView}.
2627
*
27-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
28+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
2829
*
2930
* @author Darren Davison
3031
* @author Rob Harrop
@@ -43,4 +44,10 @@ public interface FreeMarkerConfig {
4344
*/
4445
Configuration getConfiguration();
4546

47+
/**
48+
* Return the {@link TaglibFactory} used to enable JSP tags to be
49+
* accessed from FreeMarker templates.
50+
*/
51+
TaglibFactory getTaglibFactory();
52+
4653
}

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@
2121

2222
import freemarker.cache.ClassTemplateLoader;
2323
import freemarker.cache.TemplateLoader;
24+
import freemarker.ext.jakarta.jsp.TaglibFactory;
2425
import freemarker.template.Configuration;
2526
import freemarker.template.TemplateException;
27+
import jakarta.servlet.ServletContext;
2628

2729
import org.springframework.beans.factory.InitializingBean;
2830
import org.springframework.context.ResourceLoaderAware;
2931
import org.springframework.lang.Nullable;
3032
import org.springframework.ui.freemarker.FreeMarkerConfigurationFactory;
3133
import org.springframework.util.Assert;
34+
import org.springframework.web.context.ServletContextAware;
3235

3336
/**
3437
* Bean to configure FreeMarker for web usage, via the "configLocation",
@@ -62,7 +65,7 @@
6265
* &lt;@spring.bind "person.age"/&gt;
6366
* age is ${spring.status.value}</pre>
6467
*
65-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
68+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
6669
*
6770
* @author Darren Davison
6871
* @author Rob Harrop
@@ -75,11 +78,14 @@
7578
* @see FreeMarkerView
7679
*/
7780
public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory
78-
implements FreeMarkerConfig, InitializingBean, ResourceLoaderAware {
81+
implements FreeMarkerConfig, InitializingBean, ResourceLoaderAware, ServletContextAware {
7982

8083
@Nullable
8184
private Configuration configuration;
8285

86+
@Nullable
87+
private TaglibFactory taglibFactory;
88+
8389

8490
/**
8591
* Set a preconfigured {@link Configuration} to use for the FreeMarker web
@@ -92,6 +98,14 @@ public void setConfiguration(Configuration configuration) {
9298
this.configuration = configuration;
9399
}
94100

101+
/**
102+
* Initialize the {@link TaglibFactory} for the given ServletContext.
103+
*/
104+
@Override
105+
public void setServletContext(ServletContext servletContext) {
106+
this.taglibFactory = new TaglibFactory(servletContext);
107+
}
108+
95109

96110
/**
97111
* Initialize FreeMarkerConfigurationFactory's {@link Configuration}
@@ -128,4 +142,13 @@ public Configuration getConfiguration() {
128142
return this.configuration;
129143
}
130144

145+
/**
146+
* Return the TaglibFactory object wrapped by this bean.
147+
*/
148+
@Override
149+
public TaglibFactory getTaglibFactory() {
150+
Assert.state(this.taglibFactory != null, "No TaglibFactory available");
151+
return this.taglibFactory;
152+
}
153+
131154
}

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

Lines changed: 101 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,39 @@
1919
import java.io.FileNotFoundException;
2020
import java.io.IOException;
2121
import java.nio.charset.Charset;
22+
import java.util.Collections;
23+
import java.util.Enumeration;
2224
import java.util.Locale;
2325
import java.util.Map;
2426

2527
import freemarker.core.Environment;
2628
import freemarker.core.ParseException;
29+
import freemarker.ext.jakarta.jsp.TaglibFactory;
30+
import freemarker.ext.jakarta.servlet.AllHttpScopesHashModel;
31+
import freemarker.ext.jakarta.servlet.FreemarkerServlet;
32+
import freemarker.ext.jakarta.servlet.HttpRequestHashModel;
33+
import freemarker.ext.jakarta.servlet.HttpRequestParametersHashModel;
34+
import freemarker.ext.jakarta.servlet.HttpSessionHashModel;
35+
import freemarker.ext.jakarta.servlet.ServletContextHashModel;
2736
import freemarker.template.Configuration;
2837
import freemarker.template.DefaultObjectWrapperBuilder;
2938
import freemarker.template.ObjectWrapper;
3039
import freemarker.template.SimpleHash;
3140
import freemarker.template.Template;
3241
import freemarker.template.TemplateException;
33-
import freemarker.template.TemplateModel;
34-
import freemarker.template.TemplateModelException;
42+
import jakarta.servlet.GenericServlet;
43+
import jakarta.servlet.ServletConfig;
3544
import jakarta.servlet.ServletContext;
45+
import jakarta.servlet.ServletException;
46+
import jakarta.servlet.ServletRequest;
47+
import jakarta.servlet.ServletResponse;
3648
import jakarta.servlet.http.HttpServletRequest;
3749
import jakarta.servlet.http.HttpServletResponse;
50+
import jakarta.servlet.http.HttpSession;
3851

3952
import org.springframework.beans.BeansException;
4053
import org.springframework.beans.factory.BeanFactoryUtils;
54+
import org.springframework.beans.factory.BeanInitializationException;
4155
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
4256
import org.springframework.context.ApplicationContextException;
4357
import org.springframework.lang.Nullable;
@@ -78,10 +92,7 @@
7892
* {@link #setEncoding(String)}, {@link FreeMarkerConfigurer#setDefaultEncoding(String)},
7993
* or {@link Configuration#setDefaultEncoding(String)}.
8094
*
81-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
82-
* As of Spring Framework 6.0, FreeMarker templates are rendered in a minimal
83-
* fashion without JSP support, just exposing request attributes in addition
84-
* to the MVC-provided model map for alignment with common Servlet resources.
95+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
8596
*
8697
* @author Darren Davison
8798
* @author Juergen Hoeller
@@ -102,6 +113,12 @@ public class FreeMarkerView extends AbstractTemplateView {
102113
@Nullable
103114
private Configuration configuration;
104115

116+
@Nullable
117+
private TaglibFactory taglibFactory;
118+
119+
@Nullable
120+
private ServletContextHashModel servletContextHashModel;
121+
105122

106123
/**
107124
* Set the encoding used to decode byte sequences to character sequences when
@@ -154,6 +171,10 @@ protected String getEncoding() {
154171
* Set the FreeMarker {@link Configuration} to be used by this view.
155172
* <p>If not set, the default lookup will occur: a single {@link FreeMarkerConfig}
156173
* is expected in the current web application context, with any bean name.
174+
* <strong>Note:</strong> using this method will cause a new instance of {@link TaglibFactory}
175+
* to created for every single {@link FreeMarkerView} instance. This can be quite expensive
176+
* in terms of memory and initial CPU usage. In production it is recommended that you use
177+
* a {@link FreeMarkerConfig} which exposes a single shared {@link TaglibFactory}.
157178
*/
158179
public void setConfiguration(@Nullable Configuration configuration) {
159180
this.configuration = configuration;
@@ -190,10 +211,23 @@ protected Configuration obtainConfiguration() {
190211
*/
191212
@Override
192213
protected void initServletContext(ServletContext servletContext) throws BeansException {
193-
if (getConfiguration() == null) {
214+
if (getConfiguration() != null) {
215+
this.taglibFactory = new TaglibFactory(servletContext);
216+
}
217+
else {
194218
FreeMarkerConfig config = autodetectConfiguration();
195219
setConfiguration(config.getConfiguration());
220+
this.taglibFactory = config.getTaglibFactory();
221+
}
222+
223+
GenericServlet servlet = new GenericServletAdapter();
224+
try {
225+
servlet.init(new DelegatingServletConfig());
196226
}
227+
catch (ServletException ex) {
228+
throw new BeanInitializationException("Initialization of GenericServlet adapter failed", ex);
229+
}
230+
this.servletContextHashModel = new ServletContextHashModel(servlet, getObjectWrapper());
197231
}
198232

199233
/**
@@ -288,6 +322,9 @@ protected void exposeHelpers(Map<String, Object> model, HttpServletRequest reque
288322
* bean property, retrieved via {@code getTemplate}. It delegates to the
289323
* {@code processTemplate} method to merge the template instance with
290324
* the given template model.
325+
* <p>Adds the standard Freemarker hash models to the model: request parameters,
326+
* request, session and application (ServletContext), as well as the JSP tag
327+
* library hash model.
291328
* <p>Can be overridden to customize the behavior, for example to render
292329
* multiple templates into a single view.
293330
* @param model the model to use for rendering
@@ -316,8 +353,7 @@ protected void doRender(Map<String, Object> model, HttpServletRequest request,
316353

317354
/**
318355
* Build a FreeMarker template model for the given model Map.
319-
* <p>The default implementation builds a {@link SimpleHash} for the
320-
* given MVC model with an additional fallback to request attributes.
356+
* <p>The default implementation builds a {@link AllHttpScopesHashModel}.
321357
* @param model the model to use for rendering
322358
* @param request current HTTP request
323359
* @param response current servlet response
@@ -326,11 +362,33 @@ protected void doRender(Map<String, Object> model, HttpServletRequest request,
326362
protected SimpleHash buildTemplateModel(Map<String, Object> model, HttpServletRequest request,
327363
HttpServletResponse response) {
328364

329-
SimpleHash fmModel = new RequestHashModel(getObjectWrapper(), request);
365+
AllHttpScopesHashModel fmModel = new AllHttpScopesHashModel(getObjectWrapper(), getServletContext(), request);
366+
fmModel.put(FreemarkerServlet.KEY_JSP_TAGLIBS, this.taglibFactory);
367+
fmModel.put(FreemarkerServlet.KEY_APPLICATION, this.servletContextHashModel);
368+
fmModel.put(FreemarkerServlet.KEY_SESSION, buildSessionModel(request, response));
369+
fmModel.put(FreemarkerServlet.KEY_REQUEST, new HttpRequestHashModel(request, response, getObjectWrapper()));
370+
fmModel.put(FreemarkerServlet.KEY_REQUEST_PARAMETERS, new HttpRequestParametersHashModel(request));
330371
fmModel.putAll(model);
331372
return fmModel;
332373
}
333374

375+
/**
376+
* Build a FreeMarker {@link HttpSessionHashModel} for the given request,
377+
* detecting whether a session already exists and reacting accordingly.
378+
* @param request current HTTP request
379+
* @param response current servlet response
380+
* @return the FreeMarker HttpSessionHashModel
381+
*/
382+
private HttpSessionHashModel buildSessionModel(HttpServletRequest request, HttpServletResponse response) {
383+
HttpSession session = request.getSession(false);
384+
if (session != null) {
385+
return new HttpSessionHashModel(session, getObjectWrapper());
386+
}
387+
else {
388+
return new HttpSessionHashModel(null, request, response, getObjectWrapper());
389+
}
390+
}
391+
334392
/**
335393
* Retrieve the FreeMarker {@link Template} to be rendered by this view, for
336394
* the specified locale and using the {@linkplain #setEncoding(String) configured
@@ -391,31 +449,46 @@ protected void processTemplate(Template template, SimpleHash model, HttpServletR
391449

392450

393451
/**
394-
* Extension of FreeMarker {@link SimpleHash}, adding a fallback to request attributes.
395-
* Similar to the formerly used {@link freemarker.ext.servlet.AllHttpScopesHashModel},
396-
* just limited to common request attribute exposure.
452+
* Simple adapter class that extends {@link GenericServlet}.
453+
* Needed for JSP access in FreeMarker.
397454
*/
398455
@SuppressWarnings("serial")
399-
private static class RequestHashModel extends SimpleHash {
456+
private static class GenericServletAdapter extends GenericServlet {
457+
458+
@Override
459+
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
460+
// no-op
461+
}
462+
}
463+
400464

401-
private final HttpServletRequest request;
465+
/**
466+
* Internal implementation of the {@link ServletConfig} interface,
467+
* to be passed to the servlet adapter.
468+
*/
469+
private class DelegatingServletConfig implements ServletConfig {
402470

403-
public RequestHashModel(ObjectWrapper wrapper, HttpServletRequest request) {
404-
super(wrapper);
405-
this.request = request;
471+
@Override
472+
@Nullable
473+
public String getServletName() {
474+
return FreeMarkerView.this.getBeanName();
475+
}
476+
477+
@Override
478+
@Nullable
479+
public ServletContext getServletContext() {
480+
return FreeMarkerView.this.getServletContext();
481+
}
482+
483+
@Override
484+
@Nullable
485+
public String getInitParameter(String paramName) {
486+
return null;
406487
}
407488

408489
@Override
409-
public TemplateModel get(String key) throws TemplateModelException {
410-
TemplateModel model = super.get(key);
411-
if (model != null) {
412-
return model;
413-
}
414-
Object obj = this.request.getAttribute(key);
415-
if (obj != null) {
416-
return wrap(obj);
417-
}
418-
return wrap(null);
490+
public Enumeration<String> getInitParameterNames() {
491+
return Collections.enumeration(Collections.emptySet());
419492
}
420493
}
421494

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
* check for the existence of the specified template resources and only return
4848
* a non-null {@code View} object if the template was actually found.
4949
*
50-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
50+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
5151
*
5252
* @author Juergen Hoeller
5353
* @author Sam Brannen

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ void setUp() throws Exception {
8181
this.templateLoaderPath = Files.createTempDirectory("servlet-").toAbsolutePath();
8282

8383
fc.setTemplateLoaderPaths("classpath:/", "file://" + this.templateLoaderPath);
84+
fc.setServletContext(servletContext);
8485
fc.afterPropertiesSet();
8586

8687
wac.setServletContext(servletContext);

0 commit comments

Comments
 (0)