Skip to content

WW-4164 Support per-package and per-action action extensions - #2093

Open
lukaszlenart wants to merge 19 commits into
mainfrom
feature/WW-4164-per-action-extensions
Open

lukaszlenart wants to merge 19 commits into
mainfrom
feature/WW-4164-per-action-extensions

Conversation

@lukaszlenart

@lukaszlenart lukaszlenart commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

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 an extension attribute (struts-7.5.dtd) with the same format as struts.action.extension: comma-separated, ordered, first entry is the default, an empty entry means "no extension". Resolution: action → package → parent packages → global constant.

  • Inbound — DefaultActionMapper strips an extension declared only by packages/actions when the stripped name resolves (exact or wildcard match, never through default-action-ref) to an action that declares an extension list. Everything else maps exactly as today, so static files and undeclared actions are untouched.
  • Enforcement — new actionExtension interceptor (ActionExtensionInterceptor), placed after httpMethod in basicStack, paramsPrepareParamsStack and defaultStack. A disallowed extension returns the bad-extension result when the action can resolve one (configurable via badExtensionResultName), 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.
  • Outbound — ActionExtensionResolver sets the target action's extension for s:url/s:a, s:form, redirectAction and postback: 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.
  • Startup WARNs when an action declares extensions but its stack lacks the interceptor, and when the configured ActionMapper does 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 extension attribute (pinned by ActionExtensionEndToEndTest and the existing suites).

7.5.0 Version Notes / Migration Guide lines

  • New extension attribute on <package> and <action> (struts-7.5.dtd).
  • New actionExtension interceptor in basicStack, paramsPrepareParamsStack and defaultStack; custom stacks need it to enforce declared extensions.
  • Component, ServletActionRedirectResult and PostbackResult now require an injected ActionExtensionResolver; tests that build them by hand need container.inject(...) or setActionExtensionResolver(...).
  • New ActionMapper.supportsDeclaredExtensions() (default false; DefaultActionMapper and its subclasses that use super.getMapping return true, RestActionMapper returns false). Custom mappers that honour declared extensions should override it; otherwise a startup WARN reports that the declared extensions are not applied.
  • New RuntimeConfiguration.getMatchedActionConfig(namespace, name) (default method: exact/wildcard match without the default-action-ref fallback); custom implementations should override it.
  • A link to a name served only by a default-action-ref that declares extensions is built with the global extension and is rejected by that action — link to the action's real name.
  • Convention/annotation support is a follow-up ticket.

struts-site (separate PR)

"Extensions" section in core-developers/action-configuration.md, next to "HTTP Methods":

Since 7.5.0 a package or an action can declare which extensions it answers on with the extension attribute, using the same format as struts.action.extension (extension="html", extension="json,", extension="" for no extension). An action inherits the list from its package and parent packages; without one, the global struts.action.extension applies. Requests with another extension are rejected by the actionExtension interceptor with a bad-extension result, or 404 when no such result is mapped. URLs generated by <s:url>, <s:a>, <s:form>, redirectAction and postback use the target action's list. Chained actions and <s:action> are not checked, and mappers other than DefaultActionMapper may not honour the attribute. The attribute canonicalises URLs; it is not an access-control mechanism.

🤖 Generated with Claude Code

lukaszlenart and others added 13 commits November 2, 2026 07:31
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
lukaszlenart marked this pull request as ready for review November 2, 2026 08:02
@lukaszlenart
lukaszlenart requested a balanced review from Copilot November 2, 2026 08:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

lukaszlenart and others added 5 commits November 2, 2026 09:31
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>
…xtensions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Nov 2, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants