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 b2a5b0d058eaae229bb950eb79a1d4b7b06569b7
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Oct 10 17:08:54 2026 +0200

    WW-3476 feat(config): store scoped constants on PackageConfig
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../struts2/config/entities/PackageConfig.java     | 43 ++++++++++++
 .../struts2/config/entities/PackageConfigTest.java | 82 ++++++++++++++++++++++
 2 files changed, 125 insertions(+)

diff --git 
a/core/src/main/java/org/apache/struts2/config/entities/PackageConfig.java 
b/core/src/main/java/org/apache/struts2/config/entities/PackageConfig.java
index 391ead116..85f580a82 100644
--- a/core/src/main/java/org/apache/struts2/config/entities/PackageConfig.java
+++ b/core/src/main/java/org/apache/struts2/config/entities/PackageConfig.java
@@ -59,6 +59,8 @@ public class PackageConfig extends Located implements 
Comparable<PackageConfig>,
     protected boolean isFinal = false; // a final package is unextendable
     protected boolean needsRefresh;
     protected boolean strictMethodInvocation = true;
+    protected Map<String, String> scopedConstants;
+    protected Map<String, String> allScopedConstants;
 
     protected PackageConfig(String name) {
         this.name = name;
@@ -69,6 +71,8 @@ public class PackageConfig extends Located implements 
Comparable<PackageConfig>,
         resultTypeConfigs = new LinkedHashMap<>();
         globalExceptionMappingConfigs = new ArrayList<>();
         parents = new ArrayList<>();
+        scopedConstants = new LinkedHashMap<>();
+        allScopedConstants = Collections.emptyMap();
     }
 
     protected PackageConfig(PackageConfig orig) {
@@ -90,6 +94,8 @@ public class PackageConfig extends Located implements 
Comparable<PackageConfig>,
         this.parents = new ArrayList<>(orig.parents);
         this.location = orig.location;
         this.strictMethodInvocation = orig.strictMethodInvocation;
+        this.scopedConstants = new LinkedHashMap<>(orig.scopedConstants);
+        this.allScopedConstants = orig.allScopedConstants;
     }
 
     public boolean isAbstract() {
@@ -189,6 +195,22 @@ public class PackageConfig extends Located implements 
Comparable<PackageConfig>,
         return retMap;
     }
 
+    /**
+     * @return scoped constants declared by this package itself, without 
inherited ones
+     * @since 7.5.0
+     */
+    public Map<String, String> getScopedConstants() {
+        return scopedConstants;
+    }
+
+    /**
+     * @return scoped constants in effect for this package: inherited ones, 
overridden by its own
+     * @since 7.5.0
+     */
+    public Map<String, String> getAllScopedConstants() {
+        return allScopedConstants;
+    }
+
     /**
      * returns the List of all the ExceptionMappingConfigs available in the 
current package.
      * ExceptionMappingConfigs defined in ancestor packages will be included 
in this list.
@@ -391,6 +413,7 @@ public class PackageConfig extends Located implements 
Comparable<PackageConfig>,
         if (!Objects.equals(globalExceptionMappingConfigs, 
that.globalExceptionMappingConfigs))
             return false;
         if (!Objects.equals(parents, that.parents)) return false;
+        if (!Objects.equals(scopedConstants, that.scopedConstants)) return 
false;
         if (!Objects.equals(defaultInterceptorRef, that.defaultInterceptorRef))
             return false;
         if (!Objects.equals(defaultActionRef, that.defaultActionRef))
@@ -412,6 +435,7 @@ public class PackageConfig extends Located implements 
Comparable<PackageConfig>,
         result = 31 * result + (resultTypeConfigs != null ? 
resultTypeConfigs.hashCode() : 0);
         result = 31 * result + (globalExceptionMappingConfigs != null ? 
globalExceptionMappingConfigs.hashCode() : 0);
         result = 31 * result + (parents != null ? parents.hashCode() : 0);
+        result = 31 * result + (scopedConstants != null ? 
scopedConstants.hashCode() : 0);
         result = 31 * result + (defaultInterceptorRef != null ? 
defaultInterceptorRef.hashCode() : 0);
         result = 31 * result + (defaultActionRef != null ? 
defaultActionRef.hashCode() : 0);
         result = 31 * result + (defaultResultType != null ? 
defaultResultType.hashCode() : 0);
@@ -605,6 +629,14 @@ public class PackageConfig extends Located implements 
Comparable<PackageConfig>,
             return this;
         }
 
+        /**
+         * @since 7.5.0
+         */
+        public Builder addScopedConstant(String name, String value) {
+            target.scopedConstants.put(name, value);
+            return this;
+        }
+
         public Builder location(Location loc) {
             target.location = loc;
             return this;
@@ -655,11 +687,22 @@ public class PackageConfig extends Located implements 
Comparable<PackageConfig>,
             target.resultTypeConfigs = 
Collections.unmodifiableMap(target.resultTypeConfigs);
             target.globalExceptionMappingConfigs = 
Collections.unmodifiableList(target.globalExceptionMappingConfigs);
             target.parents = Collections.unmodifiableList(target.parents);
+            target.scopedConstants = 
Collections.unmodifiableMap(target.scopedConstants);
+            target.allScopedConstants = mergeScopedConstants(target);
             PackageConfig result = target;
             target = new PackageConfig(result);
             return result;
         }
 
