You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2025/11/21 09:14:20 UTC
(struts) 03/05: feat(freemarker): WW-5256 add configurable whitespace stripping
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 <lu...@apache.org>
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 <no...@anthropic.com>
---
.../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