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
commit ee38d3ae4f969735bc385095838d7630708b4966 Author: Lukasz Lenart <[email protected]> AuthorDate: Sun Oct 25 09:45:49 2026 +0100 WW-5816 feat(validation): add a range validator for java.time.LocalDate DateRangeFieldValidator compares against java.util.Date bounds, so on a LocalDate property it throws ClassCastException. LocalDateRangeFieldValidator is a RangeValidatorSupport<LocalDate>, registered as "localdate", with ISO min/max bounds (or minExpression/maxExpression), both inclusive. An XML min such as "2026-01-01" reaches the generic setMin(T) as a LocalDate through the usual parameter conversion, which a test pins. @LocalDateRangeFieldValidator mirrors @DateRangeFieldValidator without dateFormat, since the bounds are ISO only. It is processed on a method and inside @Validations via the new localDateRangeFields() member. The builder parses the bounds with LocalDate.parse, so a malformed bound fails when the validators are built instead of passing through as a string. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../AnnotationValidationConfigurationBuilder.java | 43 +++++ .../annotations/LocalDateRangeFieldValidator.java | 181 +++++++++++++++++++++ .../struts2/validator/annotations/Validations.java | 7 + .../validators/LocalDateRangeFieldValidator.java | 75 +++++++++ .../struts2/validator/validators/default.xml | 1 + .../validator/AnnotationLocalDateRangeAction.java | 40 +++++ .../AnnotationMalformedLocalDateRangeAction.java | 32 ++++ ...notationValidationConfigurationBuilderTest.java | 36 ++++ .../LocalDateRangeFieldValidatorTest.java | 154 ++++++++++++++++++ 9 files changed, 569 insertions(+) diff --git a/core/src/main/java/org/apache/struts2/validator/AnnotationValidationConfigurationBuilder.java b/core/src/main/java/org/apache/struts2/validator/AnnotationValidationConfigurationBuilder.java index 7c9650a05..ac0844666 100644 --- a/core/src/main/java/org/apache/struts2/validator/AnnotationValidationConfigurationBuilder.java +++ b/core/src/main/java/org/apache/struts2/validator/AnnotationValidationConfigurationBuilder.java @@ -29,6 +29,7 @@ import org.apache.struts2.validator.annotations.EmailValidator; import org.apache.struts2.validator.annotations.ExpressionValidator; import org.apache.struts2.validator.annotations.FieldExpressionValidator; import org.apache.struts2.validator.annotations.IntRangeFieldValidator; +import org.apache.struts2.validator.annotations.LocalDateRangeFieldValidator; import org.apache.struts2.validator.annotations.LongRangeFieldValidator; import org.apache.struts2.validator.annotations.RegexFieldValidator; import org.apache.struts2.validator.annotations.RequiredFieldValidator; @@ -46,6 +47,7 @@ import java.lang.reflect.Method; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -125,6 +127,10 @@ public class AnnotationValidationConfigurationBuilder { } } + // Process LocalDateRangeFieldValidator + else if (a instanceof LocalDateRangeFieldValidator v) { + result.add(processLocalDateRangeFieldValidatorAnnotation(v, fieldName, methodName)); + } // Process EmailValidator else if (a instanceof EmailValidator v) { ValidatorConfig temp = processEmailValidatorAnnotation(v, fieldName, methodName); @@ -267,6 +273,9 @@ public class AnnotationValidationConfigurationBuilder { } } } + for (LocalDateRangeFieldValidator v : validations.localDateRangeFields()) { + result.add(processLocalDateRangeFieldValidatorAnnotation(v, fieldName, methodName)); + } EmailValidator[] emv = validations.emails(); if (emv != null) { for (EmailValidator v : emv) { @@ -891,6 +900,40 @@ public class AnnotationValidationConfigurationBuilder { .build(); } + private ValidatorConfig processLocalDateRangeFieldValidatorAnnotation(LocalDateRangeFieldValidator v, String fieldName, String methodName) { + String validatorType = "localdate"; + + Map<String, Object> params = new HashMap<>(); + + if (fieldName != null) { + params.put("fieldName", fieldName); + } else if (StringUtils.isNotEmpty(v.fieldName())) { + params.put("fieldName", v.fieldName()); + } + if (StringUtils.isNotEmpty(v.min())) { + params.put("min", LocalDate.parse(v.min())); + } + if (StringUtils.isNotEmpty(v.max())) { + params.put("max", LocalDate.parse(v.max())); + } + if (StringUtils.isNotEmpty(v.minExpression())) { + params.put("minExpression", v.minExpression()); + } + if (StringUtils.isNotEmpty(v.maxExpression())) { + params.put("maxExpression", v.maxExpression()); + } + + validatorFactory.lookupRegisteredValidatorType(validatorType); + return new ValidatorConfig.Builder(validatorType) + .addParams(params) + .addParam("methodName", methodName) + .shortCircuit(v.shortCircuit()) + .defaultMessage(v.message()) + .messageKey(v.key()) + .messageParams(v.messageParams()) + .build(); + } + private ValidatorConfig processDateRangeFieldValidatorAnnotation(DateRangeFieldValidator v, String fieldName, String methodName) { String validatorType = "date"; diff --git a/core/src/main/java/org/apache/struts2/validator/annotations/LocalDateRangeFieldValidator.java b/core/src/main/java/org/apache/struts2/validator/annotations/LocalDateRangeFieldValidator.java new file mode 100644 index 000000000..428ee7605 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/validator/annotations/LocalDateRangeFieldValidator.java @@ -0,0 +1,181 @@ +/* + * 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.validator.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * <!-- START SNIPPET: description --> + * This validator checks that a {@link java.time.LocalDate} field has a value within a specified range, both + * bounds inclusive. The min and max values are ISO dates (yyyy-MM-dd). + * <!-- END SNIPPET: description --> + * + * <p><u>Annotation usage:</u></p> + * + * <!-- START SNIPPET: usage --> + * <p>The annotation must be applied at method level.</p> + * <!-- END SNIPPET: usage --> + * + * <p><u>Annotation parameters:</u></p> + * + * <!-- START SNIPPET: parameters --> + * <table class='confluenceTable' summary=''> + * <tr> + * <th class='confluenceTh'> Parameter </th> + * <th class='confluenceTh'> Required </th> + * <th class='confluenceTh'> Default </th> + * <th class='confluenceTh'> Notes </th> + * </tr> + * <tr> + * <td class='confluenceTd'>message</td> + * <td class='confluenceTd'>yes</td> + * <td class='confluenceTd'> </td> + * <td class='confluenceTd'>field error message</td> + * </tr> + * <tr> + * <td class='confluenceTd'>key</td> + * <td class='confluenceTd'>no</td> + * <td class='confluenceTd'> </td> + * <td class='confluenceTd'>i18n key from language specific properties file.</td> + * </tr> + * <tr> + * <td class='confluenceTd'>messageParams</td> + * <td class='confluenceTd'>no</td> + * <td class='confluenceTd'> </td> + * <td class='confluenceTd'>Additional params to be used to customize message - will be evaluated against the Value Stack</td> + * </tr> + * <tr> + * <td class='confluenceTd'>fieldName</td> + * <td class='confluenceTd'>no</td> + * <td class='confluenceTd'> </td> + * <td class='confluenceTd'> </td> + * </tr> + * <tr> + * <td class='confluenceTd'>shortCircuit</td> + * <td class='confluenceTd'>no</td> + * <td class='confluenceTd'>false</td> + * <td class='confluenceTd'>If this validator should be used as shortCircuit.</td> + * </tr> + * <tr> + * <td class='confluenceTd'>type</td> + * <td class='confluenceTd'>yes</td> + * <td class='confluenceTd'>ValidatorType.FIELD</td> + * <td class='confluenceTd'>Enum value from ValidatorType. Either FIELD or SIMPLE can be used here.</td> + * </tr> + * <tr> + * <td class='confluenceTd'> min </td> + * <td class='confluenceTd'> no </td> + * <td class='confluenceTd'> </td> + * <td class='confluenceTd'> ISO date (yyyy-MM-dd). The minimum the date must be. </td> + * </tr> + * <tr> + * <td class='confluenceTd'>minExpression</td> + * <td class='confluenceTd'>no</td> + * <td class='confluenceTd'> </td> + * <td class='confluenceTd'>OGNL expression used to obtain the minimum the date must be.</td> + * </tr> + * <tr> + * <td class='confluenceTd'> max </td> + * <td class='confluenceTd'> no </td> + * <td class='confluenceTd'> </td> + * <td class='confluenceTd'> ISO date (yyyy-MM-dd). The maximum date can be. </td> + * </tr> + * <tr> + * <td class='confluenceTd'>maxExpression</td> + * <td class='confluenceTd'>no</td> + * <td class='confluenceTd'> </td> + * <td class='confluenceTd'>OGNL expression used to obtain the maximum date can be.</td> + * </tr> + * </table> + * + * <p>If neither <em>min</em> nor <em>max</em> is set, nothing will be done.</p> + * <!-- END SNIPPET: parameters --> + * + * <p><u>Example code:</u></p> + * + * <pre> + * <!-- START SNIPPET: example --> + * @LocalDateRangeFieldValidator(message = "Default message", key = "i18n.key", shortCircuit = true, min = "2005-01-01", max = "2005-12-31") + * <!-- END SNIPPET: example --> + * </pre> + * + * @since 7.5.0 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface LocalDateRangeFieldValidator { + + /** + * @return The minimum the date must be, as an ISO date (yyyy-MM-dd). + */ + String min() default ""; + + /** + * @return An expression which will be evaluated against the Value Stack to get the min value + */ + String minExpression() default ""; + + /** + * @return The maximum the date can be, as an ISO date (yyyy-MM-dd). + */ + String max() default ""; + + /** + * @return An expression which will be evaluated against the Value Stack to get the max value + */ + String maxExpression() default ""; + + /** + * @return The default error message for this validator. + * NOTE: It is required to set a message, if you are not using the message key for 18n lookup! + */ + String message() default ""; + + /** + * @return The message key to lookup for i18n. + */ + String key() default ""; + + /** + * @return Additional params to be used to customize message - will be evaluated against the Value Stack + */ + String[] messageParams() default {}; + + /** + * @return The optional fieldName for SIMPLE validator types. + */ + String fieldName() default ""; + + /** + * If this is activated, the validator will be used as short-circuit. + * + * Adds the short-circuit="true" attribute value if <tt>true</tt>. + * @return true if validator will be used as short-circuit. Default is false. + */ + boolean shortCircuit() default false; + + /** + * @return The validation type for this field/method. + */ + ValidatorType type() default ValidatorType.FIELD; + +} diff --git a/core/src/main/java/org/apache/struts2/validator/annotations/Validations.java b/core/src/main/java/org/apache/struts2/validator/annotations/Validations.java index aee0bfe85..7cfbb8627 100644 --- a/core/src/main/java/org/apache/struts2/validator/annotations/Validations.java +++ b/core/src/main/java/org/apache/struts2/validator/annotations/Validations.java @@ -67,6 +67,11 @@ import java.lang.annotation.Target; * <td class='confluenceTd'> Add list of DateRangeFieldValidators </td> * </tr> * <tr> + * <td class='confluenceTd'> localDateRangeFields </td> + * <td class='confluenceTd'> no </td> + * <td class='confluenceTd'> Add list of LocalDateRangeFieldValidators </td> + * </tr> + * <tr> * <td class='confluenceTd'> emails </td> * <td class='confluenceTd'> no </td> * <td class='confluenceTd'> Add list of EmailValidators </td> @@ -186,6 +191,8 @@ public @interface Validations { DateRangeFieldValidator[] dateRangeFields() default {}; + LocalDateRangeFieldValidator[] localDateRangeFields() default {}; + EmailValidator[] emails() default {}; CreditCardValidator[] creditCards() default {}; diff --git a/core/src/main/java/org/apache/struts2/validator/validators/LocalDateRangeFieldValidator.java b/core/src/main/java/org/apache/struts2/validator/validators/LocalDateRangeFieldValidator.java new file mode 100644 index 000000000..157cde259 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/validator/validators/LocalDateRangeFieldValidator.java @@ -0,0 +1,75 @@ +/* + * 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.validator.validators; + +import java.time.LocalDate; + +/** + * <!-- START SNIPPET: javadoc --> + * + * Field Validator that checks if a {@link LocalDate} is within a specific range, both bounds inclusive. + * Use {@link DateRangeFieldValidator} for a {@link java.util.Date} property. + * + * <!-- END SNIPPET: javadoc --> + * + * <!-- START SNIPPET: parameters --> + * <ul> + * <li>fieldName - The field name this validator is validating. Required if using Plain-Validator Syntax otherwise not required</li> + * <li>min - the min date as an ISO date (yyyy-MM-dd). If not specified will not be checked.</li> + * <li>max - the max date as an ISO date (yyyy-MM-dd). If not specified will not be checked.</li> + * <li>minExpression - expression to calculate the minimum value (if none is specified, it will not be checked) </li> + * <li>maxExpression - expression to calculate the maximum value (if none is specified, it will not be checked) </li> + * </ul> + * <!-- END SNIPPET: parameters --> + * + * <!-- START SNIPPET: parameters-warning --> + * Do not use ${minExpression} and ${maxExpression} as an expression as this will turn into infinitive loop! + * <!-- END SNIPPET: parameters-warning --> + * + * <pre> + * <!-- START SNIPPET: examples --> + * <validators> + * <field name="birthday"> + * <field-validator type="localdate"> + * <param name="min">1990-01-01</param> + * <param name="max">2000-01-01</param> + * <message>Birthday must be within ${min} and ${max}</message> + * </field-validator> + * </field> + * + * <field name="birthday"> + * <field-validator type="localdate"> + * <param name="minExpression">${minValue}</param> <!-- will be evaluated as: LocalDate getMinValue() --> + * <param name="maxExpression">${maxValue}</param> <!-- will be evaluated as: LocalDate getMaxValue() --> + * <message>Birthday must be within ${min} and ${max}</message> + * </field-validator> + * </field> + * </validators> + * <!-- END SNIPPET: examples --> + * </pre> + * + * @since 7.5.0 + */ +public final class LocalDateRangeFieldValidator extends RangeValidatorSupport<LocalDate> { + + public LocalDateRangeFieldValidator() { + super(LocalDate.class); + } + +} diff --git a/core/src/main/resources/org/apache/struts2/validator/validators/default.xml b/core/src/main/resources/org/apache/struts2/validator/validators/default.xml index a3e6f9365..bb3968da8 100644 --- a/core/src/main/resources/org/apache/struts2/validator/validators/default.xml +++ b/core/src/main/resources/org/apache/struts2/validator/validators/default.xml @@ -32,6 +32,7 @@ <validator name="short" class="org.apache.struts2.validator.validators.ShortRangeFieldValidator"/> <validator name="double" class="org.apache.struts2.validator.validators.DoubleRangeFieldValidator"/> <validator name="date" class="org.apache.struts2.validator.validators.DateRangeFieldValidator"/> + <validator name="localdate" class="org.apache.struts2.validator.validators.LocalDateRangeFieldValidator"/> <validator name="expression" class="org.apache.struts2.validator.validators.ExpressionValidator"/> <validator name="fieldexpression" class="org.apache.struts2.validator.validators.FieldExpressionValidator"/> <validator name="email" class="org.apache.struts2.validator.validators.EmailValidator"/> diff --git a/core/src/test/java/org/apache/struts2/validator/AnnotationLocalDateRangeAction.java b/core/src/test/java/org/apache/struts2/validator/AnnotationLocalDateRangeAction.java new file mode 100644 index 000000000..b0693b4b6 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/validator/AnnotationLocalDateRangeAction.java @@ -0,0 +1,40 @@ +/* + * 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.validator; + +import org.apache.struts2.ActionSupport; +import org.apache.struts2.validator.annotations.LocalDateRangeFieldValidator; +import org.apache.struts2.validator.annotations.Validations; + +public class AnnotationLocalDateRangeAction extends ActionSupport { + + @LocalDateRangeFieldValidator(fieldName = "birthday", min = "2026-01-01", max = "2026-12-31", + message = "Birthday must be between ${min} and ${max}") + @Validations( + localDateRangeFields = { + @LocalDateRangeFieldValidator(fieldName = "holiday", min = "2026-07-01", max = "2026-08-31", + message = "Holiday must be between ${min} and ${max}") + } + ) + @Override + public String execute() { + return SUCCESS; + } + +} diff --git a/core/src/test/java/org/apache/struts2/validator/AnnotationMalformedLocalDateRangeAction.java b/core/src/test/java/org/apache/struts2/validator/AnnotationMalformedLocalDateRangeAction.java new file mode 100644 index 000000000..717709612 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/validator/AnnotationMalformedLocalDateRangeAction.java @@ -0,0 +1,32 @@ +/* + * 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.validator; + +import org.apache.struts2.ActionSupport; +import org.apache.struts2.validator.annotations.LocalDateRangeFieldValidator; + +public class AnnotationMalformedLocalDateRangeAction extends ActionSupport { + + @LocalDateRangeFieldValidator(fieldName = "birthday", min = "01/01/2026", message = "Birthday is too early") + @Override + public String execute() { + return SUCCESS; + } + +} diff --git a/core/src/test/java/org/apache/struts2/validator/AnnotationValidationConfigurationBuilderTest.java b/core/src/test/java/org/apache/struts2/validator/AnnotationValidationConfigurationBuilderTest.java index 0e55dbb79..b7d5bcc27 100644 --- a/core/src/test/java/org/apache/struts2/validator/AnnotationValidationConfigurationBuilderTest.java +++ b/core/src/test/java/org/apache/struts2/validator/AnnotationValidationConfigurationBuilderTest.java @@ -18,6 +18,7 @@ */ package org.apache.struts2.validator; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.struts2.ActionContext; import org.apache.struts2.ActionInvocation; import org.apache.struts2.ActionSupport; @@ -36,6 +37,7 @@ import org.apache.struts2.validator.validators.ConditionalVisitorFieldValidator; import org.apache.struts2.validator.validators.ConversionErrorFieldValidator; import org.apache.struts2.validator.validators.CreditCardValidator; import org.apache.struts2.validator.validators.DateRangeFieldValidator; +import org.apache.struts2.validator.validators.LocalDateRangeFieldValidator; import org.apache.struts2.validator.validators.DoubleRangeFieldValidator; import org.apache.struts2.validator.validators.EmailValidator; import org.apache.struts2.validator.validators.ExpressionValidator; @@ -53,6 +55,8 @@ import org.apache.struts2.StrutsConstants; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -355,6 +359,38 @@ public class AnnotationValidationConfigurationBuilderTest extends XWorkTestCase assertEquals(2, shortValidators); } + public void testLocalDateRangeAnnotations() throws Exception { + AnnotationActionValidatorManager manager = createValidationManager(AnnotationLocalDateRangeAction.class, Locale.US); + + List<Validator> validators = manager.getValidators(AnnotationLocalDateRangeAction.class, null); + + assertEquals(2, validators.size()); + for (Validator validator : validators) { + assertEquals("localdate", validator.getValidatorType()); + LocalDateRangeFieldValidator localDateValidator = (LocalDateRangeFieldValidator) validator; + if ("birthday".equals(localDateValidator.getFieldName())) { + assertEquals(LocalDate.of(2026, 1, 1), localDateValidator.getMin()); + assertEquals(LocalDate.of(2026, 12, 31), localDateValidator.getMax()); + assertEquals("Birthday must be between ${min} and ${max}", localDateValidator.getDefaultMessage()); + } else { + assertEquals("holiday", localDateValidator.getFieldName()); + assertEquals(LocalDate.of(2026, 7, 1), localDateValidator.getMin()); + assertEquals(LocalDate.of(2026, 8, 31), localDateValidator.getMax()); + } + } + } + + public void testLocalDateRangeAnnotationRejectsANonIsoBound() throws Exception { + AnnotationActionValidatorManager manager = createValidationManager(AnnotationMalformedLocalDateRangeAction.class, Locale.US); + + try { + manager.getValidators(AnnotationMalformedLocalDateRangeAction.class, null); + fail("A non-ISO min must fail when the validators are built"); + } catch (Exception e) { + assertTrue(ExceptionUtils.getRootCause(e) instanceof DateTimeParseException); + } + } + private AnnotationActionValidatorManager createValidationManager(final Class<? extends ActionSupport> actionClass, Locale locale) throws Exception { loadConfigurationProviders(new ConfigurationProvider() { public void destroy() { diff --git a/core/src/test/java/org/apache/struts2/validator/validators/LocalDateRangeFieldValidatorTest.java b/core/src/test/java/org/apache/struts2/validator/validators/LocalDateRangeFieldValidatorTest.java new file mode 100644 index 000000000..b1d6eb495 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/validator/validators/LocalDateRangeFieldValidatorTest.java @@ -0,0 +1,154 @@ +/* + * 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.validator.validators; + +import org.apache.struts2.ActionContext; +import org.apache.struts2.ObjectFactory; +import org.apache.struts2.XWorkTestCase; +import org.apache.struts2.text.TextProviderFactory; +import org.apache.struts2.util.ValueStack; +import org.apache.struts2.util.ValueStackFactory; +import org.apache.struts2.validator.DummyValidatorContext; +import org.apache.struts2.validator.ValidatorContext; +import org.apache.struts2.validator.ValidatorFactory; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; + +public class LocalDateRangeFieldValidatorTest extends XWorkTestCase { + + private static final LocalDate MIN = LocalDate.of(2026, 1, 1); + private static final LocalDate MAX = LocalDate.of(2026, 12, 31); + + private TextProviderFactory tpf; + + public void testPassesInsideTheRange() { + assertEquals(0, validate(LocalDate.of(2026, 6, 15)).getFieldErrors().size()); + } + + public void testPassesOnTheMinBound() { + assertEquals(0, validate(MIN).getFieldErrors().size()); + } + + public void testPassesOnTheMaxBound() { + assertEquals(0, validate(MAX).getFieldErrors().size()); + } + + public void testFailsBelowTheMinBound() { + ValidatorContext context = validate(MIN.minusDays(1)); + + assertEquals("Birthday must be within 2026-01-01 and 2026-12-31", context.getFieldErrors().get("birthday").get(0)); + } + + public void testFailsAboveTheMaxBound() { + ValidatorContext context = validate(MAX.plusDays(1)); + + assertEquals("Birthday must be within 2026-01-01 and 2026-12-31", context.getFieldErrors().get("birthday").get(0)); + } + + public void testSkipsANullValue() { + assertEquals(0, validate(null).getFieldErrors().size()); + } + + public void testReadsTheBoundsFromExpressions() throws Exception { + LocalDateAction action = new LocalDateAction(MIN.minusDays(1)); + ValidatorContext context = new DummyValidatorContext(action, tpf); + LocalDateRangeFieldValidator validator = prepareValidator(action, context); + validator.setMinExpression("${minValue}"); + validator.setMaxExpression("${maxValue}"); + + validator.validate(action); + + assertEquals(1, context.getFieldErrors().get("birthday").size()); + } + + public void testIsRegisteredAsLocaldate() { + String className = container.getInstance(ValidatorFactory.class).lookupRegisteredValidatorType("localdate"); + + assertEquals(LocalDateRangeFieldValidator.class.getName(), className); + } + + public void testConvertsIsoBoundsFromConfigurationParams() throws Exception { + Map<String, Object> params = new HashMap<>(); + params.put("min", "2026-01-01"); + params.put("max", "2026-12-31"); + + LocalDateRangeFieldValidator validator = (LocalDateRangeFieldValidator) container.getInstance(ObjectFactory.class) + .buildValidator(LocalDateRangeFieldValidator.class.getName(), params, ActionContext.getContext().getContextMap()); + + assertEquals(MIN, validator.getMin()); + assertEquals(MAX, validator.getMax()); + } + + private ValidatorContext validate(LocalDate birthday) { + LocalDateAction action = new LocalDateAction(birthday); + ValidatorContext context = new DummyValidatorContext(action, tpf); + LocalDateRangeFieldValidator validator = prepareValidator(action, context); + validator.setMin(MIN); + validator.setMax(MAX); + + try { + validator.validate(action); + } catch (Exception e) { + throw new IllegalStateException(e); + } + return context; + } + + private LocalDateRangeFieldValidator prepareValidator(LocalDateAction action, ValidatorContext context) { + ValueStack valueStack = container.getInstance(ValueStackFactory.class).createValueStack(); + valueStack.push(action); + + LocalDateRangeFieldValidator validator = new LocalDateRangeFieldValidator(); + validator.setValueStack(valueStack); + validator.setValidatorContext(context); + validator.setFieldName("birthday"); + validator.setDefaultMessage("Birthday must be within ${min} and ${max}"); + return validator; + } + + @Override + public void setUp() throws Exception { + super.setUp(); + tpf = container.getInstance(TextProviderFactory.class); + } + + public static class LocalDateAction { + + private final LocalDate birthday; + + public LocalDateAction(LocalDate birthday) { + this.birthday = birthday; + } + + public LocalDate getBirthday() { + return birthday; + } + + public LocalDate getMinValue() { + return MIN; + } + + public LocalDate getMaxValue() { + return MAX; + } + } + +}
