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.git


The following commit(s) were added to refs/heads/main by this push:
     new 65a8f8668 WW-5768 Make struts.ui.theme and struts.ui.templateDir 
scopable per package (#2008)
65a8f8668 is described below

commit 65a8f86689ec887033fbca52f69d0ae18787c082
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Oct 12 21:30:00 2026 +0200

    WW-5768 Make struts.ui.theme and struts.ui.templateDir scopable per package 
(#2008)
    
    * WW-5768 feat(ui): make struts.ui.theme and struts.ui.templateDir scopable 
per package
    
    A package can now override the default UI theme and template directory with
    <scoped-constant>, and child packages inherit it through extends. Only the 
last
    fallback in UIBean.getTheme()/getTemplateDir() changes: it asks the
    ScopedConstantProvider, which returns the package override or the global
    constant. The tag attribute, the enclosing form and the request/session/
    application attributes keep their precedence.
    
    setDefaultUITheme/setDefaultTemplateDir and their fields are no longer read 
and
    are deprecated for removal. Components built without the container now need 
a
    ScopedConstantProvider, the same way they already need a CspNonceReader; the
    javatemplates tests set a stub.
    
    The empty-registry validation test now loads only the bootstrap beans, 
because
    StrutsDefaultConfigurationProvider registers the UI names.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    
    * WW-5768 docs(ui): add @deprecated Javadoc to the deprecated UIBean fields
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../components/StrutsUiScopableConstants.java      |  37 ++++++
 .../java/org/apache/struts2/components/UIBean.java |  26 +++-
 .../StrutsDefaultConfigurationProvider.java        |   3 +
 core/src/main/resources/struts-beans.xml           |   2 +
 .../components/UIBeanScopedConstantsTest.java      | 133 +++++++++++++++++++++
 .../org/apache/struts2/components/UIBeanTest.java  |   6 +-
 .../config/ScopedConstantsValidationTest.java      |  32 ++++-
 .../apache/struts2/components/struts-scoped-ui.xml |  34 ++++++
 .../struts2/config/struts-scoped-constants-ui.xml  |  30 +++++
 .../struts2/views/java/simple/ActionErrorTest.java |   1 +
 .../views/java/simple/ActionMessageTest.java       |   1 +
 .../struts2/views/java/simple/AnchorTest.java      |   1 +
 .../struts2/views/java/simple/CheckboxTest.java    |   1 +
 .../views/java/simple/DateTextFieldTest.java       |   1 +
 .../struts2/views/java/simple/FieldErrorTest.java  |   1 +
 .../apache/struts2/views/java/simple/FileTest.java |   1 +
 .../apache/struts2/views/java/simple/FormTest.java |   1 +
 .../apache/struts2/views/java/simple/HeadTest.java |   1 +
 .../struts2/views/java/simple/HiddenTest.java      |   1 +
 .../struts2/views/java/simple/LabelTest.java       |   1 +
 .../apache/struts2/views/java/simple/LinkTest.java |   1 +
 .../struts2/views/java/simple/PasswordTest.java    |   1 +
 .../struts2/views/java/simple/ResetTest.java       |   1 +
 .../struts2/views/java/simple/ScriptTest.java      |   1 +
 .../struts2/views/java/simple/SelectTest.java      |   1 +
 .../struts2/views/java/simple/SubmitTest.java      |   1 +
 .../struts2/views/java/simple/TextAreaTest.java    |   1 +
 .../struts2/views/java/simple/TextFieldTest.java   |   1 +
 .../struts2/views/java/simple/TokenTest.java       |   1 +
 29 files changed, 315 insertions(+), 8 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/components/StrutsUiScopableConstants.java
 
b/core/src/main/java/org/apache/struts2/components/StrutsUiScopableConstants.java
new file mode 100644
index 000000000..1fb0a468e
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/components/StrutsUiScopableConstants.java
@@ -0,0 +1,37 @@
+/*
+ * 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.components;
+
+import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.config.ScopableConstants;
+
+import java.util.Set;
+
+/**
+ * Lets a package override the default UI theme and template directory with 
{@code <scoped-constant>}.
+ *
+ * @since 7.5.0
+ */
+public class StrutsUiScopableConstants implements ScopableConstants {
+
+    @Override
+    public Set<String> getNames() {
+        return Set.of(StrutsConstants.STRUTS_UI_THEME, 
StrutsConstants.STRUTS_UI_TEMPLATEDIR);
+    }
+}
diff --git a/core/src/main/java/org/apache/struts2/components/UIBean.java 
b/core/src/main/java/org/apache/struts2/components/UIBean.java
index 36c51340a..f193230a4 100644
--- a/core/src/main/java/org/apache/struts2/components/UIBean.java
+++ b/core/src/main/java/org/apache/struts2/components/UIBean.java
@@ -19,6 +19,7 @@
 package org.apache.struts2.components;
 
 import org.apache.struts2.config.ConfigurationException;
+import org.apache.struts2.config.ScopedConstantProvider;
 import org.apache.struts2.inject.Inject;
 import org.apache.struts2.util.TextParseUtil;
 import org.apache.struts2.util.ValueStack;
@@ -528,11 +529,17 @@ public abstract class UIBean extends Component {
     // dynamic attributes
     protected Map<String, Object> dynamicAttributes = new HashMap<>();
 
+    /** @deprecated no longer read; use {@link #scopedConstantProvider} */
+    @Deprecated(since = "7.5.0", forRemoval = true)
     protected String defaultTemplateDir;
+    /** @deprecated no longer read; use {@link #scopedConstantProvider} */
+    @Deprecated(since = "7.5.0", forRemoval = true)
     protected String defaultUITheme;
     protected String uiThemeExpansionToken;
     protected String uiStaticContentPath;
 
+    protected ScopedConstantProvider scopedConstantProvider;
+
     protected TemplateEngineManager templateEngineManager;
 
     protected CspNonceReader cspNonceReader;
@@ -540,16 +547,27 @@ public abstract class UIBean extends Component {
     protected HtmlConstraintProvider htmlConstraintProvider;
     protected boolean html5ConstraintsEnabled;
 
-    @Inject(StrutsConstants.STRUTS_UI_TEMPLATEDIR)
+    /**
+     * @deprecated no longer read; set {@code struts.ui.templateDir} globally 
or per package with {@code <scoped-constant>}
+     */
+    @Deprecated(since = "7.5.0", forRemoval = true)
     public void setDefaultTemplateDir(String dir) {
         this.defaultTemplateDir = dir;
     }
 
-    @Inject(StrutsConstants.STRUTS_UI_THEME)
+    /**
+     * @deprecated no longer read; set {@code struts.ui.theme} globally or per 
package with {@code <scoped-constant>}
+     */
+    @Deprecated(since = "7.5.0", forRemoval = true)
     public void setDefaultUITheme(String theme) {
         this.defaultUITheme = theme;
     }
 
+    @Inject
+    public void setScopedConstantProvider(ScopedConstantProvider 
scopedConstantProvider) {
+        this.scopedConstantProvider = scopedConstantProvider;
+    }
+
     @Inject(StrutsConstants.STRUTS_UI_THEME_EXPANSION_TOKEN)
     public void setUIThemeExpansionToken(String uiThemeExpansionToken) {
         this.uiThemeExpansionToken = uiThemeExpansionToken;
@@ -643,7 +661,7 @@ public abstract class UIBean extends Component {
 
         // Default template set
         if (isBlank(result)) {
-            result = defaultTemplateDir;
+            result = 
scopedConstantProvider.getValue(StrutsConstants.STRUTS_UI_TEMPLATEDIR);
         }
 
         // Defaults to 'template'
@@ -675,7 +693,7 @@ public abstract class UIBean extends Component {
 
         // Default theme set
         if (isBlank(result)) {
-            result = defaultUITheme;
+            result = 
scopedConstantProvider.getValue(StrutsConstants.STRUTS_UI_THEME);
         }
 
         return result;
diff --git 
a/core/src/main/java/org/apache/struts2/config/providers/StrutsDefaultConfigurationProvider.java
 
b/core/src/main/java/org/apache/struts2/config/providers/StrutsDefaultConfigurationProvider.java
index 736af6c91..c8aea1c7b 100644
--- 
a/core/src/main/java/org/apache/struts2/config/providers/StrutsDefaultConfigurationProvider.java
+++ 
b/core/src/main/java/org/apache/struts2/config/providers/StrutsDefaultConfigurationProvider.java
@@ -23,9 +23,11 @@ import org.apache.struts2.DefaultActionProxyFactory;
 import org.apache.struts2.DefaultUnknownHandlerManager;
 import org.apache.struts2.FileManagerFactory;
 import org.apache.struts2.UnknownHandlerManager;
+import org.apache.struts2.components.StrutsUiScopableConstants;
 import org.apache.struts2.config.Configuration;
 import org.apache.struts2.config.ConfigurationException;
 import org.apache.struts2.config.ConfigurationProvider;
+import org.apache.struts2.config.ScopableConstants;
 import org.apache.struts2.config.ScopedConstantProvider;
 import org.apache.struts2.config.impl.DefaultConfiguration;
 import org.apache.struts2.conversion.NullHandler;
@@ -132,6 +134,7 @@ public class StrutsDefaultConfigurationProvider implements 
ConfigurationProvider
                 .factory(UnknownHandlerManager.class, 
DefaultUnknownHandlerManager.class, Scope.SINGLETON)
 
                 .factory(ScopedConstantProvider.class, 
StrutsScopedConstantProvider.class, Scope.SINGLETON)
+                .factory(ScopableConstants.class, 
StrutsUiScopableConstants.class, Scope.SINGLETON)
 
                 // silly workarounds for ognl since there is no way to flush 
its caches
                 .factory(PropertyAccessor.class, List.class.getName(), 
XWorkListPropertyAccessor.class, Scope.SINGLETON)
diff --git a/core/src/main/resources/struts-beans.xml 
b/core/src/main/resources/struts-beans.xml
index abfca2af6..922dc91d7 100644
--- a/core/src/main/resources/struts-beans.xml
+++ b/core/src/main/resources/struts-beans.xml
@@ -260,6 +260,8 @@
 
     <bean type="org.apache.struts2.config.ScopedConstantProvider" name="struts"
           
class="org.apache.struts2.config.providers.StrutsScopedConstantProvider" 
scope="singleton"/>
+    <bean type="org.apache.struts2.config.ScopableConstants" name="struts"
+          class="org.apache.struts2.components.StrutsUiScopableConstants" 
scope="singleton"/>
 
     <bean type="org.apache.struts2.url.QueryStringBuilder" 
name="strutsQueryStringBuilder"
           class="org.apache.struts2.url.StrutsQueryStringBuilder" 
scope="singleton"/>
diff --git 
a/core/src/test/java/org/apache/struts2/components/UIBeanScopedConstantsTest.java
 
b/core/src/test/java/org/apache/struts2/components/UIBeanScopedConstantsTest.java
new file mode 100644
index 000000000..d5800f046
--- /dev/null
+++ 
b/core/src/test/java/org/apache/struts2/components/UIBeanScopedConstantsTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.components;
+
+import org.apache.struts2.ActionContext;
+import org.apache.struts2.ActionSupport;
+import org.apache.struts2.StrutsInternalTestCase;
+import org.apache.struts2.config.entities.ActionConfig;
+import org.apache.struts2.dispatcher.AttributeMap;
+import org.apache.struts2.dispatcher.DispatcherConstants;
+import org.apache.struts2.dispatcher.RequestMap;
+import org.apache.struts2.mock.MockActionInvocation;
+import org.apache.struts2.mock.MockActionProxy;
+import org.apache.struts2.util.ValueStack;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import java.util.Map;
+
+public class UIBeanScopedConstantsTest extends StrutsInternalTestCase {
+
+    private ValueStack stack;
+    private MockHttpServletRequest request;
+    private MockHttpServletResponse response;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        
initDispatcherWithConfigs("struts-default.xml,org/apache/struts2/components/struts-scoped-ui.xml");
+        stack = ActionContext.getContext().getValueStack();
+        request = new MockHttpServletRequest();
+        response = new MockHttpServletResponse();
+    }
+
+    public void testPackageOverrideIsUsedWithoutTagAttribute() {
+        enterPackage("admin");
+
+        TextField txtFld = newTextField();
+
+        assertEquals("css_xhtml", txtFld.getTheme());
+        assertEquals("admin-template", txtFld.getTemplateDir());
+    }
+
+    public void testChildPackageInheritsOverride() {
+        enterPackage("adminChild");
+
+        TextField txtFld = newTextField();
+
+        assertEquals("css_xhtml", txtFld.getTheme());
+        assertEquals("admin-template", txtFld.getTemplateDir());
+    }
+
+    public void testGlobalValueWithoutOverride() {
+        enterPackage("plain");
+
+        TextField txtFld = newTextField();
+
+        assertEquals("xhtml", txtFld.getTheme());
+        assertEquals("template", txtFld.getTemplateDir());
+    }
+
+    public void testGlobalValueWithoutInvocation() {
+        TextField txtFld = newTextField();
+
+        assertEquals("xhtml", txtFld.getTheme());
+        assertEquals("template", txtFld.getTemplateDir());
+    }
+
+    public void testTagAttributeWinsOverPackageOverride() {
+        enterPackage("admin");
+
+        TextField txtFld = newTextField();
+        txtFld.setTheme("simple");
+        txtFld.setTemplateDir("tag-template");
+
+        assertEquals("simple", txtFld.getTheme());
+        assertEquals("tag-template", txtFld.getTemplateDir());
+    }
+
+    public void testFormThemeWinsOverPackageOverride() {
+        enterPackage("admin");
+
+        Form form = new Form(stack, request, response);
+        container.inject(form);
+        form.setTheme("simple");
+
+        assertEquals("simple", newTextField().getTheme());
+    }
+
+    public void testScopeAttributeWinsOverPackageOverride() {
+        enterPackage("admin");
+        request.setAttribute("theme", "simple");
+        request.setAttribute("templateDir", "attr-template");
+        Map<String, Object> context = stack.getContext();
+        context.put(DispatcherConstants.REQUEST, new RequestMap(request));
+        context.put("attr", new AttributeMap(context));
+
+        TextField txtFld = newTextField();
+
+        assertEquals("simple", txtFld.getTheme());
+        assertEquals("attr-template", txtFld.getTemplateDir());
+    }
+
+    private TextField newTextField() {
+        TextField txtFld = new TextField(stack, request, response);
+        container.inject(txtFld);
+        return txtFld;
+    }
+
+    private void enterPackage(String packageName) {
+        MockActionProxy proxy = new MockActionProxy();
+        proxy.setConfig(new ActionConfig.Builder(packageName, "test", 
ActionSupport.class.getName()).build());
+        MockActionInvocation invocation = new MockActionInvocation();
+        invocation.setProxy(proxy);
+        ActionContext.getContext().withActionInvocation(invocation);
+    }
+}
diff --git a/core/src/test/java/org/apache/struts2/components/UIBeanTest.java 
b/core/src/test/java/org/apache/struts2/components/UIBeanTest.java
index cc194bd08..41b19575c 100644
--- a/core/src/test/java/org/apache/struts2/components/UIBeanTest.java
+++ b/core/src/test/java/org/apache/struts2/components/UIBeanTest.java
@@ -183,6 +183,7 @@ public class UIBeanTest extends StrutsInternalTestCase {
         MockHttpServletResponse res = new MockHttpServletResponse();
 
         TextField txtFld = new TextField(stack, req, res);
+        container.inject(txtFld);
 
         Template defaultTemplate = txtFld.buildTemplateName(null, 
defaultTemplateName);
         Template customTemplate = txtFld.buildTemplateName(customTemplateName, 
defaultTemplateName);
@@ -203,7 +204,7 @@ public class UIBeanTest extends StrutsInternalTestCase {
 
         TextField txtFld = new TextField(stack, req, res);
         txtFld.setTemplateDir(explicitTemplateDir);
-        txtFld.setDefaultTemplateDir(defaultTemplateDir);
+        txtFld.setScopedConstantProvider(name -> defaultTemplateDir);
 
         assertEquals(explicitTemplateDir, txtFld.getTemplateDir());
     }
@@ -215,7 +216,7 @@ public class UIBeanTest extends StrutsInternalTestCase {
         MockHttpServletResponse res = new MockHttpServletResponse();
 
         TextField txtFld = new TextField(stack, req, res);
-        txtFld.setDefaultTemplateDir(defaultTemplateDir);
+        txtFld.setScopedConstantProvider(name -> defaultTemplateDir);
 
         assertEquals(defaultTemplateDir, txtFld.getTemplateDir());
     }
@@ -226,6 +227,7 @@ public class UIBeanTest extends StrutsInternalTestCase {
         MockHttpServletResponse res = new MockHttpServletResponse();
 
         TextField txtFld = new TextField(stack, req, res);
+        txtFld.setScopedConstantProvider(name -> null);
 
         assertEquals("template", txtFld.getTemplateDir());
     }
diff --git 
a/core/src/test/java/org/apache/struts2/config/ScopedConstantsValidationTest.java
 
b/core/src/test/java/org/apache/struts2/config/ScopedConstantsValidationTest.java
index 59dfedc02..78b6163db 100644
--- 
a/core/src/test/java/org/apache/struts2/config/ScopedConstantsValidationTest.java
+++ 
b/core/src/test/java/org/apache/struts2/config/ScopedConstantsValidationTest.java
@@ -19,8 +19,14 @@
 package org.apache.struts2.config;
 
 import org.apache.struts2.ActionContext;
+import org.apache.struts2.FileManagerFactory;
+import org.apache.struts2.config.impl.DefaultConfiguration;
 import org.apache.struts2.config.providers.StrutsDefaultConfigurationProvider;
 import org.apache.struts2.inject.Container;
+import org.apache.struts2.inject.ContainerBuilder;
+import org.apache.struts2.inject.Scope;
+import org.apache.struts2.util.fs.DefaultFileManagerFactory;
+import org.apache.struts2.util.location.LocatableProperties;
 import org.junit.After;
 import org.junit.Test;
 
@@ -49,12 +55,20 @@ public class ScopedConstantsValidationTest {
                 .isEqualTo(Map.of("sample.scopable", "x"));
     }
 
+    @Test
+    public void uiThemeAndTemplateDirAreScopable() {
+        Configuration configuration = load("struts-scoped-constants-ui.xml");
+
+        assertThat(configuration.getPackageConfig("ui").getScopedConstants())
+                .isEqualTo(Map.of("struts.ui.theme", "css_xhtml", 
"struts.ui.templateDir", "admin-template"));
+    }
+
     @Test
     public void unregisteredNameFails() {
         assertThatThrownBy(() -> 
load("struts-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].");
+                        + "Scopable constants: [sample.scopable, sample.unset, 
struts.ui.templateDir, struts.ui.theme].");
     }
 
     @Test
@@ -66,7 +80,7 @@ public class ScopedConstantsValidationTest {
 
     @Test
     public void anyScopedConstantFailsWhenNothingIsScopable() {
-        assertThatThrownBy(() -> 
load("struts-scoped-constants-no-registry.xml"))
+        assertThatThrownBy(() -> 
loadWithBootstrapBeansOnly("struts-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.");
@@ -86,4 +100,18 @@ public class ScopedConstantsValidationTest {
         configurationManager.addContainerProvider(new 
StrutsXmlConfigurationProvider("org/apache/struts2/config/" + fixture));
         return configurationManager.getConfiguration();
     }
+
+    private Configuration loadWithBootstrapBeansOnly(String fixture) {
+        configurationManager = new 
ConfigurationManager(Container.DEFAULT_NAME);
+        configurationManager.addContainerProvider(new 
StrutsDefaultConfigurationProvider() {
+            @Override
+            public void register(ContainerBuilder builder, LocatableProperties 
props) {
+                DefaultConfiguration.bootstrapFactories(builder)
+                        .factory(FileManagerFactory.class, 
DefaultFileManagerFactory.class, Scope.SINGLETON);
+                DefaultConfiguration.BOOTSTRAP_CONSTANTS.forEach((name, value) 
-> props.setProperty(name, String.valueOf(value)));
+            }
+        });
+        configurationManager.addContainerProvider(new 
StrutsXmlConfigurationProvider("org/apache/struts2/config/" + fixture));
+        return configurationManager.getConfiguration();
+    }
 }
diff --git 
a/core/src/test/resources/org/apache/struts2/components/struts-scoped-ui.xml 
b/core/src/test/resources/org/apache/struts2/components/struts-scoped-ui.xml
new file mode 100644
index 000000000..a156ba7af
--- /dev/null
+++ b/core/src/test/resources/org/apache/struts2/components/struts-scoped-ui.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="admin" extends="struts-default" namespace="/admin">
+        <scoped-constant name="struts.ui.theme" value="css_xhtml"/>
+        <scoped-constant name="struts.ui.templateDir" value="admin-template"/>
+    </package>
+
+    <package name="adminChild" extends="admin" namespace="/admin/child"/>
+
+    <package name="plain" extends="struts-default" namespace="/plain"/>
+</struts>
diff --git 
a/core/src/test/resources/org/apache/struts2/config/struts-scoped-constants-ui.xml
 
b/core/src/test/resources/org/apache/struts2/config/struts-scoped-constants-ui.xml
new file mode 100644
index 000000000..c1ca79b0d
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/struts2/config/struts-scoped-constants-ui.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="ui" namespace="/ui">
+        <scoped-constant name="struts.ui.theme" value="css_xhtml"/>
+        <scoped-constant name="struts.ui.templateDir" value="admin-template"/>
+    </package>
+</struts>
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ActionErrorTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ActionErrorTest.java
index 9129f24bc..5fef5f66c 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ActionErrorTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ActionErrorTest.java
@@ -75,6 +75,7 @@ public class ActionErrorTest extends AbstractTest {
         super.setUp();
         this.tag = new ActionError(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ActionMessageTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ActionMessageTest.java
index 8d8895372..450262b78 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ActionMessageTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ActionMessageTest.java
@@ -75,6 +75,7 @@ public class ActionMessageTest extends AbstractTest {
         super.setUp();
         this.tag = new ActionError(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/AnchorTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/AnchorTest.java
index d7512ccd3..8cd4baeb2 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/AnchorTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/AnchorTest.java
@@ -114,6 +114,7 @@ public class AnchorTest extends AbstractTest {
         this.tag.setUrlRenderer(new ServletUrlRenderer());
         this.tag.setNotExcludedAcceptedPatterns(new 
DefaultNotExcludedAcceptedPatternsChecker());
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/CheckboxTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/CheckboxTest.java
index acc811704..efd4e1dbf 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/CheckboxTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/CheckboxTest.java
@@ -93,6 +93,7 @@ public class CheckboxTest extends 
AbstractCommonAttributesTest {
         tag = new Checkbox(stack, request, response);
         tag.setNotExcludedAcceptedPatterns(new 
DefaultNotExcludedAcceptedPatternsChecker());
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/DateTextFieldTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/DateTextFieldTest.java
index 7b1d36069..1cc501441 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/DateTextFieldTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/DateTextFieldTest.java
@@ -59,6 +59,7 @@ public class DateTextFieldTest extends 
AbstractCommonAttributesTest {
         super.setUp();
         this.tag = new DateTextField(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FieldErrorTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FieldErrorTest.java
index fca239293..686fe047a 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FieldErrorTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FieldErrorTest.java
@@ -103,6 +103,7 @@ public class FieldErrorTest extends AbstractTest {
         super.setUp();
         this.tag = new FieldError(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FileTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FileTest.java
index dce590b30..23cf749bd 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FileTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FileTest.java
@@ -53,6 +53,7 @@ public class FileTest extends AbstractCommonAttributesTest {
         super.setUp();
         this.tag = new File(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FormTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FormTest.java
index 9ee9c9152..b37ae6e0e 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FormTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/FormTest.java
@@ -83,5 +83,6 @@ public class FormTest extends AbstractCommonAttributesTest {
         EasyMock.replay(renderer);
         tag.setUrlRenderer(renderer);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 }
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/HeadTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/HeadTest.java
index ace29fdbc..384f162e9 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/HeadTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/HeadTest.java
@@ -42,6 +42,7 @@ public class HeadTest extends AbstractTest {
         super.setUp();
         this.tag = new Head(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/HiddenTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/HiddenTest.java
index c517a62e8..4bcb21fb0 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/HiddenTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/HiddenTest.java
@@ -52,6 +52,7 @@ public class HiddenTest extends AbstractTest {
         super.setUp();
         this.tag = new Hidden(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/LabelTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/LabelTest.java
index 90cda064c..b897d956e 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/LabelTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/LabelTest.java
@@ -51,6 +51,7 @@ public class LabelTest extends AbstractCommonAttributesTest {
         super.setUp();
         this.tag = new Label(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/LinkTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/LinkTest.java
index 69c1a11bb..f12d2878a 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/LinkTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/LinkTest.java
@@ -106,5 +106,6 @@ public class LinkTest extends AbstractTest {
         super.setUp();
         this.tag = new Link(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 }
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/PasswordTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/PasswordTest.java
index ca12ff316..e941a6612 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/PasswordTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/PasswordTest.java
@@ -74,6 +74,7 @@ public class PasswordTest extends 
AbstractCommonAttributesTest {
         super.setUp();
         this.tag = new Password(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ResetTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ResetTest.java
index 14464145f..6ef1cc8fe 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ResetTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ResetTest.java
@@ -72,6 +72,7 @@ public class ResetTest extends AbstractCommonAttributesTest {
         super.setUp();
         this.tag = new Reset(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ScriptTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ScriptTest.java
index ad042e964..f35ad883c 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ScriptTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/ScriptTest.java
@@ -75,5 +75,6 @@ public class ScriptTest extends AbstractTest {
         this.tag = new Script(stack, request, response);
         tag.setNotExcludedAcceptedPatterns(new 
DefaultNotExcludedAcceptedPatternsChecker());
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 }
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/SelectTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/SelectTest.java
index 441d4e152..aecf22c7b 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/SelectTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/SelectTest.java
@@ -117,6 +117,7 @@ public class SelectTest extends 
AbstractCommonAttributesTest {
         super.setUp();
         this.tag = new Select(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/SubmitTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/SubmitTest.java
index 4760bcc5c..d3b8b87ef 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/SubmitTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/SubmitTest.java
@@ -157,6 +157,7 @@ public class SubmitTest extends 
AbstractCommonAttributesTest {
         super.setUp();
         this.tag = new Submit(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/TextAreaTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/TextAreaTest.java
index 08d08caba..6aab92dab 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/TextAreaTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/TextAreaTest.java
@@ -74,6 +74,7 @@ public class TextAreaTest extends 
AbstractCommonAttributesTest {
         super.setUp();
         this.tag = new TextArea(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/TextFieldTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/TextFieldTest.java
index 331442f13..e4b77b66e 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/TextFieldTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/TextFieldTest.java
@@ -55,6 +55,7 @@ public class TextFieldTest extends 
AbstractCommonAttributesTest {
         super.setUp();
         this.tag = new TextField(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
     }
 
     @Override
diff --git 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/TokenTest.java
 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/TokenTest.java
index 4680e2bb1..cc28e0a55 100644
--- 
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/TokenTest.java
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/TokenTest.java
@@ -52,6 +52,7 @@ public class TokenTest extends AbstractTest {
         super.setUp();
         this.tag = new Token(stack, request, response);
         this.tag.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        this.tag.setScopedConstantProvider(name -> null);
 
         ActionContext.of()
             .withSession(new HashMap<>())

Reply via email to