4

I followed this blog post and tried to implement a custom validator to validate a composite primary key constraint and it fails with:

javax.validation.ValidationException: HV000064: Unable to instantiate ConstraintValidator: com.directory.domain.model.validators.StorePoolValidator.
    at com.directory.domain.repositories.StorePoolRepositoryTest.shouldReturnPoolByStore(StorePoolRepositoryTest.java:30)
Caused by: java.lang.NoSuchMethodException: com.directory.domain.model.validators.StorePoolValidator.<init>()
    at com.directory.domain.repositories.StorePoolRepositoryTest.shouldReturnPoolByStore(StorePoolRepositoryTest.java:30)

Here is the code of the annotation interface of the validator:

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Retention(RUNTIME)
@Target({FIELD})
@Constraint(validatedBy = StorePoolValidator.class)
public @interface UniqueStorePoolConstraint {
    String message() default "Store pool validation failed";

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

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

Here is the validator class:

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.List;

public class StorePoolValidator implements ConstraintValidator<UniqueStorePoolConstraint, StorePoolId> {

    private StorePoolRepository storePoolRepository;

    public StorePoolValidator(StorePoolRepository storePoolRepository) {
        this.storePoolRepository = storePoolRepository;
    }

    @Override
    public void initialize(UniqueStorePoolConstraint constraintAnnotation) {

    }

    @Override
    public boolean isValid(StorePoolId id, ConstraintValidatorContext context) {
        final List<StorePool> storePools = storePoolRepository.findAllByStoreNumber(id.getThirdNumber());

        return storePools.isEmpty();
    }
}

Here is the entity class:

@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@Table(name = "STORE_POOLS")
public class StorePool implements Serializable {

    public StorePool() {
    }

    @EmbeddedId
    @UniqueStorePoolConstraint
    private StorePoolId id;
}

and its primary key class:

import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.validation.constraints.NotNull;
import java.io.Serializable;

@Getter
@Setter
@Builder
@ToString
@Embeddable
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class StorePoolId implements Serializable {

    @Column(name = "pool", nullable = false)
    @NotNull
    private Integer pool;

    @Column(name = "third_number", nullable = false)
    @NotNull
    private Integer thirdNumber;
}

I tried to run the following test:

@RunWith(SpringRunner.class)
@DataJpaTest
public class StorePoolRepositoryTest {

    @Autowired
    private StorePoolRepository storePoolRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    public void shouldReturnPoolByStore() {
        final StorePool storePool = StorePoolBuilder.buildStorePool(1, 1);
        entityManager.persist(storePool);
        entityManager.flush();

        final List<StorePool> storePools = storePoolRepository.findAllByStoreNumber(1);

        assertThat(storePools).containsExactly(storePool);
    }
}

What am I missing? Thank you.

belgoros
  • 3,590
  • 7
  • 38
  • 76

4 Answers4

4

Sorry for being late in the discussion.

First of all create a ContextProvider which is the provider of bean that you need.

@Component
public class ContextProvider implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ContextProvider.applicationContext = applicationContext;
    }

    public static Object getBean(Class cls) {
        return ContextProvider.applicationContext.getBean(cls);
    }

}

Now you can have the bean like this way:

ContextProvider.getBean(StorePoolRepository.class);

That solves the NPE and you can have any component for your validation.

In your case StorePoolValidator,

@Service
public class StorePoolValidator implements ConstraintValidator<UniqueStorePoolConstraint, StorePoolId> {

    private StorePoolRepository storePoolRepository;

    @Override
    public void initialize(UniqueStorePoolConstraint constraintAnnotation) {
         this.storePoolRepository = (StorePoolRepository) ContextProvider.getBean(StorePoolRepository.class) 
    }

    @Override
    public boolean isValid(StorePoolId id, ConstraintValidatorContext context) {
        final List<StorePool> storePools = storePoolRepository.findAllByStoreNumber(id.getThirdNumber());

        return storePools.isEmpty();
    }
}

That's it.

Chinmoy Acharjee
  • 520
  • 5
  • 16
