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.Datewell again something we could change - Last but not least we should try to avoid
Datein the API / Model objects and try to just use simply UTCLongs
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.