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 06d6e3c67a4b901f9b427c2d420418358983fd1b
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Oct 10 17:11:12 2026 +0200

    WW-3476 feat(config): add struts-7.5.dtd with <scoped-constant>
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../config/StrutsXmlConfigurationProvider.java     |   3 +-
 .../providers/XmlDocConfigurationProvider.java     |  17 +++
 core/src/main/resources/struts-7.5.dtd             | 164 +++++++++++++++++++++
 .../XmlConfigurationProviderPackagesTest.java      |  31 ++++
 .../xwork-test-scoped-constants-duplicate.xml      |  30 ++++
 .../xwork-test-scoped-constants-old-dtd.xml        |  29 ++++
 .../providers/xwork-test-scoped-constants.xml      |  34 +++++
 7 files changed, 307 insertions(+), 1 deletion(-)

diff --git 
a/core/src/main/java/org/apache/struts2/config/StrutsXmlConfigurationProvider.java
 
b/core/src/main/java/org/apache/struts2/config/StrutsXmlConfigurationProvider.java
index 12a57a3a8..6b0a9dce9 100644
--- 
a/core/src/main/java/org/apache/struts2/config/StrutsXmlConfigurationProvider.java
+++ 
b/core/src/main/java/org/apache/struts2/config/StrutsXmlConfigurationProvider.java
@@ -51,7 +51,8 @@ public class StrutsXmlConfigurationProvider extends 
XmlConfigurationProvider {
             "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN", 
"struts-2.3.dtd",
             "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN", 
"struts-2.5.dtd",
             "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN", 
"struts-6.0.dtd",
-            "-//Apache Software Foundation//DTD Struts Configuration 6.5//EN", 
"struts-6.5.dtd");
+            "-//Apache Software Foundation//DTD Struts Configuration 6.5//EN", 
"struts-6.5.dtd",
+            "-//Apache Software Foundation//DTD Struts Configuration 7.5//EN", 
"struts-7.5.dtd");
 
     private File baseDir = null;
     private final String filename;
diff --git 
a/core/src/main/java/org/apache/struts2/config/providers/XmlDocConfigurationProvider.java
 
b/core/src/main/java/org/apache/struts2/config/providers/XmlDocConfigurationProvider.java
index da44b71f6..b6c606e66 100644
--- 
a/core/src/main/java/org/apache/struts2/config/providers/XmlDocConfigurationProvider.java
+++ 
b/core/src/main/java/org/apache/struts2/config/providers/XmlDocConfigurationProvider.java
@@ -424,6 +424,8 @@ public abstract class XmlDocConfigurationProvider 
implements ConfigurationProvid
 
         LOG.debug("Loaded {}", newPackage);
 
+        loadScopedConstants(newPackage, packageElement);
+
         // add result types (and default result) to this package
         addResultTypes(newPackage, packageElement);
 
@@ -839,6 +841,21 @@ public abstract class XmlDocConfigurationProvider 
implements ConfigurationProvid
         }
     }
 
