FreeMarkerView should support AllHttpScopesHashModel (analogous to FreeMarkerServlet in FreeMarker 2.3.14) [SPR-4962] #9637
Labels
in: web
Issues in web modules (web, webmvc, webflux, websocket)
type: enhancement
A general enhancement
Milestone
John Arkley opened SPR-4962 and commented
During development of my 1st web-app i found this because i began using appfuse-light-spring-freemarker-ibatis-1.8.1
as my prototype application. appfuseLt uses Sitemesh (opensymphony.org) to decorate Freemarker generated pages, by
implementing a Servlet filter that diverts the FreemarkerView generated response into a buffer, parses that buffer and
then inserts the generated view content as strings variables (${head} ${title}, ${body} ,,,) into the root data model and then invokes aSitemesh
sub-class of FreemarkerServlet to generate the decorated page into the HttpResponse,
i.e. It a 1 level "Tiles like" compositiing implementation that allows a decorator.flt file to insert the $(body) of a view into a multi-column page layout.
The FreeMarkerServlet wraps the data-model as the AllHttpScopesHashModel, with its not found fall-through lookup in 4 scopes, (like jsp ?),
page, Request, Session, and Application (servlet context), which the FreeMarkerView does not. Although FreeMarkerView does insert the
same set of scopes, because it is not using the AllHttpScopesHashModel to wrap them, code that works in the decorator.flt file that references
a variable with ${message}, must be coded as ${Request.message} or ${Session.message} to be referenced in a Freemarker template processed
by springs FreeMarkerView.
I tripped over this when i moved a small fragment Freemarker markup from a decorator template to a view template.
I "enhanced" FreeMarkerView.java by cloning a method (createModel) from FreeMarkerServlet.java and added an inner class clone of Freemarkers
AllHttpScopesHashModel.java (see src below) This latter choice was to make hacking up spring-webmvc.jar as fast a possible, and
due to the constructor for AllHttpScopesHashModel not being declared public; which i suspect was just a oversight by the Freemarker
coder(s), because all the other xxxxHashModel classes have public constructors.
This change also made it necessary to change the signature of FreeMarkerView's method as show here with the Map replaced with TemplateModel,
so applying this patch with break anyone who has overridden FreeMarkerView.processTemplate();
protected void processTemplate(Template template, TemplateModel model, HttpServletResponse response)
I think Freemarker should make the constructor public so you could implement this fix without using an inner class, which i suspect
has implications i don't grasp, as i am not a java expert. I have only tested my 3 views plus 2 form views on this hack, but variable lookup works the
same in FreemarkerView as in the FreemarkerServlet. I have not got the springframework tests running yet, so i built my -webmvc.jar wtih
ant by hand after recompling just the one file, FreeMarkerView.java, overwriting the un-jar'ed spring-webmvc.jar class files.
Anyone just using FreeMarkerViews for view rendering, but not using the FreeMarkerServlet (due to Sitemesh in my case) will not see this issue.
my hacked version of FreeMarkerView.java
/*
Copyright 2002-2007 the original author or authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.springframework.web.servlet.view.freemarker;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import java.util.HashMap;
import javax.servlet.GenericServlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import freemarker.core.ParseException;
import freemarker.ext.jsp.TaglibFactory;
import freemarker.ext.servlet.FreemarkerServlet;
import freemarker.ext.servlet.HttpRequestHashModel;
import freemarker.ext.servlet.HttpRequestParametersHashModel;
import freemarker.ext.servlet.HttpSessionHashModel;
import freemarker.ext.servlet.ServletContextHashModel;
import freemarker.template.Configuration;
import freemarker.template.ObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateException;
import freemarker.template.SimpleHash;
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.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.view.AbstractTemplateView;
/**
View using the FreeMarker template engine.
<p>Exposes the following JavaBean properties:
*
<ul>
*
<li><b>url</b>: the location of the FreeMarker template to be wrapped,
relative to the FreeMarker template context (directory).
<li><b>encoding</b> (optional, default is determined by FreeMarker configuration):
the encoding of the FreeMarker template file
</ul>
*
<p>Depends on a single {
@link
FreeMarkerConfig} object such as {@link
FreeMarkerConfigurer}being accessible in the current web application context, with any bean name.
Alternatively, you can set the FreeMarker {
@link
Configuration} object as bean property.See {
@link
#setConfiguration} for more details on the impacts of this approach.<p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
*
@author
Darren Davison@author
Juergen Hoeller@since
03.03.2004@see
#setUrl@see
#setExposeSpringMacroHelpers@see
#setEncoding@see
#setConfiguration@see
FreeMarkerConfig@see
FreeMarkerConfigurer*/
public class FreeMarkerView extends AbstractTemplateView {
private String encoding;
private Configuration configuration;
private TaglibFactory taglibFactory;
private ServletContextHashModel servletContextHashModel;
/** ________________________________________________________________________________________
<p>Specify the encoding in the FreeMarker Configuration rather than per
*/
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/** ________________________________________________________________________________________
*/
protected String getEncoding() {
return this.encoding;
}
/** ________________________________________________________________________________________
@link
FreeMarkerConfig}@link
TaglibFactory}@link
FreeMarkerView} instance. This can be quite expensive@link
FreeMarkerConfig} which exposes a single shared {@link
TaglibFactory}.*/
public void setConfiguration(Configuration configuration) {
this.configuration = configuration;
}
/** ________________________________________________________________________________________
*/
protected Configuration getConfiguration() {
return this.configuration;
}
/** ________________________________________________________________________________________
Invoked on startup. Looks for a single FreeMarkerConfig bean to
find the relevant Configuration for this factory.
<p>Checks that the template for the default Locale can be found:
FreeMarker will check non-Locale-specific templates if a
locale-specific one is not found.
@see
freemarker.cache.TemplateCache#getTemplate*/
protected void initApplicationContext() throws BeansException {
super.initApplicationContext();
if (getConfiguration() != null) {
this.taglibFactory = new TaglibFactory(getServletContext());
}
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());
checkTemplate();
}
/** ________________________________________________________________________________________
@link
FreeMarkerConfig} object via the ApplicationContext.@return
the Configuration instance to use for FreeMarkerViews@throws
BeansException if no Configuration instance could be found@see
#getApplicationContext@see
#setConfiguration*/
protected FreeMarkerConfig autodetectConfiguration() throws BeansException {
try {
return (FreeMarkerConfig) BeanFactoryUtils.beanOfTypeIncludingAncestors(
getApplicationContext(), FreeMarkerConfig.class, true, false);
}
catch (NoSuchBeanDefinitionException ex) {
throw new ApplicationContextException(
"Must define a single FreeMarkerConfig bean in this web application context " +
"(may be inherited): FreeMarkerConfigurer is the usual implementation. " +
"This bean may be given any name.", ex);
}
}
/** ________________________________________________________________________________________
@link
ObjectWrapper}, or the@link
ObjectWrapper#DEFAULT_WRAPPER default wrapper} if none specified.@see
freemarker.template.Configuration#getObjectWrapper()*/
protected ObjectWrapper getObjectWrapper() {
ObjectWrapper ow = getConfiguration().getObjectWrapper();
return (ow != null ? ow : ObjectWrapper.DEFAULT_WRAPPER);
}
/** ________________________________________________________________________________________
<p>Can be overridden to customize the behavior, for example in case of
@throws
ApplicationContextException if the template cannot be found or is invalid*/
protected void checkTemplate() throws ApplicationContextException {
try {
// Check that we can get the template, even if we might subsequently get it again.
getTemplate(getConfiguration().getLocale());
}
catch (ParseException ex) {
throw new ApplicationContextException(
"Failed to parse FreeMarker template for URL [" + getUrl() + "]", ex);
}
catch (IOException ex) {
throw new ApplicationContextException(
"Could not load FreeMarker template for URL [" + getUrl() + "]", ex);
}
}
/** ________________________________________________________________________________________
<p>This method can be overridden if custom behavior is needed.
*/
protected void renderMergedTemplateModel(
Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
}
/** ________________________________________________________________________________________
<p>Called by <code>renderMergedTemplateModel</code>. The default implementation
@param
model The model that will be passed to the template at merge time@param
request current HTTP request@throws
Exception if there's a fatal error while we're adding information to the context@see
#renderMergedTemplateModel*/
protected void exposeHelpers(Map model, HttpServletRequest request) throws Exception {
}
private TemplateModel createModel(ObjectWrapper wrapper, Map dataModel,
HttpServletRequest request, HttpServletResponse response)
throws TemplateModelException
{
try {
MultiHttpScopesHashModel allModel = new MultiHttpScopesHashModel(wrapper, getServletContext(), request);
allModel.putAll(dataModel); // copy the Spring dataModel Map into the super SimpleHashModel
// dataModel becomes LIKE "jsp page" scope i.e. no prefix. (searched 1st)
public class MultiHttpScopesHashModel extends SimpleHash
{
private final ObjectWrapper wrapper;
private final ServletContext context;
private final HttpServletRequest request;
private final Map unlistedModels = new HashMap();
private MultiHttpScopesHashModel(ObjectWrapper wrapper, ServletContext context, HttpServletRequest request) {
this.wrapper = wrapper;
this.context = context;
this.request = request;
}
}
}
Affects: 2.5.3
Referenced from: commits 4cf573b
The text was updated successfully, but these errors were encountered: