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-site.git
The following commit(s) were added to refs/heads/main by this push:
new 31a00069ab WW-3476 docs: document package-scoped constants (#351)
31a00069ab is described below
commit 31a00069ab2b01d0cdcc14d12cfeec1d827ab67a
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sun Oct 11 16:24:56 2026 +0200
WW-3476 docs: document package-scoped constants (#351)
* WW-3476 docs: document package-scoped constants
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* WW-3476 docs: publish missing struts-6.5.dtd
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* WW-3476 docs: address review of scoped constants section
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
source/core-developers/constant-configuration.md | 3 +
source/core-developers/package-configuration.md | 114 ++++++++++++++++
source/dtds/struts-6.5.dtd | 158 ++++++++++++++++++++++
source/dtds/struts-7.5.dtd | 164 +++++++++++++++++++++++
source/plugins/plugins-architecture.md | 1 +
5 files changed, 440 insertions(+)
diff --git a/source/core-developers/constant-configuration.md
b/source/core-developers/constant-configuration.md
index e81c8e5262..06e20b2053 100644
--- a/source/core-developers/constant-configuration.md
+++ b/source/core-developers/constant-configuration.md
@@ -38,6 +38,9 @@ In the [default.properties](default-properties) file, each
entry is treated as a
In the [web.xml](web-xml) file, any FilterDispatcher initialization parameters
are loaded as constants.
+Since Struts 7.5.0 a package can override selected constants for its own
actions, see
+[Scoped constants](package-configuration#scoped-constants).
+
### Value substitution
Since Apache Struts 2.5.6 it is possible to use value substitution when
defining `constant`s in `struts.xml` file.
diff --git a/source/core-developers/package-configuration.md
b/source/core-developers/package-configuration.md
index 4c263704cd..f4d7da2bd1 100644
--- a/source/core-developers/package-configuration.md
+++ b/source/core-developers/package-configuration.md
@@ -80,3 +80,117 @@ The optional `abstract` attribute creates a base package
that can omit the actio
</package>
</struts>
```
+
+## Scoped constants
+
+Since Struts 7.5.0 a package can override selected
[constants](constant-configuration) for its own actions with
+the `<scoped-constant>` element. Packages that `extends` it inherit the
override, and an override declared in
+the child package wins. When a package extends several packages (`extends="a,
b"`) and they override the same
+constant, the first listed package wins, the same rule as for result types.
+
+Packages created by the [Convention plugin](../plugins/convention/) have no
`<scoped-constant>` of their own.
+They only get overrides from their parent package, set with `@ParentPackage` or
+`struts.convention.default.parent.package`.
+
+The element requires the [struts-7.5.dtd](../dtds/struts-7.5.dtd) and must
come before any other child of
+`<package>`:
+
+```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"/>
+ ...
+ </package>
+</struts>
+```
+
+| Attribute | Required | Description |
+|-----------|----------|-------------------------------------------|
+| name | **yes** | the name of a scopable constant |
+| value | **yes** | the value used for actions of the package |
+
+Scoped constants are not a way to override an arbitrary constant per package.
Constants are injected into
+framework components once, at startup, so a package value only takes effect
where a component explicitly reads
+it per request. Such a component declares the constants it reads as
_scopable_, and only those can appear
+in `<scoped-constant>`.
+
+{:.alert .alert-warning}
+Struts core does not declare any constant as scopable yet. A
`<scoped-constant>` only works with a constant
+declared scopable by a plugin or by your own application code.
+
+The configuration is validated at startup, and loading fails when:
+
+- a package declares a constant that nothing has declared scopable. This also
applies to abstract packages.
+ The error message lists the scopable constants that are available.
+- a package declares the same constant more than once.
+
+Values are used as they are. Unlike in `<constant>`, `${...}` [value
substitution](constant-configuration#value-substitution)
+is not supported.
+
+### Making a constant scopable
+
+A component makes a constant scopable in two steps. First, register a bean
that implements
+`org.apache.struts2.config.ScopableConstants` and returns the constant names.
You can register more than one
+such bean. The scopable set is the union of all of them.
+
+```java
+import org.apache.struts2.config.ScopableConstants;
+
+import java.util.Set;
+
+public class MyScopableConstants implements ScopableConstants {
+ @Override
+ public Set<String> getNames() {
+ return Set.of("myplugin.theme");
+ }
+}
+```
+
+```xml
+<bean type="org.apache.struts2.config.ScopableConstants" name="myplugin"
+ class="com.example.MyScopableConstants"/>
+```
+
+Second, read the value per request through
`org.apache.struts2.config.ScopedConstantProvider`, not through
+an `@Inject`-ed constant:
+
+```java
+import org.apache.struts2.config.ScopedConstantProvider;
+import org.apache.struts2.inject.Inject;
+
+private ScopedConstantProvider scopedConstantProvider;
+
+@Inject
+public void setScopedConstantProvider(ScopedConstantProvider
scopedConstantProvider) {
+ this.scopedConstantProvider = scopedConstantProvider;
+}
+
+public String getTheme() {
+ return scopedConstantProvider.getValue("myplugin.theme");
+}
+```
+
+{:.alert .alert-info}
+Use `org.apache.struts2.inject.Inject`. The Struts container ignores
`jakarta.inject.Inject` and
+`javax.inject.Inject`, so the setter would never be called.
+
+`getValue(name)` returns the value for the package of the action being
executed, taking `extends` into account.
+Outside an action invocation, or when no package in the hierarchy overrides
the constant, it returns the global
+constant, or `null` when that is not set. It throws `IllegalArgumentException`
for a name that is not scopable.
+The global value injected elsewhere is never changed.
+
+Some constants can never be scoped, because they are read before the package
of a request is known, for
+example the [ActionMapper](action-mapper) or the multipart upload limits. To
use a different mapper per URL space,
+use the [PrefixBasedActionMapper](action-mapper#prefixbasedactionmapper)
instead.
+
+{:.alert .alert-danger}
+Do not declare security-related constants scopable, such as
`struts.parameters.requireAnnotations`,
+`struts.enable.DynamicMethodInvocation` or `struts.devMode`. Otherwise a
single package can relax a setting
+meant to protect the whole application.
+
+The default `ScopedConstantProvider` implementation can be replaced with the
`struts.scopedConstantProvider`
+[extension point](../plugins/plugins-architecture#extension-points). For now
this works only from XML or
+properties configuration. Java-based configuration cannot set it yet.
diff --git a/source/dtds/struts-6.5.dtd b/source/dtds/struts-6.5.dtd
new file mode 100644
index 0000000000..6c2ef0d05b
--- /dev/null
+++ b/source/dtds/struts-6.5.dtd
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+<!-- START SNIPPET: strutsDtd -->
+
+<!--
+ Struts configuration DTD.
+ Use the following DOCTYPE
+
+ <!DOCTYPE struts PUBLIC
+ "-//Apache Software Foundation//DTD Struts Configuration 6.5//EN"
+ "https://struts.apache.org/dtds/struts-6.5.dtd">
+-->
+
+<!ELEMENT struts ((package|include|bean|constant)*,bean-selection?,
unknown-handler-stack?)>
+<!ATTLIST struts
+ order CDATA #IMPLIED
+>
+
+<!ELEMENT package (result-types?, interceptors?, default-interceptor-ref?,
default-action-ref?, default-class-ref?, global-results?,
global-allowed-methods?, global-exception-mappings?, action*)>
+<!ATTLIST package
+ name CDATA #REQUIRED
+ extends CDATA #IMPLIED
+ namespace CDATA #IMPLIED
+ abstract CDATA #IMPLIED
+ final CDATA #IMPLIED
+ strict-method-invocation (true|false) "true"
+>
+
+<!ELEMENT result-types (result-type+)>
+
+<!ELEMENT result-type (param*)>
+<!ATTLIST result-type
+ name CDATA #REQUIRED
+ class CDATA #REQUIRED
+ default (true|false) "false"
+>
+
+<!ELEMENT interceptors (interceptor|interceptor-stack)+>
+
+<!ELEMENT interceptor (param*)>
+<!ATTLIST interceptor
+ name CDATA #REQUIRED
+ class CDATA #REQUIRED
+>
+
+<!ELEMENT interceptor-stack (interceptor-ref*)>
+<!ATTLIST interceptor-stack
+ name CDATA #REQUIRED
+>
+
+<!ELEMENT interceptor-ref (param*)>
+<!ATTLIST interceptor-ref
+ name CDATA #REQUIRED
+>
+
+<!ELEMENT default-interceptor-ref (#PCDATA)>
+<!ATTLIST default-interceptor-ref
+ name CDATA #REQUIRED
+>
+
+<!ELEMENT default-action-ref (#PCDATA)>
+<!ATTLIST default-action-ref
+ name CDATA #REQUIRED
+>
+
+<!ELEMENT default-class-ref (#PCDATA)>
+<!ATTLIST default-class-ref
+ class CDATA #REQUIRED
+>
+
+<!ELEMENT global-results (result+)>
+
+<!ELEMENT global-allowed-methods (#PCDATA)>
+
+<!ELEMENT global-exception-mappings (exception-mapping+)>
+
+<!ELEMENT action
((param|result|interceptor-ref|exception-mapping)*,allowed-methods?)>
+<!ATTLIST action
+ name CDATA #REQUIRED
+ class CDATA #IMPLIED
+ method CDATA #IMPLIED
+ converter CDATA #IMPLIED
+>
+
+<!ELEMENT param (#PCDATA)>
+<!ATTLIST param
+ name CDATA #REQUIRED
+>
+
+<!ELEMENT result (#PCDATA|param)*>
+<!ATTLIST result
+ name CDATA #IMPLIED
+ type CDATA #IMPLIED
+>
+
+<!ELEMENT exception-mapping (#PCDATA|param)*>
+<!ATTLIST exception-mapping
+ name CDATA #IMPLIED
+ exception CDATA #REQUIRED
+ result CDATA #REQUIRED
+>
+
+<!ELEMENT allowed-methods (#PCDATA)>
+
+<!ELEMENT include (#PCDATA)>
+<!ATTLIST include
+ file CDATA #REQUIRED
+>
+
+<!ELEMENT bean (#PCDATA)>
+<!ATTLIST bean
+ type CDATA #IMPLIED
+ name CDATA #IMPLIED
+ class CDATA #REQUIRED
+ scope CDATA #IMPLIED
+ static CDATA #IMPLIED
+ optional CDATA #IMPLIED
+>
+
+<!ELEMENT bean-selection (#PCDATA)>
+<!ATTLIST bean-selection
+ name CDATA #IMPLIED
+ class CDATA #IMPLIED
+>
+
+<!ELEMENT constant (#PCDATA)>
+<!ATTLIST constant
+ name CDATA #REQUIRED
+ value CDATA #REQUIRED
+>
+
+<!ELEMENT unknown-handler-stack (unknown-handler-ref*)>
+<!ELEMENT unknown-handler-ref (#PCDATA)>
+<!ATTLIST unknown-handler-ref
+ name CDATA #REQUIRED
+>
+
+<!-- END SNIPPET: strutsDtd -->
+
diff --git a/source/dtds/struts-7.5.dtd b/source/dtds/struts-7.5.dtd
new file mode 100644
index 0000000000..307f5f0e0a
--- /dev/null
+++ b/source/dtds/struts-7.5.dtd
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+<!-- START SNIPPET: strutsDtd -->
+
+<!--
+ Struts configuration DTD.
+ Use the following DOCTYPE
+
+ <!DOCTYPE struts PUBLIC
+ "-//Apache Software Foundation//DTD Struts Configuration 7.5//EN"
+ "https://struts.apache.org/dtds/struts-7.5.dtd">
+-->
+
+<!ELEMENT struts ((package|include|bean|constant)*,bean-selection?,
unknown-handler-stack?)>
+<!ATTLIST struts
+ order CDATA #IMPLIED
+>
+
+<!ELEMENT package (scoped-constant*, result-types?, interceptors?,
default-interceptor-ref?, default-action-ref?, default-class-ref?,
global-results?, global-allowed-methods?, global-exception-mappings?, action*)>
+<!ATTLIST package
+ name CDATA #REQUIRED
+ extends CDATA #IMPLIED
+ namespace CDATA #IMPLIED
+ abstract CDATA #IMPLIED
+ final CDATA #IMPLIED
+ strict-method-invocation (true|false) "true"
+>
+
+<!ELEMENT scoped-constant EMPTY>
+<!ATTLIST scoped-constant
+ name CDATA #REQUIRED
+ value CDATA #REQUIRED
+>
+
+<!ELEMENT result-types (result-type+)>
+
+<!ELEMENT result-type (param*)>
+<!ATTLIST result-type
+ name CDATA #REQUIRED
+ class CDATA #REQUIRED
+ default (true|false) "false"
+>
+
+<!ELEMENT interceptors (interceptor|interceptor-stack)+>
+
+<!ELEMENT interceptor (param*)>
+<!ATTLIST interceptor
+ name CDATA #REQUIRED
+ class CDATA #REQUIRED
+>
+
+<!ELEMENT interceptor-stack (interceptor-ref*)>
+<!ATTLIST interceptor-stack
+ name CDATA #REQUIRED
+>
+
+<!ELEMENT interceptor-ref (param*)>
+<!ATTLIST interceptor-ref
+ name CDATA #REQUIRED
+>
+
+<!ELEMENT default-interceptor-ref (#PCDATA)>
+<!ATTLIST default-interceptor-ref
+ name CDATA #REQUIRED
+>
+
+<!ELEMENT default-action-ref (#PCDATA)>
+<!ATTLIST default-action-ref
+ name CDATA #REQUIRED
+>
+
+<!ELEMENT default-class-ref (#PCDATA)>
+<!ATTLIST default-class-ref
+ class CDATA #REQUIRED
+>
+
+<!ELEMENT global-results (result+)>
+
+<!ELEMENT global-allowed-methods (#PCDATA)>
+
+<!ELEMENT global-exception-mappings (exception-mapping+)>
+
+<!ELEMENT action
((param|result|interceptor-ref|exception-mapping)*,allowed-methods?)>
+<!ATTLIST action
+ name CDATA #REQUIRED
+ class CDATA #IMPLIED
+ method CDATA #IMPLIED
+ converter CDATA #IMPLIED
+>
+
+<!ELEMENT param (#PCDATA)>
+<!ATTLIST param
+ name CDATA #REQUIRED
+>
+
+<!ELEMENT result (#PCDATA|param)*>
+<!ATTLIST result
+ name CDATA #IMPLIED
+ type CDATA #IMPLIED
+>
+
+<!ELEMENT exception-mapping (#PCDATA|param)*>
+<!ATTLIST exception-mapping
+ name CDATA #IMPLIED
+ exception CDATA #REQUIRED
+ result CDATA #REQUIRED
+>
+
+<!ELEMENT allowed-methods (#PCDATA)>
+
+<!ELEMENT include (#PCDATA)>
+<!ATTLIST include
+ file CDATA #REQUIRED
+>
+
+<!ELEMENT bean (#PCDATA)>
+<!ATTLIST bean
+ type CDATA #IMPLIED
+ name CDATA #IMPLIED
+ class CDATA #REQUIRED
+ scope CDATA #IMPLIED
+ static CDATA #IMPLIED
+ optional CDATA #IMPLIED
+>
+
+<!ELEMENT bean-selection (#PCDATA)>
+<!ATTLIST bean-selection
+ name CDATA #IMPLIED
+ class CDATA #IMPLIED
+>
+
+<!ELEMENT constant (#PCDATA)>
+<!ATTLIST constant
+ name CDATA #REQUIRED
+ value CDATA #REQUIRED
+>
+
+<!ELEMENT unknown-handler-stack (unknown-handler-ref*)>
+<!ELEMENT unknown-handler-ref (#PCDATA)>
+<!ATTLIST unknown-handler-ref
+ name CDATA #REQUIRED
+>
+
+<!-- END SNIPPET: strutsDtd -->
+
diff --git a/source/plugins/plugins-architecture.md
b/source/plugins/plugins-architecture.md
index 3d0f309059..0a0ae6bd4c 100644
--- a/source/plugins/plugins-architecture.md
+++ b/source/plugins/plugins-architecture.md
@@ -114,6 +114,7 @@ The following extension points are available in Struts 2:
| struts.securityMemberAccess | Define a custom
SecurityMemberAccess implementation, used to restrict OGNL evaluations based on
classes involved (since 6.4.0)
| prototype | com.opensymphony.xwork2.ognl.SecurityMemberAccess
|
| struts.compoundRootAccessor | Define a custom
CompoundRootAccessor implementation, used to resolve classes and manipulate the
CompoundRoot (since 6.4.0)
| singleton | com.opensymphony.xwork2.ognl.accessor.RootAccessor
|
| struts.methodAccessor | Define a custom MethodAccessor
implementation, used to evaluate OGNL method calls (since 6.4.0)
|
singleton | ognl.MethodAccessor |
+| struts.scopedConstantProvider | Resolves package-scoped constants
declared with `<scoped-constant>` (since 7.5.0)
|
singleton | org.apache.struts2.config.ScopedConstantProvider |
## Plugin Examples