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)

Reply via email to