[
https://issues.apache.org/jira/browse/WW-5695?focusedWorklogId=1037687&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-1037687
]
ASF GitHub Bot logged work on WW-5695:
--------------------------------------
Author: ASF GitHub Bot
Created on: 25/Aug/26 06:17
Start Date: 25/Aug/26 06:17
Worklog Time Spent: 10m
Work Description: Copilot commented on code in PR #1865:
URL: https://github.com/apache/struts/pull/1865#discussion_r3850064775
##########
core/src/main/java/org/apache/struts2/components/UIBean.java:
##########
@@ -903,6 +917,69 @@ public void evaluateParams() {
}
evaluateExtraParams();
+
+ // must run after evaluateExtraParams(): that is where TextField
resolves attributes.type,
+ // and the control type decides which constraints are legal
+ addConstraintAttributes(form);
+ }
+
+ /**
+ * Derives HTML5 constraint attributes for this field from the action's
validators.
+ * <p>
+ * This reaches {@link Form#getFieldValidators(String)}, which resolves
the action's validators via
+ * {@code AnnotationActionValidatorManager}, which in turn dereferences
the current
+ * {@code ActionInvocation} unconditionally. Before this feature that path
only ran under the opt-in
+ * {@code validate="true"}; with constraint derivation gated only by
+ * {@code struts.ui.html5.constraints}, every {@code html5}-themed form
now runs it, including one
+ * rendered outside action scope (a direct JSP include from a plain
servlet, say) — which would NPE.
+ * A stray {@code null} in the validator list, and a broken {@code ${}} in
a validator message
+ * unbalancing the value stack in {@code ValidatorSupport.getMessage},
land in the same call. This
+ * feature is purely decorative — a missing constraint attribute costs
nothing, a 500 costs the page —
+ * so the broad catch here is deliberate rather than a mistake.
+ *
+ * @since 7.4.0
+ */
+ protected void addConstraintAttributes(Form form) {
+ if (!html5ConstraintsEnabled || form == null || htmlConstraintProvider
== null) {
+ return;
+ }
+ String fieldName = (String) getAttributes().get("name");
+ if (fieldName == null) {
+ return;
+ }
+ try {
+ Map<String, String> constraints =
htmlConstraintProvider.constraintsFor(
+ form.getFieldValidators(fieldName), getControlType(),
stack.peek());
Review Comment:
`stack.peek()` is not necessarily the action: `ModelDrivenInterceptor`
pushes the model on top of the ValueStack. Model-driven forms therefore resolve
validator messages and i18n bundles against the model rather than the action
used by server-side validation. Obtain the action from the current
`ActionInvocation`, passing null when no invocation exists.
##########
core/src/main/java/org/apache/struts2/components/UIBean.java:
##########
@@ -903,6 +917,69 @@ public void evaluateParams() {
}
evaluateExtraParams();
+
+ // must run after evaluateExtraParams(): that is where TextField
resolves attributes.type,
+ // and the control type decides which constraints are legal
+ addConstraintAttributes(form);
+ }
+
+ /**
+ * Derives HTML5 constraint attributes for this field from the action's
validators.
+ * <p>
+ * This reaches {@link Form#getFieldValidators(String)}, which resolves
the action's validators via
+ * {@code AnnotationActionValidatorManager}, which in turn dereferences
the current
+ * {@code ActionInvocation} unconditionally. Before this feature that path
only ran under the opt-in
+ * {@code validate="true"}; with constraint derivation gated only by
+ * {@code struts.ui.html5.constraints}, every {@code html5}-themed form
now runs it, including one
+ * rendered outside action scope (a direct JSP include from a plain
servlet, say) — which would NPE.
+ * A stray {@code null} in the validator list, and a broken {@code ${}} in
a validator message
+ * unbalancing the value stack in {@code ValidatorSupport.getMessage},
land in the same call. This
+ * feature is purely decorative — a missing constraint attribute costs
nothing, a 500 costs the page —
+ * so the broad catch here is deliberate rather than a mistake.
+ *
+ * @since 7.4.0
+ */
+ protected void addConstraintAttributes(Form form) {
+ if (!html5ConstraintsEnabled || form == null || htmlConstraintProvider
== null) {
+ return;
+ }
+ String fieldName = (String) getAttributes().get("name");
+ if (fieldName == null) {
+ return;
+ }
+ try {
+ Map<String, String> constraints =
htmlConstraintProvider.constraintsFor(
+ form.getFieldValidators(fieldName), getControlType(),
stack.peek());
+ if (constraints.isEmpty()) {
+ return;
+ }
+ constraints = new LinkedHashMap<>(constraints);
+ constraints.keySet().removeIf(this::isAlreadyRendered);
+ if (!constraints.isEmpty()) {
+ addParameter("constraints", constraints);
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to derive HTML5 constraint attributes for field
[{}], skipping", fieldName, e);
+ }
Review Comment:
Catching the exception does not make this path fail-safe when
`ValidatorSupport.getMessage` throws after pushing the action/validator: its
pops are not in a `finally`, so subsequent rendering continues with a corrupted
ValueStack. Capture the stack depth before derivation and restore it in a
`finally`, or make `ValidatorSupport` guarantee balanced cleanup.
##########
core/src/main/java/org/apache/struts2/components/EcmaScriptSafeRegex.java:
##########
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.components;
+
+/**
+ * Decides whether a Java regular expression can be handed to a browser as an
HTML5 {@code pattern}
+ * attribute without changing meaning.
+ * <p>
+ * This is an allowlist by design. A denylist of Java-only constructs would
violate the
+ * never-false-reject rule the first time it missed one, because a missed
construct becomes a pattern
+ * the browser interprets differently and the user cannot get past. Anything
not provably common to
+ * both engines is rejected, and the field simply gets no client-side check.
+ *
+ * @since 7.4.0
+ */
+public final class EcmaScriptSafeRegex {
+
+ /**
+ * Escapes with identical meaning in both engines.
+ * <p>
+ * {@code \s} and {@code \S} are deliberately absent. Java's {@code \s} is
ASCII-only by default
+ * while ECMAScript's is the wider Unicode set, so {@code ^\S+$} accepts a
value containing NBSP
+ * on the server and rejects it in the browser. {@code \d} and {@code \w}
are safe — both engines
+ * are ASCII-only for those, and JavaScript never widens them.
+ */
+ private static final String ALLOWED_ESCAPES =
"dDwWbBnrtf\\.*+?()[]{}|^$/-";
Review Comment:
Java 17 treats `\b`/`\B` boundaries as Unicode-aware, while the browser's
ECMAScript boundary is based on ASCII word characters. For example, Java 17
accepts `^\bäiti\b$` for `äiti`, but the emitted HTML pattern rejects it,
violating the never-false-reject rule. Remove both escapes from the safe subset
(JDK 19 changed Java's behavior, but Struts still targets Java 17).
##########
core/src/main/java/org/apache/struts2/components/HtmlConstraintProvider.java:
##########
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.components;
+
+import org.apache.struts2.validator.Validator;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Maps a field's validators onto the HTML attributes a theme should render
for it.
+ * <p>
+ * The default implementation is deliberately conservative — see {@link
StrutsHtmlConstraintProvider}.
+ * Applications wanting a best-effort mapping (an {@code email} validator
becoming
+ * {@code type="email"}, say) should register their own implementation instead.
Review Comment:
The advertised `type="email"` replacement cannot work with the current
rendering path. `html5/text.ftl` emits `type="text"` before `constraints.ftl`
renders the provider map, producing a duplicate `type`; HTML keeps the first
value. Either restrict this contract to post-rendered constraint attributes or
merge provider overrides into the normal attribute model before the template
emits `type`.
##########
core/src/main/java/org/apache/struts2/components/UIBean.java:
##########
@@ -903,6 +917,69 @@ public void evaluateParams() {
}
evaluateExtraParams();
+
+ // must run after evaluateExtraParams(): that is where TextField
resolves attributes.type,
+ // and the control type decides which constraints are legal
+ addConstraintAttributes(form);
+ }
+
+ /**
+ * Derives HTML5 constraint attributes for this field from the action's
validators.
+ * <p>
+ * This reaches {@link Form#getFieldValidators(String)}, which resolves
the action's validators via
+ * {@code AnnotationActionValidatorManager}, which in turn dereferences
the current
+ * {@code ActionInvocation} unconditionally. Before this feature that path
only ran under the opt-in
+ * {@code validate="true"}; with constraint derivation gated only by
+ * {@code struts.ui.html5.constraints}, every {@code html5}-themed form
now runs it, including one
+ * rendered outside action scope (a direct JSP include from a plain
servlet, say) — which would NPE.
+ * A stray {@code null} in the validator list, and a broken {@code ${}} in
a validator message
+ * unbalancing the value stack in {@code ValidatorSupport.getMessage},
land in the same call. This
+ * feature is purely decorative — a missing constraint attribute costs
nothing, a 500 costs the page —
+ * so the broad catch here is deliberate rather than a mistake.
+ *
+ * @since 7.4.0
+ */
+ protected void addConstraintAttributes(Form form) {
+ if (!html5ConstraintsEnabled || form == null || htmlConstraintProvider
== null) {
+ return;
+ }
+ String fieldName = (String) getAttributes().get("name");
+ if (fieldName == null) {
+ return;
+ }
+ try {
+ Map<String, String> constraints =
htmlConstraintProvider.constraintsFor(
+ form.getFieldValidators(fieldName), getControlType(),
stack.peek());
+ if (constraints.isEmpty()) {
+ return;
+ }
+ constraints = new LinkedHashMap<>(constraints);
+ constraints.keySet().removeIf(this::isAlreadyRendered);
+ if (!constraints.isEmpty()) {
+ addParameter("constraints", constraints);
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to derive HTML5 constraint attributes for field
[{}], skipping", fieldName, e);
+ }
+ }
+
+ /**
+ * True when the developer already supplied this attribute explicitly — as
a declared tag attribute
+ * (e.g. {@code maxlength}) or a dynamic one (e.g. {@code min} on a
numeric textfield, which is not a
+ * declared attribute of any component) — so a derived constraint of the
same name must not be
+ * rendered a second time. The developer's own value always wins.
+ * <p>
+ * {@code required} is deliberately excluded from the declared-attribute
half of this check:
+ * {@code requiredLabel} stores an unrelated boolean under the same {@code
attributes.required} key,
+ * purely to draw a label asterisk in the xhtml theme, and that must never
suppress a genuine
+ * {@code required} constraint derived from a {@code required}/{@code
requiredstring} validator. A
+ * {@code required} attribute the developer typed by hand as a dynamic
attribute still wins.
+ */
+ private boolean isAlreadyRendered(String attributeName) {
+ if (dynamicAttributes.containsKey(attributeName)) {
+ return true;
+ }
+ return !"required".equals(attributeName) &&
getAttributes().containsKey(attributeName);
}
Review Comment:
HTML attribute names are ASCII case-insensitive, but this exact-key check
misses dynamic attributes such as `MAXLENGTH` or `Min`. Because derived
attributes render before dynamic ones, the browser keeps the derived duplicate
and the developer's value does not win as documented. Compare dynamic keys
case-insensitively.
##########
core/src/main/java/org/apache/struts2/components/HtmlControlType.java:
##########
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.components;
+
+import java.util.EnumSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * The kind of HTML form control a {@link UIBean} renders, used to decide
which HTML5 constraint
+ * attributes are legal on it.
+ * <p>
+ * This models the <em>control</em> rather than the {@code type} attribute,
because {@code textarea}
+ * and {@code select} have no {@code type} attribute yet still accept {@code
required}.
+ *
+ * @since 7.4.0
+ */
+public enum HtmlControlType {
+
+ TEXT, SEARCH, TEL, PASSWORD, EMAIL, URL,
+ NUMBER, RANGE,
+ DATE, MONTH, WEEK, TIME, DATETIME_LOCAL,
+ CHECKBOX, RADIO, FILE, HIDDEN, SELECT,
Review Comment:
`CHECKBOX` and `HIDDEN` are public control kinds, but the built-in
`Checkbox` and `Hidden` components inherit `OTHER`; only the six new overrides
ever expose concrete types. A replacement provider therefore cannot distinguish
these controls, undermining the swappable mapping policy. Return their actual
control types and keep the conservative behavior in
`StrutsHtmlConstraintProvider`.
##########
core/src/main/java/org/apache/struts2/components/StrutsHtmlConstraintProvider.java:
##########
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.components;
+
+import org.apache.struts2.validator.Validator;
+import org.apache.struts2.validator.validators.CreditCardValidator;
+import org.apache.struts2.validator.validators.DoubleRangeFieldValidator;
+import org.apache.struts2.validator.validators.EmailValidator;
+import org.apache.struts2.validator.validators.RangeValidatorSupport;
+import org.apache.struts2.validator.validators.RegexFieldValidator;
+import org.apache.struts2.validator.validators.RequiredFieldValidator;
+import org.apache.struts2.validator.validators.RequiredStringValidator;
+import org.apache.struts2.validator.validators.StringLengthFieldValidator;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Default {@link HtmlConstraintProvider}.
+ * <p>
+ * Governed by one rule: never false-reject. A constraint is emitted only when
the browser cannot
+ * reject input the server would accept. In particular this implementation
<em>never sets or changes
+ * an input's {@code type}</em> — switching a field to {@code type="number"}
would reject
+ * {@code 1234,50}, which the framework's locale-aware conversion accepts in a
comma-decimal locale,
+ * and the browsers' {@code email}/{@code url} grammars differ from the
framework's validators.
+ * Range constraints are therefore emitted only on a control the developer
already made numeric.
+ *
+ * @since 7.4.0
+ */
+public class StrutsHtmlConstraintProvider implements HtmlConstraintProvider {
+
+ /**
+ * The HTML5 boolean attribute; its canonical serialisation repeats the
attribute name as the value.
+ */
+ private static final String REQUIRED = "required";
+
+ @Override
+ public Map<String, String> constraintsFor(List<Validator> validators,
HtmlControlType control, Object action) {
+ Map<String, String> attributes = new LinkedHashMap<>();
+ if (validators == null || validators.isEmpty() || control == null) {
+ return attributes;
+ }
+ for (Validator validator : validators) {
+ addConstraints(attributes, validator, control);
+ addMessage(attributes, validator, action);
+ }
+ return attributes;
+ }
+
+ protected void addConstraints(Map<String, String> attributes, Validator
validator, HtmlControlType control) {
+ if (validator instanceof RequiredStringValidator) {
+ addRequiredString(attributes, control);
+ } else if (validator instanceof RequiredFieldValidator) {
+ addRequiredField(attributes, control);
+ } else if (validator instanceof StringLengthFieldValidator
lengthValidator) {
+ addLength(attributes, lengthValidator, control);
+ } else if (validator instanceof RegexFieldValidator regexValidator) {
+ addPattern(attributes, regexValidator, control);
+ } else if (validator instanceof DoubleRangeFieldValidator
doubleValidator) {
+ addDoubleRange(attributes, doubleValidator, control);
+ } else if (validator instanceof RangeValidatorSupport<?>
rangeValidator) {
+ addRange(attributes, rangeValidator, control);
+ }
+ }
+
+ /**
+ * {@code requiredstring} fails on null, empty and (by default) blank, so
the browser's
+ * {@code required} can only reject what the server would also reject.
Safe on any text-entry control.
+ */
+ protected void addRequiredString(Map<String, String> attributes,
HtmlControlType control) {
+ if (!control.supportsLength()) {
+ return;
+ }
+ attributes.put(REQUIRED, REQUIRED);
+ }
+
+ /**
+ * {@code required} fails only on null, an empty array or an empty
collection. A control that submits
+ * an empty string rather than omitting the parameter therefore passes
server-side while the browser
+ * blocks it — an empty text input, a select with an empty-valued header
option, and an unticked
+ * checkbox (CheckboxInterceptor substitutes "false") are all in that
group. Only RADIO and FILE omit
+ * the parameter entirely when empty, so only they agree with the browser.
+ */
+ protected void addRequiredField(Map<String, String> attributes,
HtmlControlType control) {
+ if (control != HtmlControlType.RADIO && control !=
HtmlControlType.FILE) {
+ return;
+ }
+ attributes.put(REQUIRED, REQUIRED);
+ }
+
+ protected void addLength(Map<String, String> attributes,
StringLengthFieldValidator validator, HtmlControlType control) {
+ // with trim=true the server measures the trimmed value, so a
maxlength taken from it would
+ // stop the user typing input the server would have accepted
+ if (!control.supportsLength() || validator.isTrim()) {
+ return;
+ }
+ if (validator.getMinLength() > -1) {
+ attributes.put("minlength",
String.valueOf(validator.getMinLength()));
+ }
+ if (validator.getMaxLength() > -1) {
+ attributes.put("maxlength",
String.valueOf(validator.getMaxLength()));
+ }
+ }
+
+ protected void addPattern(Map<String, String> attributes,
RegexFieldValidator validator, HtmlControlType control) {
+ // HTML pattern accepts no flags, so a case-insensitive rule cannot be
expressed at all
+ if (!control.supportsPattern() || !validator.isCaseSensitive()) {
+ return;
+ }
+ // trim defaults to true, and the server matches the trimmed value
while pattern matches the
+ // raw one: "[a-z]+" would accept "abc " server-side and be blocked by
the browser
+ if (validator.isTrimed()) {
+ return;
+ }
+ // Both extend RegexFieldValidator but do not match their regex
against the raw value:
+ // CreditCardValidator strips all whitespace first, and both carry
grammars the browser
+ // does not share. Neither is expressible as a pattern.
+ if (validator instanceof EmailValidator || validator instanceof
CreditCardValidator) {
+ return;
+ }
+ String regex = validator.getRegex();
+ if (EcmaScriptSafeRegex.isSafe(regex)) {
+ attributes.put("pattern", regex);
+ }
+ }
+
+ protected void addRange(Map<String, String> attributes,
RangeValidatorSupport<?> validator, HtmlControlType control) {
+ if (!isNumericRange(control)) {
+ // Temporal controls support ranges too, but min/max there need
per-control ISO
+ // formatting (date -> yyyy-MM-dd, month -> yyyy-MM, week ->
yyyy-'W'ww, time -> HH:mm).
+ // Deliberately deferred; DateRangeFieldValidator therefore emits
nothing for now.
+ return;
+ }
+ // min is guarded by isIntegral; see the comment on that method. The
shipped Integer/Short/Long
+ // range validators always pass it, but a custom
RangeValidatorSupport<Double> would not.
+ Object min = validator.getMin();
+ if (isIntegral(min)) {
+ putIfPresent(attributes, "min", min);
+ }
+ putIfPresent(attributes, "max", validator.getMax());
+ }
+
+ protected void addDoubleRange(Map<String, String> attributes,
DoubleRangeFieldValidator validator, HtmlControlType control) {
+ if (!isNumericRange(control)) {
+ // Temporal controls support ranges too, but min/max there need
per-control ISO
+ // formatting (date -> yyyy-MM-dd, month -> yyyy-MM, week ->
yyyy-'W'ww, time -> HH:mm).
+ // Deliberately deferred; DateRangeFieldValidator therefore emits
nothing for now.
+ return;
+ }
+ // exclusive bounds have no HTML equivalent; omitting them leaves the
browser more
+ // permissive than the server, which is the safe direction
+ Double minInclusive = validator.getMinInclusive();
+ if (isIntegral(minInclusive)) {
+ putIfPresent(attributes, "min", minInclusive);
+ }
+ putIfPresent(attributes, "max", validator.getMaxInclusive());
+ }
+
+ private boolean isNumericRange(HtmlControlType control) {
+ return control.supportsRange() && (control == HtmlControlType.NUMBER
|| control == HtmlControlType.RANGE);
+ }
+
+ /**
+ * A fractional {@code min} moves the HTML step base off zero, and with
the default {@code step="1"}
+ * the browser then rejects whole numbers the server accepts. {@code max}
does not participate in the
+ * step base, so only {@code min} needs this guard.
+ */
+ private boolean isIntegral(Object value) {
+ if (!(value instanceof java.lang.Number number)) {
+ return false;
+ }
+ double asDouble = number.doubleValue();
+ return !Double.isNaN(asDouble) && !Double.isInfinite(asDouble) &&
asDouble == Math.floor(asDouble);
+ }
Review Comment:
Converting arbitrary `Number` values to `double` can round a fractional
custom bound to an integer. A `BigDecimal("1.0000000000000000001")` passes this
check, then renders that fractional value as `min`, shifts the HTML step base,
and can reject values accepted by the server. Determine integrality from the
decimal representation instead.
Issue Time Tracking
-------------------
Worklog Id: (was: 1037687)
Time Spent: 40m (was: 0.5h)
> 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: 40m
> 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)