3

I faced basically the same issue but with Mock testing and here's how I solved it:

  1. Here is how my custom constraint validation looks like:

    Interface:

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = UniqueUsernameValidator.class)
    public @interface UniqueUsername {
    
     String message() default "This username is in use";
    
     Class<?>[] groups() default {};
    
     Class<? extends Payload>[] payload() default {};
    
    }
    

    Implementation:

     @RequiredArgsConstructor
     public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, String> {
    
         private final UserRepository repository;
    
         @Override
         public boolean isValid(String username, ConstraintValidatorContext constraintValidatorContext) {
             return !repository.existsById(username);
         }
    }
    
  2. Annotate your test class with @SpringBootTest

  3. Set up MockMvc through Spring configuration:

    @ExtendWith(MockitoExtension.class)
    @SpringBootTest
    public class RegistrationTest {
    
        @Autowired
        private WebApplicationContext wac;
    
        private MockMvc mockMvc;
    
        @BeforeEach
        void setUp() {
            this.mockMvc = MockMvcBuilders
                    .webAppContextSetup(wac)
                    .alwaysDo(print())
                    .build();
        }
    
        // your tests
    
    }
    
Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
Ismayil Karimli
  • 385
  • 1
  • 4
  • 8
2

Need to be added a constructor empty by default StorePoolValidator in order to init the validator.

public StorePoolValidator() {
}

UPDATE

In order to use the repository, you can add the Validator as a service. Then the answer is this:

@Service
public class StorePoolValidator implements ConstraintValidator<UniqueStorePoolConstraint, StorePoolId> {

    @Autowired
    private StorePoolRepository storePoolRepository;

    @Override
    public void initialize(UniqueStorePoolConstraint constraintAnnotation) {

    }

    @Override
    public boolean isValid(StorePoolId id, ConstraintValidatorContext context) {
        final List<StorePool> storePools = storePoolRepository.findAllByStoreNumber(id.getThirdNumber());

        return storePools.isEmpty();
    }
}
Jonathan JOhx
  • 5,784
  • 2
  • 17
  • 33
  • The previous error has gone, but I have a NPE in `isValid` method -> `storePoolRepository` in `StorePoolValidator` class is null :( – belgoros Jul 26 '19 at 07:28
  • @belgoros yes, I answered your first question, so the second one too I updated my answer, so let me know how it goes, kind regards. – Jonathan JOhx Jul 26 '19 at 13:51
  • 1
    @johntatn-jox Thank you for your response. Unfortunately, no changes, still the same NPE, no matter if I annotate `StorePoolValidator` with `@Service`, autowire `StorePoolRepository` or use an empty contructor as before: `Caused by: java.lang.NullPointerException... at com...directory.validators.StorePoolValidator.isValid` – belgoros Jul 26 '19 at 14:20
  • Ok in the test, add @Autowired private ApplicationContext context; , so let me know it, if not then could you please share a basic repo with this code so that I can help you? – Jonathan JOhx Jul 26 '19 at 15:12
  • No, it didn't solve the problem, even if autowiring the `ApplicationContext` in tests looks weird. I put the link to the blog article I followed to try to make it work. – belgoros Jul 26 '19 at 15:19
  • Yes, I reviewed it, so you can fix adding @SpringBootTest after @RunWith(xxxx) that annotation will load the application context. – Jonathan JOhx Jul 26 '19 at 15:32
  • In my case, instead of addind an empty constructor, I have had to make the Validator public (it had package visibility) and now it can be instantiated on tests. – Benjamín Valero Dec 01 '21 at 19:10
1

The Spring framework automatically detects all classes which implement the ConstraintValidator interface. The framework instantiates them and wires all dependencies like the class was a regular Spring bean. So you don't need to put @Service and @Autowired annotations. More details here.

  • 1
    Going through this Doloszewski's blog made me think the same, but the repository breaks it for me. Curently I get the same error, and it is all good when I get rid of the repo. @belgoros seem to have the same problem – devaga May 04 '21 at 08:26