Skip to content

Commit

Permalink
Restore Freemarker support now it supports Jakarta
Browse files Browse the repository at this point in the history
Closes gh-30186
  • Loading branch information
snicoll committed Jul 18, 2024
1 parent ade8128 commit e4edd32
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
* <p>The simplest way to use this class is to specify a "templateLoaderPath";
* FreeMarker does not need any further configuration then.
*
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
*
* @author Darren Davison
* @author Juergen Hoeller
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
* <p>See the {@link FreeMarkerConfigurationFactory} base class for configuration
* details.
*
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
*
* @author Darren Davison
* @since 03.03.2004
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*
* <p>Detected and used by {@link FreeMarkerView}.
*
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
*
* @author Rossen Stoyanchev
* @since 5.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
* &lt;@spring.bind "person.age"/&gt;
* age is ${spring.status.value}</pre>
*
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
*
* @author Rossen Stoyanchev
* @since 5.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
*
* @author Rossen Stoyanchev
* @author Sam Brannen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
* <p>The view class for all views generated by this resolver can be specified
* via the "viewClass" property. See {@link UrlBasedViewResolver} for details.
*
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
*
* @author Rossen Stoyanchev
* @since 5.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

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

import freemarker.ext.jakarta.jsp.TaglibFactory;
import freemarker.template.Configuration;

/**
Expand All @@ -24,7 +25,7 @@
*
* <p>Detected and used by {@link FreeMarkerView}.
*
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
*
* @author Darren Davison
* @author Rob Harrop
Expand All @@ -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();

}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -62,7 +65,7 @@
* &lt;@spring.bind "person.age"/&gt;
* age is ${spring.status.value}</pre>
*
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
*
* @author Darren Davison
* @author Rob Harrop
Expand All @@ -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
Expand All @@ -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}
Expand Down Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -78,10 +92,7 @@
* {@link #setEncoding(String)}, {@link FreeMarkerConfigurer#setDefaultEncoding(String)},
* or {@link Configuration#setDefaultEncoding(String)}.
*
* <p>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.
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
*
* @author Darren Davison
* @author Juergen Hoeller
Expand All @@ -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
Expand Down Expand Up @@ -154,6 +171,10 @@ protected String getEncoding() {
* Set the FreeMarker {@link Configuration} to be used by this view.
* <p>If not set, the default lookup will occur: a single {@link FreeMarkerConfig}
* is expected in the current web application context, with any bean name.
* <strong>Note:</strong> 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;
Expand Down Expand Up @@ -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());
}

/**
Expand Down Expand Up @@ -288,6 +322,9 @@ protected void exposeHelpers(Map<String, Object> 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.
* <p>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.
* <p>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
Expand Down Expand Up @@ -316,8 +353,7 @@ protected void doRender(Map<String, Object> model, HttpServletRequest request,

/**
* Build a FreeMarker template model for the given model Map.
* <p>The default implementation builds a {@link SimpleHash} for the
* given MVC model with an additional fallback to request attributes.
* <p>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
Expand All @@ -326,11 +362,33 @@ protected void doRender(Map<String, Object> model, HttpServletRequest request,
protected SimpleHash buildTemplateModel(Map<String, Object> 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
Expand Down Expand Up @@ -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<String> getInitParameterNames() {
return Collections.enumeration(Collections.emptySet());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.33 or higher.
*
* @author Juergen Hoeller
* @author Sam Brannen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit e4edd32

Please sign in to comment.