This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 192d33c29 WW-5704 fix(core): emit required on radio/file only when the 
bound value would fail the validator (#1939)
192d33c29 is described below

commit 192d33c2941565b9d316936e681ad89755a31087
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Sep 14 16:45:42 2026 +0200

    WW-5704 fix(core): emit required on radio/file only when the bound value 
would fail the validator (#1939)
    
    * WW-5704 fix(core): emit required on radio/file only when the bound value 
would fail the validator
    
    RequiredFieldValidator never sees the request; it inspects the bound
    property, and fails only on null, an empty array or an empty collection.
    A primitive int behind a radio group, or a file property prepare()
    loaded from an existing entity, can therefore never fail server-side,
    while the browser's required still blocks an unselected group or an
    empty file input - a false reject.
    
    A radio or file input omits its parameter when left empty, so the
    property keeps whatever it holds at render time. UIBean now hands that
    value (the tag's resolved nameValue) to HtmlConstraintProvider, and the
    default provider asks the validator itself - the predicate is extracted
    as RequiredFieldValidator.isMissing so the two cannot drift - whether it
    would reject that value; required is emitted only then.
    
    File kept a String-typed nameValue, and OGNL renders a null property as
    "" under that conversion, which would have hidden a missing attachment;
    no file template reads nameValue, so File now keeps the raw property
    like ListUIBean does.
    
    The HtmlConstraintProvider signature change is unreleased (7.4.0).
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    * WW-5704 fix(core): keep a property-less file input rendering under 
throwExceptionOnFailure
    
    Keeping the raw property switched File from the String conversion,
    which never throws, to the lookup that honours
    struts.el.throwExceptionOnFailure. An UploadedFilesAware action has no
    property behind <s:file name="upload"/> - it receives the part by name -
    so that flag would have turned every such page into a 500. Only the
    missing-property failure is tolerated; any other expression failure
    still surfaces.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../java/org/apache/struts2/components/File.java   | 29 ++++++++
 .../struts2/components/HtmlConstraintProvider.java |  5 +-
 .../components/StrutsHtmlConstraintProvider.java   | 23 ++++--
 .../java/org/apache/struts2/components/UIBean.java |  2 +-
 .../validators/RequiredFieldValidator.java         | 20 ++++--
 .../struts2/components/ConstraintAction.java       | 10 +++
 .../components/ConstraintAttributesTest.java       |  8 +--
 .../StrutsHtmlConstraintProviderTest.java          | 51 ++++++++++---
 .../validators/RequiredFieldValidatorTest.java     | 17 +++++
 .../views/jsp/ui/Html5ConstraintRenderingTest.java | 84 ++++++++++++++++++++++
 .../components/ConstraintAction-validation.xml     |  5 ++
 11 files changed, 226 insertions(+), 28 deletions(-)

diff --git a/core/src/main/java/org/apache/struts2/components/File.java 
b/core/src/main/java/org/apache/struts2/components/File.java
index 58bfe42ce..0ce9a6ba7 100644
--- a/core/src/main/java/org/apache/struts2/components/File.java
+++ b/core/src/main/java/org/apache/struts2/components/File.java
@@ -18,6 +18,8 @@
  */
 package org.apache.struts2.components;
 
+import ognl.NoSuchPropertyException;
+import org.apache.struts2.StrutsException;
 import org.apache.struts2.util.ValueStack;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
@@ -67,6 +69,33 @@ public class File extends UIBean {
         return HtmlControlType.FILE;
     }
 
+    /**
+     * A file input never renders its value, so keep the raw property: 
converting null to a String
+     * yields "", which would hide a missing attachment from the constraint 
derivation.
+     */
+    @Override
+    protected Class<?> getValueClassType() {
+        return null;
+    }
+
+    /**
+     * Unlike the String conversion, the raw lookup honours {@code 
struts.el.throwExceptionOnFailure}.
+     * A file input is routinely bound to no property at all — an {@code 
UploadedFilesAware} action
+     * receives the part by name — so that one failure is not the 
misconfiguration the flag exists
+     * to surface.
+     */
+    @Override
+    protected void applyValueParameter(String translatedName) {
+        try {
+            super.applyValueParameter(translatedName);
+        } catch (StrutsException e) {
+            if (!(e.getCause() instanceof NoSuchPropertyException)) {
+                throw e;
+            }
+            LOG.debug("No property [{}] behind the file input, rendering it 
without a bound value", translatedName);
+        }
+    }
+
     public void evaluateParams() {
         super.evaluateParams();
 
diff --git 
a/core/src/main/java/org/apache/struts2/components/HtmlConstraintProvider.java 
b/core/src/main/java/org/apache/struts2/components/HtmlConstraintProvider.java
index 070bf6bec..b6eb36779 100644
--- 
a/core/src/main/java/org/apache/struts2/components/HtmlConstraintProvider.java
+++ 
b/core/src/main/java/org/apache/struts2/components/HtmlConstraintProvider.java
@@ -45,7 +45,10 @@ public interface HtmlConstraintProvider {
      * @param action     the object the field's validators run against, used 
to resolve i18n validator
      *                   messages: the action, or the visited object for a 
field reached through a
      *                   {@code visitor} validator; may be null
+     * @param value      the field's current value as the tag resolved it — 
what the page renders and,
+     *                   for a control that omits its parameter when left 
empty, what the server will
+     *                   validate on such a submit; may be null
      * @return attribute name to value; never null, possibly empty
      */
-    Map<String, String> constraintsFor(List<Validator> validators, 
HtmlControlType control, Object action);
+    Map<String, String> constraintsFor(List<Validator> validators, 
HtmlControlType control, Object action, Object value);
 }
diff --git 
a/core/src/main/java/org/apache/struts2/components/StrutsHtmlConstraintProvider.java
 
b/core/src/main/java/org/apache/struts2/components/StrutsHtmlConstraintProvider.java
index 775939d8e..d7aa859fe 100644
--- 
a/core/src/main/java/org/apache/struts2/components/StrutsHtmlConstraintProvider.java
+++ 
b/core/src/main/java/org/apache/struts2/components/StrutsHtmlConstraintProvider.java
@@ -65,13 +65,14 @@ public class StrutsHtmlConstraintProvider implements 
HtmlConstraintProvider {
     private static final String BLANK = "[\\x00-\\x20]*";
 
     @Override
-    public Map<String, String> constraintsFor(List<Validator> validators, 
HtmlControlType control, Object action) {
+    public Map<String, String> constraintsFor(List<Validator> validators, 
HtmlControlType control, Object action,
+                                              Object value) {
         Map<String, String> attributes = new LinkedHashMap<>();
         if (validators == null || validators.isEmpty() || control == null) {
             return attributes;
         }
         for (Validator validator : validators) {
-            addConstraints(attributes, validator, control);
+            addConstraints(attributes, validator, control, value);
             if (control != HtmlControlType.UNSUPPORTED) {
                 addMessage(attributes, validator, action);
             }
@@ -105,11 +106,12 @@ public class StrutsHtmlConstraintProvider implements 
HtmlConstraintProvider {
         attributes.computeIfPresent(PATTERN_ATTRIBUTE, (name, regex) -> "(?:" 
+ regex + ")|" + BLANK);
     }
 
-    protected void addConstraints(Map<String, String> attributes, Validator 
validator, HtmlControlType control) {
+    protected void addConstraints(Map<String, String> attributes, Validator 
validator, HtmlControlType control,
+                                  Object value) {
         if (validator instanceof RequiredStringValidator) {
             addRequiredString(attributes, control);
-        } else if (validator instanceof RequiredFieldValidator) {
-            addRequiredField(attributes, control);
+        } else if (validator instanceof RequiredFieldValidator 
requiredValidator) {
+            addRequiredField(attributes, requiredValidator, control, value);
         } else if (validator instanceof StringLengthFieldValidator 
lengthValidator) {
             addLength(attributes, lengthValidator, control);
         } else if (validator instanceof RegexFieldValidator regexValidator) {
@@ -138,12 +140,19 @@ public class StrutsHtmlConstraintProvider implements 
HtmlConstraintProvider {
      * 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.
+     * <p>
+     * An omitted parameter leaves the property at whatever it already holds, 
which is the value being
+     * rendered — a primitive's 0, or what {@code prepare()} loaded. The 
validator itself decides whether
+     * that value would fail, so the attribute is emitted only when the two 
sides agree on it.
      */
-    protected void addRequiredField(Map<String, String> attributes, 
HtmlControlType control) {
+    protected void addRequiredField(Map<String, String> attributes, 
RequiredFieldValidator validator,
+                                    HtmlControlType control, Object value) {
         if (control != HtmlControlType.RADIO && control != 
HtmlControlType.FILE) {
             return;
         }
-        attributes.put(REQUIRED_ATTRIBUTE, REQUIRED_ATTRIBUTE);
+        if (validator.isMissing(value)) {
+            attributes.put(REQUIRED_ATTRIBUTE, REQUIRED_ATTRIBUTE);
+        }
     }
 
     protected void addLength(Map<String, String> attributes, 
StringLengthFieldValidator validator, HtmlControlType control) {
diff --git a/core/src/main/java/org/apache/struts2/components/UIBean.java 
b/core/src/main/java/org/apache/struts2/components/UIBean.java
index fb961c8f3..36c51340a 100644
--- a/core/src/main/java/org/apache/struts2/components/UIBean.java
+++ b/core/src/main/java/org/apache/struts2/components/UIBean.java
@@ -968,7 +968,7 @@ public abstract class UIBean extends Component {
                 }
             }
             Map<String, String> constraints = 
htmlConstraintProvider.constraintsFor(
-                validators, getControlType(), validated);
+                validators, getControlType(), validated, 
getAttributes().get(ATTR_NAME_VALUE));
             if (constraints.isEmpty()) {
                 return;
             }
diff --git 
a/core/src/main/java/org/apache/struts2/validator/validators/RequiredFieldValidator.java
 
b/core/src/main/java/org/apache/struts2/validator/validators/RequiredFieldValidator.java
index 9bc94e390..1790022e5 100644
--- 
a/core/src/main/java/org/apache/struts2/validator/validators/RequiredFieldValidator.java
+++ 
b/core/src/main/java/org/apache/struts2/validator/validators/RequiredFieldValidator.java
@@ -66,12 +66,22 @@ public class RequiredFieldValidator extends 
FieldValidatorSupport {
         String fieldName = getFieldName();
         Object value = this.getFieldValue(fieldName, object);
 
-        if (value == null) {
-            addFieldError(fieldName, object);
-        } else if (value.getClass().isArray() && Array.getLength(value) == 0) {
-            addFieldError(fieldName, object);
-        } else if (Collection.class.isAssignableFrom(value.getClass()) && 
((Collection) value).isEmpty()) {
+        if (isMissing(value)) {
             addFieldError(fieldName, object);
         }
     }
+
+    /**
+     * @return whether this validator rejects the value: null, an empty array 
or an empty collection
+     * @since 7.4.0
+     */
+    public boolean isMissing(Object value) {
+        if (value == null) {
+            return true;
+        }
+        if (value.getClass().isArray()) {
+            return Array.getLength(value) == 0;
+        }
+        return value instanceof Collection<?> collection && 
collection.isEmpty();
+    }
 }
diff --git 
a/core/src/test/java/org/apache/struts2/components/ConstraintAction.java 
b/core/src/test/java/org/apache/struts2/components/ConstraintAction.java
index 1d0bed30a..1f26af95f 100644
--- a/core/src/test/java/org/apache/struts2/components/ConstraintAction.java
+++ b/core/src/test/java/org/apache/struts2/components/ConstraintAction.java
@@ -32,6 +32,7 @@ public class ConstraintAction extends ActionSupport {
     private ConstraintUser contact = new ConstraintUser();
     private String code;
     private String choice;
+    private int priority;
     private Object attachment;
 
     public String getUsername() {
@@ -88,6 +89,15 @@ public class ConstraintAction extends ActionSupport {
         this.choice = choice;
     }
 
+    public int getPriority() {
+        return priority;
+    }
+
+    @StrutsParameter
+    public void setPriority(int priority) {
+        this.priority = priority;
+    }
+
     public Object getAttachment() {
         return attachment;
     }
diff --git 
a/core/src/test/java/org/apache/struts2/components/ConstraintAttributesTest.java
 
b/core/src/test/java/org/apache/struts2/components/ConstraintAttributesTest.java
index 6b9313bc8..2203f1256 100644
--- 
a/core/src/test/java/org/apache/struts2/components/ConstraintAttributesTest.java
+++ 
b/core/src/test/java/org/apache/struts2/components/ConstraintAttributesTest.java
@@ -145,7 +145,7 @@ public class ConstraintAttributesTest extends 
AbstractUITagTest {
 
         declaredMaxlength = "5";
         TextFieldTag field = startField(null);
-        ((UIBean) field.getComponent()).setHtmlConstraintProvider((validators, 
control, derivedFrom) ->
+        ((UIBean) field.getComponent()).setHtmlConstraintProvider((validators, 
control, derivedFrom, value) ->
             new java.util.LinkedHashMap<>(Map.of("Type", "email", "Maxlength", 
"9", "required", "required")));
         Map<String, Object> attributes = ((UIBean) 
field.getComponent()).getAttributes();
 
@@ -332,7 +332,7 @@ public class ConstraintAttributesTest extends 
AbstractUITagTest {
 
         TextFieldTag field = startField(null);
         List<Object> captured = new ArrayList<>();
-        ((UIBean) field.getComponent()).setHtmlConstraintProvider((validators, 
control, derivedFrom) -> {
+        ((UIBean) field.getComponent()).setHtmlConstraintProvider((validators, 
control, derivedFrom, value) -> {
             captured.add(derivedFrom);
             captured.add(validators.get(0).getMessage(derivedFrom));
             return Collections.emptyMap();
@@ -377,7 +377,7 @@ public class ConstraintAttributesTest extends 
AbstractUITagTest {
         TextFieldTag field = startField(null);
         List<Object> captured = new ArrayList<>();
         // not named `action`: that would shadow the inherited field this test 
asserts against
-        ((UIBean) field.getComponent()).setHtmlConstraintProvider((validators, 
control, derivedFrom) -> {
+        ((UIBean) field.getComponent()).setHtmlConstraintProvider((validators, 
control, derivedFrom, value) -> {
             captured.add(derivedFrom);
             return Collections.emptyMap();
         });
@@ -405,7 +405,7 @@ public class ConstraintAttributesTest extends 
AbstractUITagTest {
         initDispatcherWith("true");
 
         TextFieldTag field = startField(null);
-        ((UIBean) field.getComponent()).setHtmlConstraintProvider((validators, 
control, derivedFrom) -> {
+        ((UIBean) field.getComponent()).setHtmlConstraintProvider((validators, 
control, derivedFrom, value) -> {
             stack.push(new Object());
             throw new IllegalStateException("message resolution failed 
midway");
         });
diff --git 
a/core/src/test/java/org/apache/struts2/components/StrutsHtmlConstraintProviderTest.java
 
b/core/src/test/java/org/apache/struts2/components/StrutsHtmlConstraintProviderTest.java
index 23ad65374..b4e98fe24 100644
--- 
a/core/src/test/java/org/apache/struts2/components/StrutsHtmlConstraintProviderTest.java
+++ 
b/core/src/test/java/org/apache/struts2/components/StrutsHtmlConstraintProviderTest.java
@@ -55,7 +55,11 @@ public class StrutsHtmlConstraintProviderTest {
     }
 
     private Map<String, String> constraints(Validator validator, 
HtmlControlType control) {
-        return provider.constraintsFor(singletonList(validator), control, 
null);
+        return constraints(validator, control, null);
+    }
+
+    private Map<String, String> constraints(Validator validator, 
HtmlControlType control, Object value) {
+        return provider.constraintsFor(singletonList(validator), control, 
null, value);
     }
 
     @Test
@@ -104,6 +108,33 @@ public class StrutsHtmlConstraintProviderTest {
             .containsEntry("required", "required");
     }
 
+    @Test
+    public void 
requiredFieldEmitsNothingOnRadioWhenTheBoundValueIsAlreadySet() {
+        // a primitive int renders as 0, which is not in the list, so no radio 
is checked while the
+        // server, seeing a non-null Integer, would accept the empty submit
+        assertThat(constraints(new RequiredFieldValidator(), 
HtmlControlType.RADIO, 0)).isEmpty();
+    }
+
+    @Test
+    public void requiredFieldEmitsNothingOnFileWhenTheBoundValueIsAlreadySet() 
{
+        // prepare() populating the file property from an existing entity is 
the ordinary edit flow
+        assertThat(constraints(new RequiredFieldValidator(), 
HtmlControlType.FILE, "existing.pdf")).isEmpty();
+    }
+
+    @Test
+    public void 
requiredFieldEmitsRequiredOnRadioWhenTheBoundValueIsAnEmptyArray() {
+        // RequiredFieldValidator fails an empty array, so the sides agree
+        assertThat(constraints(new RequiredFieldValidator(), 
HtmlControlType.RADIO, new String[0]))
+            .containsEntry("required", "required");
+    }
+
+    @Test
+    public void requiredStringIgnoresTheBoundValue() {
+        // requiredstring judges the submitted value, which a text input 
always sends
+        assertThat(constraints(new RequiredStringValidator(), 
HtmlControlType.TEXT, "prefilled"))
+            .containsEntry("required", "required");
+    }
+
     @Test
     public void stringLengthEmitsLengthsWhenNotTrimming() {
         StringLengthFieldValidator validator = new 
StringLengthFieldValidator();
@@ -172,7 +203,7 @@ public class StrutsHtmlConstraintProviderTest {
         regex.setCaseSensitive(true);
         regex.setTrim(false);
 
-        assertThat(provider.constraintsFor(List.of(new 
RequiredStringValidator(), regex), HtmlControlType.TEXT, null))
+        assertThat(provider.constraintsFor(List.of(new 
RequiredStringValidator(), regex), HtmlControlType.TEXT, null, null))
             .containsEntry("pattern", "[a-z]+");
     }
 
@@ -183,7 +214,7 @@ public class StrutsHtmlConstraintProviderTest {
         regex.setCaseSensitive(true);
         regex.setTrim(false);
 
-        assertThat(provider.constraintsFor(List.of(regex, new 
RequiredStringValidator()), HtmlControlType.TEXT, null))
+        assertThat(provider.constraintsFor(List.of(regex, new 
RequiredStringValidator()), HtmlControlType.TEXT, null, null))
             .containsEntry("pattern", "[a-z]+");
     }
 
@@ -198,7 +229,7 @@ public class StrutsHtmlConstraintProviderTest {
         RequiredStringValidator required = new RequiredStringValidator();
         required.setTrim(false);
 
-        assertThat(provider.constraintsFor(List.of(required, regex), 
HtmlControlType.TEXT, null))
+        assertThat(provider.constraintsFor(List.of(required, regex), 
HtmlControlType.TEXT, null, null))
             .containsEntry("pattern", "(?:[a-z]+)|[\\x00-\\x20]*");
     }
 
@@ -336,8 +367,8 @@ public class StrutsHtmlConstraintProviderTest {
 
     @Test
     public void emptyInputIsHandled() {
-        assertThat(provider.constraintsFor(null, HtmlControlType.TEXT, 
null)).isEmpty();
-        assertThat(provider.constraintsFor(List.of(), HtmlControlType.TEXT, 
null)).isEmpty();
+        assertThat(provider.constraintsFor(null, HtmlControlType.TEXT, null, 
null)).isEmpty();
+        assertThat(provider.constraintsFor(List.of(), HtmlControlType.TEXT, 
null, null)).isEmpty();
     }
 
     @Test
@@ -349,7 +380,7 @@ public class StrutsHtmlConstraintProviderTest {
         when(validator.getMessage(action)).thenReturn("required");
 
         Map<String, String> result =
-            provider.constraintsFor(singletonList(validator), 
HtmlControlType.UNSUPPORTED, action);
+            provider.constraintsFor(singletonList(validator), 
HtmlControlType.UNSUPPORTED, action, null);
 
         assertThat(result).isEmpty();
     }
@@ -362,7 +393,7 @@ public class StrutsHtmlConstraintProviderTest {
         when(validator.getMessage(action)).thenReturn("nope");
 
         Map<String, String> result =
-            provider.constraintsFor(singletonList(validator), 
HtmlControlType.TEXT, action);
+            provider.constraintsFor(singletonList(validator), 
HtmlControlType.TEXT, action, null);
 
         assertThat(result).isEmpty();
     }
@@ -375,7 +406,7 @@ public class StrutsHtmlConstraintProviderTest {
         when(validator.getMessage(action)).thenReturn("needed");
 
         Map<String, String> result =
-            provider.constraintsFor(singletonList(validator), 
HtmlControlType.TEXT, action);
+            provider.constraintsFor(singletonList(validator), 
HtmlControlType.TEXT, action, null);
 
         assertThat(result).containsEntry("data-msg-acme.required", "needed");
     }
@@ -423,7 +454,7 @@ public class StrutsHtmlConstraintProviderTest {
         when(validator.getMessage(action)).thenReturn("not an email");
 
         Map<String, String> result =
-            provider.constraintsFor(singletonList(validator), 
HtmlControlType.TEXT, action);
+            provider.constraintsFor(singletonList(validator), 
HtmlControlType.TEXT, action, null);
 
         assertThat(result).containsEntry("data-msg-email", "not an email");
     }
diff --git 
a/core/src/test/java/org/apache/struts2/validator/validators/RequiredFieldValidatorTest.java
 
b/core/src/test/java/org/apache/struts2/validator/validators/RequiredFieldValidatorTest.java
index 26c6fef99..6d9c7c338 100644
--- 
a/core/src/test/java/org/apache/struts2/validator/validators/RequiredFieldValidatorTest.java
+++ 
b/core/src/test/java/org/apache/struts2/validator/validators/RequiredFieldValidatorTest.java
@@ -25,6 +25,7 @@ import org.apache.struts2.StrutsInternalTestCase;
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.List;
 
 public class RequiredFieldValidatorTest extends StrutsInternalTestCase {
 
@@ -93,4 +94,20 @@ public class RequiredFieldValidatorTest extends 
StrutsInternalTestCase {
         assertEquals("shorts field is required!", 
context.getFieldErrors().get("shorts").get(0));
     }
 
+    @Test
+    public void testIsMissingMatchesWhatValidateRejects() {
+        // StrutsHtmlConstraintProvider asks this predicate whether the 
browser's required would
+        // agree with the server on a field's current value
+        RequiredFieldValidator rfv = new RequiredFieldValidator();
+
+        assertTrue(rfv.isMissing(null));
+        assertTrue(rfv.isMissing(new Integer[]{}));
+        assertTrue(rfv.isMissing(new ArrayList<Short>()));
+
+        assertFalse(rfv.isMissing(0));
+        assertFalse(rfv.isMissing(""));
+        assertFalse(rfv.isMissing(new Integer[]{1}));
+        assertFalse(rfv.isMissing(List.of("a")));
+    }
+
 }
\ No newline at end of file
diff --git 
a/core/src/test/java/org/apache/struts2/views/jsp/ui/Html5ConstraintRenderingTest.java
 
b/core/src/test/java/org/apache/struts2/views/jsp/ui/Html5ConstraintRenderingTest.java
index bc85f1926..84fb7e6f7 100644
--- 
a/core/src/test/java/org/apache/struts2/views/jsp/ui/Html5ConstraintRenderingTest.java
+++ 
b/core/src/test/java/org/apache/struts2/views/jsp/ui/Html5ConstraintRenderingTest.java
@@ -18,16 +18,30 @@
  */
 package org.apache.struts2.views.jsp.ui;
 
+import ognl.NoSuchPropertyException;
+import org.apache.struts2.StrutsException;
+import org.apache.struts2.action.Action;
 import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.components.ConstraintAction;
 import org.apache.struts2.TestConfigurationProvider;
 import org.apache.struts2.mock.MockActionProxy;
 import org.apache.struts2.views.jsp.AbstractUITagTest;
 
 import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 public class Html5ConstraintRenderingTest extends AbstractUITagTest {
 
+    /**
+     * Push the action whose validators run, so a field's bound value is the 
one the provider sees.
+     */
+    @Override
+    public Action getAction() {
+        return new ConstraintAction();
+    }
+
     public void testRendersConstraintAttributes() throws Exception {
         String output = render("true");
 
@@ -113,6 +127,22 @@ public class Html5ConstraintRenderingTest extends 
AbstractUITagTest {
             2, output.split("required=\"required\"", -1).length - 1);
     }
 
+    /**
+     * The bound int renders as 0, which is not in the list, so no radio is 
checked; the server would
+     * still accept the empty submit because 0 is a non-null Integer.
+     */
+    public void testRendersNoRequiredOnARadioBackedByAPrimitive() throws 
Exception {
+        String output = renderTag("true", () -> {
+            RadioTag radio = new RadioTag();
+            radio.setName("priority");
+            radio.setList("{1,2,3}");
+            return radio;
+        });
+
+        assertTrue("expected the radios in: " + output, 
output.contains("name=\"priority\""));
+        assertFalse("expected no required in: " + output, 
output.contains("required=\"required\""));
+    }
+
     public void testRendersRequiredOnAFileInput() throws Exception {
         String output = renderTag("true", () -> {
             FileTag file = new FileTag();
@@ -124,6 +154,48 @@ public class Html5ConstraintRenderingTest extends 
AbstractUITagTest {
             output.contains("type=\"file\" name=\"attachment\"") && 
output.contains("required=\"required\""));
     }
 
+    /**
+     * The edit flow: prepare() loaded the existing attachment, so an empty 
submit keeps it and the
+     * server accepts; the browser must not insist on a new file.
+     */
+    public void 
testRendersNoRequiredOnAFileInputWhoseAttachmentIsAlreadyLoaded() throws 
Exception {
+        String output = renderTag("true", action -> action.setAttachment(new 
Object()), () -> {
+            FileTag file = new FileTag();
+            file.setName("attachment");
+            return file;
+        });
+
+        assertTrue("expected the file input in: " + output, 
output.contains("type=\"file\" name=\"attachment\""));
+        assertFalse("expected no required in: " + output, 
output.contains("required=\"required\""));
+    }
+
+    /**
+     * An UploadedFilesAware action receives the part by name and has no 
property behind the input;
+     * with struts.el.throwExceptionOnFailure the missing property must not 
turn into a 500.
+     */
+    public void testRendersAFileInputBoundToNoPropertyWhenElFailuresThrow() 
throws Exception {
+        String output = renderTag("true", 
Map.of(StrutsConstants.STRUTS_EL_THROW_EXCEPTION, "true"), action -> { }, () -> 
{
+            FileTag file = new FileTag();
+            file.setName("upload");
+            return file;
+        });
+
+        assertTrue("expected the file input in: " + output, 
output.contains("type=\"file\" name=\"upload\""));
+    }
+
+    public void 
testAFileInputStillSurfacesABrokenExpressionWhenElFailuresThrow() throws 
Exception {
+        try {
+            renderTag("true", 
Map.of(StrutsConstants.STRUTS_EL_THROW_EXCEPTION, "true"), action -> { }, () -> 
{
+                FileTag file = new FileTag();
+                file.setName("upload[");
+                return file;
+            });
+            fail("expected the broken expression to throw");
+        } catch (StrutsException e) {
+            assertFalse("only a missing property is tolerated", e.getCause() 
instanceof NoSuchPropertyException);
+        }
+    }
+
     /**
      * combobox.ftl reaches constraints.ftl through html5/text.ftl, so the 
text half of the control
      * already carries constraints; this pins that against a template rewrite.
@@ -167,11 +239,23 @@ public class Html5ConstraintRenderingTest extends 
AbstractUITagTest {
     }
 
     private String renderTag(String constraintsEnabled, 
Supplier<AbstractUITag> tagFactory) throws Exception {
+        return renderTag(constraintsEnabled, action -> { }, tagFactory);
+    }
+
+    private String renderTag(String constraintsEnabled, 
Consumer<ConstraintAction> prepare,
+                             Supplier<AbstractUITag> tagFactory) throws 
Exception {
+        return renderTag(constraintsEnabled, Map.of(), prepare, tagFactory);
+    }
+
+    private String renderTag(String constraintsEnabled, Map<String, String> 
extraConstants,
+                             Consumer<ConstraintAction> prepare, 
Supplier<AbstractUITag> tagFactory) throws Exception {
         initDispatcher(new HashMap<String, String>() {{
             put("configProviders", TestConfigurationProvider.class.getName());
             put(StrutsConstants.STRUTS_UI_HTML5_CONSTRAINTS, 
constraintsEnabled);
+            putAll(extraConstants);
         }});
         createMocks();
+        prepare.accept((ConstraintAction) action);
         ((MockActionProxy) 
actionProxy).setConfig(configuration.getRuntimeConfiguration().getActionConfig("",
 "constraintAction"));
 
         FormTag form = new FormTag();
diff --git 
a/core/src/test/resources/org/apache/struts2/components/ConstraintAction-validation.xml
 
b/core/src/test/resources/org/apache/struts2/components/ConstraintAction-validation.xml
index d91997de3..1eb3dd111 100644
--- 
a/core/src/test/resources/org/apache/struts2/components/ConstraintAction-validation.xml
+++ 
b/core/src/test/resources/org/apache/struts2/components/ConstraintAction-validation.xml
@@ -53,6 +53,11 @@
             <message>pick one</message>
         </field-validator>
     </field>
+    <field name="priority">
+        <field-validator type="required">
+            <message>pick a priority</message>
+        </field-validator>
+    </field>
     <field name="attachment">
         <field-validator type="required">
             <message>attach a file</message>

Reply via email to