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