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

Reply via email to