0

I have created an example custom constraint to explore the hibernate bean validation implementation. The constraint itself is fairly simple; given a particular String and an enum, the validator uses regex to check whether the String matches a particular pattern(chosen via EmpType enum). I have the following:

EmployeeNumber.java

package com.lmig.beanvalidation.domain.customconstraints.employeenumber;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
//implementation of ConstraintValidator interface, i.e. the class that performs custom logic to validate the value
@Constraint(validatedBy = { EmployeeNumberValidator.class})
public @interface EmployeeNumber {

    //Validation message for failures
    String message() default "{EmployeeNumber.standard}";
    //allows "grouping"; can choose under what circumstances this will fire via interfaces
    Class<?>[] groups() default {};

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

    EmpType value() default EmpType.STANDARD;
}

EmpType.java

package com.lmig.beanvalidation.domain.customconstraints.employeenumber;

public enum EmpType {
    ADMIN,STANDARD;
}

EmployeeNumberValidator.java

package com.lmig.beanvalidation.domain.customconstraints.employeenumber;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.apache.commons.lang3.StringUtils;

/**
 * Backing validator class for EmployeeNumber bean validation annotation
 */
public class EmployeeNumberValidator implements ConstraintValidator<EmployeeNumber, String> {

    private static Pattern STANDARD_PATTERN = Pattern.compile("E\\d{6}");
    private static Pattern ADMIN_PATTERN = Pattern.compile("A\\d{6}");

    protected EmpType empType;

    /**
     * checks if emp number is not blank, and matches either standard or admin employee number format based on empType
     * 
     * @param empNum String to check for validity
     * @param constraintValidatorContext context for validation annotation
     * 
     * @return true if empNum matches empType specific regex, false if empNum is blank or does not match any regex
     */
    @Override
    public boolean isValid(final String empNum, final ConstraintValidatorContext constraintValidatorContext) {
        boolean isValid;

        if(StringUtils.isBlank(empNum)) {
            isValid = false;
        } else if(empType.equals(EmpType.STANDARD)) {
            isValid = isStandardNumberValid(empNum);
        } else {
            isValid = isAdminNumberValid(empNum);

            if(!isValid) {
                constraintValidatorContext.disableDefaultConstraintViolation();;
                constraintValidatorContext
                        .buildConstraintViolationWithTemplate("{EmployeeNumber.admin}")
                        .addConstraintViolation();
            }
        }
        return isValid;
    }

    /**
     * Compares empNum against Standard employee number pattern regex
     * @param empNum string to compare against standard pattern regex
     * 
     * @return true if match, false otherwise
     */
    private boolean isStandardNumberValid(final String empNum) {
        final Matcher matcher = STANDARD_PATTERN.matcher(empNum);

        return matcher.matches();
    }

    /**
     * Compares empNum against Admin employee number pattern regex
     * @param empNum string to compare against admin pattern regex
     *
     * @return true if match, false otherwise
     */
    private boolean isAdminNumberValid(final String empNum) {
        final Matcher matcher = ADMIN_PATTERN.matcher(empNum);

        return matcher.matches();
    }
}

Using the following custom messages:

ValidationMessages.properties

EmployeeNumber.standard=Standard employee Number must start with E, followed by 6 digits
EmployeeNumber.admin=Admin employee number must start with A, followed by 6 digits

And the following test class:

EmployeeNumberValidatorTest

package com.lmig.beanvalidation.domain.customconstraints.employeenumber;

import static com.lmig.beanvalidation.testutils.ValidatorUtils.getErrorMessagesFromSet;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import org.junit.Test;

public class EmployeeNumberValidatorTest {

    private final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    private final Validator validator = factory.getValidator();

    @Test
    public void test_standardEmployeeNumber_employeeNumberIsValid_noErrorsReturned() {
        EmpNumTestClass input = new EmpNumTestClass("E123456", "A123456");
        final Set<ConstraintViolation<EmpNumTestClass>> errors = validator.validate(input);


        final List<String> actualMessages = getErrorMessagesFromSet(errors);

        assertThat(actualMessages).isEmpty();
    }

    @Test
    public void test_standardEmployeeNumber_employeeNumberIsInvalid_standardEmployeeNumberValidationFailure() {
        final Set<ConstraintViolation<EmpNumTestClass>> errors = validator.validateValue(EmpNumTestClass.class,
                "standardEmpNum", "invalid");

        final List<String> actualMessages = getErrorMessagesFromSet(errors);

        assertThat(actualMessages).hasSize(1).contains("Standard employee Number must start with E, followed by 6 digits");
    }

    @Test
    public void test_adminEmployeeNumber_employeeNumberIsValid_noErrorsReturned() {
        final Set<ConstraintViolation<EmpNumTestClass>> errors = validator.validateValue(EmpNumTestClass.class, 
                "adminEmpNum", "A123456");

        final List<String> actualMessages = getErrorMessagesFromSet(errors);

        assertThat(actualMessages).isEmpty();
    }

