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
commit 2a4d1aa8d2c83c65c763f72f7761947dcc441abb Author: Lukasz Lenart <[email protected]> AuthorDate: Mon Nov 2 08:09:35 2026 +0100 WW-4164 fix(core): skip extension resolution when nothing declares extensions Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../dispatcher/mapper/ActionExtensionResolver.java | 33 ++++++++++++++++++++-- .../mapper/ActionExtensionResolverTest.java | 8 ++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java b/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java index b4d450cd8..574fffbbe 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java @@ -22,6 +22,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.struts2.ActionContext; import org.apache.struts2.StrutsConstants; import org.apache.struts2.config.Configuration; +import org.apache.struts2.config.RuntimeConfiguration; import org.apache.struts2.config.entities.ActionConfig; import org.apache.struts2.inject.Inject; @@ -37,6 +38,7 @@ public class ActionExtensionResolver { private Configuration configuration; private List<String> extensions; + private volatile DeclarationState declarationState; /** * @param configuration the configuration used to look up the target action @@ -60,7 +62,8 @@ public class ActionExtensionResolver { * Sets the extension of {@code target} to the current request's extension when the target accepts it, otherwise * to the target's first accepted extension, which is also used when the current request has no extension, as in * {@link DefaultActionMapper#lookupExtension(String)}. Leaves {@code target} unchanged when it already has an extension, - * when the target action is unknown, or when neither the target nor the global constant declares extensions. + * when no action in the configuration declares extensions, when the target action is unknown, or when neither the + * target nor the global constant declares extensions. * * @param target the mapping of the action the URL points at * @since 7.5.0 @@ -69,7 +72,11 @@ public class ActionExtensionResolver { if (target.getExtension() != null) { return; } - ActionConfig actionConfig = configuration.getRuntimeConfiguration() + RuntimeConfiguration runtimeConfiguration = configuration.getRuntimeConfiguration(); + if (!anyActionDeclaresExtensions(runtimeConfiguration)) { + return; + } + ActionConfig actionConfig = runtimeConfiguration .getActionConfig(target.getNamespace(), StringUtils.substringBefore(target.getName(), "!")); if (actionConfig == null) { return; @@ -82,6 +89,18 @@ public class ActionExtensionResolver { target.setExtension(current == null ? effective.get(0) : ActionExtensions.select(effective, current)); } + private boolean anyActionDeclaresExtensions(RuntimeConfiguration runtimeConfiguration) { + DeclarationState cached = declarationState; + if (cached != null && cached.runtimeConfiguration == runtimeConfiguration) { + return cached.anyDeclared; + } + boolean anyDeclared = runtimeConfiguration.getActionConfigs().values().stream() + .flatMap(actionConfigs -> actionConfigs.values().stream()) + .anyMatch(actionConfig -> actionConfig.getExtensions() != null); + declarationState = new DeclarationState(runtimeConfiguration, anyDeclared); + return anyDeclared; + } + private String currentExtension() { ActionContext context = ActionContext.getContext(); if (context == null || context.getActionMapping() == null) { @@ -89,4 +108,14 @@ public class ActionExtensionResolver { } return context.getActionMapping().getExtension(); } + + private static final class DeclarationState { + private final RuntimeConfiguration runtimeConfiguration; + private final boolean anyDeclared; + + private DeclarationState(RuntimeConfiguration runtimeConfiguration, boolean anyDeclared) { + this.runtimeConfiguration = runtimeConfiguration; + this.anyDeclared = anyDeclared; + } + } } diff --git a/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java b/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java index 061302865..c1626f46a 100644 --- a/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java +++ b/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java @@ -102,6 +102,14 @@ public class ActionExtensionResolverTest extends StrutsInternalTestCase { assertThat(resolve("/plain", "undeclared")).isNull(); } + public void testNoDeclarationsAnywhereLeavesMappingUntouched() { + loadConfigurationProviders(new StrutsXmlConfigurationProvider("org/apache/struts2/config/providers/xwork-test-actions.xml")); + resolver.setConfiguration(configuration); + withCurrentExtension("xyz"); + + assertThat(resolve("", "Bar")).isNull(); + } + private String resolve(String namespace, String name) { ActionMapping target = new ActionMapping(name, namespace, null, null); resolver.applyTo(target);
