I have a custom annotation for hide/show fields based on logged-in user roles.
I overrode the AnnotationIntrospector hasIgnoreMarker method to implement my logic.
This works only for the first logged-in user, and subsequent users inherent the first user permission even if they are not authorized to access first user fields. They basically see fields based on whoever the first logged-in user was.
I have tried to use request scope but got the same result.
Any guidance will be appreciated.
Custom annotation
#Documented
#Retention(RUNTIME)
#PreAuthorize("permitAll()")
#Inherited
public #interface RestrictFor {
String[] roles() default {};
}
Introspector
#Component
#RequiredArgsConstructor
#Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class JsonSerializerRestrictToIntrospector extends NopAnnotationIntrospector {
private final LoggedUserProvider loggedUserProvider;
#Override
public Version version() {
return PackageVersion.VERSION;
}
#Override
public boolean hasIgnoreMarker(AnnotatedMember m) {
final RestrictFor annotation = m.getAnnotation(RestrictFor.class);
if (annotation != null && isNotBlank(List.of(annotation.roles()))) {
final Set<String> roles = new HashSet<>(Arrays.asList(annotation.roles()));
roles.retainAll(loggedUserProvider.getRoles());
return isNotEmpty(roles.toArray(new String[0]));
}
return false;
}
}
Config class
#Configuration
#RequiredArgsConstructor
class JacksonConfig {
private final JsonSerializerRestrictToIntrospector serializerRestrictToIntrospector;
#Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> {
builder.annotationIntrospector(serializerRestrictToIntrospector);
};
}
}
My purpose is to use a specific ConstraintValidator in 2 scenarios below
use annotations on POJO to validate the whole object (the popular way)
validate a single value with specific validator's isValid function (for some configurable dynamic validation request)
The validator must support services injection, so I must get it from spring but not create a new validator instance manually.
followed my test codes:
annotation
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = { IdNumberValidator.class })
public #interface IdNumber {
String message() default "id number is not available";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
validator
public class IdNumberValidator implements ConstraintValidator<IdNumber, String>, BeanNameAware {
#Autowired
private IUserService usrService;
private String bn;
#Override
public boolean isValid(String value, ConstraintValidatorContext context){
System.out.println("my name is:" + bn);
return usrService.getUserByAccount(value).isPresent();
}
#Override
public void setBeanName(String name) {
this.bn = name;
}
}
pojo
#Getter
#Setter
public class TestPOJO {
private long id;
#IdNumber
private String idn;
}
service
#Service
public class TestValidatorService {
#Autowired
private Validator validator;
#Autowired
private ApplicationContext context;
public void validatePojo(TestPOJO pojo){
BeanPropertyBindingResult e = new BeanPropertyBindingResult(pojo, "TestPOJO");
validator.validate(pojo, e);
if(e.hasErrors()){
for(ObjectError oe : e.getAllErrors()){
System.out.println(oe.toString());
}
}
}
public void validatePojoByDynamicValidator(TestPOJO pojo){
IdNumberValidator validator = context.getBean("com.test.IdNumberValidator", IdNumberValidator.class); // got the name via BeanNameAware but seems not working
System.out.println(validator.isValid(pojo.getIdn(), null));
}
}
In the test case for service, function "validatePojo" passed but "validatePojoByDynamicValidator" did not.
Any solution for this problem? Thanks!
I have a spring project and want to enforce uniqueness in the database on a field and get the error message back to the UI.
I have read this SO answer and it makes sense so #Column(unique = true) makes the constraint on the table but doesn't enforce it.
So the question becomes how to create a #Unique annotation that checks with the database and returns a error message into BindingResult on POST handler.
An example would be great.
UPDATE
I tried the following way to make a custom validator:
The objects (note I have added #valid to get the validator messages to navigate up to BindingResult)
Person.java
#Entity
public class Person {
public Person() {}
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// other stuff
#UniqueNid
private BigInteger nid;
EpisodePerson.java
#Entity
public class EpisodePerson {
public EpisodePerson(){};
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#Valid
private Person person;
EpisodeViewModel (DTO)
public class EpisodeViewModel {
#Valid
private Episode episode = new Episode();
#Valid
private List<EpisodePerson> persons = new ArrayList<>();
UniqueNid.java
#Documented
#Target({ElementType.METHOD, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = UniqueNiaValidator.class)
public #interface UniqueNid {
String message() default "{Duplicate ID}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
UniqueNidValidator.java
public class UniqueNidValidator implements ConstraintValidator<UniqueNid, BigInteger> {
public UniqueNidValidator(){};
private PersonRepository personRepository;
#Autowired
public void setPersonRepository(PersonRepository personRepository) {this.personRepository = personRepository;}
public UniqueNidValidator(PersonRepository personRepository) {
this.personRepository = personRepository;
}
#Override
public void initialize(UniqueNid constraint) {
}
#Override
public boolean isValid(BigInteger nid, ConstraintValidatorContext context) {
return nid != null && personRepository.existsByNid(nid);
}
}
PersonRepository.java
...
Boolean existsByNid(BigInteger nid);
...
Application.java
#SpringBootApplication
#EnableAutoConfiguration(exclude = { org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration.class })
public class Demo3Application extends SpringBootServletInitializer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(WebApplicationInitializer.class);
}
public static void main(String[] args) {
SpringApplication.run(Demo3Application.class, args);
}
#Bean
public javax.validation.Validator localValidatorFactoryBean() {
return new LocalValidatorFactoryBean();
}
}
When I go to submit a person I get :
Stack Trace (abbreviated)
java.lang.NullPointerException: null
at com.example.validators.UniqueNidValidator.isValid(UniqueNidValidator.java:31) ~[main/:na]
UPDATE 2
I have also tried this configuration
public class UniqueNidValidator implements ConstraintValidator<UniqueNid, BigInteger> {
public UniqueNidValidator(){};
private PersonRepository personRepository;
public UniqueNidValidator(PersonRepository personRepository) {
this.personRepository = personRepository;
}
#Override
public void initialize(UniqueNid constraint) {
}
#Override
public boolean isValid(BigInteger nid, ConstraintValidatorContext context) {
System.out.println("About to check " +nid.toString());
System.out.println("person repo " +personRepository.toString() );
return personRepository.existsByNid(nid);
}
}
which gives:
java.lang.NullPointerException: null
at com.example.validators.UniqueNiaValidator.isValid(UniqueNiaValidator.java:29) ~[main/:na]
When I try to print the repo to console.
You'll need to create a custom validation that checks the database. For the database check you can obviously use the probably already existing Spring Data Repository and it's exists() method.
A custom validation consists of an annotation to mark the fields to be checked and a class implementing the actual check.
On minor challenge is that the class needs a default constructor and doesn't really support injecting dependencies. So anything you need, you have to basically access from some static reference, including e.g. the repository. So you probably have a separate bean which puts the repository into that static reference.
Such a bean that "catches" a repository and makes it available in a static variable might look like this.
#Component
public class RepositoryCatcher{
public static MyRepository;
public RepositoryCatcher(MyRepository r){
repository = r;
}
}
From the exception you mentioned it seems that the only possible NullPointerException is when the personRepository is incorrectly injected to the validator.
Please give a try to the solution below:
Remove the following bean from your Demo3Application and let Spring Boot create the default one instead.
#Bean
public javax.validation.Validator localValidatorFactoryBean() {
return new LocalValidatorFactoryBean();
}
Remove the setter for the repository from the validator but leave the dependency in the constructor as it is right now.
#Autowired
public void setPersonRepository(PersonRepository personRepository {
this.personRepository = personRepository;
}
It's not entirely true that custom validators require a default constructor as mentioned by Jens in his answer. Spring will inject dependencies based on the constructor even though a validator isn't mark as a managed component. The #Autowired annotation is also redundant.
In addition, you probably made a mistake in the condition. You should check if a person doesn't exist (Notice the ! mark in the second part).
return nid != null && !personRepository.existsByNid(nid);
I encourage you to look into a blog post which addresses your issue. Sample code is available in the GitHub repository. You can run, test it, and then compare with your solution.
This is the validator that worked and errors into BindingResult:
UniqueNidValidator.java
public class UniqueNiaValidator implements ConstraintValidator<UniqueNid, BigInteger> {
public UniqueNiaValidator(){};
#Autowired
private PersonRepository personRepository;
public UniqueNiaValidator(PersonRepository personRepository) {
this.personRepository = personRepository;
}
#Override
public void initialize(UniqueNid constraint) {
}
#Override
public boolean isValid(BigInteger nid, ConstraintValidatorContext context) {
return !personRepository.existsByNid(nid);
}
}
Note the !personRepository.existByNid(nid);
Further more the reason that the repo was blank the second time around was because it was getting called twice as outlined here
But checking for RDBMS constraint violations on Beans probably isn't a good idea anyway.
I am implementing a custom AccessDecisionVoter and I have a JPA repository which I need to autowire in the custom AccessDecisionVoter implementation. #Autowire is simply not working for neither a Service or Jpa Repository inside this class.
Project Structure
Application.java
#SpringBootApplication
#ComponentScan(basePackages="com")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
DynamicAuthorizationVoter.java
#Component
public class DynamicAuthorizationVoter implements AccessDecisionVoter<FilterInvocation> {
#Autowired
private PrivilegeRepository privilegeRepo;
#Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
#Override
public boolean supports(Class clazz) {
return true;
}
#Override
public int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> collection) {
String url = determineModule(object);
if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
return ACCESS_ABSTAIN;
}
return isAccessGranted(authentication, object.getRequestUrl())? ACCESS_GRANTED : ACCESS_DENIED;
}
String determineModule(FilterInvocation filterObject){
String url = filterObject.getRequestUrl();
return url;
}
boolean isAccessGranted(Authentication authObject, String url){
Set<Privilege> privileges = privilegeRepo.findByUrl(url);
String userRole;
for(GrantedAuthority authority : authObject.getAuthorities()){
userRole = authority.getAuthority();
for(Privilege priv : privileges){
if(priv.equals(userRole)){
return true;
}
}
}
return false;
}
}
PrivilegeRepository.java
public interface PrivilegeRepository extends JpaRepository<Privilege, Long> {
Set<Privilege> findByName(String name);
Set<Privilege> findByUrl(String url);
}
For #Autowire to work inside the DynamicAuthorizationVoter class, I changed the #Component to #Service, #Configuration and everything else I found here on SO but none of them works. This JPA Repository is #Autowired everywhere else.
I will appreciate all the help.
-Adil
Usually, if you don't see any error during deployment, autowired worked fine because it is required by default. See the #Autowired documentation
Anyway, try to use an #Autowired constructor instead of an #Autowired property.
private PrivilegeRepository privilegeRepo;
#Autowired
public DynamicAuthorizationVoter(PrivilegeRepository privilegeRepo){
this.privilegeRepo = privilegeRepo;
}
With that, you could add a breakpoint to this constructor and debug it to know if the autowire process works well.
Also, remember that to use the DynamicAuthorizationVoter instance you mustn't declare it as new. You must include the following code in the related class where you want to use it.
#Autowired
AccessDecisionVoter dynamicAuthorizationVoter;
Hope it helps,
I have a custom Hibernate Validator for my entities. One of my validators uses an Autowired Spring #Repository. The application works fine and my repository is Autowired successfully on my validator.
The problem is i can't find a way to test my validator, cause i can't inject my repository inside it.
Person.class:
#Entity
#Table(schema = "dbo", name = "Person")
#PersonNameMustBeUnique
public class Person {
#Id
#GeneratedValue
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#Column()
#NotBlank()
private String name;
//getters and setters
//...
}
PersonNameMustBeUnique.class
#Target({ TYPE, ANNOTATION_TYPE })
#Retention(RUNTIME)
#Constraint(validatedBy = { PersonNameMustBeUniqueValidator.class })
#Documented
public #interface PersonNameMustBeUnique{
String message() default "";
Class<?>[] groups() default {};
Class<? extends javax.validation.Payload>[] payload() default {};
}
The validator:
public class PersonNameMustBeUniqueValidatorimplements ConstraintValidator<PersonNameMustBeUnique, Person> {
#Autowired
private PersonRepository repository;
#Override
public void initialize(PersonNameMustBeUnique constraintAnnotation) { }
#Override
public boolean isValid(Person entidade, ConstraintValidatorContext context) {
if ( entidade == null ) {
return true;
}
context.disableDefaultConstraintViolation();
boolean isValid = nameMustBeUnique(entidade, context);
return isValid;
}
private boolean nameMustBeUnique(Person entidade, ConstraintValidatorContext context) {
//CALL REPOSITORY TO CHECK IF THE NAME IS UNIQUE
//ADD errors if not unique...
}
}
And the context file has a validator bean:
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
Again, it works fine, but i don't know how to test it.
My test file is:
#RunWith(MockitoJUnitRunner.class)
public class PersonTest {
Person e;
static Validator validator;
#BeforeClass
public static void setUpClass() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
#Test
public void name__must_not_be_null() {
e = new Person();
e.setName(null);
Set<ConstraintViolation<Person>> violations = validator.validate(e);
assertViolacao(violations, "name", "Name must not be null");
}
}
I was facing very similar problem: How to write pure unit test of custom validator wich has autowired configuration bean?
I could manage to solve it by following code (inspired by this answer of user abhishekrvce).
This is pure unit test of custom validator with #Autowired configuration bean, which reads the data from configuration file (not showed in code).
#Import({MyValidator.class})
#ContextConfiguration(classes = MyConfiguration.class, initializers = ConfigFileApplicationContextInitializer.class)
class MyValidatorTest {
private LocalValidatorFactoryBean validator;
#Autowired
private ConfigurableApplicationContext applicationContext;
#BeforeEach
void initialize() {
SpringConstraintValidatorFactory springConstraintValidatorFactory
= new SpringConstraintValidatorFactory(
applicationContext.getAutowireCapableBeanFactory());
validator = new LocalValidatorFactoryBean();
validator.setConstraintValidatorFactory(springConstraintValidatorFactory);
validator.setApplicationContext(applicationContext);
validator.afterPropertiesSet();
}
#Test
void isValid()
{
Set<ConstraintViolation<MyObject>> constraintViolations = validator
.validate(myObjectInstance);
assertThat(constraintViolations).hasSize(1);
}
}
U can add the following bean to your Spring Context in the test:
#RunWith(SpringRunner.class)
#Import(LocalValidatorFactoryBean.class)
public class PersonTest {
#Autowired
private Validator validator;
{
validator.validate(new Person());
}
...
}
On #BeforeClass:
#BeforeClass
public static void setUpClass() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
And in your test you need to replace the beans with your mocked bean:
myValidator.initialize(null);
BeanValidatorTestUtils.replaceValidatorInContext(validator, usuarioValidoValidator, e);
The class that do all the magic:
public class BeanValidatorTestUtils {
#SuppressWarnings({ "rawtypes", "unchecked" })
public static <A extends Annotation, E> void replaceValidatorInContext(Validator validator,
final ConstraintValidator<A, ?> validatorInstance,
E instanceToBeValidated) {
final Class<A> anotacaoDoValidador = (Class<A>)
((ParameterizedType) validatorInstance.getClass().getGenericInterfaces()[0])
.getActualTypeArguments()[0];
ValidationContextBuilder valCtxBuilder = ReflectionTestUtils.<ValidationContextBuilder>invokeMethod(validator,
"getValidationContext");
ValidationContext<E> validationContext = valCtxBuilder.forValidate(instanceToBeValidated);
ConstraintValidatorManager constraintValidatorManager = validationContext.getConstraintValidatorManager();
final ConcurrentHashMap nonSpyHashMap = new ConcurrentHashMap();
ConcurrentHashMap spyHashMap = spy(nonSpyHashMap);
doAnswer(new Answer<Object>() {
#Override public Object answer(InvocationOnMock invocation) throws Throwable {
Object key = invocation.getArguments()[0];
Object keyAnnotation = ReflectionTestUtils.getField(key, "annotation");
if (anotacaoDoValidador.isInstance(keyAnnotation)) {
return validatorInstance;
}
return nonSpyHashMap.get(key);
}
}).when(spyHashMap).get(any());
ReflectionTestUtils.setField(constraintValidatorManager, "constraintValidatorCache", spyHashMap);
}
}
We also faced the similar problem where #Autowiring was failing (not initialised) in ConstrainValidator Class. Our ConstraintValidator Implemented class was using a value which supposed to be read from the application.yml file. Below solution helped us as this is using a pure spring scope. Hope this helps, with proper SpringJunit4ClassRunner.
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
import org.springframework.web.context.WebApplicationContext;
#WebAppConfiguration
#ContextConfiguration(classes = {ApplicationConfig.class})
#RunWith(SpringJUnit4ClassRunner.class)
#TestPropertySource(properties = {
"spring.someConfigValue.InApplicationYaml=Value1",
})
public class MyTest {
#Autowired
private WebApplicationContext webApplicationContext;
LocalValidatorFactoryBean validator;
#Before
public void setup() {
SpringConstraintValidatorFactory springConstraintValidatorFactory
= new SpringConstraintValidatorFactory(webApplicationContext.getAutowireCapableBeanFactory());
validator = new LocalValidatorFactoryBean();
validator.setConstraintValidatorFactory(springConstraintValidatorFactory);
validator.setApplicationContext(webApplicationContext);
validator.afterPropertiesSet();
}
#Test
public void should_have_no_violations_for_all_valid_fields() {
Set<ConstraintViolation<PojoClassWhichHaveConstraintValidationAnnotation>> violations = validator.validate(pojoClassObjectWhichHaveConstraintValidationAnnotation);
assertTrue(violations.isEmpty());
}
}
#Configuration
public class ApplicationConfig {
#Value("${spring.someConfigValue.InApplicationYaml=Value1}")
public String configValueToBeReadFromApplicationYamlFile;
}
Recently I had the same problem with my custom validator. I needed to validate a model being passed to a controller's method (method level validation). The validator invoked but the dependencies (#Autowired) could not be injected. It took me some days searching and debugging the whole process. Finally, I could make it work. I hope my experience save some time for others with the same problem. Here is my solution:
Having a jsr-303 custom validator like this:
#Retention(RetentionPolicy.RUNTIME)
#Target({ ElementType.FIELD,
ElementType.PARAMETER,
ElementType.TYPE,
ElementType.METHOD,
ElementType.LOCAL_VARIABLE,
ElementType.CONSTRUCTOR,
ElementType.TYPE_PARAMETER,
ElementType.TYPE_USE })
#Constraint(validatedBy = SampleValidator.class)
public #interface ValidSample {
String message() default "Default sample validation error";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class SampleValidator implements ConstraintValidator<ValidSample, SampleModel> {
#Autowired
private SampleService service;
public void initialize(ValidSample constraintAnnotation) {
//init
}
public boolean isValid(SampleModel sample, ConstraintValidatorContext context) {
service.doSomething();
return true;
}
}
You should configure spring test like this:
#ComponentScan(basePackages = { "your base packages" })
#Configurable
#EnableWebMvc
class SpringTestConfig {
#Autowired
private WebApplicationContext wac;
#Bean
public Validator validator() {
SpringConstraintValidatorFactory scvf = new SpringConstraintValidatorFactory(wac.getAutowireCapableBeanFactory());
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setConstraintValidatorFactory(scvf);
validator.setApplicationContext(wac);
validator.afterPropertiesSet();
return validator;
}
#Bean
public MethodValidationPostProcessor mvpp() {
MethodValidationPostProcessor mvpp = new MethodValidationPostProcessor();
mvpp.setValidatorFactory((ValidatorFactory) validator());
return mvpp;
}
#Bean
SampleService sampleService() {
return Mockito.mock(SampleService.class);
}
}
#WebAppConfiguration
#ContextConfiguration(classes = { SpringTestConfig.class, AnotherConfig.class })
public class ASampleSpringTest extends AbstractTestNGSpringContextTests {
#Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
#BeforeClass
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.webAppContextSetup(wac)
.build();
}
#Test
public void testSomeMethodInvokingCustomValidation(){
// test implementation
// for example:
mockMvc.perform(post("/url/mapped/to/controller")
.accept(MediaType.APPLICATION_JSON_UTF8)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(json))
.andExpect(status().isOk());
}
}
Note that, here I am using testng, but you can use JUnit 4. The whole configuration would be the same except that you would run the test with #RunWith(SpringJUnit4ClassRunner.class) and do not extend the AbstractTestNGSpringContextTests.
Now, #ValidSample can be used in places mentioned in #Target() of the custom annotation.
Attention: If you are going to use the #ValidSample annotation on method level (like validating method arguments), then you should put class level annotation #Validated on the class where its method is using your annotation, for example on a controller or on a service class.
A solution with JUnit4 and Mockito:
#Import(LocalValidatorFactoryBean.class)
#RunWith(SpringRunner.class)
public class MyCustomValidatorTest {
#Autowired
private Validator validator;
#MockBean
private PersonRepository repository;
#Test
public void name_must_not_be_null() {
// given
when(repository.findByName(any())).thenReturn(Collection.emptyList());
Person person = new Person();
person.setName(null);
// when
Set<ConstraintViolation<Person>> violations = validator.validate(person);
// then
assertViolation(violations, "name", "Name must not be null");
}
}
You can test the validator stand alone and use reflection for inject the autowired attribute.
Constraint annotation:
#Target({ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = EmailAlreadyExistsValidator.class)
public #interface EmailAlreadyExists {
String message() default "Email already exists in the database";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validator:
public class EmailAlreadyExistsValidator implements
ConstraintValidator<EmailAlreadyExists, String> {
#Autowired
private UserRepository repository;
#Override
public void initialize(EmailAlreadyExists constraintAnnotation) {}
public boolean isValid(String email, ConstraintValidatorContext context) {
Optional<User> opUser = repository.findByEmail(email);
return (opUser.isEmpty());
}
}
Unit Test (ReflectionTestUtils do the magic):
public class EmailAlreadyExistsValidatorTest {
#Mock
private EmailAlreadyExists emailAlreadyExists;
#Mock
private ConstraintValidatorContext constraintValidatorContext;
#Mock
private UserRepository repository;
private EmailAlreadyExistsValidator validator;
#BeforeEach
public void beforeEach() {
MockitoAnnotations.openMocks(this);
validator = new EmailAlreadyExistsValidator();
validator.initialize(emailAlreadyExists);
ReflectionTestUtils.setField(validator, "repository", repository);
}
#Test
#DisplayName("Given an user with existent email then validation must fail")
public void isValid_existentPassword_mustFail() {
final String existentEmail = "testuser#test.com";
User savedUser = new User("1213443455",
"Test User",
existentEmail,
"12345",
new Date());
Optional<User> opUser = Optional.of(savedUser);
when(repository.findByEmail(anyString())).thenReturn(opUser);
assertFalse(validator.isValid(existentEmail,constraintValidatorContext));
}
}
It might be a bit late but I faced the same issue lately so I'll post how I solved the problem, as this could help other people.
The problem is basically that Hibernate's standard Validator implementation that you get by calling Validation.buildDefaultValidatorFactory().getValidator() does not know anything about Spring's application context so it cannot inject dependencies in your custom constraint validators.
In a Spring application the implementation of both the Validator and the ValidatorFactory interface is the class LocalValidatorFactoryBean, which can delegate to the ApplicationContext to instantiate constraint validators with dependencies injected.
What you need to do is
Instantiate your constraint validators with their (mocked, I presume) dependencies
Create your own ValidatorFactory that holds all the constraint validators from bulletpoint 1
Instantiate your Validator from such factory
This is the custom validator factory
public class CustomLocalValidatorFactoryBean extends LocalValidatorFactoryBean {
private final List<ConstraintValidator<?, ?>> customConstraintValidators;
public CustomLocalValidatorFactoryBean(List<ConstraintValidator<?, ?>> customConstraintValidators) {
this.customConstraintValidators = customConstraintValidators;
setProviderClass(HibernateValidator.class);
afterPropertiesSet();
}
#Override
protected void postProcessConfiguration(Configuration<?> configuration) {
super.postProcessConfiguration(configuration);
ConstraintValidatorFactory defaultConstraintValidatorFactory =
configuration.getDefaultConstraintValidatorFactory();
configuration.constraintValidatorFactory(
new ConstraintValidatorFactory() {
#Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
for (ConstraintValidator<?, ?> constraintValidator : customConstraintValidators) {
if (key.equals(constraintValidator.getClass())) //noinspection unchecked
return (T) constraintValidator;
}
return defaultConstraintValidatorFactory.getInstance(key);
}
#Override
public void releaseInstance(ConstraintValidator<?, ?> instance) {
defaultConstraintValidatorFactory
.releaseInstance(instance);
}
}
);
}
}
then in your test class you'd just do something like this:
class MyTestSuite {
private final PersonRepository mockPersonRepository = Mockito.mock(PersonRepository.class);
private final List<ConstraintValidator<?,?>> customConstraintValidators =
Collections.singletonList(new PersonNameMustBeUniqueValidator(mockPersonRepository));
private final ValidatorFactory customValidatorFactory =
new CustomLocalValidatorFactoryBean(customConstraintValidators);
private final Validator validator = customValidatorFactory.getValidator();
#Test
void myTestCase() {
// mock the dependency: Mockito.when(mockPersonRepository...)
Person p = new Person();
//setters omitted
Set<ConstraintViolation<?>> violations = validator.validate(p);
//assertions on the set of constraint violations
}
}
Hope that helps. You can check out this post of mine for more details: https://codemadeclear.com/index.php/2021/01/26/how-to-mock-dependencies-when-unit-testing-custom-validators/
I've implemented by overriding default Hibernate ConstraintValidatorFactory in my UnitTests
LocalValidatorFactoryBean localValidatorFactory = new LocalValidatorFactoryBean();
localValidatorFactory.setProviderClass(HibernateValidator.class);
localValidatorFactory.setConstraintValidatorFactory(new ConstraintValidatorFactoryImpl() {
#Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> arg0) {
T ret = super.getInstance(arg0);
if (ret instanceof UniqueEmailValidator) {
((UniqueEmailValidator) ret).setUserService(userService);
}
return ret;
}
});
localValidatorFactory.afterPropertiesSet();
Spring Boot 2 allows to inject Bean in custom Validator without any fuss.The Spring framework automatically detects all classes which implement the ConstraintValidator interface, instantiate them, and wire all dependencies.
I had Similar problem , this is how i have implemented.
Step 1 Interface
#Documented
#Constraint(validatedBy = UniqueFieldValidator.class)
#Target({ ElementType.METHOD,ElementType.ANNOTATION_TYPE,ElementType.PARAMETER })
#Retention(RetentionPolicy.RUNTIME)
public #interface UniqueField {
String message() default "Duplicate Name";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Step 2 Validator
public class UniqueFieldValidator implements ConstraintValidator<UniqueField, Person> {
#Autowired
PersionList personRepository;
private static final Logger log = LoggerFactory.getLogger(PersonRepository.class);
#Override
public boolean isValid(Person object, ConstraintValidatorContext context) {
log.info("Validating Person for Duplicate {}",object);
return personRepository.isPresent(object);
}
}
Usage
#Component
#Validated
public class PersonService {
#Autowired
PersionList personRepository;
public void addPerson(#UniqueField Person person) {
personRepository.add(person);
}
}