    @Test
    public void test_adminEmployeeNumber_employeeNumberIsInvalid_adminEmployeeNumberValidationFailure() {
        final Set<ConstraintViolation<EmpNumTestClass>> errors = validator.validateValue(EmpNumTestClass.class,
                "adminEmpNum", "invalid");

        final List<String> actualMessages = getErrorMessagesFromSet(errors);

        assertThat(actualMessages).hasSize(1).contains("Admin employee number must start with A, followed by 6 digits");
    }

    class EmpNumTestClass {

        @EmployeeNumber(value = EmpType.STANDARD)
        private String standardEmpNum;
        @EmployeeNumber(value = EmpType.ADMIN)
        private String adminEmpNum;

        EmpNumTestClass(final String standardEmpNum, final String adminEmpNum) {
            this.standardEmpNum = standardEmpNum;
            this.adminEmpNum= adminEmpNum;
        }


    }
}

I'm getting a null pointer exception on the else if in the EmployeeNumberValidator class when any of the tests are ran:

javax.validation.ValidationException: HV000028: Unexpected exception during isValid call.

    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:177)
    at org.hibernate.validator.internal.engine.constraintvalidation.SimpleConstraintTree.validateConstraints(SimpleConstraintTree.java:68)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:73)
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.doValidateConstraint(MetaConstraint.java:127)
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:120)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:533)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:496)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:465)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:430)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:380)
    at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:169)
    at com.lmig.beanvalidation.domain.customconstraints.employeenumber.EmployeeNumberValidatorTest.test_standardEmployeeNumber_employeeNumberIsValid_noErrorsReturned(EmployeeNumberValidatorTest.java:24)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.NullPointerException
    at com.lmig.beanvalidation.domain.customconstraints.employeenumber.EmployeeNumberValidator.isValid(EmployeeNumberValidator.java:35)
    at com.lmig.beanvalidation.domain.customconstraints.employeenumber.EmployeeNumberValidator.isValid(EmployeeNumberValidator.java:14)
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:171)
    ... 33 more

I have tested a variation of this on a domain object. The test layout is similar to the above but rather than use an inner class in the test, it uses a class from the src folder tree and it runs without issue. I cant find any difference between the tests aside from the use of an inner class(as part of an attempt to test the EmpType validation variations without polluting or otherwise manipulating the src domain class in a manner that it wont be used in a production scenario). I've attempted several approaches to this(seen in test file; most use validateValue, I have tested validateProperty and the standard validate function using a populated object) but receive the same error for all the variations I have used.

The error as mentioned implies that empType is not set, but i'm not sure why this would be the case given I have passed in the value for both annotations present in the test inner class, and to confirm I have tried passing in the EmpType value on the src class and running the tests for it, and haven't encountered the null pointer. Is there some sort of constraint around using an inner class with these annotations that I haven't read about yet? I am using the hibernate-validator version 6.0.14 final

jbailie1991
  • 1,305
  • 2
  • 21
  • 42
  • Have you read the Stacktrace? It leads to the line where the exception occurs and that line contains variable `empType` which isn't initialized and doesn't make sense anyway. – Tom Jan 21 '19 at 12:29
  • Based on several examples(including official hibernate docs), the variable empType is not set by a standard setter or constructor, the value is taken from the annotation and should be initialised that way. I havent seen any examples thus far that provide any sort of mutator mechanism. For reference:https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-customconstraints-simple . Out of curiosity, why, in your opinion, does the empType variable and subsequent null pointer not make sense? – jbailie1991 Jan 21 '19 at 12:38
  • @Tom for further reference: http://dolszewski.com/java/custom-parametrized-validation-annotation/ , https://dzone.com/articles/create-your-own-constraint-with-bean-validation-20 – jbailie1991 Jan 21 '19 at 12:44
  • It doesn't make sense, because you don't need it. You'll get the value as `empNum` and can verify that without `empType`. – Tom Jan 21 '19 at 12:52
  • "the value is taken from the annotation and should be initialised that way": no `empType` won't be initialized and value will be assigned to `empNum`. Remove `empType` (or convert into a local variable) and only work with `empNum` (or convert _manually_ convert the String from `empNum` into an enum entry and assign that to `empType`) – Tom Jan 21 '19 at 12:54
  • This is for example purposes, the plan is to integrate this into a project I'm currently working on so there will be cases where this particular examples structure and logic won't be as simple, i.e. an enum that can't be directly correlated with the value being validated, so either way I still need to solve this particular issue for future applications. I understand where you're coming from for this specific example, but my use cases won't always be of this nature – jbailie1991 Jan 21 '19 at 12:59
  • The issue is `empType` won't be initialized automatically, thus if you need it, then you need to initialize it manually. There isn't more to this. – Tom Jan 21 '19 at 13:08
  • So how is it initialised for other cases, like the one I mentioned int he question that does work? – jbailie1991 Jan 21 '19 at 13:09
  • Which one you mean? This: http://dolszewski.com/java/custom-parametrized-validation-annotation/ ? If yes, look where he initializes his variable. You don't have a `initialize` method to do the same. – Tom Jan 21 '19 at 13:32

0 Answers0