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 model, HttpServletRequest reque * 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 @@ protected void doRender(Map model, HttpServletRequest request, /** * 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 @@ protected void doRender(Map model, HttpServletRequest request, protected SimpleHash buildTemplateModel(Map model, HttpServletRequest request, HttpServletResponse response) { - SimpleHash fmModel = new RequestHashModel(getObjectWrapper(), request); + AllHttpScopesHashModel fmModel = new AllHttpScopesHashModel(getObjectWrapper(), getServletContext(), request); + fmModel.put(FreemarkerServlet.KEY_JSP_TAGLIBS, this.taglibFactory); + fmModel.put(FreemarkerServlet.KEY_APPLICATION, this.servletContextHashModel); + fmModel.put(FreemarkerServlet.KEY_SESSION, buildSessionModel(request, response)); + fmModel.put(FreemarkerServlet.KEY_REQUEST, new HttpRequestHashModel(request, response, getObjectWrapper())); + fmModel.put(FreemarkerServlet.KEY_REQUEST_PARAMETERS, new HttpRequestParametersHashModel(request)); fmModel.putAll(model); return fmModel; } + /** + * Build a FreeMarker {@link HttpSessionHashModel} for the given request, + * detecting whether a session already exists and reacting accordingly. + * @param request current HTTP request + * @param response current servlet response + * @return the FreeMarker HttpSessionHashModel + */ + private HttpSessionHashModel buildSessionModel(HttpServletRequest request, HttpServletResponse response) { + HttpSession session = request.getSession(false); + if (session != null) { + return new HttpSessionHashModel(session, getObjectWrapper()); + } + else { + return new HttpSessionHashModel(null, request, response, getObjectWrapper()); + } + } + /** * Retrieve the FreeMarker {@link Template} to be rendered by this view, for * the specified locale and using the {@linkplain #setEncoding(String) configured @@ -391,31 +449,46 @@ protected void processTemplate(Template template, SimpleHash model, HttpServletR /** - * Extension of FreeMarker {@link SimpleHash}, adding a fallback to request attributes. - * Similar to the formerly used {@link freemarker.ext.servlet.AllHttpScopesHashModel}, - * just limited to common request attribute exposure. + * Simple adapter class that extends {@link GenericServlet}. + * Needed for JSP access in FreeMarker. */ @SuppressWarnings("serial") - private static class RequestHashModel extends SimpleHash { + private static class GenericServletAdapter extends GenericServlet { + + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) { + // no-op + } + } + - private final HttpServletRequest request; + /** + * Internal implementation of the {@link ServletConfig} interface, + * to be passed to the servlet adapter. + */ + private class DelegatingServletConfig implements ServletConfig { - public RequestHashModel(ObjectWrapper wrapper, HttpServletRequest request) { - super(wrapper); - this.request = request; + @Override + @Nullable + public String getServletName() { + return FreeMarkerView.this.getBeanName(); + } + + @Override + @Nullable + public ServletContext getServletContext() { + return FreeMarkerView.this.getServletContext(); + } + + @Override + @Nullable + public String getInitParameter(String paramName) { + return null; } @Override - public TemplateModel get(String key) throws TemplateModelException { - TemplateModel model = super.get(key); - if (model != null) { - return model; - } - Object obj = this.request.getAttribute(key); - if (obj != null) { - return wrap(obj); - } - return wrap(null); + public Enumeration getInitParameterNames() { + return Collections.enumeration(Collections.emptySet()); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java index 9fed609f1015..f79fb9d24466 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java @@ -47,7 +47,7 @@ * check for the existence of the specified template resources and only return * a non-null {@code View} object if the template was actually found. * - *

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 configs = new HashMap<>(); - FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); - configurer.setConfiguration(new TestConfiguration()); - configs.put("configurer", configurer); - given(wac.getBeansOfType(FreeMarkerConfig.class, true, false)).willReturn(configs); - given(wac.getServletContext()).willReturn(sc); - + configureFreemarker(new TestConfiguration()); freeMarkerView.setUrl(TEMPLATE_NAME); freeMarkerView.setApplicationContext(wac); @@ -112,16 +129,7 @@ void validTemplateName() throws Exception { @Test void keepExistingContentType() throws Exception { - WebApplicationContext wac = mock(); - MockServletContext sc = new MockServletContext(); - - Map configs = new HashMap<>(); - FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); - configurer.setConfiguration(new TestConfiguration()); - configs.put("configurer", configurer); - given(wac.getBeansOfType(FreeMarkerConfig.class, true, false)).willReturn(configs); - given(wac.getServletContext()).willReturn(sc); - + configureFreemarker(new TestConfiguration()); freeMarkerView.setUrl(TEMPLATE_NAME); freeMarkerView.setApplicationContext(wac); @@ -139,28 +147,62 @@ void keepExistingContentType() throws Exception { } @Test - void requestAttributeVisible() throws Exception { - WebApplicationContext wac = mock(); - MockServletContext sc = new MockServletContext(); + void freemarkerModelHasJspTagLibs() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + HttpServletResponse response = new MockHttpServletResponse(); + Map model = Collections.emptyMap(); + testFreemarkerModel(request, response, model, dataModel -> { + assertThat(dataModel.containsKey(FreemarkerServlet.KEY_JSP_TAGLIBS)).isTrue(); + assertThat(dataModel.get(FreemarkerServlet.KEY_JSP_TAGLIBS)).isNotNull(); + }); + } - Map configs = new HashMap<>(); - FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); - configurer.setConfiguration(new TestConfiguration()); - configs.put("configurer", configurer); - given(wac.getBeansOfType(FreeMarkerConfig.class, true, false)).willReturn(configs); - given(wac.getServletContext()).willReturn(sc); + @Test + void freemarkerModelHasHttpServletContext() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + HttpServletResponse response = new MockHttpServletResponse(); + Map model = Collections.emptyMap(); + testFreemarkerModel(request, response, model, dataModel -> { + assertThat(dataModel.containsKey(FreemarkerServlet.KEY_APPLICATION)).isTrue(); + assertThat(dataModel.get(FreemarkerServlet.KEY_APPLICATION)).isInstanceOf(ServletContextHashModel.class); + }); + } - freeMarkerView.setUrl(TEMPLATE_NAME); - freeMarkerView.setApplicationContext(wac); + @Test + void freemarkerModelHasHttpSession() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + HttpServletResponse response = new MockHttpServletResponse(); + Map model = Collections.emptyMap(); + testFreemarkerModel(request, response, model, dataModel -> { + assertThat(dataModel.containsKey(FreemarkerServlet.KEY_SESSION)).isTrue(); + assertThat(dataModel.get(FreemarkerServlet.KEY_SESSION)).isInstanceOf(HttpSessionHashModel.class); + }); + } + @Test + void freemarkerModelHasHttpServletRequest() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); - request.addPreferredLocale(Locale.US); - request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac); - request.setAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE, new AcceptHeaderLocaleResolver()); HttpServletResponse response = new MockHttpServletResponse(); + Map model = Collections.emptyMap(); + testFreemarkerModel(request, response, model, dataModel -> { + assertThat(dataModel.containsKey(FreemarkerServlet.KEY_REQUEST)).isTrue(); + assertThat(dataModel.get(FreemarkerServlet.KEY_REQUEST)).isInstanceOf(HttpRequestHashModel.class); + }); + } - request.setAttribute("myattr", "myvalue"); - freeMarkerView.render(null, request, response); + @Test + void freemarkerModelHasRequestAttributes() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("req1", "value1"); + request.addParameter("req2", "value2"); + + testFreemarkerModel(request, new MockHttpServletResponse(), Collections.emptyMap(), dataModel -> { + assertThat(dataModel.containsKey(FreemarkerServlet.KEY_REQUEST_PARAMETERS)).isTrue(); + assertThat((TemplateHashModelEx) dataModel.get(FreemarkerServlet.KEY_REQUEST_PARAMETERS)).satisfies(requestParameters -> { + assertThat(requestParameters.get("req1")).isInstanceOf(SimpleScalar.class).hasToString("value1"); + assertThat(requestParameters.get("req2")).isInstanceOf(SimpleScalar.class).hasToString("value2"); + }); + }); } @Test @@ -169,6 +211,7 @@ void freeMarkerViewResolver() throws Exception { FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); configurer.setConfiguration(new TestConfiguration()); + configurer.setServletContext(sc); StaticWebApplicationContext wac = new StaticWebApplicationContext(); wac.setServletContext(sc); @@ -198,10 +241,50 @@ void freeMarkerViewResolver() throws Exception { } + private void testFreemarkerModel(HttpServletRequest request, HttpServletResponse response, Map model, + ThrowingConsumer dataModelAssertions) throws Exception { + + AtomicBoolean consumerCalled = new AtomicBoolean(); + Consumer delegate = object -> { + consumerCalled.set(true); + assertThat(object).isInstanceOf(AllHttpScopesHashModel.class) + .asInstanceOf(InstanceOfAssertFactories.type(AllHttpScopesHashModel.class)) + .satisfies(dataModelAssertions); + }; + + configureFreemarker(new TestConfiguration(delegate)); + + freeMarkerView.setUrl(TEMPLATE_NAME); + freeMarkerView.setApplicationContext(wac); + + request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac); + request.setAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE, new AcceptHeaderLocaleResolver()); + + freeMarkerView.render(model, request, response); + assertThat(consumerCalled).isTrue(); + + } + + private void configureFreemarker(Configuration configuration) { + Map configs = new HashMap<>(); + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setConfiguration(configuration); + configurer.setServletContext(this.servletContext); + configs.put("configurer", configurer); + given(wac.getBeansOfType(FreeMarkerConfig.class, true, false)).willReturn(configs); + } + private static class TestConfiguration extends Configuration { - TestConfiguration() { + private final Consumer modelAssertions; + + TestConfiguration(Consumer modelAssertions) { super(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS); + this.modelAssertions = modelAssertions; + } + + TestConfiguration() { + this(model -> {}); } @Override @@ -209,10 +292,9 @@ public Template getTemplate(String name, final Locale locale) throws IOException if (name.equals(TEMPLATE_NAME) || name.equals("templates/test.ftl")) { return new Template(name, new StringReader("test"), this) { @Override - public void process(Object model, Writer writer) { - assertThat(locale).isEqualTo(Locale.US); - assertThat(model).asInstanceOf(type(SimpleHash.class)).satisfies( - fmModel -> assertThat(fmModel.get("myattr")).asString().isEqualTo("myvalue")); + public Environment createProcessingEnvironment(Object dataModel, Writer out) throws TemplateException, IOException { + modelAssertions.accept(dataModel); + return super.createProcessingEnvironment(dataModel, out); } }; }