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 f6e9a2a029..5b0405c5f4 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 @@ import org.springframework.util.CollectionUtils; *
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 a98666a2a5..6a35e066ec 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 @@ import org.springframework.lang.Nullable; *
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 7a4ede1119..7fe883e51f 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 @@ import freemarker.template.Configuration; * *
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 b1a4318837..6b9ef2722d 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 @@ import org.springframework.util.Assert; * <@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 43d1b93954..1c5ec18756 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 @@ import org.springframework.web.server.ServerWebExchange; * 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 eb5663a440..49255c2b77 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 @@ import org.springframework.web.reactive.result.view.UrlBasedViewResolver; *
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 e6b6990134..a7a97fcfb8 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 @@ import freemarker.template.Configuration; * *
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 24c1aa9445..a87012d227 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 java.util.List; 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 @@ import org.springframework.util.Assert; * <@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 @@ import org.springframework.util.Assert; * @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 class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory 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 class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory 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 0a1c2bee02..3408028bdc 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 @@ package org.springframework.web.servlet.view.freemarker; 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 @@ import org.springframework.web.servlet.view.AbstractTemplateView; * {@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 @@ public class FreeMarkerView extends AbstractTemplateView { * 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 @@ public class FreeMarkerView extends AbstractTemplateView { */ @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 @@ public class FreeMarkerView extends AbstractTemplateView { * bean property, retrieved via {@code getTemplate}. It delegates to the * {@code processTemplate} method to merge the template instance with * the given template model. + *
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 @@ public class FreeMarkerView extends AbstractTemplateView { /** * Build a FreeMarker template model for the given model 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 @@ public class FreeMarkerView extends AbstractTemplateView {
protected SimpleHash buildTemplateModel(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 79bac00e11..4ccfe11e16 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 @@ public class FreeMarkerMacroTests {
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 aca0639e6a..c2f3f84293 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.FileNotFoundException;
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.BDDMockito.given;
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