This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch feature/WW-5256-freemarker-compress in repository https://gitbox.apache.org/repos/asf/struts.git
commit 6c87d10fbca24c4c18e276e4b59c84fd87b32f1b Author: Lukasz Lenart <[email protected]> AuthorDate: Fri Nov 21 10:06:04 2025 +0100 feat(freemarker): WW-5256 add configurable whitespace stripping - Add struts.freemarker.whitespaceStripping configuration option - Automatically disable whitespace stripping in devMode - Add @since 7.2.0 tags to new Compress component and configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --- .../java/org/apache/struts2/StrutsConstants.java | 10 ++ .../org/apache/struts2/components/Compress.java | 2 + .../views/freemarker/FreemarkerManager.java | 120 +++++++++++---------- .../org/apache/struts2/views/jsp/CompressTag.java | 1 + .../org/apache/struts2/default.properties | 4 + 5 files changed, 82 insertions(+), 55 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java index 3436f7773..457e9642c 100644 --- a/core/src/main/java/org/apache/struts2/StrutsConstants.java +++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java @@ -326,6 +326,15 @@ public final class StrutsConstants { public static final String STRUTS_FREEMARKER_WRAPPER_ALT_MAP = "struts.freemarker.wrapper.altMap"; + /** + * Controls FreeMarker whitespace stripping during template compilation. + * When enabled (default), removes indentation and trailing whitespace from lines containing only FTL tags. + * Automatically disabled when devMode is enabled. + * + * @since 7.2.0 + */ + public static final String STRUTS_FREEMARKER_WHITESPACE_STRIPPING = "struts.freemarker.whitespaceStripping"; + /** * Extension point for the Struts CompoundRootAccessor */ @@ -676,6 +685,7 @@ public final class StrutsConstants { /** * See {@link org.apache.struts2.interceptor.csp.CspNonceReader} + * * @since 6.8.0 */ public static final String STRUTS_CSP_NONCE_READER = "struts.csp.nonce.reader"; diff --git a/core/src/main/java/org/apache/struts2/components/Compress.java b/core/src/main/java/org/apache/struts2/components/Compress.java index 6131e5f33..aa6765c07 100644 --- a/core/src/main/java/org/apache/struts2/components/Compress.java +++ b/core/src/main/java/org/apache/struts2/components/Compress.java @@ -63,6 +63,8 @@ import java.io.Writer; * <!-- END SNIPPET: example --> * </pre> * "shouldCompress" is a field with getter define on action used in expression evaluation + * + * @since 7.2.0 */ @StrutsTag(name = "compress", tldTagClass = "org.apache.struts2.views.jsp.CompressTag", description = "Compress wrapped content") diff --git a/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java b/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java index ddc3682e6..dcbebc767 100644 --- a/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java +++ b/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java @@ -18,6 +18,7 @@ */ package org.apache.struts2.views.freemarker; +import org.apache.commons.lang3.BooleanUtils; import org.apache.struts2.FileManager; import org.apache.struts2.FileManagerFactory; import org.apache.struts2.inject.Container; @@ -111,23 +112,23 @@ import java.util.Set; public class FreemarkerManager { // copied from freemarker servlet - so that there is no dependency on it - public static final String INITPARAM_TEMPLATE_PATH = "TemplatePath"; - public static final String INITPARAM_NOCACHE = "NoCache"; - public static final String INITPARAM_CONTENT_TYPE = "ContentType"; - public static final String DEFAULT_CONTENT_TYPE = "text/html"; - public static final String INITPARAM_DEBUG = "Debug"; - - public static final String KEY_REQUEST = "Request"; - public static final String KEY_SESSION = "Session"; - public static final String KEY_APPLICATION = "Application"; - public static final String KEY_APPLICATION_PRIVATE = "__FreeMarkerServlet.Application__"; - public static final String KEY_JSP_TAGLIBS = "JspTaglibs"; - - // Note these names start with dot, so they're essentially invisible from a freemarker script. - private static final String ATTR_REQUEST_MODEL = ".freemarker.Request"; - private static final String ATTR_REQUEST_PARAMETERS_MODEL = ".freemarker.RequestParameters"; - private static final String ATTR_APPLICATION_MODEL = ".freemarker.Application"; - private static final String ATTR_JSP_TAGLIBS_MODEL = ".freemarker.JspTaglibs"; + public static final String INITPARAM_TEMPLATE_PATH = "TemplatePath"; + public static final String INITPARAM_NOCACHE = "NoCache"; + public static final String INITPARAM_CONTENT_TYPE = "ContentType"; + public static final String DEFAULT_CONTENT_TYPE = "text/html"; + public static final String INITPARAM_DEBUG = "Debug"; + + public static final String KEY_REQUEST = "Request"; + public static final String KEY_SESSION = "Session"; + public static final String KEY_APPLICATION = "Application"; + public static final String KEY_APPLICATION_PRIVATE = "__FreeMarkerServlet.Application__"; + public static final String KEY_JSP_TAGLIBS = "JspTaglibs"; + + // Note these names start with dot, so they're essentially invisible from a freemarker script. + private static final String ATTR_REQUEST_MODEL = ".freemarker.Request"; + private static final String ATTR_REQUEST_PARAMETERS_MODEL = ".freemarker.RequestParameters"; + private static final String ATTR_APPLICATION_MODEL = ".freemarker.Application"; + private static final String ATTR_JSP_TAGLIBS_MODEL = ".freemarker.JspTaglibs"; // for sitemesh public static final String ATTR_TEMPLATE_MODEL = ".freemarker.TemplateModel"; // template model stored in request for siteMesh @@ -162,7 +163,6 @@ public class FreemarkerManager { public static final String KEY_EXCEPTION = "exception"; - protected String templatePath; protected boolean nocache; protected boolean debug; @@ -176,7 +176,9 @@ public class FreemarkerManager { protected boolean cacheBeanWrapper; protected int mruMaxStrongSize; protected String templateUpdateDelay; - protected Map<String,TagLibraryModelProvider> tagLibraries; + protected boolean whitespaceStripping = true; + protected boolean devMode; + protected Map<String, TagLibraryModelProvider> tagLibraries; private FileManager fileManager; private FreemarkerThemeTemplateLoader themeTemplateLoader; @@ -203,7 +205,17 @@ public class FreemarkerManager { @Inject(value = StrutsConstants.STRUTS_FREEMARKER_TEMPLATES_CACHE_UPDATE_DELAY, required = false) public void setTemplateUpdateDelay(String delay) { - templateUpdateDelay = delay; + templateUpdateDelay = delay; + } + + @Inject(value = StrutsConstants.STRUTS_FREEMARKER_WHITESPACE_STRIPPING, required = false) + public void setWhitespaceStripping(String whitespaceStripping) { + this.whitespaceStripping = BooleanUtils.toBoolean(whitespaceStripping); + } + + @Inject(value = StrutsConstants.STRUTS_DEVMODE, required = false) + public void setDevMode(String devMode) { + this.devMode = BooleanUtils.toBoolean(devMode); } @Inject @@ -295,7 +307,6 @@ public class FreemarkerManager { * at the top. * * @param templateLoader the template loader - * * @see org.apache.struts2.views.freemarker.FreemarkerThemeTemplateLoader */ protected void configureTemplateLoader(TemplateLoader templateLoader) { @@ -341,8 +352,9 @@ public class FreemarkerManager { } LOG.debug("Disabled localized lookups"); configuration.setLocalizedLookup(false); - LOG.debug("Enabled whitespace stripping"); - configuration.setWhitespaceStripping(true); + boolean enableWhitespaceStripping = whitespaceStripping && !devMode; + LOG.debug("Whitespace stripping: {} (configured: {}, devMode: {})", enableWhitespaceStripping, whitespaceStripping, devMode); + configuration.setWhitespaceStripping(enableWhitespaceStripping); LOG.debug("Sets NewBuiltinClassResolver to TemplateClassResolver.SAFER_RESOLVER"); configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER); LOG.debug("Sets HTML as an output format and escaping policy"); @@ -413,7 +425,7 @@ public class FreemarkerManager { request.setAttribute(ATTR_REQUEST_PARAMETERS_MODEL, reqParametersModel); } model.put(ATTR_REQUEST_PARAMETERS_MODEL, reqParametersModel); - model.put(KEY_REQUEST_PARAMETERS_STRUTS,reqParametersModel); + model.put(KEY_REQUEST_PARAMETERS_STRUTS, reqParametersModel); return model; } @@ -426,56 +438,55 @@ public class FreemarkerManager { } - /** + /** * Create the template loader. The default implementation will create a * {@link ClassTemplateLoader} if the template path starts with "class://", * a {@link FileTemplateLoader} if the template path starts with "file://", * and a {@link WebappTemplateLoader} otherwise. * * @param servletContext the servlet path - * @param templatePath the template path to create a loader for + * @param templatePath the template path to create a loader for * @return a newly created template loader */ protected TemplateLoader createTemplateLoader(ServletContext servletContext, String templatePath) { TemplateLoader templatePathLoader = null; - try { - if(templatePath!=null){ - if (templatePath.startsWith("class://")) { - // substring(7) is intentional as we "reuse" the last slash - templatePathLoader = new ClassTemplateLoader(getClass(), templatePath.substring(7)); - } else if (templatePath.startsWith("file://")) { - templatePathLoader = new FileTemplateLoader(new File(templatePath.substring(7))); - } - } - } catch (IOException e) { - LOG.error("Invalid template path specified: {}", e.getMessage(), e); - } - - // presume that most apps will require the class and webapp template loader - // if people wish to - return templatePathLoader != null ? - new MultiTemplateLoader(new TemplateLoader[]{ - templatePathLoader, - new WebappTemplateLoader(servletContext), - new StrutsClassTemplateLoader() - }) - : new MultiTemplateLoader(new TemplateLoader[]{ - new WebappTemplateLoader(servletContext), - new StrutsClassTemplateLoader() - }); - } + try { + if (templatePath != null) { + if (templatePath.startsWith("class://")) { + // substring(7) is intentional as we "reuse" the last slash + templatePathLoader = new ClassTemplateLoader(getClass(), templatePath.substring(7)); + } else if (templatePath.startsWith("file://")) { + templatePathLoader = new FileTemplateLoader(new File(templatePath.substring(7))); + } + } + } catch (IOException e) { + LOG.error("Invalid template path specified: {}", e.getMessage(), e); + } + + // presume that most apps will require the class and webapp template loader + // if people wish to + return templatePathLoader != null ? + new MultiTemplateLoader(new TemplateLoader[]{ + templatePathLoader, + new WebappTemplateLoader(servletContext), + new StrutsClassTemplateLoader() + }) + : new MultiTemplateLoader(new TemplateLoader[]{ + new WebappTemplateLoader(servletContext), + new StrutsClassTemplateLoader() + }); + } /** * Load the settings from the /freemarker.properties file on the classpath * * @param servletContext the servlet context - * * @see freemarker.template.Configuration#setSettings for the definition of valid settings */ protected void loadSettings(ServletContext servletContext) { - try (InputStream in = fileManager.loadFile(ClassLoaderUtil.getResource("freemarker.properties", getClass()))){ + try (InputStream in = fileManager.loadFile(ClassLoaderUtil.getResource("freemarker.properties", getClass()))) { if (in != null) { Properties p = new Properties(); p.load(in); @@ -528,7 +539,6 @@ public class FreemarkerManager { } - public ScopesHashModel buildTemplateModel(ValueStack stack, Object action, ServletContext servletContext, HttpServletRequest request, HttpServletResponse response, ObjectWrapper wrapper) { ScopesHashModel model = buildScopesHashModel(servletContext, request, response, wrapper, stack); populateContext(model, stack, action, request, response); diff --git a/core/src/main/java/org/apache/struts2/views/jsp/CompressTag.java b/core/src/main/java/org/apache/struts2/views/jsp/CompressTag.java index 4b9821e53..27d43a467 100644 --- a/core/src/main/java/org/apache/struts2/views/jsp/CompressTag.java +++ b/core/src/main/java/org/apache/struts2/views/jsp/CompressTag.java @@ -28,6 +28,7 @@ import java.io.Serial; /** * @see org.apache.struts2.components.Compress + * @since 7.2.0 */ public class CompressTag extends ComponentTagSupport { diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties index 175530e0e..60c77514f 100644 --- a/core/src/main/resources/org/apache/struts2/default.properties +++ b/core/src/main/resources/org/apache/struts2/default.properties @@ -207,6 +207,10 @@ struts.freemarker.wrapper.altMap=true ### check WW-3766 for more details struts.freemarker.mru.max.strong.size=0 +### Controls FreeMarker whitespace stripping during template compilation. +### Automatically disabled when devMode is enabled. +struts.freemarker.whitespaceStripping=true + ### configure the XSLTResult class to use stylesheet caching. ### Set to true for developers and false for production. struts.xslt.nocache=false
