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>