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 2050da78c94b0cc7d96cb4de5909cf1af3b3cb0e
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Oct 10 17:13:04 2026 +0200

    WW-3476 feat(config): reject unregistered scoped constants at startup
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/struts2/config/ScopableConstants.java   | 44 +++++++++++
 .../struts2/config/impl/DefaultConfiguration.java  | 25 ++++++
 .../struts2/config/SampleScopableConstants.java    | 29 +++++++
 .../config/ScopedConstantsValidationTest.java      | 89 ++++++++++++++++++++++
 .../struts2/config/SecondScopableConstants.java    | 29 +++++++
 .../scoped-constants-abstract-unregistered.xml     | 32 ++++++++
 .../config/scoped-constants-no-registry.xml        | 29 +++++++
 .../struts2/config/scoped-constants-registered.xml | 32 ++++++++
 .../struts2/config/scoped-constants-two-beans.xml  | 35 +++++++++
 .../config/scoped-constants-unregistered.xml       | 32 ++++++++
 10 files changed, 376 insertions(+)

diff --git 
a/core/src/main/java/org/apache/struts2/config/ScopableConstants.java 
b/core/src/main/java/org/apache/struts2/config/ScopableConstants.java
new file mode 100644
index 000000000..fd21ac513
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/config/ScopableConstants.java
@@ -0,0 +1,44 @@
+/*
+ * 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.config;
+
+import org.apache.struts2.inject.Container;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Declares constants a package may override with {@code <scoped-constant>}. 
Register implementations as named beans;
+ * the scopable set is the union over all of them.
+ *
+ * @since 7.5.0
+ */
+public interface ScopableConstants {
+
+    Set<String> getNames();
+
+    static Set<String> collectNames(Container container) {
+        Set<String> names = new HashSet<>();
+        for (String beanName : 
container.getInstanceNames(ScopableConstants.class)) {
+            names.addAll(container.getInstance(ScopableConstants.class, 
beanName).getNames());
+        }
+        return Collections.unmodifiableSet(names);
+    }
+}
diff --git 
a/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java 
b/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java
index 53bf20dc8..4f908bfb6 100644
--- 
a/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java
+++ 
b/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java
@@ -36,6 +36,7 @@ import org.apache.struts2.config.FileManagerFactoryProvider;
 import org.apache.struts2.config.FileManagerProvider;
 import org.apache.struts2.config.PackageProvider;
 import org.apache.struts2.config.RuntimeConfiguration;
+import org.apache.struts2.config.ScopableConstants;
 import org.apache.struts2.config.entities.ActionConfig;
 import org.apache.struts2.config.entities.InterceptorMapping;
 import org.apache.struts2.config.entities.PackageConfig;
