Lukasz Lenart created WW-5695:
---------------------------------
Summary: 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
Fix For: 7.4.0
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)