A little greedy question here, hope this one could also help others who want to know more about annotation validation
I am currently studying Spring, and for now, I
Consider adding compile-time validation for the field names. For example, in @Arne answer the strings "birthday" and "confirmBirthday" are not guaranteed to match actual field names at compile time. If you want to add that functionality, here's an example from my code for a slightly different example that assumes there are exactly two fields. The purpose is to assert that two fields are ordered... For example, it could be used for "beginDate" and "endDate".
public class OrderedValidator extends AbstractProcessor implements ConstraintValidator<Ordered, Object>
{
private String field1;
private String field2;
private Messager messager;
public void initialize(Ordered constraintAnnotation)
{
this.field1 = constraintAnnotation.field1();
this.field2 = constraintAnnotation.field2();
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv)
{
super.init(processingEnv);
messager = processingEnv.getMessager();
}
@SuppressWarnings("unchecked")
public boolean isValid(Object value, ConstraintValidatorContext context)
{
Object field1Value = new BeanWrapperImpl(value).getPropertyValue(field1);
Object field2Value = new BeanWrapperImpl(value).getPropertyValue(field2);
boolean valid = true;
if (field1Value != null && field2Value != null)
{
if (field1Value.getClass().equals(field2Value.getClass()))
{
valid = ((Comparable) field1Value).compareTo((Comparable) field2Value) <= 0;
}
}
return valid;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Ordered.class))
{
if (annotatedElement.getKind() != ElementKind.CLASS)
{
messager.printMessage(Diagnostic.Kind.ERROR, "Only classes can be annotated with " + Ordered.class.getSimpleName());
return true;
}
TypeElement typeElement = (TypeElement) annotatedElement;
List<? extends Element> elements = typeElement.getEnclosedElements();
boolean field1Found = false;
boolean field2Found = false;
for (Element e : elements)
{
if (e.getKind() == ElementKind.FIELD && field1 != null && field1.equals(e.getSimpleName()))
{
field1Found = true;
}
else if (e.getKind() == ElementKind.FIELD && field2 != null && field2.equals(e.getSimpleName()))
{
field2Found = true;
}
}
if (field1 != null && !field1Found)
{
messager.printMessage(Diagnostic.Kind.ERROR, "Could not find field named " + field1);
return true;
}
if (field2 != null && !field2Found)
{
messager.printMessage(Diagnostic.Kind.ERROR, "Could not find field named " + field2);
return true;
}
}
return false;
}
}
For this you can use a type level annotation only because a field level annotation has no access to other fields!
I did something similar to allow a choice validation (exactly one of a number of properties has to be not null). In your case the @AllOrNone
annotation (or whatever name you prefer) would need an array of field names and you will get the whole object of the annotated type to the validator:
@Target(ElementType.TYPE)
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = AllOrNoneValidator.class)
public @interface AllOrNone {
String[] value();
String message() default "{AllOrNone.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class AllOrNoneValidator implements ConstraintValidator<AllOrNone, Object> {
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private String[] fields;
@Override
public void initialize(AllOrNone constraintAnnotation) {
fields = constraintAnnotation.value();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
long notNull = Stream.of(fields)
.map(field -> PARSER.parseExpression(field).getValue(value))
.filter(Objects::nonNull)
.count();
return notNull == 0 || notNull == fields.length;
}
}
(As you said you use Spring I used SpEL to allow even nested fields access)
Now you can annotate your Subscriber
type:
@AllOrNone({"birthday", "confirmBirthday"})
public class Subscriber {
private String name;
private String email;
private Integer age;
private String phone;
private Gender gender;
private Date birthday;
private Date confirmBirthday;
private String birthdayMessage;
private Boolean receiveNewsletter;
}