+    /**
+     * @since 7.5.0
+     */
+    protected void loadScopedConstants(PackageConfig.Builder packageContext, 
Element packageElement) {
+        Set<String> declared = new HashSet<>();
+        iterateChildrenByTagName(packageElement, "scoped-constant", 
constantElement -> {
+            String name = constantElement.getAttribute("name");
+            if (!declared.add(name)) {
+                throw new ConfigurationException(format("Package [%s] declares 
scoped constant [%s] more than once",
+                        packageContext.getName(), name), constantElement);
+            }
+            packageContext.addScopedConstant(name, 
constantElement.getAttribute("value"));
+        });
+    }
+
     /**
      * Load all the global results for this package from the XML element.
      *
diff --git a/core/src/main/resources/struts-7.5.dtd 
b/core/src/main/resources/struts-7.5.dtd
new file mode 100644
index 000000000..307f5f0e0
--- /dev/null
+++ b/core/src/main/resources/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/core/src/test/java/org/apache/struts2/config/providers/XmlConfigurationProviderPackagesTest.java
 
b/core/src/test/java/org/apache/struts2/config/providers/XmlConfigurationProviderPackagesTest.java
index 0c4bd9714..25350d5c1 100644
--- 
a/core/src/test/java/org/apache/struts2/config/providers/XmlConfigurationProviderPackagesTest.java
+++ 
b/core/src/test/java/org/apache/struts2/config/providers/XmlConfigurationProviderPackagesTest.java
@@ -24,7 +24,9 @@ import org.apache.struts2.config.RuntimeConfiguration;
 import org.apache.struts2.config.entities.PackageConfig;
 
 import java.util.List;
+import java.util.Map;
 
+import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * Created by IntelliJ IDEA.
@@ -198,6 +200,35 @@ public class XmlConfigurationProviderPackagesTest extends 
ConfigurationTestBase
         assertEquals(expectedNoDefaultClassRefPackage, 
configuration.getPackageConfig(noDefaultClassRefPkgName));
     }
 
+    public void testScopedConstantsLoadAndInherit() throws 
ConfigurationException {
+        
buildConfigurationProvider(getXmlConfigFilePath("xwork-test-scoped-constants.xml"));
+
+        PackageConfig parent = configuration.getPackageConfig("parent");
+        PackageConfig child = configuration.getPackageConfig("child");
+
+        assertEquals(Map.of("first", "parent-first", "second", 
"parent-second"), parent.getScopedConstants());
+        assertEquals(Map.of("second", "child-second"), 
child.getScopedConstants());
+        assertEquals(Map.of("first", "parent-first", "second", 
"child-second"), child.getAllScopedConstants());
+    }
+
+    public void testDuplicateScopedConstantInOnePackageFails() {
+        try {
+            
buildConfigurationProvider(getXmlConfigFilePath("xwork-test-scoped-constants-duplicate.xml"));
+            fail("Should have thrown a ConfigurationException");
+        } catch (ConfigurationException e) {
+            assertThat(e).hasStackTraceContaining("Package [dup] declares 
scoped constant [first] more than once");
+        }
+    }
+
+    public void testScopedConstantIsRejectedUnderOlderDtd() {
+        try {
+            
buildConfigurationProvider(getXmlConfigFilePath("xwork-test-scoped-constants-old-dtd.xml"));
+            fail("Should have thrown a ConfigurationException");
+        } catch (ConfigurationException e) {
+            assertThat(e).hasStackTraceContaining("\"scoped-constant\" must be 
declared");
+        }
+    }
+
     private String getXmlConfigFilePath(String fileName) {
         return "org/apache/struts2/config/providers/" + fileName;
     }
diff --git 
a/core/src/test/resources/org/apache/struts2/config/providers/xwork-test-scoped-constants-duplicate.xml
 
b/core/src/test/resources/org/apache/struts2/config/providers/xwork-test-scoped-constants-duplicate.xml
new file mode 100644
index 000000000..92b9ec116
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/config/providers/xwork-test-scoped-constants-duplicate.xml
@@ -0,0 +1,30 @@
+<?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.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+        "-//Apache Software Foundation//DTD Struts Configuration 7.5//EN"
+        "struts-7.5.dtd">
+<struts>
+    <package name="dup" namespace="/dup">
+        <scoped-constant name="first" value="one"/>
+        <scoped-constant name="first" value="two"/>
+    </package>
+</struts>
diff --git 
a/core/src/test/resources/org/apache/struts2/config/providers/xwork-test-scoped-constants-old-dtd.xml
 
b/core/src/test/resources/org/apache/struts2/config/providers/xwork-test-scoped-constants-old-dtd.xml
new file mode 100644
index 000000000..35817e260
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/config/providers/xwork-test-scoped-constants-old-dtd.xml
@@ -0,0 +1,29 @@
+<?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.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+        "-//Apache Software Foundation//DTD Struts Configuration 6.5//EN"
+        "struts-6.5.dtd">
+<struts>
+    <package name="old" namespace="/old">
+        <scoped-constant name="first" value="one"/>
+    </package>
+</struts>
diff --git 
a/core/src/test/resources/org/apache/struts2/config/providers/xwork-test-scoped-constants.xml
 
b/core/src/test/resources/org/apache/struts2/config/providers/xwork-test-scoped-constants.xml
new file mode 100644
index 000000000..ef0797de8
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/config/providers/xwork-test-scoped-constants.xml
@@ -0,0 +1,34 @@
+<?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.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+        "-//Apache Software Foundation//DTD Struts Configuration 7.5//EN"
+        "struts-7.5.dtd">
+<struts>
+    <package name="parent" namespace="/parent">
+        <scoped-constant name="first" value="parent-first"/>
+        <scoped-constant name="second" value="parent-second"/>
+    </package>
+
+    <package name="child" namespace="/child" extends="parent">
+        <scoped-constant name="second" value="child-second"/>
+    </package>
+</struts>

Reply via email to