@@ -449,6 +450,8 @@ public class DefaultConfiguration implements Configuration {
      * @throws ConfigurationException in case of any configuration errors
      */
     protected synchronized RuntimeConfiguration buildRuntimeConfiguration() 
throws ConfigurationException {
+        validateScopedConstants();
+
         Map<String, Map<String, ActionConfig>> namespaceActionConfigs = new 
LinkedHashMap<>();
         Map<String, String> namespaceConfigs = new LinkedHashMap<>();
 
@@ -488,6 +491,28 @@ public class DefaultConfiguration implements Configuration 
{
                 Collections.unmodifiableMap(namespaceConfigs), matcher, 
appendNamedParameters, fallbackToEmptyNamespace);
     }
 
+    /**
+     * @since 7.5.0
+     */
+    protected void validateScopedConstants() throws ConfigurationException {
+        Set<String> scopableNames = ScopableConstants.collectNames(container);
+        for (PackageConfig packageConfig : packageContexts.values()) {
+            for (String name : packageConfig.getScopedConstants().keySet()) {
+                if (!scopableNames.contains(name)) {
+                    throw new ConfigurationException(String.format("Package 
[%s] declares scoped constant [%s], which is not scopable. %s",
+                            packageConfig.getName(), name, 
describeScopable(scopableNames)), packageConfig);
+                }
+            }
+        }
+    }
+
+    private static String describeScopable(Set<String> scopableNames) {
+        if (scopableNames.isEmpty()) {
+            return "No constants are scopable in this configuration.";
+        }
+        return "Scopable constants: " + new TreeSet<>(scopableNames) + ".";
+    }
+
     private void setDefaultResults(Map<String, ResultConfig> results, 
PackageConfig packageContext) {
         String defaultResult = packageContext.getFullDefaultResultType();
 
diff --git 
a/core/src/test/java/org/apache/struts2/config/SampleScopableConstants.java 
b/core/src/test/java/org/apache/struts2/config/SampleScopableConstants.java
new file mode 100644
index 000000000..c09d58755
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/config/SampleScopableConstants.java
@@ -0,0 +1,29 @@
+/*
+ * 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.config;
+
+import java.util.Set;
+
+public class SampleScopableConstants implements ScopableConstants {
+
+    @Override
+    public Set<String> getNames() {
+        return Set.of("sample.scopable", "sample.unset");
+    }
+}
diff --git 
a/core/src/test/java/org/apache/struts2/config/ScopedConstantsValidationTest.java
 
b/core/src/test/java/org/apache/struts2/config/ScopedConstantsValidationTest.java
new file mode 100644
index 000000000..5cbb1a341
--- /dev/null
+++ 
b/core/src/test/java/org/apache/struts2/config/ScopedConstantsValidationTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.config;
+
+import org.apache.struts2.ActionContext;
+import org.apache.struts2.config.providers.StrutsDefaultConfigurationProvider;
+import org.apache.struts2.inject.Container;
+import org.junit.After;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class ScopedConstantsValidationTest {
+
+    private ConfigurationManager configurationManager;
+
+    @After
+    public void tearDown() {
+        if (configurationManager != null) {
+            configurationManager.destroyConfiguration();
+        }
+        ActionContext.clear();
+    }
+
+    @Test
+    public void registeredNameLoads() {
+        Configuration configuration = load("scoped-constants-registered.xml");
+
+        
assertThat(configuration.getPackageConfig("registered").getScopedConstants())
+                .isEqualTo(Map.of("sample.scopable", "x"));
+    }
+
+    @Test
+    public void unregisteredNameFails() {
+        assertThatThrownBy(() -> load("scoped-constants-unregistered.xml"))
+                .isInstanceOf(ConfigurationException.class)
+                .hasStackTraceContaining("Package [unregistered] declares 
scoped constant [sample.unknown], which is not scopable. "
+                        + "Scopable constants: [sample.scopable, 
sample.unset].");
+    }
+
+    @Test
+    public void abstractPackageIsValidatedToo() {
+        assertThatThrownBy(() -> 
load("scoped-constants-abstract-unregistered.xml"))
+                .isInstanceOf(ConfigurationException.class)
+                .hasStackTraceContaining("Package [abstractBase] declares 
scoped constant [sample.unknown], which is not scopable.");
+    }
+
+    @Test
+    public void anyScopedConstantFailsWhenNothingIsScopable() {
+        assertThatThrownBy(() -> load("scoped-constants-no-registry.xml"))
+                .isInstanceOf(ConfigurationException.class)
+                .hasStackTraceContaining("Package [unregistered] declares 
scoped constant [sample.scopable], which is not scopable. "
+                        + "No constants are scopable in this configuration.");
+    }
+
+    @Test
+    public void namesFromAllBeansAreScopable() {
+        Configuration configuration = load("scoped-constants-two-beans.xml");
+
+        assertThat(configuration.getPackageConfig("both").getScopedConstants())
+                .isEqualTo(Map.of("sample.scopable", "x", "second.scopable", 
"y"));
+    }
+
+    private Configuration load(String fixture) {
+        configurationManager = new 
ConfigurationManager(Container.DEFAULT_NAME);
+        configurationManager.addContainerProvider(new 
StrutsDefaultConfigurationProvider());
+        configurationManager.addContainerProvider(new 
StrutsXmlConfigurationProvider("org/apache/struts2/config/" + fixture));
+        return configurationManager.getConfiguration();
+    }
+}
diff --git 
a/core/src/test/java/org/apache/struts2/config/SecondScopableConstants.java 
b/core/src/test/java/org/apache/struts2/config/SecondScopableConstants.java
new file mode 100644
index 000000000..9534dab7f
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/config/SecondScopableConstants.java
@@ -0,0 +1,29 @@
+/*
+ * 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.config;
+
+import java.util.Set;
+
+public class SecondScopableConstants implements ScopableConstants {
+
+    @Override
+    public Set<String> getNames() {
+        return Set.of("second.scopable");
+    }
+}
diff --git 
a/core/src/test/resources/org/apache/struts2/config/scoped-constants-abstract-unregistered.xml
 
b/core/src/test/resources/org/apache/struts2/config/scoped-constants-abstract-unregistered.xml
new file mode 100644
index 000000000..6eb2c623c
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/config/scoped-constants-abstract-unregistered.xml
@@ -0,0 +1,32 @@
+<?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>
+    <bean type="org.apache.struts2.config.ScopableConstants" name="sample"
+          class="org.apache.struts2.config.SampleScopableConstants"/>
+
+    <package name="abstractBase" abstract="true">
+        <scoped-constant name="sample.unknown" value="x"/>
+    </package>
+</struts>
diff --git 
a/core/src/test/resources/org/apache/struts2/config/scoped-constants-no-registry.xml
 
b/core/src/test/resources/org/apache/struts2/config/scoped-constants-no-registry.xml
new file mode 100644
index 000000000..6d622d8ac
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/config/scoped-constants-no-registry.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 7.5//EN"
+        "struts-7.5.dtd">
+<struts>
+    <package name="unregistered" namespace="/unregistered">
+        <scoped-constant name="sample.scopable" value="x"/>
+    </package>
+</struts>
diff --git 
a/core/src/test/resources/org/apache/struts2/config/scoped-constants-registered.xml
 
b/core/src/test/resources/org/apache/struts2/config/scoped-constants-registered.xml
new file mode 100644
index 000000000..f3e5a5401
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/config/scoped-constants-registered.xml
@@ -0,0 +1,32 @@
+<?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>
+    <bean type="org.apache.struts2.config.ScopableConstants" name="sample"
+          class="org.apache.struts2.config.SampleScopableConstants"/>
+
+    <package name="registered" namespace="/registered">
+        <scoped-constant name="sample.scopable" value="x"/>
+    </package>
+</struts>
diff --git 
a/core/src/test/resources/org/apache/struts2/config/scoped-constants-two-beans.xml
 
b/core/src/test/resources/org/apache/struts2/config/scoped-constants-two-beans.xml
new file mode 100644
index 000000000..d79022cf6
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/config/scoped-constants-two-beans.xml
@@ -0,0 +1,35 @@
+<?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>
+    <bean type="org.apache.struts2.config.ScopableConstants" name="sample"
+          class="org.apache.struts2.config.SampleScopableConstants"/>
+    <bean type="org.apache.struts2.config.ScopableConstants" name="second"
+          class="org.apache.struts2.config.SecondScopableConstants"/>
+
+    <package name="both" namespace="/both">
+        <scoped-constant name="sample.scopable" value="x"/>
+        <scoped-constant name="second.scopable" value="y"/>
+    </package>
+</struts>
diff --git 
a/core/src/test/resources/org/apache/struts2/config/scoped-constants-unregistered.xml
 
b/core/src/test/resources/org/apache/struts2/config/scoped-constants-unregistered.xml
new file mode 100644
index 000000000..0fd512212
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/config/scoped-constants-unregistered.xml
@@ -0,0 +1,32 @@
+<?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>
+    <bean type="org.apache.struts2.config.ScopableConstants" name="sample"
+          class="org.apache.struts2.config.SampleScopableConstants"/>
+
+    <package name="unregistered" namespace="/unregistered">
+        <scoped-constant name="sample.unknown" value="x"/>
+    </package>
+</struts>

Reply via email to