From fa5649803386a25f55be0043cff4f391f7aa9a88 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Mon, 12 Oct 2026 14:12:14 +0200 Subject: [PATCH 1/2] WW-5767 feat(config): apply value substitution to scoped-constant values values now go through the injected ValueSubstitutor at load time, the same as , so ${env.NAME} and ${system.property} resolve instead of being stored literally. Both paths share one helper. The test loads struts-default.xml so the substitutor comes from the main container that loadPackages() is injected with; the ConfigurationTestBase container registers none and could not observe the change. Co-Authored-By: Claude Opus 5.5 --- .../XmlDocConfigurationProvider.java | 16 +++-- .../ScopedConstantsSubstitutionTest.java | 65 +++++++++++++++++++ .../struts-scoped-constants-substitution.xml | 33 ++++++++++ 3 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 core/src/test/java/org/apache/struts2/config/ScopedConstantsSubstitutionTest.java create mode 100644 core/src/test/resources/org/apache/struts2/config/struts-scoped-constants-substitution.xml 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 b6c606e66d..a5547d9350 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 @@ -277,14 +277,16 @@ protected void registerBean(Element child, Map loadedBeans, Contai protected void registerConstant(Element child, LocatableProperties props) { String name = child.getAttribute("name"); - String value = child.getAttribute("value"); + String value = substituteValue(child.getAttribute("value")); + props.setProperty(name, value, child); + } - if (valueSubstitutor != null) { - LOG.debug("Substituting value [{}] using [{}]", value, valueSubstitutor.getClass().getName()); - value = valueSubstitutor.substitute(value); + private String substituteValue(String value) { + if (valueSubstitutor == null) { + return value; } - - props.setProperty(name, value, child); + LOG.debug("Substituting value [{}] using [{}]", value, valueSubstitutor.getClass().getName()); + return valueSubstitutor.substitute(value); } protected void registerUnknownHandlerStack(Element child) { @@ -852,7 +854,7 @@ protected void loadScopedConstants(PackageConfig.Builder packageContext, Element throw new ConfigurationException(format("Package [%s] declares scoped constant [%s] more than once", packageContext.getName(), name), constantElement); } - packageContext.addScopedConstant(name, constantElement.getAttribute("value")); + packageContext.addScopedConstant(name, substituteValue(constantElement.getAttribute("value"))); }); } diff --git a/core/src/test/java/org/apache/struts2/config/ScopedConstantsSubstitutionTest.java b/core/src/test/java/org/apache/struts2/config/ScopedConstantsSubstitutionTest.java new file mode 100644 index 0000000000..2dc7f53c78 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/config/ScopedConstantsSubstitutionTest.java @@ -0,0 +1,65 @@ +/* + * 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.inject.Container; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ScopedConstantsSubstitutionTest { + + private static final String PROPERTY = "ww5767.value"; + + private ConfigurationManager configurationManager; + + @Before + public void setUp() { + System.setProperty(PROPERTY, "from-system-property"); + } + + @After + public void tearDown() { + System.clearProperty(PROPERTY); + if (configurationManager != null) { + configurationManager.destroyConfiguration(); + } + ActionContext.clear(); + } + + @Test + public void valuesAreSubstitutedLikeGlobalConstants() { + configurationManager = new ConfigurationManager(Container.DEFAULT_NAME); + configurationManager.addContainerProvider(new DefaultPropertiesProvider()); + configurationManager.addContainerProvider(new StrutsXmlConfigurationProvider("struts-default.xml")); + configurationManager.addContainerProvider(new StrutsXmlConfigurationProvider( + "org/apache/struts2/config/struts-scoped-constants-substitution.xml")); + configurationManager.addContainerProvider(new StrutsBeanSelectionProvider()); + + Configuration configuration = configurationManager.getConfiguration(); + + assertThat(configuration.getPackageConfig("substituted").getScopedConstants()) + .isEqualTo(Map.of("sample.scopable", "from-system-property", "sample.unset", "literal")); + } +} diff --git a/core/src/test/resources/org/apache/struts2/config/struts-scoped-constants-substitution.xml b/core/src/test/resources/org/apache/struts2/config/struts-scoped-constants-substitution.xml new file mode 100644 index 0000000000..0b0f5e1667 --- /dev/null +++ b/core/src/test/resources/org/apache/struts2/config/struts-scoped-constants-substitution.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + From 912d3bf9b11090e098f453ed3beaab9b1c68ccb4 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Mon, 12 Oct 2026 16:09:07 +0200 Subject: [PATCH 2/2] WW-5767 test(config): cover scoped constants loaded without a ValueSubstitutor The substitutor is injected with required = false, so a provider used without injection keeps values as written. Every harness in the suite injects one through DefaultConfiguration.bootstrapFactories, so nothing exercised that branch. Co-Authored-By: Claude Opus 5.5 --- .../XmlConfigurationProviderPackagesTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 35e2cdb2f4..b866f6ce5d 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 @@ -21,8 +21,12 @@ import org.apache.struts2.config.ConfigurationException; import org.apache.struts2.config.ConfigurationProvider; import org.apache.struts2.config.RuntimeConfiguration; +import org.apache.struts2.config.StrutsXmlConfigurationProvider; import org.apache.struts2.config.entities.PackageConfig; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import javax.xml.parsers.DocumentBuilderFactory; import java.util.List; import java.util.Map; @@ -229,6 +233,20 @@ public void testScopedConstantIsRejectedUnderOlderDtd() { } } + public void testScopedConstantIsKeptAsWrittenWithoutValueSubstitutor() throws Exception { + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + Element packageElement = document.createElement("package"); + Element scopedConstant = document.createElement("scoped-constant"); + scopedConstant.setAttribute("name", "first"); + scopedConstant.setAttribute("value", "${ww5767.value}"); + packageElement.appendChild(scopedConstant); + PackageConfig.Builder packageContext = new PackageConfig.Builder("raw"); + + new StrutsXmlConfigurationProvider("unused.xml").loadScopedConstants(packageContext, packageElement); + + assertEquals(Map.of("first", "${ww5767.value}"), packageContext.build().getScopedConstants()); + } + private String getXmlConfigFilePath(String fileName) { return "org/apache/struts2/config/providers/" + fileName; }