From ac429a4ef7b6f0bc757fc8421ed347c3cd36c6f3 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 14 Feb 2023 16:36:07 +0100 Subject: [PATCH] Restore fallback to request attributes in FreeMarker template model Closes gh-29787 --- .../servlet/view/AbstractTemplateView.java | 14 +++--- .../view/freemarker/FreeMarkerView.java | 47 ++++++++++++++++--- .../view/freemarker/FreeMarkerViewTests.java | 29 +++++++++++- 3 files changed, 76 insertions(+), 14 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractTemplateView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractTemplateView.java index 3d028668df1..aca0d81d822 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractTemplateView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractTemplateView.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2023 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. @@ -66,6 +66,8 @@ public abstract class AbstractTemplateView extends AbstractUrlBasedView { /** * Set whether all request attributes should be added to the * model prior to merging with the template. Default is "false". + *

Note that some templates may make request attributes visible + * on their own, e.g. FreeMarker, without exposure in the MVC model. */ public void setExposeRequestAttributes(boolean exposeRequestAttributes) { this.exposeRequestAttributes = exposeRequestAttributes; @@ -73,7 +75,7 @@ public abstract class AbstractTemplateView extends AbstractUrlBasedView { /** * Set whether HttpServletRequest attributes are allowed to override (hide) - * controller generated model attributes of the same name. Default is "false", + * controller generated model attributes of the same name. Default is "false" * which causes an exception to be thrown if request attributes of the same * name as model attributes are found. */ @@ -122,11 +124,11 @@ public abstract class AbstractTemplateView extends AbstractUrlBasedView { String attribute = en.nextElement(); if (model.containsKey(attribute) && !this.allowRequestOverride) { throw new ServletException("Cannot expose request attribute '" + attribute + - "' because of an existing model object of the same name"); + "' because of an existing model object of the same name"); } Object attributeValue = request.getAttribute(attribute); if (logger.isDebugEnabled()) { - exposed = exposed != null ? exposed : new LinkedHashMap<>(); + exposed = (exposed != null ? exposed : new LinkedHashMap<>()); exposed.put(attribute, attributeValue); } model.put(attribute, attributeValue); @@ -144,11 +146,11 @@ public abstract class AbstractTemplateView extends AbstractUrlBasedView { String attribute = en.nextElement(); if (model.containsKey(attribute) && !this.allowSessionOverride) { throw new ServletException("Cannot expose session attribute '" + attribute + - "' because of an existing model object of the same name"); + "' because of an existing model object of the same name"); } Object attributeValue = session.getAttribute(attribute); if (logger.isDebugEnabled()) { - exposed = exposed != null ? exposed : new LinkedHashMap<>(); + exposed = (exposed != null ? exposed : new LinkedHashMap<>()); exposed.put(attribute, attributeValue); } model.put(attribute, attributeValue); 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 7d22d659eaa..d82d0bebb7f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -28,6 +28,8 @@ 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.ServletContext; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -59,6 +61,9 @@ import org.springframework.web.servlet.view.AbstractTemplateView; * of this approach. * *

Note: Spring's FreeMarker support requires FreeMarker 2.3 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. * * @author Darren Davison * @author Juergen Hoeller @@ -235,9 +240,6 @@ 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 @@ -256,7 +258,7 @@ public class FreeMarkerView extends AbstractTemplateView { // Expose model to JSP tags (as request attributes). exposeModelAsRequestAttributes(model, request); - // Expose all standard FreeMarker hash models. + // Expose FreeMarker hash model. SimpleHash fmModel = buildTemplateModel(model, request, response); // Grab the locale-specific version of the template. @@ -266,7 +268,8 @@ public class FreeMarkerView extends AbstractTemplateView { /** * Build a FreeMarker template model for the given model Map. - *

The default implementation builds a {@link SimpleHash}. + *

The default implementation builds a {@link SimpleHash} for the + * given MVC model with an additional fallback to request attributes. * @param model the model to use for rendering * @param request current HTTP request * @param response current servlet response @@ -275,7 +278,7 @@ public class FreeMarkerView extends AbstractTemplateView { protected SimpleHash buildTemplateModel(Map model, HttpServletRequest request, HttpServletResponse response) { - SimpleHash fmModel = new SimpleHash(getObjectWrapper()); + SimpleHash fmModel = new RequestHashModel(getObjectWrapper(), request); fmModel.putAll(model); return fmModel; } @@ -329,4 +332,34 @@ public class FreeMarkerView extends AbstractTemplateView { template.process(model, response.getWriter()); } + + /** + * 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. + */ + @SuppressWarnings("serial") + private static class RequestHashModel extends SimpleHash { + + private final HttpServletRequest request; + + public RequestHashModel(ObjectWrapper wrapper, HttpServletRequest request) { + super(wrapper); + this.request = request; + } + + @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); + } + } + } 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 0bc292f039c..a13b99b19d0 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 @@ -142,6 +142,33 @@ public class FreeMarkerViewTests { assertThat(response.getContentType()).isEqualTo("myContentType"); } + @Test + public void requestAttributeVisible() throws Exception { + FreeMarkerView fv = new FreeMarkerView(); + + 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); + + fv.setUrl("templateName"); + fv.setApplicationContext(wac); + + 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(); + + request.setAttribute("myattr", "myvalue"); + fv.render(null, request, response); + } + @Test public void freeMarkerViewResolver() throws Exception { MockServletContext sc = new MockServletContext(); @@ -189,7 +216,7 @@ public class FreeMarkerViewTests { assertThat(locale).isEqualTo(Locale.US); assertThat(model instanceof SimpleHash).isTrue(); SimpleHash fmModel = (SimpleHash) model; - assertThat(fmModel.get("myattr").toString()).isEqualTo("myvalue"); + assertThat(String.valueOf(fmModel.get("myattr"))).isEqualTo("myvalue"); } }; }