Repository navigation
WW-4164 Support per-package and per-action action extensions - #2093
Open
lukaszlenart wants to merge 19 commits into
Open
lukaszlenart wants to merge 19 commits into
lukaszlenart wants to merge 19 commits into
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Narrows inbound claiming in the design to actions that declare an extension list, so an undeclared action never answers on an extension declared only by another package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…Interceptor Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tensions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xtensions A name that resolves only through a namespace's default-action-ref no longer counts as a match: the mapper leaves such a URL to the container (static files keep being served) and the outbound resolver leaves the extension untouched. RuntimeConfiguration gains getMatchedActionConfig, which walks the same lookup as getActionConfig but drops a default-action-ref result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ty extension The outbound resolver now cuts the target name at '?' as well as '!', so action="name?x=1" targets pick up the declared extension. When only package or action attributes declare the empty extension, an extensionless URI that matches a declaring action is mapped instead of being dropped because the global struts.action.extension lacks "". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reject extension entries containing whitespace, word the non-default mapper warning as "may not be applied" (subclasses still reach the inbound path and outbound resolution applies regardless), document the protected actionExtensionResolver field and reorder the ActionExtensions import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
action:-prefix buttons are enforced: handleSpecialParameters rewrites the mapping name and the proxy is built from that mapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lukaszlenart
marked this pull request as ready for review
November 2, 2026 08:02
lukaszlenart
commented
Nov 2, 2026
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved parsing and extension-selection defects can break literal-bang actions, add unwanted extensions to extensionless URLs, and route unmatched paths to the index action.
4 open issues
What changed in this PR
Adds per-package and per-action URL extensions to Struts, covering inbound mapping, extension checks, and generated links and forms. This also addresses the form-extension inheritance question from WW-3193.
Changes:
- Adds XML extension declarations, inheritance, validation, and matching support.
- Introduces extension enforcement, outbound resolution, and configuration warnings.
- Adds regression tests, fixtures, and design documentation.
| File | Description |
|---|---|
plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java |
Injects the resolver into redirect tests. |
docs/superpowers/specs/2026-11-01-WW-4164-per-action-extensions-design.md |
Defines behavior and compatibility goals. |
docs/superpowers/plans/2026-11-02-WW-4164-per-action-extensions.md |
Documents implementation and verification steps. |
core/src/test/resources/org/apache/struts2/dispatcher/struts-extension-one-package.xml |
Supplies end-to-end configuration. |
core/src/test/resources/org/apache/struts2/config/providers/struts-extension.xml |
Covers declarations and inheritance. |
core/src/test/resources/org/apache/struts2/config/providers/struts-extension-whitespace.xml |
Supplies invalid whitespace input. |
core/src/test/resources/org/apache/struts2/config/providers/struts-extension-reload.xml |
Supplies changed declarations for reload tests. |
core/src/test/resources/org/apache/struts2/config/providers/struts-extension-invalid.xml |
Supplies invalid dotted input. |
core/src/test/resources/org/apache/struts2/config/providers/struts-extension-default-action.xml |
Covers default-action and wildcard interactions. |
core/src/test/java/org/apache/struts2/views/jsp/URLTagTest.java |
Tests declared extensions in URLs. |
core/src/test/java/org/apache/struts2/views/jsp/ui/FormTagTest.java |
Tests declared extensions in forms. |
core/src/test/java/org/apache/struts2/result/ServletActionRedirectResultTest.java |
Tests redirect extension selection. |
core/src/test/java/org/apache/struts2/result/PostbackResultTest.java |
Tests postback selection and injection. |
core/src/test/java/org/apache/struts2/interceptor/ActionExtensionInterceptorTest.java |
Tests extension acceptance and rejection. |
core/src/test/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapperTest.java |
Tests declared-extension routing and reloads. |
core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionsTest.java |
Tests parsing and selection helpers. |
core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java |
Tests outbound extension resolution. |
core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java |
Tests mapper compatibility warnings. |
core/src/test/java/org/apache/struts2/dispatcher/ActionExtensionEndToEndTest.java |
Tests routing, rejection, and static-file fallback. |
core/src/test/java/org/apache/struts2/config/providers/XmlConfigurationProviderExtensionTest.java |
Tests parsing, inheritance, and warnings. |
core/src/main/resources/struts-default.xml |
Registers the interceptor in standard stacks. |
core/src/main/resources/struts-beans.xml |
Registers the resolver singleton. |
core/src/main/resources/struts-7.5.dtd |
Allows package and action extension attributes. |
core/src/main/java/org/apache/struts2/result/ServletActionRedirectResult.java |
Applies target extensions to redirects. |
core/src/main/java/org/apache/struts2/result/PostbackResult.java |
Applies target extensions to postbacks. |
core/src/main/java/org/apache/struts2/interceptor/ResultResolution.java |
Extracts shared rejection-result lookup. |
core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java |
Uses shared result lookup. |
core/src/main/java/org/apache/struts2/interceptor/ActionExtensionInterceptor.java |
Checks declared extensions on requests. |
core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java |
Maps declared extensions with cached configuration. |
core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensions.java |
Provides ordered extension-list helpers. |
core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java |
Selects extensions for target actions. |
core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java |
Warns about non-default mappers. |
core/src/main/java/org/apache/struts2/config/RuntimeConfiguration.java |
Adds matched-action lookup API. |
core/src/main/java/org/apache/struts2/config/providers/XmlDocConfigurationProvider.java |
Parses and validates extension attributes. |
core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java |
Resolves inheritance and tracks fallback matches. |
core/src/main/java/org/apache/struts2/config/impl/ActionConfigMatcher.java |
Preserves extensions in wildcard matches. |
core/src/main/java/org/apache/struts2/config/entities/PackageConfig.java |
Stores package declarations and inheritance. |
core/src/main/java/org/apache/struts2/config/entities/ActionConfig.java |
Stores action extension lists. |
core/src/main/java/org/apache/struts2/components/ServletUrlRenderer.java |
Resolves extensions for form actions. |
core/src/main/java/org/apache/struts2/components/Component.java |
Resolves extensions for component-generated URLs. |
Review effort: Balanced
Also drops the redundant @SInCE comment on Component.actionExtensionResolver. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… extensionless links - Resolver cuts a target name at the last ! only when DMI is enabled, so a literal report!daily resolves its own action - Resolver treats an extensionless current request as "" for declared targets, keeping it when allowed - Mapper declaration probe refuses names cleanupActionName replaced with the default action name - Mapper declaration probe derives the name through parseActionName, matching the final mapping for DMI on and off Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…owed With slashes in action names the raw name spans the whole path after the namespace, so the declaration probe rebuilds the URI from the probe's namespace and name instead of comparing only the last segment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
lukaszlenart
commented
Nov 2, 2026
…xtensions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.