+        private static Map<String, String> mergeScopedConstants(PackageConfig 
config) {
+            Map<String, String> merged = new LinkedHashMap<>();
+            for (PackageConfig parent : config.parents) {
+                merged.putAll(parent.getAllScopedConstants());
+            }
+            merged.putAll(config.scopedConstants);
+            return Collections.unmodifiableMap(merged);
+        }
+
         @Override
         public String toString() {
             return "[BUILDER] " + target.toString();
diff --git 
a/core/src/test/java/org/apache/struts2/config/entities/PackageConfigTest.java 
b/core/src/test/java/org/apache/struts2/config/entities/PackageConfigTest.java
index b0fd702d7..8619a8b5c 100644
--- 
a/core/src/test/java/org/apache/struts2/config/entities/PackageConfigTest.java
+++ 
b/core/src/test/java/org/apache/struts2/config/entities/PackageConfigTest.java
@@ -20,6 +20,10 @@ package org.apache.struts2.config.entities;
 
 import org.apache.struts2.XWorkTestCase;
 
+import java.util.Map;
+
+import static org.junit.Assert.assertThrows;
+
 public class PackageConfigTest extends XWorkTestCase {
 
     public void testFullDefaultInterceptorRef() {
@@ -92,4 +96,82 @@ public class PackageConfigTest extends XWorkTestCase {
         assertFalse(child.isStrictMethodInvocation());
     }
 
+    public void testScopedConstantsChildOverridesParent() {
+        PackageConfig parent = new PackageConfig.Builder("parent")
+                .addScopedConstant("first", "parent-first")
+                .addScopedConstant("second", "parent-second")
+                .build();
+
+        PackageConfig child = new PackageConfig.Builder("child")
+                .addParent(parent)
+                .addScopedConstant("second", "child-second")
+                .build();
+
+        assertEquals(Map.of("second", "child-second"), 
child.getScopedConstants());
+        assertEquals(Map.of("first", "parent-first", "second", 
"child-second"), child.getAllScopedConstants());
+    }
+
+    public void testScopedConstantsInheritedThroughTwoLevels() {
+        PackageConfig grandparent = new PackageConfig.Builder("grandparent")
+                .addScopedConstant("first", "grandparent-first")
+                .build();
+        PackageConfig parent = new PackageConfig.Builder("parent")
+                .addParent(grandparent)
+                .build();
+
+        PackageConfig child = new PackageConfig.Builder("child")
+                .addParent(parent)
+                .build();
+
+        assertEquals(Map.of(), child.getScopedConstants());
+        assertEquals(Map.of("first", "grandparent-first"), 
child.getAllScopedConstants());
+    }
+
+    public void testScopedConstantsFirstAddedParentWins() {
+        PackageConfig first = new PackageConfig.Builder("first")
+                .addScopedConstant("shared", "from-first")
+                .build();
+        PackageConfig second = new PackageConfig.Builder("second")
+                .addScopedConstant("shared", "from-second")
+                .build();
+
+        PackageConfig child = new PackageConfig.Builder("child")
+                .addParent(first)
+                .addParent(second)
+                .build();
+
+        assertEquals("from-first", 
child.getAllScopedConstants().get("shared"));
+    }
+
+    public void testScopedConstantsSurviveBuilderCopy() {
+        PackageConfig parent = new PackageConfig.Builder("parent")
+                .addScopedConstant("first", "parent-first")
+                .build();
+        PackageConfig original = new PackageConfig.Builder("child")
+                .addParent(parent)
+                .addScopedConstant("second", "child-second")
+                .build();
+
+        PackageConfig copy = new PackageConfig.Builder(original).build();
+
+        assertEquals(original.getScopedConstants(), copy.getScopedConstants());
+        assertEquals(original.getAllScopedConstants(), 
copy.getAllScopedConstants());
+    }
+
+    public void testScopedConstantsAreUnmodifiableAfterBuild() {
+        PackageConfig config = new PackageConfig.Builder("pkg")
+                .addScopedConstant("first", "value")
+                .build();
+
+        assertThrows(UnsupportedOperationException.class, () -> 
config.getScopedConstants().put("x", "y"));
+        assertThrows(UnsupportedOperationException.class, () -> 
config.getAllScopedConstants().put("x", "y"));
+    }
+
+    public void testScopedConstantsTakePartInEquality() {
+        PackageConfig plain = new PackageConfig.Builder("pkg").build();
+        PackageConfig scoped = new 
PackageConfig.Builder("pkg").addScopedConstant("first", "value").build();
+
+        assertFalse(plain.equals(scoped));
+    }
+
 }

Reply via email to