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 01417ea53b9e62f079980b6d69bee73a7d8ae1ec Author: Lukasz Lenart <[email protected]> AuthorDate: Mon Aug 24 19:56:54 2026 +0200 WW-5695 feat(components): add HtmlControlType Models the kind of form control a UIBean renders, so constraint derivation can ask which HTML5 attributes are legal rather than string-matching a type attribute. Models the control, not the attribute, because textarea and select have no type yet still accept required. from() never throws; unknown input becomes OTHER, which supports nothing. Co-Authored-By: Claude Opus 5 <[email protected]> --- .../apache/struts2/components/HtmlControlType.java | 78 ++++++++++++++++++++++ .../struts2/components/HtmlControlTypeTest.java | 60 +++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/core/src/main/java/org/apache/struts2/components/HtmlControlType.java b/core/src/main/java/org/apache/struts2/components/HtmlControlType.java new file mode 100644 index 000000000..6e69617b9 --- /dev/null +++ b/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, + TEXTAREA, + OTHER; + + private static final Set<HtmlControlType> TEXT_ENTRY = EnumSet.of(TEXT, SEARCH, TEL, PASSWORD, EMAIL, URL); + private static final Set<HtmlControlType> NUMERIC = EnumSet.of(NUMBER, RANGE); + private static final Set<HtmlControlType> TEMPORAL = EnumSet.of(DATE, MONTH, WEEK, TIME, DATETIME_LOCAL); + + /** + * Resolves a raw {@code type} attribute value. Never throws: the attribute is OGNL-evaluated, so at + * runtime it can be any string. Anything unrecognised becomes {@link #OTHER}, which supports no + * constraints at all — so an unknown control degrades to emitting nothing. + */ + public static HtmlControlType from(String type) { + if (type == null) { + return OTHER; + } + String normalised = type.trim().toUpperCase(Locale.ROOT).replace('-', '_'); + if (normalised.isEmpty()) { + return OTHER; + } + try { + return valueOf(normalised); + } catch (IllegalArgumentException e) { + return OTHER; + } + } + + public boolean supportsPattern() { + return TEXT_ENTRY.contains(this); + } + + public boolean supportsLength() { + return TEXT_ENTRY.contains(this) || this == TEXTAREA; + } + + public boolean supportsRange() { + return NUMERIC.contains(this) || TEMPORAL.contains(this); + } +} diff --git a/core/src/test/java/org/apache/struts2/components/HtmlControlTypeTest.java b/core/src/test/java/org/apache/struts2/components/HtmlControlTypeTest.java new file mode 100644 index 000000000..3a08f6ae5 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/components/HtmlControlTypeTest.java @@ -0,0 +1,60 @@ +package org.apache.struts2.components; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HtmlControlTypeTest { + + @Test + public void resolvesKnownTypes() { + assertThat(HtmlControlType.from("text")).isEqualTo(HtmlControlType.TEXT); + assertThat(HtmlControlType.from("number")).isEqualTo(HtmlControlType.NUMBER); + assertThat(HtmlControlType.from("datetime-local")).isEqualTo(HtmlControlType.DATETIME_LOCAL); + } + + @Test + public void isLenientAboutCaseAndWhitespace() { + assertThat(HtmlControlType.from(" NuMbEr ")).isEqualTo(HtmlControlType.NUMBER); + } + + @Test + public void neverThrowsOnUnusableInput() { + assertThat(HtmlControlType.from(null)).isEqualTo(HtmlControlType.OTHER); + assertThat(HtmlControlType.from("")).isEqualTo(HtmlControlType.OTHER); + assertThat(HtmlControlType.from(" ")).isEqualTo(HtmlControlType.OTHER); + assertThat(HtmlControlType.from("supercolor")).isEqualTo(HtmlControlType.OTHER); + } + + @Test + public void otherSupportsNothing() { + assertThat(HtmlControlType.OTHER.supportsPattern()).isFalse(); + assertThat(HtmlControlType.OTHER.supportsLength()).isFalse(); + assertThat(HtmlControlType.OTHER.supportsRange()).isFalse(); + } + + @Test + public void patternIsTextEntryOnly() { + assertThat(HtmlControlType.TEXT.supportsPattern()).isTrue(); + assertThat(HtmlControlType.PASSWORD.supportsPattern()).isTrue(); + assertThat(HtmlControlType.NUMBER.supportsPattern()).isFalse(); + assertThat(HtmlControlType.TEXTAREA.supportsPattern()).isFalse(); + assertThat(HtmlControlType.SELECT.supportsPattern()).isFalse(); + } + + @Test + public void lengthIsTextEntryPlusTextarea() { + assertThat(HtmlControlType.TEXT.supportsLength()).isTrue(); + assertThat(HtmlControlType.TEXTAREA.supportsLength()).isTrue(); + assertThat(HtmlControlType.NUMBER.supportsLength()).isFalse(); + assertThat(HtmlControlType.CHECKBOX.supportsLength()).isFalse(); + } + + @Test + public void rangeIsNumericAndTemporalOnly() { + assertThat(HtmlControlType.NUMBER.supportsRange()).isTrue(); + assertThat(HtmlControlType.RANGE.supportsRange()).isTrue(); + assertThat(HtmlControlType.DATE.supportsRange()).isTrue(); + assertThat(HtmlControlType.TEXT.supportsRange()).isFalse(); + } +}
