Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FreeMarkerView should support AllHttpScopesHashModel (analogous to FreeMarkerServlet in FreeMarker 2.3.14) [SPR-4962] #9637

Closed
spring-projects-issues opened this issue Jun 26, 2008 · 3 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Milestone

Comments

@spring-projects-issues
Copy link
Collaborator

spring-projects-issues commented Jun 26, 2008

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

  http://www.apache.org/licenses/LICENSE-2.0
  • 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;

    /** ________________________________________________________________________________________

    • Set the encoding of the FreeMarker template file. Default is determined
    • by the FreeMarker Configuration: "ISO-8859-1" if not specified otherwise.

    <p>Specify the encoding in the FreeMarker Configuration rather than per

    • template if all your templates share a common encoding.
      */
      public void setEncoding(String encoding) {
      this.encoding = encoding;
      }

    /** ________________________________________________________________________________________

    • Return the encoding for the FreeMarker template.
      */
      protected String getEncoding() {
      return this.encoding;
      }

    /** ________________________________________________________________________________________

    • Set the FreeMarker Configuration to be used by this view.
    • If this is 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(Configuration configuration) {
      this.configuration = configuration;
      }

    /** ________________________________________________________________________________________

    • Return the FreeMarker configuration used by this view.
      */
      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();
      }

    /** ________________________________________________________________________________________

    • Autodetect a {@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);
      }
      }

    /** ________________________________________________________________________________________

    • Return the configured FreeMarker {@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);
      }

    /** ________________________________________________________________________________________

    • Check that the FreeMarker template used for this view exists and is valid.

    <p>Can be overridden to customize the behavior, for example in case of

    • multiple templates to be rendered into a single view.
    • @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);
      }
      }

    /** ________________________________________________________________________________________

    • Process the model map by merging it with the FreeMarker template.
    • Output is directed to the servlet response.

    <p>This method can be overridden if custom behavior is needed.

    */
    protected void renderMergedTemplateModel(
    Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {

    exposeHelpers(model, request);
    doRender(model, request, response);
    

    }

    /** ________________________________________________________________________________________

    • Expose helpers unique to each rendering operation. This is necessary so that
    • different rendering operations can't overwrite each other's formats etc.

    <p>Called by <code>renderMergedTemplateModel</code>. The default implementation

    • is empty. This method can be overridden to add custom helpers to the model.
    • @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)

		allModel.putUnlistedModel(FreemarkerServlet.KEY_JSP_TAGLIBS, this.taglibFactory);
		allModel.putUnlistedModel(FreemarkerServlet.KEY_APPLICATION, this.servletContextHashModel);
		// servletModel becomes Application. scope (searched last)

		allModel.putUnlistedModel(FreemarkerServlet.KEY_SESSION, buildSessionModel(request, response));
		// SessionModel becomes Session. scope (searched 3rd)

		allModel.putUnlistedModel(FreemarkerServlet.KEY_REQUEST, 
																	new HttpRequestHashModel(request, response, getObjectWrapper()));
		// RequestModel becomes Request. scope (searched 2nd)

		allModel.putUnlistedModel(FreemarkerServlet.KEY_REQUEST_PARAMETERS, 
																		new HttpRequestParametersHashModel(request));
		// RequestParameters. scope you have to reference with RequestParameters. prefix (not searched)
		
		return (TemplateModel) allModel;
		
	} catch (Exception e) {
			throw new TemplateModelException(e);
	}
}


/** ________________________________________________________________________________________
 * Render the FreeMarker view to the given response, using the given model
 * map which contains the complete template model to use.
 * <p>The default implementation renders the template specified by the "url"
 * bean property, retrieved via <code>getTemplate</code>. It delegates to the
 * <code>processTemplate</code> 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 template model to use for rendering
 * @param request current HTTP request
 * @param response current servlet response
 * @throws IOException if the template file could not be retrieved
 * @throws Exception if rendering failed
 * @see #setUrl
 * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale
 * @see #getTemplate(java.util.Locale)
 * @see #processTemplate
 * @see freemarker.ext.servlet.FreemarkerServlet
 */
protected void doRender(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
	// Expose model to JSP tags (as request attributes).
	exposeModelAsRequestAttributes(model, request);

	// Expose all standard FreeMarker hash models.
	TemplateModel allScopesModel = createModel(getObjectWrapper(), model, request, response);
	
	if (logger.isDebugEnabled()) {
		logger.debug("Rendering FreeMarker template [" + getUrl() + "] in FreeMarkerView '" + getBeanName() + "'");
	}
	// Grab the locale-specific version of the template.
	Locale locale = RequestContextUtils.getLocale(request);
	processTemplate(getTemplate(locale), allScopesModel, response);
}

/** ________________________________________________________________________________________
 * 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 template for the given locale,
 * to be rendering by this view.
 * <p>By default, the template specified by the "url" bean property
 * will be retrieved.
 * @param locale the current locale
 * @return the FreeMarker template to render
 * @throws IOException if the template file could not be retrieved
 * @see #setUrl
 * @see #getTemplate(String, java.util.Locale)
 */
protected Template getTemplate(Locale locale) throws IOException {
	return getTemplate(getUrl(), locale);
}

/** ________________________________________________________________________________________
 * Retrieve the FreeMarker template specified by the given name,
 * using the encoding specified by the "encoding" bean property.
 * <p>Can be called by subclasses to retrieve a specific template,
 * for example to render multiple templates into a single view.
 * @param name the file name of the desired template
 * @param locale the current locale
 * @return the FreeMarker template
 * @throws IOException if the template file could not be retrieved
 */
protected Template getTemplate(String name, Locale locale) throws IOException {
	return (getEncoding() != null ?
			getConfiguration().getTemplate(name, locale, getEncoding()) :
			getConfiguration().getTemplate(name, locale));
}

/** ________________________________________________________________________________________
 * Process the FreeMarker template to the servlet response.
 * <p>Can be overridden to customize the behavior.
 * @param template the template to process
 * @param model the model for the template
 * @param response servlet response (use this to get the OutputStream or Writer)
 * @throws IOException if the template file could not be retrieved
 * @throws TemplateException if thrown by FreeMarker
 * @see freemarker.template.Template#process(Object, java.io.Writer)
 */
protected void processTemplate(Template template, TemplateModel model, HttpServletResponse response)
		throws IOException, TemplateException {

	template.process(model, response.getWriter());
}


/** ________________________________________________________________________________________
 * Simple adapter class that extends {@link GenericServlet}.
 * Needed for JSP access in FreeMarker.
 */
private static class GenericServletAdapter extends GenericServlet {

	public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
		// no-op
	}
}


