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();
+    }
+}

Reply via email to