This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch feature/WW-5695-html5-constraint-validation in repository https://gitbox.apache.org/repos/asf/struts.git
commit 30947b0b5e08063dc4c9b7b4af8091e5c54873bf Author: Lukasz Lenart <[email protected]> AuthorDate: Mon Aug 24 18:17:28 2026 +0200 WW-5695 docs(validation): design HTML5 constraint validation and retire the JS validator Design covering the replacement of the generated JavaScript client-side validator with native HTML5 constraint attributes derived from the action's validators. Governing rule is never to false-reject: a constraint is emitted only when the browser cannot reject input the server would accept. That rules out changing an input's type, since type="number" refuses "1234,50" which locale-aware conversion accepts, and browser email/url regexes diverge from the framework's. Struts only adds constraints safe for the type already present. Also records why WW-2975 is superseded rather than fixed: constraints riding on each input remove the central tagNames list its root cause depends on. Covers WW-5694 (deprecate, 7.4.0), WW-5695 (constraints, 7.4.0) and WW-5696 (remove, 8.0.0). The implementation plan drawn from this covers 7.4.0 only. Co-Authored-By: Claude Opus 5 <[email protected]> --- ...026-08-24-html5-constraint-validation-design.md | 344 +++++++++++++++++++++ 1 file changed, 344 insertions(+) diff --git a/docs/superpowers/specs/2026-08-24-html5-constraint-validation-design.md b/docs/superpowers/specs/2026-08-24-html5-constraint-validation-design.md new file mode 100644 index 000000000..58ed7f93b --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-html5-constraint-validation-design.md @@ -0,0 +1,344 @@ +# HTML5 constraint validation, and retiring the JavaScript client-side validator + +**Tickets:** not yet filed — three proposed, see "Ticket structure" +**Supersedes:** [WW-2975](https://issues.apache.org/jira/browse/WW-2975) (to be closed Won't Fix) +**Target:** 7.4.0 (deprecate + new feature), 8.0.0 (remove) +**Date:** 2026-08-24 +**Status:** Design approved, pending implementation plan + +**Plan scope:** the implementation plan drawn from this spec covers the **7.4.0 work only** — the deprecation +and the new constraint feature. The 8.0.0 removal is specified here so the deprecation is written against a +known endpoint, but it is a separate ticket, a separate release and a separate plan. + +## Problem + +`xhtml/form-close-validate.ftl` generates a `validateForm_<id>()` function by iterating +`attributes.tagNames`. That list is seeded by `Form.evaluateExtraParams` (`Form.java:207`) and appended to +by `UIBean.evaluateParams` (`UIBean.java:834`) — but **only** when `findAncestor(Form.class)` finds the form +on the component stack. + +Any input that reaches the form's markup by another route is therefore invisible to client-side validation: +raw HTML, a custom tag, an included fragment, or a component handed pre-rendered field markup. The generated +function is then an empty shell: + +```js +function validateForm_doubleValidationAction() { + ... + var errors = false; + var continueValidation = true; + + return !errors; +} +``` + +This is WW-2975, reported 2009-01-30 against 2.1.6 and reproduced unchanged on `main` (7.4.0-SNAPSHOT) on +2026-08-24. The reproduction renders a form whose action declares a `double` validator for +`myUpDownSelectTag`: a nested `<s:updownselect>` produces the `if (form.elements['myUpDownSelectTag'])` +block, while an identical raw `<input name="myUpDownSelectTag">` produces nothing at all. + +The ticket's second complaint is also live, at LOW confidence (the repo has **no** JavaScript test +infrastructure — no `package.json`, nothing). `validation.js` `addErrorXHTML` walks `row.parentNode` up +until it finds a `TR`. A field with no `TR` ancestor sends that walk off the top of the document, and the +handler is `catch (err) { alert(err) }` — so the user gets a raw JavaScript error instead of a validation +message. + +### Why this is not being fixed in place + +The feature is legacy on every axis. It exists only in `xhtml` (the default theme) and `css_xhtml` +(`parent = xhtml`); the `html5` theme has `parent = simple` and never had it. It supports eight validators. +It reports errors by inserting `<tr>` elements, so it only works with the table layout. It has zero test +coverage beyond four golden files. + +The successor is native HTML5 constraint validation, emitted per field. That dissolves the root cause rather +than patching it: with constraints riding on each `<input>`, there is no central field list for a foreign +field to be missing from. + +## Goals + +- Emit HTML5 constraint attributes from the `html5` theme, derived from the action's validators. +- Never emit a constraint the browser would enforce more strictly than the server does. +- Deprecate the JavaScript client-side validator in 7.4.0, remove it in 8.0.0. +- Change nothing about existing renderings on upgrade to 7.4.0. + +## Non-goals + +- Fixing the `tagNames` scope or `addError` fragility in the deprecated path. It is documented as a known + limitation and deleted in 8.0.0. +- Shipping any JavaScript. Struts emits messages as `data-*` attributes and stops there. +- Changing an input's `type` attribute. See "The never-change-the-type rule". +- Constraint support in `xhtml`, `css_xhtml` or `simple`. This is an `html5` theme feature. +- Touching `struts.ui.theme`'s default. `xhtml` remains the default theme in 7.4.0. + +## Approach + +Derive constraints in Java from the action's validators, behind a swappable container bean, and render them +from a single new `html5` template include. + +### The never-change-the-type rule + +`min`/`max` are inert on `type="text"`; they only apply to `number`, `range` and the temporal types. Emitting +them therefore means switching the input to `type="number"` — and that **is** a false rejection: a browser +`type="number"` refuses `1234,50`, which Struts' locale-aware conversion accepts in a comma-decimal locale. +The same argument rules out `type="email"` and `type="url"`, whose browser regexes differ from +`EmailValidator` and `URLValidator`. + +So: **Struts never sets or changes `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, and `min`/`max` +become pure additions. + +### Mapping table + +| Validator | Emits | Condition | +|---|---|---| +| `required` | `required` | always | +| `requiredstring` | `required` | always — server is stricter on whitespace-only input, which is safe | +| `stringlength` | `minlength` / `maxlength` | only when `trim="false"` | +| `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` | — | never; browser regexes diverge from Struts' | +| `creditcard`, `fieldexpression`, `expression`, `conversion`, visitor | — | no safe mapping | + +`stringlength` with `trim="true"` is excluded because the server measures the *trimmed* value: a +`maxlength` derived from it would stop the user typing input the server would have accepted. + +`RegexFieldValidator` uses `matcher.matches()`, so it is fully anchored and matches HTML5 `pattern` +semantics. The divergence is syntactic, not positional. + +### ECMAScript-safe regex detection + +This is the least-solved part of the design and the strongest argument for the provider being swappable. + +A denylist of Java-only constructs (`\p{Alpha}`, possessive quantifiers, `\A`/`\z`, lookbehind) violates the +never-false-reject rule the first time it misses one: a missed construct becomes a `pattern` the browser +interprets differently. So detection is an **allowlist** — literals, `\d \w \s` and their negations, +character classes without POSIX or Unicode property syntax, grouping, alternation, anchors, and bounded +quantifiers. Anything outside it emits no `pattern`. + +This is conservative to the point that some legitimate regexes will silently get no client-side check. That +is the correct failure direction under the agreed rule, and it is the piece most likely to need tuning after +real use. + +## Components + +### `HtmlControlType` (new enum) + +The provider's real question is not "what string is in `type`" but "which constraint attributes are legal on +this control". `textarea` and `select` have no `type` attribute at all yet do accept `required`, so the enum +models the *control*, not the attribute — hence `HtmlControlType`, not `HtmlInputType`. + +```java +public enum HtmlControlType { + TEXT, SEARCH, TEL, PASSWORD, EMAIL, URL, + NUMBER, RANGE, + DATE, MONTH, WEEK, TIME, DATETIME_LOCAL, + CHECKBOX, RADIO, FILE, HIDDEN, SELECT, + TEXTAREA, + OTHER; + + public static HtmlControlType from(String type); + public boolean supportsPattern(); // text-entry only + public boolean supportsLength(); // text-entry + TEXTAREA + public boolean supportsRange(); // numeric + temporal +} +``` + +`from()` must never throw. `type` is an OGNL-evaluated tag attribute, so at runtime it can be any string — a +typo, or an input type newer than this enum. Unknown and `null` normalise to `OTHER`, which supports nothing, +so an unrecognised type degrades to emitting no constraints. The conservative default falls out for free. + +The enum appears in the public signature of an extension point. Adding members later stays binary- and +source-compatible for callers, but can make an exhaustive `switch` in a custom provider non-exhaustive. That +belongs in the release notes. + +### `HtmlConstraintProvider` (new interface) and `StrutsHtmlConstraintProvider` + +```java +public interface HtmlConstraintProvider { + Map<String, String> constraintsFor(List<Validator> validators, HtmlControlType control); +} +``` + +Named per the project convention of `Struts*` for default implementations rather than `Default*`. Registered +**once** in `struts-beans.xml` as `type="...HtmlConstraintProvider" name="struts"`, following the +`UrlRenderer` model — a bean registered under two types builds two instances, which is not wanted here. + +The default implementation encodes the mapping table. Because the agreed policy is deliberately restrictive, +the swappable bean is how applications that want `type="email"` or best-effort `pattern` get served. + +### `StrutsConstants.STRUTS_UI_HTML5_CONSTRAINTS` + +`"struts.ui.html5.constraints"`, following the `struts.ui.checkbox.submitUnchecked` naming precedent. +Defaults to `false` in `default.properties` for 7.4.0 and `true` for 8.0.0. + +Off by default matters: the `html5` theme shipped in 7.2.x, so emitting `required` on upgrade would start +blocking submits on forms that render unchanged today. The 8.0.0 flip lands in a major with a migration entry. + +### `UIBean` hook + +`UIBean.evaluateParams` already resolves `final Form form = findAncestor(Form.class)` and appends to +`tagNames`. Immediately after that block, when the constant is on and a form was found: + +``` +form.getFieldValidators(translatedName) + → provider.constraintsFor(validators, HtmlControlType.from(type)) + → addParameter("constraints", map) +``` + +Gating the *computation* on the constant keeps the cost at zero when off. Themes that do not render +`attributes.constraints` simply ignore it. + +### `Form.getFieldValidators(String)` (new) + +`Form.getValidators(String)` re-runs the action-mapping lookup and +`actionValidatorManager.getValidators(actionClass, actionName, methodName)` on every call, so a 20-field form +would do 20 full lookups. `getFieldValidators` resolves the action's validator list **once**, memoises it on +the form's attributes, and filters by field name per call. + +The existing `getValidators(String)` stays untouched for the deprecated `form-close-validate.ftl` and is +deleted with it in 8.0.0. + +### `html5/constraints.ftl` (new), included from `common-attributes.ftl` + +```freemarker +<#if attributes.constraints??><#list attributes.constraints as k, v> ${k}="${v?html}"<#rt/></#list></#if> +``` + +Including it from `common-attributes.ftl` means every `html5` input picks it up without per-template edits. + +`constraintsFor` returns the full set of attributes to render, not only constraints — messages ride the same +map as `data-msg-<validatorType>` entries: `data-msg-required`, `data-msg-stringlength`, `data-msg-regex`. +The text comes from `validator.getMessage(action)`, which resolves through `DelegatingValidatorContext` and +`textProviderFactory`, so it is properly i18n'd. + +A `data-msg-*` entry is emitted for **every** validator carrying a message, including those that produce no +constraint. An `email` validator therefore contributes `data-msg-email` and nothing else — which is exactly +the case where an application most needs the message, since Struts could not express the rule natively. +Struts ships nothing that consumes these attributes. + +### Deliberately unchanged + +`requiredLabel` keeps meaning "draw a `*` next to the label". It never produces a `required` attribute — only +a `required` *validator* does. Conflating the two is the most likely regression in this work. + +## Deprecation and removal + +`validate="true"` is not one feature but four edits to the rendered form: + +- `xhtml/form-validate.ftl` injects the `validation.js` `<script>` tag +- the same template rewrites `onsubmit` to `return validateForm_<id>();` +- `xhtml/form.ftl` overwrites `onreset` with `clearErrorMessages(this);clearErrorLabels(this);` +- `xhtml/form-close.ftl` includes `form-close-validate.ftl` + +### 7.4.0 — deprecate, change nothing + +Following the WW-5510 tooltip precedent (`c2a5bfe3c`): + +- `@Deprecated(since = "7.4.0", forRemoval = true)` on `Form.setValidate`, `Form.getValidators(String)` and + `Form.evaluateClientSideJsEnablement`, **and** on the mirrored `FormTag.setValidate`. WW-5510 annotated + both the component and the JSP tag layer; both surfaces are public API. +- `<strong>Deprecated since 7.4.0</strong>` banners inside the relevant `<!-- START SNIPPET: ... -->` blocks + in the `Form` Javadoc. Those snippets are pulled into the website by the `remote_file_content` plugin, so + omitting this leaves the site advertising the feature as current. +- A deprecation banner in the `form-close-validate.ftl` header comment, since a `.ftl` carries no annotation + and anyone who overrode that template needs to see it there. +- **No runtime warning.** WW-5510 shipped annotations and Javadoc only. `validate="true"` is opt-in, so every + affected user made a deliberate choice and will be reading release notes. +- `struts.ui.html5.constraints` ships `false`. + +Two traps: `Dispatcher` and `ValidationInterceptor` also declare unrelated `setValidate` methods that must +not be annotated. And WW-5510's annotations say `since = "7.0.1"` while its fix version was 7.0.3 — use the +real target version here. + +### 8.0.0 — remove + +Delete `xhtml/form-close-validate.ftl`, `xhtml/form-validate.ftl`, `xhtml/validation.js` and +`css_xhtml/validation.js`. Strip the `validate` branches from `xhtml/form.ftl` and `xhtml/form-close.ftl`. +Drop `validate`, `performValidation` and `tagNames` from `Form`, `FormTag` (including its +`clearTagStateForTagPoolingServers` reset) and the `tagNames` append in `UIBean`. Flip the constant's default +to `true`. + +`html5/form.ftl` also reads `attributes.validate` in its `onsubmit` guard and must be updated in the same +change. + +Retire the `Formtag-2.txt`, `Formtag-11.txt`, `Formtag-22.txt` and `Formtag-24.txt` fixtures and their +`FormTagTest` methods. + +`tagNames` dies here, which is what finally closes WW-2975's root cause rather than patching it. + +## Testing + +`HtmlControlType` and `StrutsHtmlConstraintProvider` are plain objects — JUnit 4 (`org.junit.Test`). Anything +that renders a tag must extend `AbstractUITagTest`, which is JUnit 3 style: methods named `testXxx()`, and a +Jupiter `@Test` there silently never runs. + +The negative cases carry the weight, because they are what protects the never-false-reject rule: + +- `stringlength trim="true"` → no `minlength`/`maxlength` +- `regex caseSensitive="false"` → no `pattern` +- a regex using Java-only syntax → no `pattern` +- `int`/`double` on `TEXT` → no `min`/`max`; on `NUMBER` → both +- `email`/`url` validators → never set or change `type` +- `creditcard`, `fieldexpression`, visitor → empty map +- `HtmlControlType.from(null)`, `from("NuMbEr")`, `from("supercolor")` → `OTHER`/`NUMBER`/`OTHER`, never throws + +Tag-rendering tests use the existing `verify(resource)` golden-file pattern with new `.txt` fixtures, +covering constraints on and off via the constant, and `requiredLabel="true"` **not** producing a `required` +attribute. + +**Harness trap, encountered during triage:** a form-validation tag test needs +`initDispatcher(configProviders = TestConfigurationProvider)` *and* `createMocks()` in `setUp`, plus the +`prepareMockInvocation()` EasyMock helper from `FormTagTest`. Without them `evaluateClientSideJsEnablement` +finds no `ValidationInterceptor`, `performValidation` stays `false`, and no validation function is emitted at +all — while `onsubmit` still calls the missing function. A test written without that setup passes or fails +for entirely the wrong reason. + +The WW-2975 reproduction test is **not** committed. It asserts behaviour scheduled for deletion, so it stays +attached to the triage comment as evidence rather than becoming a permanent red or an inverted test pinning a +known-bad behaviour. + +## Ticket structure + +WW-2975 is a 2009 Improvement carrying six fix-version bumps and is the wrong vehicle. Close it **Won't Fix**, +superseded, with a triage comment recording the reproduction and the empty `validateForm_` output. + +The repo convention is a matched deprecate/remove pair — WW-5682/WW-5683, WW-5673, WW-5654 all follow it. + +| Summary | Type | Fix version | +|---|---|---| +| Deprecate JavaScript client-side validation in the xhtml and css_xhtml themes | Improvement | 7.4.0 | +| Derive HTML5 constraint attributes from validators in the html5 theme | New Feature | 7.4.0 | +| Remove deprecated JavaScript client-side validation | Improvement | 8.0.0 | + +Cross-link [WW-4395](https://issues.apache.org/jira/browse/WW-4395) ("Make email validator regex comply with +RFC 6531", Open, 7.4.0) from the feature ticket: that regex's divergence from browsers is precisely why +`type="email"` is never emitted. + +## Documentation + +In `struts-site`: + +- Delete `core-developers/pure-java-script-client-side-validation.md`. +- Rewrite `core-developers/client-side-validation.md` around the `html5` theme: the mapping table, the + `trim` and `caseSensitive` conditions, the never-change-the-type rule, and the `data-msg-*` attributes. +- Mark `core-developers/client-validation-example.md` as deprecated. +- Fix the stale claim in the pure-JavaScript page that messages are "not the internationalized version" — + `ValidatorSupport.getMessage` resolves through `DelegatingValidatorContext` and `textProviderFactory`. +- Leave the "Available since Struts 7.2.0" claim in `tag-developers/html5-theme.md` alone — it is correct. + `git tag --contains e24d2f2d3` returns `STRUTS_7_2_0`, so the theme shipped in 7.2.0 and **WW-5444's fix + version of 7.2.1 is the wrong record**. Correct the Jira ticket, not the docs. +- Update `tag-developers/form-tag.md` for the deprecated `validate` attribute. + +The Migration Guide entry lives on the cwiki as part of the Version Notes process, not in `struts-site`. + +## Risks + +- **The allowlist is too strict.** Real applications will have regexes that get no `pattern`. Mitigated by + the swappable provider; expect tuning after the first release. +- **`requiredLabel` conflation.** Explicitly tested against. +- **8.0.0 removal is user-visible.** Forms relying on `validate="true"` lose client-side checking. Server-side + validation is unaffected, so this is a UX regression rather than a correctness or security one, but it + needs a prominent migration entry. +- **Constraint derivation runs per field.** Mitigated by memoising the validator list on the form; worth a + sanity check on a wide form before release.
