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 617faf1b1607c0eb8cbf06ea4f23a16839310981 Author: Lukasz Lenart <[email protected]> AuthorDate: Sat Oct 10 17:14:56 2026 +0200 WW-3476 feat(config): add ScopedConstantProvider resolving package overrides Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../java/org/apache/struts2/StrutsConstants.java | 7 ++ .../struts2/config/ScopedConstantProvider.java | 35 ++++++ .../config/StrutsBeanSelectionProvider.java | 1 + .../config/StrutsScopedConstantProvider.java | 79 +++++++++++++ .../StrutsDefaultConfigurationProvider.java | 4 + core/src/main/resources/struts-beans.xml | 3 + .../config/StrutsScopedConstantProviderTest.java | 129 +++++++++++++++++++++ .../struts2/config/scoped-constants-provider.xml | 46 ++++++++ 8 files changed, 304 insertions(+) diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java index 3d3fef328..cfa3e30a7 100644 --- a/core/src/main/java/org/apache/struts2/StrutsConstants.java +++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java @@ -628,6 +628,13 @@ public final class StrutsConstants { */ public static final String STRUTS_PARAMETER_ALLOWLISTER = "struts.parameterAllowlister"; + /** + * The {@link org.apache.struts2.config.ScopedConstantProvider} implementation class. + * + * @since 7.5.0 + */ + public static final String STRUTS_SCOPED_CONSTANT_PROVIDER = "struts.scopedConstantProvider"; + /** * Enables evaluation of OGNL expressions * diff --git a/core/src/main/java/org/apache/struts2/config/ScopedConstantProvider.java b/core/src/main/java/org/apache/struts2/config/ScopedConstantProvider.java new file mode 100644 index 000000000..53e655483 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/config/ScopedConstantProvider.java @@ -0,0 +1,35 @@ +/* + * 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; + +/** + * Resolves a scopable constant for the current action invocation: the package's {@code <scoped-constant>}, inherited + * through {@code extends}, or else the global constant. + * + * @since 7.5.0 + */ +public interface ScopedConstantProvider { + + /** + * @param name a constant registered through a {@link ScopableConstants} bean + * @return the scoped value, or the global value ({@code null} when there is none) + * @throws IllegalArgumentException when {@code name} is not scopable + */ + String getValue(String name); +} diff --git a/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java b/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java index b0dda639b..8c141c6c7 100644 --- a/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java +++ b/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java @@ -454,6 +454,7 @@ public class StrutsBeanSelectionProvider extends AbstractBeanSelectionProvider { alias(ProxyService.class, StrutsConstants.STRUTS_PROXYSERVICE, builder, props, Scope.SINGLETON); alias(ParameterAuthorizer.class, StrutsConstants.STRUTS_PARAMETER_AUTHORIZER, builder, props, Scope.SINGLETON); alias(ParameterAllowlister.class, StrutsConstants.STRUTS_PARAMETER_ALLOWLISTER, builder, props, Scope.SINGLETON); + alias(ScopedConstantProvider.class, StrutsConstants.STRUTS_SCOPED_CONSTANT_PROVIDER, builder, props, Scope.SINGLETON); alias(SecurityMemberAccess.class, StrutsConstants.STRUTS_MEMBER_ACCESS, builder, props, Scope.PROTOTYPE); alias(OgnlGuard.class, StrutsConstants.STRUTS_OGNL_GUARD, builder, props, Scope.SINGLETON); diff --git a/core/src/main/java/org/apache/struts2/config/StrutsScopedConstantProvider.java b/core/src/main/java/org/apache/struts2/config/StrutsScopedConstantProvider.java new file mode 100644 index 000000000..2bbb40e31 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/config/StrutsScopedConstantProvider.java @@ -0,0 +1,79 @@ +/* + * 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.ActionInvocation; +import org.apache.struts2.ActionProxy; +import org.apache.struts2.config.entities.PackageConfig; +import org.apache.struts2.inject.Container; +import org.apache.struts2.inject.Inject; + +import java.util.Optional; +import java.util.Set; + +/** + * @since 7.5.0 + */ +public class StrutsScopedConstantProvider implements ScopedConstantProvider { + + private Container container; + private Configuration configuration; + private volatile Set<String> scopableNames; + + @Inject + public void setContainer(Container container) { + this.container = container; + } + + @Inject + public void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } + + @Override + public String getValue(String name) { + if (!getScopableNames().contains(name)) { + throw new IllegalArgumentException(String.format( + "Constant [%s] is not scopable; register it through a %s bean", name, ScopableConstants.class.getName())); + } + return currentPackage() + .map(packageConfig -> packageConfig.getAllScopedConstants().get(name)) + .orElseGet(() -> container.getInstance(String.class, name)); + } + + private Optional<PackageConfig> currentPackage() { + ActionContext context = ActionContext.getContext(); + ActionInvocation invocation = context != null ? context.getActionInvocation() : null; + ActionProxy proxy = invocation != null ? invocation.getProxy() : null; + if (proxy == null || proxy.getConfig() == null) { + return Optional.empty(); + } + return Optional.ofNullable(configuration.getPackageConfig(proxy.getConfig().getPackageName())); + } + + private Set<String> getScopableNames() { + Set<String> names = scopableNames; + if (names == null) { + names = ScopableConstants.collectNames(container); + scopableNames = names; + } + return names; + } +} 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 40d757586..c66911bb9 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 @@ -26,6 +26,8 @@ import org.apache.struts2.UnknownHandlerManager; import org.apache.struts2.config.Configuration; import org.apache.struts2.config.ConfigurationException; import org.apache.struts2.config.ConfigurationProvider; +import org.apache.struts2.config.ScopedConstantProvider; +import org.apache.struts2.config.StrutsScopedConstantProvider; import org.apache.struts2.config.impl.DefaultConfiguration; import org.apache.struts2.conversion.NullHandler; import org.apache.struts2.conversion.impl.ArrayConverter; @@ -130,6 +132,8 @@ public class StrutsDefaultConfigurationProvider implements ConfigurationProvider .factory(UnknownHandlerManager.class, DefaultUnknownHandlerManager.class, Scope.SINGLETON) + .factory(ScopedConstantProvider.class, StrutsScopedConstantProvider.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) .factory(PropertyAccessor.class, ArrayList.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 d12535b9c..81109bf06 100644 --- a/core/src/main/resources/struts-beans.xml +++ b/core/src/main/resources/struts-beans.xml @@ -258,6 +258,9 @@ <bean type="org.apache.struts2.interceptor.parameter.ParameterAllowlister" name="struts" class="org.apache.struts2.interceptor.parameter.OgnlParameterAllowlister" scope="singleton"/> + <bean type="org.apache.struts2.config.ScopedConstantProvider" name="struts" + class="org.apache.struts2.config.StrutsScopedConstantProvider" scope="singleton"/> + <bean type="org.apache.struts2.url.QueryStringBuilder" name="strutsQueryStringBuilder" class="org.apache.struts2.url.StrutsQueryStringBuilder" scope="singleton"/> <bean type="org.apache.struts2.url.QueryStringParser" name="strutsQueryStringParser" diff --git a/core/src/test/java/org/apache/struts2/config/StrutsScopedConstantProviderTest.java b/core/src/test/java/org/apache/struts2/config/StrutsScopedConstantProviderTest.java new file mode 100644 index 000000000..be3ccc53e --- /dev/null +++ b/core/src/test/java/org/apache/struts2/config/StrutsScopedConstantProviderTest.java @@ -0,0 +1,129 @@ +/* + * 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.ActionSupport; +import org.apache.struts2.XWorkTestCase; +import org.apache.struts2.config.entities.ActionConfig; +import org.apache.struts2.mock.MockActionInvocation; +import org.apache.struts2.mock.MockActionProxy; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class StrutsScopedConstantProviderTest extends XWorkTestCase { + + private ScopedConstantProvider provider; + + @Override + protected void setUp() throws Exception { + super.setUp(); + loadConfigurationProviders(new StrutsXmlConfigurationProvider("org/apache/struts2/config/scoped-constants-provider.xml")); + provider = container.getInstance(ScopedConstantProvider.class); + } + + public void testDefaultProviderIsStrutsImplementation() { + assertTrue(provider instanceof StrutsScopedConstantProvider); + } + + public void testPackageValueWinsInsideInvocation() { + enterPackage("override"); + + assertEquals("override", provider.getValue("sample.scopable")); + } + + public void testInheritedValueResolvesForChildPackage() { + enterPackage("child"); + + assertEquals("base", provider.getValue("sample.scopable")); + } + + public void testEmptyOverrideIsReturnedAsIs() { + enterPackage("empty"); + + assertEquals("", provider.getValue("sample.scopable")); + } + + public void testGlobalValueWithoutOverride() { + enterPackage("plain"); + + assertEquals("global", provider.getValue("sample.scopable")); + } + + public void testGlobalValueWithoutInvocation() { + assertEquals("global", provider.getValue("sample.scopable")); + } + + public void testGlobalValueWithoutActionContext() { + ActionContext context = ActionContext.getContext(); + ActionContext.clear(); + try { + assertEquals("global", provider.getValue("sample.scopable")); + } finally { + context.bind(); + } + } + + public void testGlobalValueWhenInvocationHasNoProxy() { + ActionContext.getContext().withActionInvocation(new MockActionInvocation()); + + assertEquals("global", provider.getValue("sample.scopable")); + } + + public void testGlobalValueWhenProxyHasNoConfig() { + MockActionInvocation invocation = new MockActionInvocation(); + invocation.setProxy(new MockActionProxy()); + ActionContext.getContext().withActionInvocation(invocation); + + assertEquals("global", provider.getValue("sample.scopable")); + } + + public void testGlobalValueWhenPackageIsUnknown() { + enterPackage("missing"); + + assertEquals("global", provider.getValue("sample.scopable")); + } + + public void testUnsetConstantWithoutOverrideIsNull() { + enterPackage("plain"); + + assertNull(provider.getValue("sample.unset")); + } + + public void testUnregisteredNameThrows() { + assertThatThrownBy(() -> provider.getValue("sample.unknown")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("[sample.unknown]"); + } + + public void testGlobalConstantUntouchedByScopedRead() { + enterPackage("override"); + provider.getValue("sample.scopable"); + + assertEquals("global", container.getInstance(String.class, "sample.scopable")); + } + + 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/resources/org/apache/struts2/config/scoped-constants-provider.xml b/core/src/test/resources/org/apache/struts2/config/scoped-constants-provider.xml new file mode 100644 index 000000000..84a163ad6 --- /dev/null +++ b/core/src/test/resources/org/apache/struts2/config/scoped-constants-provider.xml @@ -0,0 +1,46 @@ +<?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"/> + + <constant name="sample.scopable" value="global"/> + + <package name="base" abstract="true"> + <scoped-constant name="sample.scopable" value="base"/> + </package> + + <package name="child" namespace="/child" extends="base"/> + + <package name="override" namespace="/override" extends="base"> + <scoped-constant name="sample.scopable" value="override"/> + </package> + + <package name="empty" namespace="/empty" extends="base"> + <scoped-constant name="sample.scopable" value=""/> + </package> + + <package name="plain" namespace="/plain"/> +</struts>