/** ________________________________________________________________________________________
 * Internal implementation of the {@link ServletConfig} interface,
 * to be passed to the servlet adapter.
 */
private class DelegatingServletConfig implements ServletConfig {

	public String getServletName() {
		return FreeMarkerView.this.getBeanName();
	}

	public ServletContext getServletContext() {
		return FreeMarkerView.this.getServletContext();
	}

	public String getInitParameter(String paramName) {
		return null;
	}

	public Enumeration getInitParameterNames() {
		return Collections.enumeration(Collections.EMPTY_SET);
	}
}

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;
}

/**
 * Stores a model in the hash so that it doesn't show up in <tt>keys()</tt>
 * and <tt>values()</tt> methods. Used to put the Application, Session,
 * Request, RequestParameters and JspTaglibs objects.
 * @param key the key under which the model is stored
 * @param model the stored model
 */
public void putUnlistedModel(String key, TemplateModel model)
{
    unlistedModels.put(key, model);
}

public TemplateModel get(String key) throws TemplateModelException {
    // Lookup in page scope
    TemplateModel model = super.get(key);
    if(model != null) {
        return model;
    }

    // Look in unlisted models
    model = (TemplateModel)unlistedModels.get(key);
    if(model != null) {
        return model;
    }
    
    // Lookup in request scope
    Object obj = request.getAttribute(key);
    if(obj != null) {
        return wrapper.wrap(obj);
    }

    // Lookup in session scope
    HttpSession session = request.getSession(false);
    if(session != null) {
        obj = session.getAttribute(key);
        if(obj != null) {
            return wrapper.wrap(obj);
        }
    }

    // Lookup in application scope
    obj = context.getAttribute(key);
    if(obj != null) {
        return wrapper.wrap(obj);
    }

    // return wrapper's null object (probably null).        
    return wrapper.wrap(null);
}

}

}


Affects: 2.5.3

Referenced from: commits 4cf573b

@spring-projects-issues
Copy link
Collaborator Author

Attila Szegedi commented

AllHttpScopesHashModel can now be publicly constructed -- as of FreeMarker 2.3.14, just released few days ago.

@spring-projects-issues
Copy link
Collaborator Author

Juergen Hoeller commented

Finally addressed for Spring 3.0 M3, updating to FreeMarker 2.3.15.

Juergen

@spring-projects-issues
Copy link
Collaborator Author

ismail Seyfi commented

I just ran into this issue with Spring 3.0.4 and Freemarker 2.3.16 in Oracle WebLogic Server PS2 (10.3.3). The combination works fine in Tomcat 6.

@spring-projects-issues spring-projects-issues added type: enhancement A general enhancement in: web Issues in web modules (web, webmvc, webflux, websocket) labels Jan 11, 2019
@spring-projects-issues spring-projects-issues added this to the 3.0 M3 milestone Jan 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

2 participants