A common case is to validate date inputs in Java e.g. to avoid the nice and friendly typos like 06/01/1500.
Nevertheless by default where is not to much support to handle this very common case. So let’s fix this together:
Annotation first
The first we need is a JSR annotation which we use to annotate the code. We provide from the start proper default values, to avoid none sense data.
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Documented // Note: We use here already a validator which we will add in a sec too @Constraint(validatedBy = InDateRangeValidator.class) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface InDateRange { // used to get later in the resource bundle the i18n text String message() default "{validation.date.InDateRange.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; // min value, we for now just a string String min() default "1900-01-01"; // max date value we support String max() default "2999-12-31"; }
Validator next
As we have the annotation now, we still need to add some code behind for the JSR bean validation to make it really do something. I assume that both classes are in the same package.
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class InDateRangeValidator implements ConstraintValidator<InDateRange, java.util.Date> { private final SimpleDateFormat dateParser = new SimpleDateFormat("yyyy-MM-dd"); private InDateRange constraintAnnotation; @Override public void initialize(InDateRange constraintAnnotation) { this.constraintAnnotation = constraintAnnotation; } @Override public boolean isValid(java.util.Date value, ConstraintValidatorContext context) { try { final Date min = dateParser.parse(constraintAnnotation.min()); final Date max = dateParser.parse(constraintAnnotation.max()); return value == null || (value.after(min) && value.before(max)); } catch (ParseException ex) { throw new RuntimeException(ex); } } }
The validator is not very smart now, but it will do the job for. Let’s use it first, will we?
Apply our date validation
class SomeBean { @InDateRange private Date date; }
Well not to surprising but the interesting part is of course always the test case we should add to verify all is good and working as we like it.
import java.util.Date; import java.util.Set; import org.junit.Test; import static org.junit.Assert.*; import org.junit.BeforeClass; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import org.joda.time.LocalDate; public class TestInDateRange { private static Validator validator; @BeforeClass public static void setUp() { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); validator = factory.getValidator(); } @Test public void testDateRange() { SomeBean test = new SomeBean(); test.date = new LocalDate(10001, 01, 01).toDate(); Set<ConstraintViolation<TestClass>> validate = validator.validate(test); System.out.println(validate); assertEquals(1, validate.size()); assertEquals("date", validate.iterator().next().getPropertyPath().toString()); } }
i18n File
Assuming you use a default maven project we should add in /src/main/resources
the file ValidationMessages.properties
if it not already exists:
validation.date.InDateRange.message=date must be between {min} and {max}.
min
and max
, which automatically add the values from our annotation into the user message.Going further
- Well as you noticed right now we parse the min and max date each time. We could of course just do this once as it is quite static
- We use currently
java.util.Date
well again something we could change - Last but not least we should try to avoid
Date
in the API / Model objects and try to just use simply UTCLong
s
SimpleDateFormat is not thread safe and using it in multiple threads (as you may well do if it is static field) is a no go 🙂
Thanks for pointing this out! I adjusted the example.