This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch WW-5641-json-writer-reader-override in repository https://gitbox.apache.org/repos/asf/struts.git
commit 29bfea5a6c3b0453a100945ecd810b79cefc6f8a Author: Lukasz Lenart <[email protected]> AuthorDate: Mon Jul 6 19:25:30 2026 +0200 WW-5641 docs: implementation plan for JSON writer/reader override fix Co-Authored-By: Claude Opus 4.8 <[email protected]> --- ...26-07-06-WW-5641-json-writer-reader-override.md | 337 +++++++++++++++++++++ 1 file changed, 337 insertions(+) diff --git a/docs/superpowers/plans/2026-07-06-WW-5641-json-writer-reader-override.md b/docs/superpowers/plans/2026-07-06-WW-5641-json-writer-reader-override.md new file mode 100644 index 000000000..90829da20 --- /dev/null +++ b/docs/superpowers/plans/2026-07-06-WW-5641-json-writer-reader-override.md @@ -0,0 +1,337 @@ +# WW-5641: Restore `struts.json.writer` / `struts.json.reader` Override — 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:** Restore the documented ability to override the JSON serializer/deserializer via the `struts.json.writer` / `struts.json.reader` constants, which regressed in the 7.2.x JSON hardening rework. + +**Architecture:** Revert `JSONUtil` to 7.1.x-style deferred resolution: inject the `Container` and resolve the writer/reader by the *effective* constant value at injection time, instead of relying on the build-time default alias set by `JSONBeanSelectionProvider`. Change is confined to `JSONUtil` plus new test fixtures. `StrutsJSONWriter`/`StrutsJSONReader`, the DoS-limit constants, and the `<bean-selection>` machinery are untouched. + +**Tech Stack:** Java, Maven, Struts internal IoC container, JUnit (`junit.framework.TestCase` via `StrutsTestCase`). + +**Spec:** `docs/superpowers/specs/2026-07-06-WW-5641-json-writer-reader-override-regression-design.md` +**Ticket:** [WW-5641](https://issues.apache.org/jira/browse/WW-5641) + +## Global Constraints + +- Commit messages MUST be prefixed with the ticket id: `WW-5641 <type>: <desc>`. +- Work on the feature branch `WW-5641-json-writer-reader-override` (already checked out). Never commit to `main`. +- Do NOT change `StrutsJSONWriter`, `StrutsJSONReader`, the DoS-limit constants (`struts.json.maxDepth`, `maxElements`, `maxLength`, `maxStringLength`, `maxKeyLength`), or any core class. +- `JSONUtil.setWriter(JSONWriter)` and `JSONUtil.setReader(JSONReader)` MUST remain public plain setters — existing tests (`JSONResultTest`, `JSONInterceptorTest`, `JSONUtilTest`) call them directly on `new JSONUtil()`. Only the `@Inject` annotation is removed from them. +- `JSONUtil` stays `prototype` scope; the writer/reader stay `prototype`. `serialize(...)` must not depend on `container` (only the injected `writer`/`reader` fields). +- Test idiom for this module: extend `StrutsTestCase`, name tests `testXxx()`, use inherited `assertEquals` (JUnit 3/4 style). No `@Test` annotations. + +--- + +### Task 1: Reproduce the regression, then fix it (RED → GREEN) + +The fix (~6 lines) and its regression test are inseparable — neither is independently shippable — so they form one task with a full TDD cycle and a single green commit. + +**Files:** +- Create: `plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONWriter.java` +- Create: `plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONReader.java` +- Create: `plugins/json/src/test/resources/struts-json-override.xml` +- Create: `plugins/json/src/test/java/org/apache/struts2/json/JSONWriterOverrideTest.java` +- Modify: `plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java` (imports; `setReader` `:65-68`; `setWriter` `:74-77`) + +**Interfaces:** +- Consumes: `org.apache.struts2.json.JSONWriter` (methods: `String write(Object)`; `String write(Object, Collection<Pattern>, Collection<Pattern>, boolean)`; `void setIgnoreHierarchy(boolean)`; `void setEnumAsBean(boolean)`; `void setDateFormatter(String)`; `void setCacheBeanInfo(boolean)`; `void setExcludeProxyProperties(boolean)`). `org.apache.struts2.json.JSONReader` (methods: `Object read(String)`; `void setMaxElements(int)`; `void setMaxDepth(int)`; `void setMaxStringLength(int)`; ` [...] +- Produces: the corrected `JSONUtil.setContainer(Container)` injection point. No new API for later tasks. + +- [ ] **Step 1: Create the marker writer fixture** + +Create `plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONWriter.java`: + +```java +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.json; + +import java.util.Collection; +import java.util.regex.Pattern; + +/** + * Marker writer proving that a user-configured {@code struts.json.writer} override + * wins over the default {@link StrutsJSONWriter}. Emits a sentinel so output-level + * assertions are unambiguous. + */ +public class CustomTestJSONWriter implements JSONWriter { + + public static final String SENTINEL = "{\"__customWriter__\":true}"; + + @Override + public String write(Object object) throws JSONException { + return SENTINEL; + } + + @Override + public String write(Object object, Collection<Pattern> excludeProperties, + Collection<Pattern> includeProperties, + boolean excludeNullProperties) throws JSONException { + return SENTINEL; + } + + @Override public void setIgnoreHierarchy(boolean ignoreHierarchy) {} + @Override public void setEnumAsBean(boolean enumAsBean) {} + @Override public void setDateFormatter(String defaultDateFormat) {} + @Override public void setCacheBeanInfo(boolean cacheBeanInfo) {} + @Override public void setExcludeProxyProperties(boolean excludeProxyProperties) {} +} +``` + +- [ ] **Step 2: Create the marker reader fixture** + +Create `plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONReader.java`: + +```java +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.json; + +/** + * Marker reader proving that a user-configured {@code struts.json.reader} override + * wins over the default {@link StrutsJSONReader}. + */ +public class CustomTestJSONReader implements JSONReader { + + public static final String SENTINEL = "__customReader__"; + + @Override + public Object read(String string) throws JSONException { + return SENTINEL; + } + + @Override public void setMaxElements(int maxElements) {} + @Override public void setMaxDepth(int maxDepth) {} + @Override public void setMaxStringLength(int maxStringLength) {} + @Override public void setMaxKeyLength(int maxKeyLength) {} +} +``` + +- [ ] **Step 3: Create the application-style override config** + +Create `plugins/json/src/test/resources/struts-json-override.xml`. This mimics an application overriding both components the documented way; it is loaded *after* the JSON plugin's `struts-plugin.xml` (see Step 4), reproducing the real provider ordering: + +```xml +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE struts PUBLIC + "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN" + "https://struts.apache.org/dtds/struts-6.0.dtd"> +<struts> + <bean type="org.apache.struts2.json.JSONWriter" name="customTestWriter" + class="org.apache.struts2.json.CustomTestJSONWriter" scope="prototype"/> + <bean type="org.apache.struts2.json.JSONReader" name="customTestReader" + class="org.apache.struts2.json.CustomTestJSONReader" scope="prototype"/> + <constant name="struts.json.writer" value="customTestWriter"/> + <constant name="struts.json.reader" value="customTestReader"/> +</struts> +``` + +- [ ] **Step 4: Create the failing regression test** + +Create `plugins/json/src/test/java/org/apache/struts2/json/JSONWriterOverrideTest.java`. It boots the real `Dispatcher` config chain `struts-default.xml,struts-plugin.xml,struts-json-override.xml` — defaults → JSON plugin `<bean-selection>` → app override, in that order — which is what reproduces the bug (`XWorkTestCase` alone does not load the plugin `struts-plugin.xml`). Assertions check the *effective* writer/reader used by `JSONUtil`: + +```java +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.json; + +import org.apache.struts2.junit.StrutsTestCase; + +import java.util.HashMap; +import java.util.Map; + +/** + * Regression test for WW-5641: {@code struts.json.writer} / {@code struts.json.reader} + * overrides from an application config were ignored on the 7.2.x line. + * + * <p>Boots the real Dispatcher config chain so the provider ordering that causes the bug + * (JSON plugin {@code <bean-selection>} running before the app override) is reproduced.</p> + */ +public class JSONWriterOverrideTest extends StrutsTestCase { + + @Override + protected void setupBeforeInitDispatcher() throws Exception { + Map<String, String> params = new HashMap<>(); + params.put("config", "struts-default.xml,struts-plugin.xml,struts-json-override.xml"); + dispatcherInitParams = params; + } + + /** + * The effective writer used by JSONUtil must be the override, not StrutsJSONWriter. + */ + public void testCustomWriterIsUsedBySerialize() throws Exception { + JSONUtil jsonUtil = container.getInstance(JSONUtil.class); + String output = jsonUtil.serialize(new Object(), false); + assertEquals("struts.json.writer override was ignored; default StrutsJSONWriter was used", + CustomTestJSONWriter.SENTINEL, output); + } + + /** + * The effective reader used by JSONUtil must be the override, not StrutsJSONReader. + */ + public void testCustomReaderIsUsed() { + JSONUtil jsonUtil = container.getInstance(JSONUtil.class); + assertEquals("struts.json.reader override was ignored; default StrutsJSONReader was used", + CustomTestJSONReader.class, jsonUtil.getReader().getClass()); + } +} +``` + +- [ ] **Step 5: Run the test to verify it FAILS (reproduces the bug)** + +Run: `mvn test -DskipAssembly -pl plugins/json -Dtest=JSONWriterOverrideTest` +Expected: BOTH tests FAIL. +- `testCustomWriterIsUsedBySerialize`: actual output is `{}` (StrutsJSONWriter on an empty `Object`), not the sentinel. +- `testCustomReaderIsUsed`: `getReader()` returns a `StrutsJSONReader`, not `CustomTestJSONReader`. + +If either test unexpectedly PASSES, the bootstrap is not exercising the real ordering — stop and re-check the `config` init param before proceeding. + +- [ ] **Step 6: Apply the fix — add the `Container` import to `JSONUtil`** + +In `plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java`, add the import next to the existing `org.apache.struts2.inject.Inject` import (line 43): + +```java +import org.apache.struts2.inject.Container; +import org.apache.struts2.inject.Inject; +``` + +- [ ] **Step 7: Apply the fix — defer writer/reader resolution to a container lookup** + +Replace the current annotated setters (`JSONUtil.java:65-77`): + +```java + @Inject + public void setReader(JSONReader reader) { + this.reader = reader; + } + + public JSONReader getReader() { + return reader; + } + + @Inject + public void setWriter(JSONWriter writer) { + this.writer = writer; + } +``` + +with (remove `@Inject` from both setters; add `setContainer` that resolves by the effective constant value — mirroring 7.1.x): + +```java + @Inject + public void setContainer(Container container) { + setWriter(container.getInstance(JSONWriter.class, + container.getInstance(String.class, JSONConstants.JSON_WRITER))); + setReader(container.getInstance(JSONReader.class, + container.getInstance(String.class, JSONConstants.JSON_READER))); + } + + public void setReader(JSONReader reader) { + this.reader = reader; + } + + public JSONReader getReader() { + return reader; + } + + public void setWriter(JSONWriter writer) { + this.writer = writer; + } +``` + +Rationale: `container.getInstance(String.class, "struts.json.writer")` reads the constant from the fully-built container, so it returns the application's override value (`customTestWriter`); `getInstance(JSONWriter.class, "customTestWriter")` then returns the user's bean. The DoS-limit `@Inject` setters on the resolved `StrutsJSONWriter`/`StrutsJSONReader` still fire because the container instantiates whatever bean it resolves. Reusing `setWriter`/`setReader` keeps the field assignments [...] + +- [ ] **Step 8: Run the regression test to verify it PASSES** + +Run: `mvn test -DskipAssembly -pl plugins/json -Dtest=JSONWriterOverrideTest` +Expected: BOTH tests PASS. + +- [ ] **Step 9: Run the full JSON plugin suite to verify no regressions** + +Run: `mvn test -DskipAssembly -pl plugins/json` +Expected: BUILD SUCCESS, all tests pass — including the DoS-limit and bean-info caching tests, and the `new JSONUtil()` + `setWriter(...)`/`setReader(...)` tests in `JSONResultTest` / `JSONInterceptorTest` / `JSONUtilTest` (unaffected because the setters remain public and `serialize` never touches `container`). + +- [ ] **Step 10: Commit the fix and its regression test together** + +```bash +git add plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java \ + plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONWriter.java \ + plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONReader.java \ + plugins/json/src/test/java/org/apache/struts2/json/JSONWriterOverrideTest.java \ + plugins/json/src/test/resources/struts-json-override.xml +git commit -m "WW-5641 fix: resolve JSON writer/reader from constant at runtime + +Restore deferred by-name resolution in JSONUtil so struts.json.writer and +struts.json.reader overrides from an application config are honored again. +The 7.2.x <bean-selection> alias is chosen at plugin-parse time, before the +app config is folded in, so it locked the default StrutsJSONWriter/Reader. + +Co-Authored-By: Claude Opus 4.8 <[email protected]>" +``` + +--- + +## Self-Review + +**Spec coverage:** +- Root cause / Option A fix in `JSONUtil` → Steps 6-7. ✓ +- "Only `@Inject` removed, setters stay public" constraint → Step 7 + Global Constraints. ✓ +- `<bean-selection>` / `StrutsJSONWriter` / DoS limits untouched → no task modifies them; Global Constraints forbids it. ✓ +- Thread-safety (prototype preserved, `serialize` container-free) → Global Constraints + Step 9 verification. ✓ +- Regression test covers BOTH writer and reader → Steps 1-4, asserted in Step 4. ✓ +- `StrutsTestCase` bootstrap (real Dispatcher ordering), not `XWorkTestCase` → Step 4 + Step 5 guard. ✓ +- Effective-writer/reader test contract (not default binding) → Step 4 assertions. ✓ +- Verify FAIL-before / PASS-after → Steps 5 and 8. ✓ +- Full module suite green → Step 9. ✓ + +**Placeholder scan:** No TBD/TODO/"handle edge cases"; every code step shows complete code. ✓ + +**Type consistency:** `JSONWriter`/`JSONReader` method signatures in the fixtures match the interfaces verified in the repo. `serialize(Object, boolean)` and `getReader()` exist on `JSONUtil`. `Container.getInstance(Class)` / `getInstance(Class, String)` and `JSONConstants.JSON_WRITER`/`JSON_READER` names are exact. Bean names (`customTestWriter`/`customTestReader`) match between the override XML and the constant values. ✓
