This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch WW-3476-scoped-constants
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 88fd4c7d6da9c12e227853a8f03ece52468d431b
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Oct 10 16:30:44 2026 +0200

    WW-3476 docs: add design spec for package-scoped constants
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../2026-10-10-WW-3476-scoped-constants-design.md  | 179 +++++++++++++++++++++
 1 file changed, 179 insertions(+)

diff --git 
a/docs/superpowers/specs/2026-10-10-WW-3476-scoped-constants-design.md 
b/docs/superpowers/specs/2026-10-10-WW-3476-scoped-constants-design.md
new file mode 100644
index 000000000..f6a6be2e8
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-10-WW-3476-scoped-constants-design.md
@@ -0,0 +1,179 @@
+# WW-3476 — Package-scoped constants
+
+**Ticket:** [WW-3476](https://issues.apache.org/jira/browse/WW-3476) — *Allow 
overwriting of constants for each package*
+**Target:** 7.5.0 (ticket fix version)
+**Date:** 2026-10-10
+**Status:** Design approved, pending implementation plan
+
+## Problem
+
+The ticket (2010, 2.2.1) asks to override constants per package — its examples 
are the ActionMapper, the locale
+and the JSP location. Struts constants cannot do this today:
+
+- Constants are global. They are injected once, at bootstrap, into container 
singletons
+  (`@Inject(StrutsConstants.X)` in ~30 core classes) or read via 
`container.getInstance(String.class, name)`.
+- `<package>` has no constant-like child (`struts-6.5.dtd`), and 
`PackageConfig` has nowhere to store one.
+
+A scoped value therefore cannot transparently replace what a singleton already 
holds. Making every constant
+package-aware would need a container per package — an 8.0-sized change. This 
design instead adds an opt-in
+mechanism that fits 7.x.
+
+## Goals
+
+- A package can declare an override for a constant; child packages inherit it 
through `extends`.
+- A component that wants a scoped value reads it at request time against the 
current invocation's package,
+  falling back to the global constant.
+- The shared singleton is never mutated — the same rule `WithLazyParams` 
follows since 7.3.0.
+- A misconfigured override fails at startup, never silently.
+- Purely additive: no published DTD, constructor or interface changes.
+
+## Non-goals
+
+- **No adopters in this change.** No existing constant becomes scopable. The 
mechanism ships alone; each adopter
+  (UI theme, i18n, Convention result path, …) is a follow-up ticket.
+- No `${...}` expressions — values are static strings.
+- No action-level scope (`<scoped-constant>` inside `<action>`).
+- No Convention-plugin annotation. Convention-built packages extend an XML 
parent package and inherit its
+  overrides.
+- No typed conversion — values are `String`, as with 
`container.getInstance(String.class, name)`.
+- No user-overridable provider (no `StrutsBeanSelectionProvider` alias / new 
constant) until there is a need.
+
+## Inherently non-scopable constants
+
+Anything consumed before the action is resolved can never be scoped, because 
the package is not yet known:
+the ActionMapper (it is what determines the package), multipart parsing limits 
(`struts.multipart.*`, applied
+in `Dispatcher` before mapping), and similar bootstrap/dispatch settings. The 
design does not special-case
+these — they are simply never registered as scopable. The per-URL-space 
ActionMapper need from the ticket is
+already served by `PrefixBasedActionMapper` (`struts.mapper.prefixMapping`, 
WW-3260).
+
+Security switches consumed at request time 
(`struts.parameters.requireAnnotations`, CSP nonce source,
+`struts.enable.DynamicMethodInvocation`, `devMode`) are technically scopable 
but should not be registered
+without a deliberate decision: a package-level override could quietly relax an 
application-wide hardening flag.
+
+## Design
+
+### 1. Configuration syntax — `struts-7.5.dtd`
+
+A new element, distinct from the global `<constant>` so it is never mistaken 
for one:
+
+```xml
+<!DOCTYPE struts PUBLIC
+    "-//Apache Software Foundation//DTD Struts Configuration 7.5//EN"
+    "https://struts.apache.org/dtds/struts-7.5.dtd";>
+<struts>
+    <package name="admin" extends="struts-default" namespace="/admin">
+        <scoped-constant name="some.scopable.constant" value="x"/>
+        <action name="..." class="..."/>
+    </package>
+</struts>
+```
+
+- New `core/src/main/resources/struts-7.5.dtd`: a copy of `struts-6.5.dtd` with
+  - `scoped-constant*` added first in the `package` content model;
+  - `<!ELEMENT scoped-constant EMPTY>` with `name CDATA #REQUIRED` and `value 
CDATA #REQUIRED`;
+  - the header comment's public id / URL updated to 7.5.
+- Registered in `StrutsXmlConfigurationProvider` next to the 6.5 mapping 
(`StrutsXmlConfigurationProvider.java:54`).
+- Published DTDs are unchanged. Precedent: WW-5409 added `struts-6.5.dtd` for 
the single `final` attribute.
+- The DTD name follows the ticket's fix version; if the release ships under 
another number, rename it with it.
+
+### 2. Storage — `PackageConfig`
+
+- New field: the package's own scoped constants, `Map<String, String>`, 
insertion-ordered.
+- `Builder.addScopedConstant(String name, String value)`.
+- `getScopedConstants()` — own declarations only (unmodifiable).
+- `getAllScopedConstants()` — merged map: parents first, then own entries, so 
the nearest declaration wins.
+  Multiple parents merge in the same `putAll` order as 
`getAllResultTypeConfigs()`.
+- The merged map is computed eagerly in `Builder.build()` and stored 
unmodifiable. Parents are always built
+  before their children, so the request-time lookup does not walk the 
hierarchy.
+- The copy constructor `PackageConfig(PackageConfig orig)` copies both maps.
+
+### 3. Parsing — `XmlDocConfigurationProvider`
+
+- `addPackage(Element)` reads `scoped-constant` children and calls 
`addScopedConstant`.
+- A duplicate `name` within one package is a `ConfigurationException` carrying 
the element's location.
+- Under an older DOCTYPE, the validating parser already rejects the element — 
no extra code.
+
+### 4. Registry — `ScopableConstants`
+
+```java
+package org.apache.struts2.config;
+
+public interface ScopableConstants {
+    Set<String> getNames();
+}
+```
+
+- Adopters register a named bean, in `struts-beans.xml` (core) or a plugin's 
`struts-plugin.xml`:
+  `<bean type="org.apache.struts2.config.ScopableConstants" name="ui" 
class="...UiScopableConstants"/>`
+- The registry is the union of all registered beans' names, collected via
+  `container.getInstanceNames(ScopableConstants.class)` — the existing pattern 
for `UnknownHandler`,
+  `FileManager` and `PackageProvider`.
+- This change registers no beans; only tests do.
+
+### 5. Resolution — `ScopedConstantProvider`
+
+```java
+package org.apache.struts2.config;
+
+public interface ScopedConstantProvider {
+    String getValue(String name);
+}
+```
+
+Default implementation `StrutsScopedConstantProvider`, bound in 
`struts-beans.xml`. It injects `Container` and
+`Configuration` (the latter is a container factory, 
`DefaultConfiguration.java:291`). `getValue(name)`:
+
+1. `name` not in the registry → `IllegalArgumentException`. Only an adopter's 
coding error reaches this; user
+   configuration cannot.
+2. No `ActionContext`, or no `ActionInvocation` on it (bootstrap, a JSP 
rendered without an action) → the global
+   value, `container.getInstance(String.class, name)`.
+3. Otherwise: the invocation's `ActionConfig.getPackageName()` → 
`configuration.getPackageConfig(...)` →
+   `getAllScopedConstants().get(name)`; when absent, the global value.
+
+Config reload needs no extra code: the container and `Configuration` are 
rebuilt, and lookups read the new
+`PackageConfig`s.
+
+### 6. Validation — `DefaultConfiguration.rebuildRuntimeConfiguration()`
+
+After all package providers have loaded, for every package, each name in 
`getScopedConstants()` (own
+declarations — inherited ones were validated on the parent) must be in the 
registry. Otherwise a
+`ConfigurationException`:
+
+> Package [admin] declares scoped constant [some.constant], which is not 
scopable. Scopable constants: [a, b].
+
+or, when the registry is empty, "No constants are scopable in this 
configuration." The exception carries the
+package's location (`PackageConfig` is `Located`); per-element locations are 
not stored.
+
+## Testing
+
+JUnit 3/4 only — match each target file's existing style; no Jupiter.
+
+- **Parsing** — `XmlConfigurationProviderPackagesTest` (extends 
`ConfigurationTestBase`), with new fixtures next to
+  `xwork-test-package-final.xml`:
+  - a 7.5 fixture with `<scoped-constant>` loads, and the values land on 
`PackageConfig`;
+  - a duplicate name in one package fails with `ConfigurationException`;
+  - `<scoped-constant>` under the 6.5 DOCTYPE fails validation;
+  - existing 6.5/6.0/2.x fixtures still load (covered by the current suite).
+- **Merging** — `PackageConfigTest`: child overrides parent; grandchild 
inherits through two levels; a package
+  with no declarations inherits everything; the multiple-parents winner is 
pinned.
+- **Validation** — a test-only `ScopableConstants` bean: a registered name 
loads; an unregistered name throws with
+  package and constant in the message; with no beans registered, any 
`<scoped-constant>` throws.
+- **Provider** — `StrutsScopedConstantProviderTest`: the package value wins 
inside an invocation; the global value
+  is returned with no context, no invocation, or no override; an inherited 
value resolves for a child package;
+  an unregistered name throws; the global `container.getInstance(String.class, 
name)` is unchanged after a scoped
+  read.
+- **Watch it fail** — mutate each guarded behaviour (merge order, the 
validation call, the fallback) with Edit,
+  confirm via `git diff --stat`, see the test go red, restore.
+
+## Compatibility
+
+Additive only: a new DTD, a new element, two new interfaces, one new default 
bean, new `PackageConfig` methods.
+Existing constructors, builders, interfaces and published DTDs are untouched — 
suitable for the 7.x line.
+
+## Delivery
+
+1. Core PR: `WW-3476 feat(config): add package-scoped constants`.
+2. struts-site PR: publish `dtds/struts-7.5.dtd`, and document 
`<scoped-constant>` and the adopter API
+   (`WW-3476 docs: ...`). The public DTD URL must resolve before the release 
ships.
+3. Version Notes entry at release time.
+4. Follow-up tickets, one per adopter, filed when wanted.

Reply via email to