diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java index f6e9a2a02944..5b0405c5f401 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java @@ -62,7 +62,7 @@ *
The simplest way to use this class is to specify a "templateLoaderPath"; * FreeMarker does not need any further configuration then. * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher. * * @author Darren Davison * @author Juergen Hoeller diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java index a98666a2a573..6a35e066ec4a 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java @@ -45,7 +45,7 @@ *
See the {@link FreeMarkerConfigurationFactory} base class for configuration * details. * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher. * * @author Darren Davison * @since 03.03.2004 diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfig.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfig.java index 7a4ede111930..7fe883e51f94 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfig.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfig.java @@ -24,7 +24,7 @@ * *
Detected and used by {@link FreeMarkerView}. * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher. * * @author Rossen Stoyanchev * @since 5.0 diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java index b1a4318837e6..6b9ef2722d44 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java @@ -56,7 +56,7 @@ * <@spring.bind "person.age"/> * age is ${spring.status.value} * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher. * * @author Rossen Stoyanchev * @since 5.0 diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java index 43d1b9395470..1c5ec1875647 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java @@ -90,7 +90,7 @@ * sets the supported media type to {@code "text/html;charset=UTF-8"} by default. * Thus, those default values are likely suitable for most applications. * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher. * * @author Rossen Stoyanchev * @author Sam Brannen diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewResolver.java index eb5663a44045..49255c2b77e9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewResolver.java @@ -26,7 +26,7 @@ *
The view class for all views generated by this resolver can be specified * via the "viewClass" property. See {@link UrlBasedViewResolver} for details. * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher. * * @author Rossen Stoyanchev * @since 5.0 diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfig.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfig.java index e6b699013438..a7a97fcfb866 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfig.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfig.java @@ -16,6 +16,7 @@ package org.springframework.web.servlet.view.freemarker; +import freemarker.ext.jakarta.jsp.TaglibFactory; import freemarker.template.Configuration; /** @@ -24,7 +25,7 @@ * *
Detected and used by {@link FreeMarkerView}. * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher. * * @author Darren Davison * @author Rob Harrop @@ -43,4 +44,10 @@ public interface FreeMarkerConfig { */ Configuration getConfiguration(); + /** + * Return the {@link TaglibFactory} used to enable JSP tags to be + * accessed from FreeMarker templates. + */ + TaglibFactory getTaglibFactory(); + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfigurer.java index 24c1aa9445fd..a87012d22700 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfigurer.java @@ -21,14 +21,17 @@ import freemarker.cache.ClassTemplateLoader; import freemarker.cache.TemplateLoader; +import freemarker.ext.jakarta.jsp.TaglibFactory; import freemarker.template.Configuration; import freemarker.template.TemplateException; +import jakarta.servlet.ServletContext; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ResourceLoaderAware; import org.springframework.lang.Nullable; import org.springframework.ui.freemarker.FreeMarkerConfigurationFactory; import org.springframework.util.Assert; +import org.springframework.web.context.ServletContextAware; /** * Bean to configure FreeMarker for web usage, via the "configLocation", @@ -62,7 +65,7 @@ * <@spring.bind "person.age"/> * age is ${spring.status.value} * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher. * * @author Darren Davison * @author Rob Harrop @@ -75,11 +78,14 @@ * @see FreeMarkerView */ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory - implements FreeMarkerConfig, InitializingBean, ResourceLoaderAware { + implements FreeMarkerConfig, InitializingBean, ResourceLoaderAware, ServletContextAware { @Nullable private Configuration configuration; + @Nullable + private TaglibFactory taglibFactory; + /** * Set a preconfigured {@link Configuration} to use for the FreeMarker web @@ -92,6 +98,14 @@ public void setConfiguration(Configuration configuration) { this.configuration = configuration; } + /** + * Initialize the {@link TaglibFactory} for the given ServletContext. + */ + @Override + public void setServletContext(ServletContext servletContext) { + this.taglibFactory = new TaglibFactory(servletContext); + } + /** * Initialize FreeMarkerConfigurationFactory's {@link Configuration} @@ -128,4 +142,13 @@ public Configuration getConfiguration() { return this.configuration; } + /** + * Return the TaglibFactory object wrapped by this bean. + */ + @Override + public TaglibFactory getTaglibFactory() { + Assert.state(this.taglibFactory != null, "No TaglibFactory available"); + return this.taglibFactory; + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java index 0a1c2bee0263..3408028bdc81 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java @@ -19,25 +19,39 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Enumeration; import java.util.Locale; import java.util.Map; import freemarker.core.Environment; import freemarker.core.ParseException; +import freemarker.ext.jakarta.jsp.TaglibFactory; +import freemarker.ext.jakarta.servlet.AllHttpScopesHashModel; +import freemarker.ext.jakarta.servlet.FreemarkerServlet; +import freemarker.ext.jakarta.servlet.HttpRequestHashModel; +import freemarker.ext.jakarta.servlet.HttpRequestParametersHashModel; +import freemarker.ext.jakarta.servlet.HttpSessionHashModel; +import freemarker.ext.jakarta.servlet.ServletContextHashModel; import freemarker.template.Configuration; import freemarker.template.DefaultObjectWrapperBuilder; import freemarker.template.ObjectWrapper; import freemarker.template.SimpleHash; import freemarker.template.Template; import freemarker.template.TemplateException; -import freemarker.template.TemplateModel; -import freemarker.template.TemplateModelException; +import jakarta.servlet.GenericServlet; +import jakarta.servlet.ServletConfig; import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContextException; import org.springframework.lang.Nullable; @@ -78,10 +92,7 @@ * {@link #setEncoding(String)}, {@link FreeMarkerConfigurer#setDefaultEncoding(String)}, * or {@link Configuration#setDefaultEncoding(String)}. * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher. - * As of Spring Framework 6.0, FreeMarker templates are rendered in a minimal - * fashion without JSP support, just exposing request attributes in addition - * to the MVC-provided model map for alignment with common Servlet resources. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher. * * @author Darren Davison * @author Juergen Hoeller @@ -102,6 +113,12 @@ public class FreeMarkerView extends AbstractTemplateView { @Nullable private Configuration configuration; + @Nullable + private TaglibFactory taglibFactory; + + @Nullable + private ServletContextHashModel servletContextHashModel; + /** * Set the encoding used to decode byte sequences to character sequences when @@ -154,6 +171,10 @@ protected String getEncoding() { * Set the FreeMarker {@link Configuration} to be used by this view. *
If not set, the default lookup will occur: a single {@link FreeMarkerConfig}
* is expected in the current web application context, with any bean name.
+ * Note: using this method will cause a new instance of {@link TaglibFactory}
+ * to created for every single {@link FreeMarkerView} instance. This can be quite expensive
+ * in terms of memory and initial CPU usage. In production it is recommended that you use
+ * a {@link FreeMarkerConfig} which exposes a single shared {@link TaglibFactory}.
*/
public void setConfiguration(@Nullable Configuration configuration) {
this.configuration = configuration;
@@ -190,10 +211,23 @@ protected Configuration obtainConfiguration() {
*/
@Override
protected void initServletContext(ServletContext servletContext) throws BeansException {
- if (getConfiguration() == null) {
+ if (getConfiguration() != null) {
+ this.taglibFactory = new TaglibFactory(servletContext);
+ }
+ else {
FreeMarkerConfig config = autodetectConfiguration();
setConfiguration(config.getConfiguration());
+ this.taglibFactory = config.getTaglibFactory();
+ }
+
+ GenericServlet servlet = new GenericServletAdapter();
+ try {
+ servlet.init(new DelegatingServletConfig());
}
+ catch (ServletException ex) {
+ throw new BeanInitializationException("Initialization of GenericServlet adapter failed", ex);
+ }
+ this.servletContextHashModel = new ServletContextHashModel(servlet, getObjectWrapper());
}
/**
@@ -288,6 +322,9 @@ protected void exposeHelpers(Map Adds the standard Freemarker hash models to the model: request parameters,
+ * request, session and application (ServletContext), as well as the JSP tag
+ * library hash model.
* Can be overridden to customize the behavior, for example to render
* multiple templates into a single view.
* @param model the model to use for rendering
@@ -316,8 +353,7 @@ protected void doRender(Map The default implementation builds a {@link SimpleHash} for the
- * given MVC model with an additional fallback to request attributes.
+ * The default implementation builds a {@link AllHttpScopesHashModel}.
* @param model the model to use for rendering
* @param request current HTTP request
* @param response current servlet response
@@ -326,11 +362,33 @@ protected void doRender(Map Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
+ * Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
*
* @author Juergen Hoeller
* @author Sam Brannen
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerMacroTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerMacroTests.java
index 79bac00e11b9..4ccfe11e16c6 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerMacroTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerMacroTests.java
@@ -81,6 +81,7 @@ void setUp() throws Exception {
this.templateLoaderPath = Files.createTempDirectory("servlet-").toAbsolutePath();
fc.setTemplateLoaderPaths("classpath:/", "file://" + this.templateLoaderPath);
+ fc.setServletContext(servletContext);
fc.afterPropertiesSet();
wac.setServletContext(servletContext);
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewTests.java
index aca0639e6a85..c2f3f84293ae 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewTests.java
@@ -20,14 +20,30 @@
import java.io.IOException;
import java.io.StringReader;
import java.io.Writer;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
-
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+import freemarker.core.Environment;
+import freemarker.ext.jakarta.servlet.AllHttpScopesHashModel;
+import freemarker.ext.jakarta.servlet.FreemarkerServlet;
+import freemarker.ext.jakarta.servlet.HttpRequestHashModel;
+import freemarker.ext.jakarta.servlet.HttpSessionHashModel;
+import freemarker.ext.jakarta.servlet.ServletContextHashModel;
import freemarker.template.Configuration;
-import freemarker.template.SimpleHash;
+import freemarker.template.SimpleScalar;
import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateHashModelEx;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.assertj.core.api.ThrowingConsumer;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContextException;
@@ -51,8 +67,11 @@
import static org.mockito.Mockito.mock;
/**
+ * Tests for {@link FreeMarkerView}.
+ *
* @author Juergen Hoeller
* @author Sam Brannen
+ * @author Stephane Nicoll
* @since 14.03.2004
*/
class FreeMarkerViewTests {
@@ -60,41 +79,39 @@ class FreeMarkerViewTests {
private static final String TEMPLATE_NAME = "templateName";
+ private final WebApplicationContext wac = mock();
+
+ private final ServletContext servletContext = new MockServletContext();
+
private final FreeMarkerView freeMarkerView = new FreeMarkerView();
+ @BeforeEach
+ void setup() {
+ given(this.wac.getServletContext()).willReturn(this.servletContext);
+ }
+
@Test
void noFreeMarkerConfig() {
- WebApplicationContext wac = mock();
- given(wac.getBeansOfType(FreeMarkerConfig.class, true, false)).willReturn(new HashMap<>());
- given(wac.getServletContext()).willReturn(new MockServletContext());
+ given(this.wac.getBeansOfType(FreeMarkerConfig.class, true, false)).willReturn(new HashMap<>());
freeMarkerView.setUrl("anythingButNull");
assertThatExceptionOfType(ApplicationContextException.class)
- .isThrownBy(() -> freeMarkerView.setApplicationContext(wac))
- .withMessageContaining("Must define a single FreeMarkerConfig bean");
+ .isThrownBy(() -> freeMarkerView.setApplicationContext(this.wac))
+ .withMessageContaining("Must define a single FreeMarkerConfig bean");
}
@Test
void noTemplateName() {
assertThatIllegalArgumentException()
- .isThrownBy(freeMarkerView::afterPropertiesSet)
- .withMessageContaining("Property 'url' is required");
+ .isThrownBy(freeMarkerView::afterPropertiesSet)
+ .withMessageContaining("Property 'url' is required");
}
@Test
void validTemplateName() throws Exception {
- WebApplicationContext wac = mock();
- MockServletContext sc = new MockServletContext();
-
- Map