Skip to content

Instantly share code, notes, and snippets.

@abhi2495
Last active July 20, 2020 14:39
Show Gist options
  • Save abhi2495/f5e84a9cf0b8b0ac8aeef40acee8be74 to your computer and use it in GitHub Desktop.
Save abhi2495/f5e84a9cf0b8b0ac8aeef40acee8be74 to your computer and use it in GitHub Desktop.
##################################################################################
##################################################################################
######### IF YOU FOUND THIS GIST USEFUL, PLEASE LEAVE A STAR. THANKS. ############
##################################################################################
##################################################################################
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://hostname:3306/my_schema?serverTimezone=UTC
username: admin
password: admin
jpa:
database-platform: org.hibernate.dialect.MySQL8Dialect
hibernate:
ddl-auto: none
show-sql: true
@Target(TYPE)
@Retention(RUNTIME)
@Constraint(validatedBy = RoleValidator.class)
public @interface CheckRole {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.4.Final</version>
</dependency>
@RestController
public class MyController {
@Autowired
private MyTableRepository repository;
@GetMapping("/hello")
public void hello() {
MyEntity myEntity = new MyEntity();
myEntity.setRole("ROLE_ROOT");
myEntity.setUserId(3);
repository.save(myEntity); //saves successfully
MyEntity myEntity2 = new MyEntity();
myEntity2.setRole("ROLE_ROOT");
myEntity2.setUserId(4);
repository.save(myEntity2); //Throws Constraint Violation Exception
}
}

For simple custom constraints (which do not make a DB call or need autowiring inside the validation class), please refer to here. This Gist is about how to make Database calls through autowired Spring JPA Repositories as part of the constraint validation.

Use Case:

  • My Table has 3 columns - id, role and user_id. Now there can be only 1 row for which role can take the value ROLE_ROOT. And once that record is inserted, it can't be updated.

How I am implementing:

  • Using custom javax.validation.ConstraintValidator and checking if record is present for role=ROLE_ROOT inside isValid

(There are alternatives to do this, like, before calling repository.save(..), call repository.findByRole(..) , check and restrict.)

/*
* This class is required to use autowiring in the validator
*/
@Component
public class ValidatorAddingCustomizer implements HibernatePropertiesCustomizer {
private final ObjectProvider<Validator> provider;
@Autowired
public ValidatorAddingCustomizer(ObjectProvider<Validator> provider) {
this.provider = provider;
}
@Override
public void customize(Map<String, Object> hibernateProperties) {
Validator validator = provider.getIfUnique();
if (validator != null) {
hibernateProperties.put("javax.persistence.validation.factory", validator);
}
}
}
import com.example.schooltimetable.MyEntity;
import com.example.schooltimetable.MySpringBootApp;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.util.Set;
/*
Using the same application profile as used while running in live. So this test basically connects to the mysql instance.
In actual scenario , please use a different profile and H2 DB for testing.
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MySpringBootApp.class)
public class RoleValidationTest {
@Autowired
Validator validator;
/*
* Assumption is the user with ROLE_ROOT is already present in DB
* In actual scenario, please set up the test data before this test case
*/
@Test
public void whenExistingRootRole_thenFail()
{
MyEntity myEntity = new MyEntity();
myEntity.setUserId(12);
myEntity.setRole("ROLE_ROOT");
Set<ConstraintViolation<MyEntity>> constraintViolations = validator.validate(myEntity);
if (constraintViolations.size() > 0) {
for (ConstraintViolation<MyEntity> violation : constraintViolations) {
System.out.println(violation.getMessage());
}
} else {
System.out.println("Valid Object");
}
Assert.assertEquals(true, constraintViolations.size()>0);
}
}
@Entity
@Table(name = "my_table")
@CheckRole
public class MyEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id; //In the database, this PK column is marked as Auto-Increment.
@Column(name = "role")
private String role;
@Column(name = "user_id")
private int userId;
public int getId() {
return id;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
}
@Repository
public interface MyTableRepository extends JpaRepository<MyEntity, Integer> {
@Transactional(propagation = Propagation.NOT_SUPPORTED) // to auto flush
@Query(value = "SELECT CASE WHEN COUNT(e) > 0 THEN true ELSE false END FROM MyEntity e WHERE e.role = :roleName")
Boolean checkIfRoleExists(@Param("roleName") String roleName);
}
/*
Note - this wont work with this validator, because we are doing constructor injection.
This is just a sample for simple validators.
*/
import com.example.schooltimetable.CheckRole;
import com.example.schooltimetable.MyEntity;
import org.hibernate.validator.internal.util.annotationfactory.AnnotationDescriptor;
import org.hibernate.validator.internal.util.annotationfactory.AnnotationFactory;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
@SpringBootTest
public class RoleValidationUnitTest {
@Test
public void whenExistingRootRole_thenFail() {
AnnotationDescriptor<CheckRole> descriptor = new AnnotationDescriptor<CheckRole>(CheckRole.class);
AnnotationFactory.create(descriptor);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
MyEntity myEntity = new MyEntity();
myEntity.setUserId(12);
myEntity.setRole("ROLE_ROOT");
Set<ConstraintViolation<MyEntity>> constraintViolations = validator.validate(myEntity);
if (constraintViolations.size() > 0) {
for (ConstraintViolation<MyEntity> violation : constraintViolations) {
System.out.println(violation.getMessage());
}
} else {
System.out.println("Valid Object");
}
Assert.assertEquals(true, constraintViolations.size() > 0);
}
}
public class RoleValidator implements ConstraintValidator<CheckRole, MyEntity> {
private static final String ROLE_TO_VALIDATE = "ROLE_ROOT";
private LoadingCache<String, Boolean> myCache;
public RoleValidator(MyTableRepository repository) {
myCache = Caffeine.newBuilder()
.maximumSize(1)
.expireAfterWrite(5, TimeUnit.MINUTES)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(repository::checkIfRoleExists);
}
@Override
public void initialize(CheckRole constraintAnnotation) {
}
@Override
public boolean isValid(MyEntity entity, ConstraintValidatorContext context) {
String roleValue = entity.getRole();
if (roleValue.equals(ROLE_TO_VALIDATE)) {
boolean isValid = !myCache.get(ROLE_TO_VALIDATE);
if (!isValid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("Cannot have duplicate entry for Role: " + ROLE_TO_VALIDATE)
.addConstraintViolation();
}
return isValid;
} else {
return true;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment