10

Is it possible to access a field value, where field name is described in annotation which annotate another field in class.

For example:

@Entity
public class User {

    @NotBlank
    private String password;

    @Match(field = "password")
    private String passwordConfirmation;
}

Annotation:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface Match {

    String message() default "{}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String field();
}

Now, is it possible to access field password from class User in ConstraintValidator implementaion class?


Edit:

I wrote something like this:

public class MatchValidator implements ConstraintValidator<Match, Object> {

private String mainField;
private String secondField;
private Class clazz;

@Override
public void initialize(final Match match) {
    clazz = User.class;

    final Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
        if (field.isAnnotationPresent(Match.class)) {
            mainField = field.getName();
        }
    }

    secondField = match.field();
}

@Override
public boolean isValid(final Object value, final ConstraintValidatorContext constraintValidatorContext) {
    try {
        Object o; //Now how to get the User entity instance?

        final Object firstObj = BeanUtils.getProperty(o, mainField);
        final Object secondObj = BeanUtils.getProperty(o, secondField);

        return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
    } catch (final Exception ignore) {
        ignore.printStackTrace();
    }
    return true;
}
}

Now the question is how can I get the User object instance and compare fields values?

kryski
  • 414
  • 2
  • 6
  • 18
  • Possible duplicate of [Is it possible to read the value of a annotation in java?](http://stackoverflow.com/q/4296910/2078908) – ursa Jun 11 '16 at 23:48
  • @ursa Thanks for hint, helped me a lot. But now I'm stuck on another issue, see edit. – kryski Jun 13 '16 at 12:26
  • @zach Don't you have the object already? The parameter `value` of the `isValid` method. – Jesper Jun 13 '16 at 12:28
  • @Jesper The `value` parameter is the value of `private String passwordConfirmation` property. – kryski Jun 13 '16 at 12:40
  • Ah yes, I see. I don't think there is a way to get the `User` object that contains the property from inside the `isValid` method of your validator. – Jesper Jun 13 '16 at 12:46
  • Is there another way to achieve this goal? – kryski Jun 13 '16 at 12:59

3 Answers3

12

@Hardy Thenks for tip. Finally wrote some code which matches (more or less) expected result.

I'll paste it here, maybe will help someone to solve his problem.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Match {

    String field();

    String message() default "";
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MatchValidator.class)
@Documented
public @interface EnableMatchConstraint {

    String message() default "Fields must match!";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

public class MatchValidator implements  ConstraintValidator<EnableMatchConstraint, Object> {

    @Override
    public void initialize(final EnableMatchConstraint constraint) {}

    @Override
    public boolean isValid(final Object o, final ConstraintValidatorContext context) {
        boolean result = true;
        try {
            String mainField, secondField, message;
            Object firstObj, secondObj;

            final Class<?> clazz = o.getClass();
            final Field[] fields = clazz.getDeclaredFields();

            for (Field field : fields) {
                if (field.isAnnotationPresent(Match.class)) {
                    mainField = field.getName();
                    secondField = field.getAnnotation(Match.class).field();
                    message = field.getAnnotation(Match.class).message();

                    if (message == null || "".equals(message))
                        message = "Fields " + mainField + " and " + secondField + " must match!";

                    firstObj = BeanUtils.getProperty(o, mainField);
                    secondObj = BeanUtils.getProperty(o, secondField);

                    result = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
                    if (!result) {
                        context.disableDefaultConstraintViolation();
                        context.buildConstraintViolationWithTemplate(message).addPropertyNode(mainField).addConstraintViolation();
                        break;
                    }
                }
            }
        } catch (final Exception e) {
            // ignore
            //e.printStackTrace();
        }
        return result;
    }
}

And how to use it...? Like this:

@Entity
@EnableMatchConstraint
public class User {

    @NotBlank
    private String password;

    @Match(field = "password")
    private String passwordConfirmation;
}
kryski
  • 414
  • 2
  • 6
  • 18
4

You either need to write a class level constraint in which you get the full User instance passed into the isValid call or you can use something like @ScriptAssert.

At the moment it is not possible to access the root bean instance as part of a "normal" field validation. There is a BVAL issue - BVAL-237 - which discusses to add this functionality, but so far it is not yet part of the Bean Validation specification.

Note, there are good reasons why the root bean is not accessible atm. Constraints which rely on the root bean being accessible will fail for the validateValue case.

Hardy
  • 18,659
  • 3
  • 49
  • 65
  • You said that I need to write class level constraint, is there possibility to use the `@Entity` class level annotation instead of writing new annotation? – kryski Jun 13 '16 at 13:25
  • 1
    Not sure what you mean with using @Entity, but the answer is no. You need to write your own customer class level constraint. – Hardy Jun 13 '16 at 15:12
1

Hack around... Hack because these internal Hibernate implementations won't work when they migrate to Java modules.

@Override
public boolean isValid(final Serializable value, final ConstraintValidatorContext context) {
    var context = (ConstraintValidatorContextImpl) context;
    //no-inspection unchecked
    var descriptor = (ConstraintDescriptorImpl<Exists>) context.getConstraintDescriptor();
    var existsAnnotation = descriptor.getAnnotationDescriptor().getAnnotation();
    // Removed b/c irrelevant
}
Nikolas Charalambidis
  • 40,893
  • 16
  • 117
  • 183
Salathiel Genese
  • 1,639
  • 2
  • 21
  • 37