This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new 59e342488 WW-5256 Decouple FreeMarker whitespace stripping from
devMode (#1743)
59e342488 is described below
commit 59e342488d9ee6c67861eef1753aeffb09125939
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Jun 15 12:22:51 2026 +0200
WW-5256 Decouple FreeMarker whitespace stripping from devMode (#1743)
* WW-5256 docs: design to decouple FreeMarker whitespace stripping from
devMode
Fixes s:textarea rendering blank lines and HTML whitespace bloat in devMode
by honoring struts.freemarker.whitespaceStripping unconditionally.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* WW-5256 docs: implementation plan to decouple whitespace stripping from
devMode
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* WW-5256 test: prove whitespace stripping wrongly disabled in devMode
* WW-5256 fix(freemarker): honor whitespaceStripping regardless of devMode
* WW-5256 docs: drop devMode note from whitespaceStripping constant
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../java/org/apache/struts2/StrutsConstants.java | 1 -
.../views/freemarker/FreemarkerManager.java | 11 +-
.../views/freemarker/FreemarkerManagerTest.java | 29 ---
...256-freemarker-whitespace-devmode-decoupling.md | 253 +++++++++++++++++++++
...emarker-whitespace-devmode-decoupling-design.md | 125 ++++++++++
5 files changed, 380 insertions(+), 39 deletions(-)
diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java
b/core/src/main/java/org/apache/struts2/StrutsConstants.java
index 8b9ff58c1..0e9ca4fc3 100644
--- a/core/src/main/java/org/apache/struts2/StrutsConstants.java
+++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java
@@ -335,7 +335,6 @@ public final class StrutsConstants {
/**
* 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
*/
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 dcbebc767..e6432d337 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
@@ -177,7 +177,6 @@ public class FreemarkerManager {
protected int mruMaxStrongSize;
protected String templateUpdateDelay;
protected boolean whitespaceStripping = true;
- protected boolean devMode;
protected Map<String, TagLibraryModelProvider> tagLibraries;
private FileManager fileManager;
@@ -213,11 +212,6 @@ public class FreemarkerManager {
this.whitespaceStripping = BooleanUtils.toBoolean(whitespaceStripping);
}
- @Inject(value = StrutsConstants.STRUTS_DEVMODE, required = false)
- public void setDevMode(String devMode) {
- this.devMode = BooleanUtils.toBoolean(devMode);
- }
-
@Inject
public void setContainer(Container container) {
Map<String, TagLibraryModelProvider> map = new HashMap<>();
@@ -352,9 +346,8 @@ public class FreemarkerManager {
}
LOG.debug("Disabled localized lookups");
configuration.setLocalizedLookup(false);
- boolean enableWhitespaceStripping = whitespaceStripping && !devMode;
- LOG.debug("Whitespace stripping: {} (configured: {}, devMode: {})",
enableWhitespaceStripping, whitespaceStripping, devMode);
- configuration.setWhitespaceStripping(enableWhitespaceStripping);
+ LOG.debug("Whitespace stripping: {}", whitespaceStripping);
+ configuration.setWhitespaceStripping(whitespaceStripping);
LOG.debug("Sets NewBuiltinClassResolver to
TemplateClassResolver.SAFER_RESOLVER");
configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
LOG.debug("Sets HTML as an output format and escaping policy");
diff --git
a/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
b/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
index d30e4e43b..8abfafc19 100644
---
a/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
+++
b/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
@@ -131,7 +131,6 @@ public class FreemarkerManagerTest extends
StrutsInternalTestCase {
FreemarkerManager manager = new FreemarkerManager();
container.inject(manager);
manager.setWhitespaceStripping("false");
- manager.setDevMode("false");
// when
manager.init(servletContext);
@@ -139,34 +138,6 @@ public class FreemarkerManagerTest extends
StrutsInternalTestCase {
// then
assertFalse(manager.config.getWhitespaceStripping());
}
-
- public void testWhitespaceStrippingDisabledInDevMode() throws Exception {
- // given
- FreemarkerManager manager = new FreemarkerManager();
- container.inject(manager);
- manager.setWhitespaceStripping("true");
- manager.setDevMode("true");
-
- // when
- manager.init(servletContext);
-
- // then
- assertFalse(manager.config.getWhitespaceStripping());
- }
-
- public void testWhitespaceStrippingEnabledWhenNotInDevMode() throws
Exception {
- // given
- FreemarkerManager manager = new FreemarkerManager();
- container.inject(manager);
- manager.setWhitespaceStripping("true");
- manager.setDevMode("false");
-
- // when
- manager.init(servletContext);
-
- // then
- assertTrue(manager.config.getWhitespaceStripping());
- }
}
class DummyFreemarkerManager extends FreemarkerManager {
diff --git
a/docs/superpowers/plans/2026-06-15-WW-5256-freemarker-whitespace-devmode-decoupling.md
b/docs/superpowers/plans/2026-06-15-WW-5256-freemarker-whitespace-devmode-decoupling.md
new file mode 100644
index 000000000..42c11a85e
--- /dev/null
+++
b/docs/superpowers/plans/2026-06-15-WW-5256-freemarker-whitespace-devmode-decoupling.md
@@ -0,0 +1,253 @@
+# FreeMarker Whitespace Stripping / devMode Decoupling Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development (recommended) or
superpowers:executing-plans to implement this plan task-by-task. Steps use
checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Make FreeMarker whitespace stripping governed solely by
`struts.freemarker.whitespaceStripping` (default `true`), removing the
`devMode` auto-disable that broke `s:textarea` rendering and bloated HTML
output in development.
+
+**Architecture:** Remove the `&& !devMode` term in
`FreemarkerManager.buildConfiguration()` and delete the now-unused `devMode`
field/setter/injection. Update the Javadoc and the unit tests accordingly.
`TextareaTest` serves as the rendering-level regression guard.
+
+**Tech Stack:** Java, FreeMarker 2.3.34, JUnit 3-style tests (`testXxx`
methods extending `StrutsInternalTestCase`), Maven.
+
+**Spec:**
`docs/superpowers/specs/2026-06-15-WW-5256-freemarker-whitespace-devmode-decoupling-design.md`
+
+**Ticket:** [WW-5256](https://issues.apache.org/jira/browse/WW-5256)
+
+---
+
+### Task 1: Prove the bug with a failing test (RED)
+
+This task documents the current broken behavior: with `devMode=true` and
whitespace stripping
+configured `true`, the manager wrongly reports stripping as disabled. This
test is temporary —
+it uses the `setDevMode` API that Task 2 removes, so Task 2 deletes it.
+
+**Files:**
+- Test:
`core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java`
+
+- [ ] **Step 1: Add the temporary failing test**
+
+Insert this method after `testWhitespaceStrippingEnabledWhenNotInDevMode`
(currently ends at line 169, before the closing `}` of the class):
+
+```java
+ // TEMP (WW-5256): documents the pre-fix bug; removed in the same change
that removes the devMode coupling.
+ public void testWhitespaceStrippingNotDisabledInDevMode() throws Exception
{
+ // given
+ FreemarkerManager manager = new FreemarkerManager();
+ container.inject(manager);
+ manager.setWhitespaceStripping("true");
+ manager.setDevMode("true");
+
+ // when
+ manager.init(servletContext);
+
+ // then
+ assertTrue(manager.config.getWhitespaceStripping());
+ }
+```
+
+- [ ] **Step 2: Run the test to verify it fails**
+
+Run: `mvn test -DskipAssembly -pl core
-Dtest=FreemarkerManagerTest#testWhitespaceStrippingNotDisabledInDevMode`
+Expected: FAIL — assertion error, `getWhitespaceStripping()` returns `false`
because the current code computes `whitespaceStripping && !devMode`.
+
+- [ ] **Step 3: Commit the failing test**
+
+```bash
+git add
core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
+git commit -m "WW-5256 test: prove whitespace stripping wrongly disabled in
devMode"
+```
+
+---
+
+### Task 2: Decouple stripping from devMode (GREEN)
+
+Apply the production fix and align the test suite. Because the `setDevMode`
setter is removed,
+the temporary test from Task 1 and the two original devMode-coupling tests are
deleted, and the
+config test drops its `setDevMode` call.
+
+**Files:**
+- Modify:
`core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java`
(field at line 180; setter at 216-219; coupling at 355-357)
+- Modify:
`core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java`
(lines 129-169)
+
+- [ ] **Step 1: Replace the coupling in `FreemarkerManager`**
+
+Find (lines 355-357):
+
+```java
+ boolean enableWhitespaceStripping = whitespaceStripping && !devMode;
+ LOG.debug("Whitespace stripping: {} (configured: {}, devMode: {})",
enableWhitespaceStripping, whitespaceStripping, devMode);
+ configuration.setWhitespaceStripping(enableWhitespaceStripping);
+```
+
+Replace with:
+
+```java
+ LOG.debug("Whitespace stripping: {}", whitespaceStripping);
+ configuration.setWhitespaceStripping(whitespaceStripping);
+```
+
+- [ ] **Step 2: Remove the unused `devMode` field**
+
+Find (line 180):
+
+```java
+ protected boolean devMode;
+```
+
+Delete this line.
+
+- [ ] **Step 3: Remove the unused `setDevMode` setter and its injection**
+
+Find (lines 216-219):
+
+```java
+
+ @Inject(value = StrutsConstants.STRUTS_DEVMODE, required = false)
+ public void setDevMode(String devMode) {
+ this.devMode = BooleanUtils.toBoolean(devMode);
+ }
+```
+
+Delete this block (the setter plus the blank line preceding it).
+
+- [ ] **Step 4: Delete the temporary test from Task 1 and the two
devMode-coupling tests**
+
+In `FreemarkerManagerTest.java`, delete these three methods entirely:
+- `testWhitespaceStrippingNotDisabledInDevMode` (added in Task 1)
+- `testWhitespaceStrippingDisabledInDevMode` (lines 143-155)
+- `testWhitespaceStrippingEnabledWhenNotInDevMode` (lines 157-169)
+
+- [ ] **Step 5: Drop the `setDevMode` call from the config test**
+
+Find (lines 129-141):
+
+```java
+ public void testWhitespaceStrippingDisabledViaConfiguration() throws
Exception {
+ // given
+ FreemarkerManager manager = new FreemarkerManager();
+ container.inject(manager);
+ manager.setWhitespaceStripping("false");
+ manager.setDevMode("false");
+
+ // when
+ manager.init(servletContext);
+
+ // then
+ assertFalse(manager.config.getWhitespaceStripping());
+ }
+```
+
+Replace with (remove the `manager.setDevMode("false");` line):
+
+```java
+ public void testWhitespaceStrippingDisabledViaConfiguration() throws
Exception {
+ // given
+ FreemarkerManager manager = new FreemarkerManager();
+ container.inject(manager);
+ manager.setWhitespaceStripping("false");
+
+ // when
+ manager.init(servletContext);
+
+ // then
+ assertFalse(manager.config.getWhitespaceStripping());
+ }
+```
+
+- [ ] **Step 6: Verify no remaining references to the removed API**
+
+Run: `grep -rn "setDevMode\|\.devMode"
core/src/main/java/org/apache/struts2/views/freemarker/
core/src/test/java/org/apache/struts2/views/freemarker/`
+Expected: no matches.
+
+- [ ] **Step 7: Run the FreemarkerManager tests**
+
+Run: `mvn test -DskipAssembly -pl core -Dtest=FreemarkerManagerTest`
+Expected: PASS — `testWhitespaceStrippingEnabledByDefault` and
`testWhitespaceStrippingDisabledViaConfiguration` both green; class compiles
with no reference to `devMode`/`setDevMode`.
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add
core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java
core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java
+git commit -m "WW-5256 fix(freemarker): honor whitespaceStripping regardless
of devMode"
+```
+
+---
+
+### Task 3: Update the configuration Javadoc
+
+**Files:**
+- Modify: `core/src/main/java/org/apache/struts2/StrutsConstants.java` (lines
335-342)
+
+- [ ] **Step 1: Remove the stale devMode sentence**
+
+Find (lines 335-342):
+
+```java
+ /**
+ * 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";
+```
+
+Replace with:
+
+```java
+ /**
+ * Controls FreeMarker whitespace stripping during template compilation.
+ * When enabled (default), removes indentation and trailing whitespace
from lines containing only FTL tags.
+ *
+ * @since 7.2.0
+ */
+ public static final String STRUTS_FREEMARKER_WHITESPACE_STRIPPING =
"struts.freemarker.whitespaceStripping";
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add core/src/main/java/org/apache/struts2/StrutsConstants.java
+git commit -m "WW-5256 docs: drop devMode note from whitespaceStripping
constant"
+```
+
+---
+
+### Task 4: Confirm rendering regression guard and full module build
+
+`TextareaTest` renders the `s:textarea` tag against expected output fixtures.
With stripping now
+unconditionally on by default, it must stay green — this is the user-visible
guarantee that the
+textarea no longer emits blank lines.
+
+**Files:**
+- Verify only:
`core/src/test/java/org/apache/struts2/views/jsp/ui/TextareaTest.java`
+
+- [ ] **Step 1: Run the textarea rendering tests**
+
+Run: `mvn test -DskipAssembly -pl core -Dtest=TextareaTest`
+Expected: PASS — rendered output matches `Textarea-1.txt` / `Textarea-2.txt`
fixtures (no internal blank lines).
+
+- [ ] **Step 2: Run the full core test module**
+
+Run: `mvn test -DskipAssembly -pl core`
+Expected: BUILD SUCCESS — no compilation errors from the removed `devMode`
API, all tests pass.
+
+- [ ] **Step 3: No commit needed**
+
+This task is verification only; no source changes.
+
+---
+
+## Self-Review
+
+**Spec coverage:**
+- Decouple stripping from devMode → Task 2 Step 1. ✓
+- Remove `devMode` field/setter/`@Inject` → Task 2 Steps 2-3. ✓
+- `default.properties` unchanged → no task needed (intentional). ✓
+- Javadoc cleanup → Task 3. ✓
+- Test updates (remove 2 devMode tests, drop stray `setDevMode` call, keep
default/explicit tests) → Task 2 Steps 4-5. ✓
+- Verification (`FreemarkerManagerTest`, textarea rendering, no internal blank
lines) → Task 2 Step 7, Task 4. ✓
+
+**Placeholder scan:** No TBD/TODO; every code step shows exact before/after
content. The one `TEMP` marker is an intentional, scoped throwaway test removed
within the same plan (Task 2 Step 4). ✓
+
+**Type/signature consistency:** `whitespaceStripping` (existing `protected
boolean`, default `true`) and `config.getWhitespaceStripping()` used
consistently across tasks; `setDevMode`/`devMode` only referenced in the
temporary Task 1 test and the deletions in Task 2. ✓
diff --git
a/docs/superpowers/specs/2026-06-15-WW-5256-freemarker-whitespace-devmode-decoupling-design.md
b/docs/superpowers/specs/2026-06-15-WW-5256-freemarker-whitespace-devmode-decoupling-design.md
new file mode 100644
index 000000000..bd74accd5
--- /dev/null
+++
b/docs/superpowers/specs/2026-06-15-WW-5256-freemarker-whitespace-devmode-decoupling-design.md
@@ -0,0 +1,125 @@
+---
+date: 2026-06-15
+ticket: WW-5256
+url: https://issues.apache.org/jira/browse/WW-5256
+status: design
+---
+
+# WW-5256 — Decouple FreeMarker whitespace stripping from devMode
+
+## Problem
+
+In `release/7.2.0-RC1`, `s:textarea` renders blank lines inside an empty
textarea, and
+the overall HTML output contains noticeably more whitespace than in 6.x.
Reported symptom
+on CRUD entry/add pages: an empty `bean.data` (null default) textarea shows up
as two blank
+lines on screen, and the raw HTML source has far more whitespace throughout
than the 6.x
+equivalent.
+
+## Root cause
+
+The FreeMarker templates are **not** the cause. `textarea.ftl` and all of its
included
+sub-templates (`css.ftl`, `scripting-events.ftl`, `common-attributes.ftl`,
+`dynamic-attributes.ftl`) are byte-for-byte identical between
`support/struts-6-x-x` and
+`release/7.2.0-RC1` apart from the `parameters` → `attributes` rename. Their
whitespace
+structure is unchanged.
+
+The regression is in `FreemarkerManager.java:355`, introduced by WW-5256
+(commit `9305a5812`, `@since 7.2.0`):
+
+```java
+// 6.x — whitespace stripping unconditionally on:
+configuration.setWhitespaceStripping(true);
+
+// 7.2.0-RC1 — stripping turned off whenever devMode is on:
+boolean enableWhitespaceStripping = whitespaceStripping && !devMode;
+configuration.setWhitespaceStripping(enableWhitespaceStripping);
+```
+
+When `struts.devMode=true` (the normal development setting), whitespace
stripping is
+forced off. FreeMarker then stops collapsing directive-only lines (e.g. `<#if
…>` /
+`</#if>`), which produces two distinct effects:
+
+1. **General whitespace bloat** — every directive-only line in every UI
template now emits
+ its newline and indentation.
+2. **Visible `s:textarea` breakage** — the directive lines around `nameValue`
sit *inside*
+ `<textarea>…</textarea>`, where whitespace is **significant content**. The
collapsed
+ newlines that 6.x removed now render as blank lines in the browser.
+
+The `&& !devMode` term also **overrides** the
`struts.freemarker.whitespaceStripping`
+config flag: a developer running in devMode has no way to turn stripping back
on.
+
+### Why the coupling was introduced (and why it was wrong)
+
+Per the WW-5256 research note
+(`thoughts/shared/research/2025-09-24-WW-5256-freemarker-whitespace-compression.md`),
the
+devMode auto-disable was copied by analogy from the new `<s:compress>` tag,
which
+intentionally disables compression in devMode with a `force` override. The
stated goal was
+"readable output while debugging." The note itself raised this as an *open
question*
+("Should whitespace stripping be automatically disabled in DevMode … or
require explicit
+configuration?") rather than a settled decision.
+
+The analogy does not hold: for most tags the extra dev whitespace is harmless
cosmetic
+noise, but for `<textarea>` (and `<pre>`) the stripped newlines are
semantically
+significant page content. "Make dev output readable" therefore silently broke
`s:textarea`
+rendering, with no escape hatch. The configurable flag from WW-5256 is the
useful part and
+is kept; the devMode auto-disable is the unvalidated part and is removed.
+
+## Design
+
+Decouple whitespace stripping from devMode. Honor
`struts.freemarker.whitespaceStripping`
+(default `true`) unconditionally. This restores 6.x behavior by default while
keeping an
+explicit opt-out for anyone who genuinely wants raw, un-collapsed template
output.
+
+### Changes
+
+1.
**`core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerManager.java`**
+ - Replace the coupling with a direct call:
+ ```java
+ configuration.setWhitespaceStripping(whitespaceStripping);
+ ```
+ - Simplify the debug log to drop the devMode reference.
+ - Remove the now-unused `devMode` field, the `setDevMode(String)` setter,
and its
+ `@Inject(value = StrutsConstants.STRUTS_DEVMODE, required = false)`
annotation. The
+ field was added in 7.2.0 solely for this coupling and is referenced
nowhere else in
+ the class, so removing it before final 7.2.0 release breaks no released
API.
+
+2. **`core/src/main/java/org/apache/struts2/StrutsConstants.java`**
+ - Remove the line "*Automatically disabled when devMode is enabled.*" from
the
+ `STRUTS_FREEMARKER_WHITESPACE_STRIPPING` Javadoc. Keep `@since 7.2.0`.
+
+3. **`core/src/main/resources/org/apache/struts2/default.properties`**
+ - No change. `struts.freemarker.whitespaceStripping=true` remains the
default.
+
+4.
**`core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerManagerTest.java`**
+ - Remove `testWhitespaceStrippingDisabledInDevMode` and
+ `testWhitespaceStrippingEnabledWhenNotInDevMode` — they assert the
removed coupling and
+ call the removed `setDevMode` setter.
+ - Remove the stray `manager.setDevMode("false")` call from
+ `testWhitespaceStrippingDisabledViaConfiguration`.
+ - Keep `testWhitespaceStrippingEnabledByDefault` and the explicit-disable
test; together
+ they fully cover the remaining behavior (default on; honored when set
false).
+
+### Behavior after the change
+
+| devMode | `struts.freemarker.whitespaceStripping` | Stripping enabled? |
+|---------|-----------------------------------------|--------------------|
+| any | unset (default) | yes (matches 6.x) |
+| any | `true` | yes |
+| any | `false` | no |
+
+## Scope and risk
+
+- Contained, single-purpose fix landing in `release/7.2.0-RC1` before the
final 7.2.0
+ release. No migration or backward-compatibility concern, since the removed
`devMode`
+ wiring was never in a released version.
+- **Not** a security change — this is purely rendering/whitespace behavior.
The normal
+ PR flow applies (no private security-triage path needed).
+- The `<s:compress>` tag's own devMode/`force` behavior is unrelated and
unchanged.
+
+## Verification
+
+- `mvn test -DskipAssembly -pl core -Dtest=FreemarkerManagerTest` passes with
the updated
+ test set.
+- Manual check (or an `AbstractUITagTest`-style assertion): an `s:textarea`
bound to a null
+ value renders as `<textarea …></textarea>` with no internal blank lines when
+ `struts.devMode=true`.