Fixes WW-4164
Also resolves the design question moved here from WW-3193 (form action inheriting the current page's extension).
What
<package>and<action>accept anextensionattribute (struts-7.5.dtd) with the same format asstruts.action.extension: comma-separated, ordered, first entry is the default, an empty entry means "no extension". Resolution: action → package → parent packages → global constant.DefaultActionMapperstrips an extension declared only by packages/actions when the stripped name resolves (exact or wildcard match, never throughdefault-action-ref) to an action that declares an extension list. Everything else maps exactly as today, so static files and undeclared actions are untouched.actionExtensioninterceptor (ActionExtensionInterceptor), placed afterhttpMethodinbasicStack,paramsPrepareParamsStackanddefaultStack. A disallowed extension returns thebad-extensionresult when the action can resolve one (configurable viabadExtensionResultName), otherwise 404. Only the URL's own action is checked (chain/<s:action>targets are not);action:-prefix buttons are checked against the button's target.ActionExtensionResolversets the target action's extension fors:url/s:a,s:form,redirectActionandpostback: keep the current extension when the target allows it, otherwise its first entry. It is a no-op when nothing in the configuration declares extensions.ActionMapperdoes not support declared extensions (ActionMapper.supportsDeclaredExtensions()).Design:
docs/superpowers/specs/2026-11-01-WW-4164-per-action-extensions-design.md. This is URL canonicalisation, not access control.Compatibility
No behaviour change for applications without an
extensionattribute (pinned byActionExtensionEndToEndTestand the existing suites).7.5.0 Version Notes / Migration Guide lines
extensionattribute on<package>and<action>(struts-7.5.dtd).actionExtensioninterceptor inbasicStack,paramsPrepareParamsStackanddefaultStack; custom stacks need it to enforce declared extensions.Component,ServletActionRedirectResultandPostbackResultnow require an injectedActionExtensionResolver; tests that build them by hand needcontainer.inject(...)orsetActionExtensionResolver(...).ActionMapper.supportsDeclaredExtensions()(defaultfalse;DefaultActionMapperand its subclasses that usesuper.getMappingreturntrue,RestActionMapperreturnsfalse). Custom mappers that honour declared extensions should override it; otherwise a startup WARN reports that the declared extensions are not applied.RuntimeConfiguration.getMatchedActionConfig(namespace, name)(default method: exact/wildcard match without thedefault-action-reffallback); custom implementations should override it.default-action-refthat declares extensions is built with the global extension and is rejected by that action — link to the action's real name.struts-site (separate PR)
"Extensions" section in
core-developers/action-configuration.md, next to "HTTP Methods":🤖 Generated with Claude Code