You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@struts.apache.org by "Lukasz Lenart (Jira)" <ji...@apache.org> on 2026/09/12 06:18:00 UTC

[jira] [Updated] (WW-5695) Derive HTML5 constraint attributes from validators in the html5 theme

     [ https://issues.apache.org/jira/browse/WW-5695?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]

Lukasz Lenart updated WW-5695:
------------------------------
    Component/s: Core Tags

> 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
>          Components: Core Tags
>            Reporter: Lukasz Lenart
>            Priority: Major
>             Fix For: 7.4.0
>
>          Time Spent: 1h
>  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)