[
https://issues.apache.org/jira/browse/WW-5695?focusedWorklogId=1037667&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-1037667
]
ASF GitHub Bot logged work on WW-5695:
--------------------------------------
Author: ASF GitHub Bot
Created on: 25/Aug/26 04:33
Start Date: 25/Aug/26 04:33
Worklog Time Spent: 10m
Work Description: lukaszlenart opened a new pull request, #1865:
URL: https://github.com/apache/struts/pull/1865
Adds HTML5 constraint validation to the `html5` theme, and deprecates the
generated-JavaScript
client-side validator it replaces.
Fixes [WW-5695](https://issues.apache.org/jira/browse/WW-5695)
Fixes [WW-5694](https://issues.apache.org/jira/browse/WW-5694)
Supersedes [WW-2975](https://issues.apache.org/jira/browse/WW-2975) (closed
Won't Fix), open since 2009.
Removal of the JavaScript validator is tracked separately as
[WW-5696](https://issues.apache.org/jira/browse/WW-5696) and is **not** in
this PR.
## Why
`xhtml/form-close-validate.ftl` builds its `validateForm_<id>()` function
from the form's `tagNames`
list, which is populated only when a UI tag finds the form via
`findAncestor(Form.class)`. Any input
reaching the markup another way — raw HTML, a custom tag, an included
fragment — is silently
unvalidated. That is WW-2975, and it still reproduced on `main`.
Rather than patch that list, this replaces the mechanism. Constraints now
ride on each `<input>`, so
there is no central field list for a field to be missing from — the root
cause disappears instead of
being worked around.
## The governing rule: never false-reject
A constraint is emitted **only** when the browser cannot reject input the
server would have accepted.
A browser rejecting what the server allows leaves the user with a form that
will not submit and no
explanation. Being conservative merely costs a field its client-side check,
which is harmless.
The main consequence: **Struts never sets or changes an input's `type`.**
Promoting a field to
`type="number"` would reject `1234,50`, which locale-aware conversion
accepts in a comma-decimal
locale, and the browsers' `email`/`url` grammars differ from
`EmailValidator`/`URLValidator`.
## What is emitted
| Validator | Emits | Condition |
|---|---|---|
| `requiredstring` | `required` | text-entry controls and `textarea` |
| `required` | `required` | only `radio` and `file` |
| `stringlength` | `minlength` / `maxlength` | text-entry or `textarea`, and
`trim="false"` |
| `regex` | `pattern` | text-entry, `caseSensitive="true"`, `trim="false"`,
ECMAScript-safe, not email/creditcard |
| `int`, `short`, `long`, `double` | `min` / `max` | only when the control
is already `type="number"`/`range`; `min` only when integral |
| `date` | — | temporal formatting deferred |
| `email`, `url`, `creditcard` | — | never |
| any validator with a message | `data-msg-<type>` | always, including those
emitting no constraint |
Struts ships **no JavaScript** consuming `data-msg-*`; they carry the
resolved i18n message for an
application's own script.
Three conditions are worth knowing because they limit reach:
- `required` is split because `RequiredFieldValidator` only fails on
null/empty-array/empty-collection.
`CheckboxInterceptor` substitutes `"false"` for an unticked box, so the
server accepts what a browser
`required` would block. Only `radio` and `file` omit the parameter
entirely when empty.
- `minlength`/`maxlength` and `pattern` both need `trim="false"`, which is
**not** the default — the
server measures/matches the trimmed value while the attribute constrains
the raw one. Expect both to
be uncommon until applications opt in.
- `\s`/`\S` are excluded from the ECMAScript-safe allowlist: Java's is
ASCII-only by default while
ECMAScript's is the wider Unicode set, so `^\S+$` would accept a value
containing NBSP server-side
and reject it in the browser.
## Compatibility
Off by default (`struts.ui.html5.constraints=false`), so nothing renders
differently on upgrade — the
`html5` theme shipped in 7.2.0 and existing forms are untouched. The
deprecation is annotations and
documentation only; all four `validateForm_` golden files are byte-identical.
The mapping policy is a swappable bean (`HtmlConstraintProvider`, under
`struts.htmlConstraintProvider`)
for applications wanting a less conservative mapping.
## Testing
Full core suite: **3257 tests, 0 failures.** `mvn apache-rat:check` clean.
The plugin modules that
consume `UIBean` (`javatemplates`, `velocity`) verified green with `-am`.
Several tests exist specifically to be able to fail: the hook-ordering test
was confirmed to fail when
the hook is moved, the memoisation test when the cache is disabled, and the
byte-exact disabled-path
test when the template regresses — the shared golden-file harness normalises
whitespace away, so it
cannot catch that class of defect on its own.
## Known follow-ups
- The `trim` guard over-restricts `minlength`: browser-reject implies
server-reject there, so it could
safely be emitted even when trimming. Widening it is a follow-up.
- Temporal `min`/`max` needs per-control ISO formatting.
- The `try/catch` guarding derivation has no regression test yet.
Documentation is a companion change in `struts-site`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Issue Time Tracking
-------------------
Worklog Id: (was: 1037667)
Remaining Estimate: 0h
Time Spent: 10m
> Derive HTML5 constraint attributes from validators in the html5 theme
> ---------------------------------------------------------------------
>
> Key: WW-5695
> URL: https://issues.apache.org/jira/browse/WW-5695
> Project: Struts 2
> Issue Type: New Feature
> Reporter: Lukasz Lenart
> Priority: Major
> Fix For: 7.4.0
>
> Time Spent: 10m
> Remaining Estimate: 0h
>
> h2. Goal
> Let the {{html5}} theme emit native HTML5 constraint attributes
> ({{required}}, {{minlength}}, {{maxlength}}, {{pattern}}, {{min}}, {{max}})
> derived from the action's validators, as the successor to the JavaScript
> client-side validator deprecated in WW-5694.
> Because constraints ride on each individual input rather than on a generated
> per-form function, this also dissolves the root cause of WW-2975 instead of
> patching it — there is no central field list for a field to be missing from.
> h2. The governing rule: never false-reject
> A constraint is emitted only when the browser cannot reject something the
> server would accept. This rules out more than it first appears:
> {{min}} and {{max}} are inert on {{type="text"}} — they only apply to
> {{number}}, {{range}} and the temporal types. Honouring an int or double
> validator therefore means switching the input to {{type="number"}}, and that
> _is_ a false rejection: a browser {{type="number"}} refuses {{1234,50}},
> which the framework's locale-aware conversion accepts in a comma-decimal
> locale. The same argument rules out {{type="email"}} and {{type="url"}},
> whose browser regexes diverge from {{EmailValidator}} and {{URLValidator}} —
> see WW-4395, still open, for the email side of that divergence.
> Hence the rule: _Struts never sets or changes an input's type. It only adds
> constraints that are safe for whatever type is already there._ A developer
> who writes {{type="number"}} has accepted that widget's semantics, so {{min}}
> and {{max}} become pure additions.
> h2. Mapping
> || Validator || Emits || Condition ||
> | required | required | always |
> | requiredstring | required | always; the server is stricter on
> whitespace-only input, which is safe |
> | stringlength | minlength / maxlength | only when {{trim="false"}} — the
> server measures the trimmed value, so a maxlength derived from a trimming
> validator would stop the user typing input the server would accept |
> | regex | pattern | only when {{caseSensitive="true"}} and the regex is
> ECMAScript-safe |
> | int, short, long | min / max | only when the control is already numeric |
> | double | min / max | only when the control is already numeric |
> | date | min / max | only when the control is already temporal |
> | email, url | nothing | browser regexes diverge from the framework's |
> | creditcard, fieldexpression, expression, conversion, visitor | nothing | no
> safe mapping |
> {{RegexFieldValidator}} uses {{matcher.matches()}}, so it is fully anchored
> and matches HTML5 {{pattern}} semantics. The divergence is syntactic, not
> positional — Java-only constructs such as POSIX classes, possessive
> quantifiers and {{\\A}} / {{\\z}} are the hazard, and
> {{caseSensitive="false"}} has no {{pattern}} equivalent because HTML allows
> no regex flags.
> ECMAScript-safety detection must be an _allowlist_, not a denylist: a
> denylist violates the rule the first time it misses a construct. Allow
> literals, {{\\d}} {{\\w}} {{\\s}} and their negations, character classes
> without POSIX or Unicode property syntax, grouping, alternation, anchors and
> bounded quantifiers; emit no {{pattern}} for anything else. This is
> deliberately strict and is the piece most likely to need tuning after real
> use.
> h2. Design
> * New constant {{struts.ui.html5.constraints}}, default {{false}} in 7.4.0.
> The {{html5}} theme shipped in 7.2.0, so emitting {{required}} on upgrade
> would start blocking submits on forms that render unchanged today. The
> default flips to {{true}} in 8.0.0.
> * New enum {{HtmlControlType}} — the provider's real question is which
> constraint attributes are legal on a control, not what string is in the type
> attribute. It models the control rather than the attribute, because textarea
> and select have no type attribute yet do accept {{required}}. Members cover
> the HTML5 input types plus TEXTAREA, SELECT and OTHER, with predicates
> {{supportsPattern()}}, {{supportsLength()}} and {{supportsRange()}}. Its
> {{from(String)}} factory must never throw: the type attribute is
> OGNL-evaluated, so at runtime it can be any string, and unknown values
> normalise to OTHER, which supports nothing — the conservative default falls
> out for free.
> * New interface {{HtmlConstraintProvider}} with default implementation
> {{StrutsHtmlConstraintProvider}}, taking the field's validators and an
> {{HtmlControlType}} and returning a map of attribute name to value.
> Registered once in {{struts-beans.xml}} following the {{UrlRenderer}} model —
> registering a bean under two types builds two instances. Because the default
> policy is deliberately restrictive, the swappable bean is how applications
> wanting best-effort mapping get served.
> * {{UIBean.evaluateParams}} already resolves the {{Form}} ancestor to
> populate {{tagNames}}; that is the hook. Gate the computation on the constant
> so the cost is zero when off.
> * New {{Form.getFieldValidators(String)}} that resolves the action's
> validator list once and memoises it on the form's attributes, then filters
> per call. The existing {{getValidators(String)}} re-runs the action-mapping
> lookup on every call, so a twenty-field form would do twenty full lookups.
> * New {{html5/constraints.ftl}}, included from {{common-attributes.ftl}} so
> every html5 input picks it up without per-template edits.
> h2. Messages
> The provider returns the full attribute set, not only constraints. Validator
> messages ride the same map as {{data-msg-}} entries keyed by validator type —
> {{data-msg-required}}, {{data-msg-stringlength}} — resolved via
> {{validator.getMessage(action)}}, which goes through
> {{DelegatingValidatorContext}} and {{textProviderFactory}} and is therefore
> properly i18n'd.
> A message is emitted for every validator that has one, _including_ those
> producing no constraint. An email validator therefore contributes
> {{data-msg-email}} and nothing else, which is exactly where an application
> most needs it. Struts ships nothing that consumes these attributes — no
> JavaScript.
> h2. Do not conflate requiredLabel
> {{requiredLabel}} keeps meaning "draw an asterisk next to the label". It must
> never produce a {{required}} attribute — only a {{required}} validator does.
> This is the most likely regression in this work and needs an explicit test.
> h2. Testing
> The negative cases carry the weight, because they are what protects the rule:
> {{stringlength trim="true"}} emits no length constraints; {{regex
> caseSensitive="false"}} emits no pattern; a Java-only regex emits no pattern;
> int and double on a text control emit no range; email and url never set or
> change the type; {{HtmlControlType.from}} never throws on null or unknown
> input.
> Note the harness trap: a form-validation tag test needs {{initDispatcher}}
> with {{TestConfigurationProvider}} _and_ {{createMocks()}} in setUp, plus a
> {{prepareMockInvocation()}} EasyMock helper. Without them
> {{evaluateClientSideJsEnablement}} finds no {{ValidationInterceptor}},
> {{performValidation}} stays false, and no validation function is emitted at
> all — so the test passes or fails for entirely the wrong reason.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)