Pages

Thursday, 27 February 2014

Annotation Based Validation Against a Database List for Spring and Hibernate

Use Case:
Validate the address type against a list of address type stored in the database. The validation could have been done using an Enum, but that binds the list to a code, whereas the requirement was to add new types of address at any point of time without a need to change the validator.

Problem:
With a small search one gets equipped to write a custom validator as the steps that were needed for creating one are quite simple.

Step 1: Define the Address Type Annotation
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
@Constraint(validatedBy = ContactTypeValidator.class)
public @interface ContactType {

    String message() default "address.type.not.defined";
       
    Class[] groups() default {};

    Class[] payload() default {};
}

Step 2:Define the Constraint Validator
The Configuration service is your service that fetches the address type stored in your Configuration table.
public class ContactTypeValidator implements ConstraintValidator {
      @Autowired
      ConfigurationService configService;

      @Override
      public void initialize(ContactType constraintAnnotation) {

      }

      @Override
      public boolean isValid(String ContactType, ConstraintValidatorContext context){
            String contactType = configService.getContactType(contactType);
            if(contactType == null){
                  return false;
            }
            return true;
      }
}

Problem:
Two problems were encountered using the above Constraint with JPA for persisting the same.
Issue 1: While running the Unit test (JUNIT) the Service bean was not autowired. On debugging further noticed that the configService autowired in the Validator was always null.
Error Stack:

javax.validation.ValidationException: HV000028: Unexpected exception during isValid call.
            at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:284)
            at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:133)
…
…
Caused by: java.lang.NullPointerException
            at com.gt.framework.annotation.validator.ContactTypeValidator.isValid(ContactTypeValidator.java:57)
            at com.gt.framework.annotation.validator.ContactTypeValidator.isValid(ContactTypeValidator.java:1)
            at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:281)
            ... 119 more

org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly
            at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:522)

Resolution: The resolution to this issue was a bit of configuration in the spring context. This would not be encountered in a web application as this is applicable only when used in a standalone setup, hence the error in the unit test.
1) Inject the LocalValidatorFactoryBean into the spring context as this is needed by spring to load the validator into context.
2) Provide the validator reference to the entityManager.

Note: When using the CustomConstraint Validator in springwebflow inject the same into flowBuilderServices. Refer this for further details


<beans:bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory" p:datasource-ref="galleryDataSource">
            <beans:property name="persistenceUnitName" value="galleryPersistanceUnit">
            <beans:property name="jpaPropertyMap">
                  <map>
                        <entry key="hibernate.dialect" value="${hibernate.dialect}">
                        <entry key="hibernate.hbm2ddl.auto" value="${hibernate.hbm2ddl.auto}">
                        <entry key="hibernate.format_sql" value="${hibernate.format_sql}">
                        <entry key="javax.persistence.validation.factory" value-ref="validator">
                  </entry></entry></entry></entry></map>
            </beans:property>
      </beans:property></beans:bean>
     
      <beans:bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id="validator">
</beans:bean>

Issue 2:
With the Bean autowired into the Validator faced another issue. The error that was seen in the stack was.
“Caused by: org.hibernate.AssertionFailure: null id in com.gt.general.entity.ContactEntity entry (don't flush the Session after an exception occurs)”
Resolution:
The issue was observed to be due to the hibernate behavior where the Bean validation is delegated to the Bean Validation implementation used where the entity manager gets flused before use. Since the validation was used on a bean that was used for save and during the pre-update stage the Id is yet to be generated in an Autogenerated mode, this error was observed.

The resolution to the problem was to change the approach to load the list of address type eagerly into a List bean and validate using the bean list instead of fetching the same from the database.
Note: This approach would work only in cases where the validation list is finite, and cannot be used where the list to be validated against is large or on a growing table.
Reference: The following reference was quite useful in pinpointing the issue faced.

Steps to Use a List Bean:
1) Define the Bean to fetch the Contact type list from the database:

@Configuration
public class PropertyConfiguration {

      @Autowired
      ConfigurationService groupConfigService;

      /**
       * Configure the bean to Provide the Contact Type allowed in the application
       * as well as the ContactTypeCount Enum which defines how many instance of
       * the contact can persists per user.
       *
       * @return Map
       */
      @Bean(name = "contactTypeMap")
      public Map getAddressType() {
            List addressTypeConfigs = groupConfigService
                        .getAllGroupConfig(AppConstants.CONTACT_TYPE);
            Map addressTypeMap = new HashMap();
            for (GroupConfig addressTypeConfig : addressTypeConfigs) {
                  addressTypeMap.put(addressTypeConfig.getConfigName(),
                              ContactCountEnum.getEnumByValue(addressTypeConfig.getConfigVal()));
            }

            logger.info("Address Type List Allowed[" + addressTypeMap + "]");

            return addressTypeMap;

      }
}

public enum ContactCountEnum {

      UNIQUE("1"), ANY("N");

      private String contactCount = null;

      private ContactCountEnum(String contactCount) {
            this.contactCount = contactCount;
      }

      public String getValue() {
            return contactCount;
      }

      public static ContactCountEnum getEnumByValue(String enumValue) {
            for (ContactCountEnum contactCountVal : ContactCountEnum.values()) {
                  if (enumValue.equals(contactCountVal.getValue())) {
                        return contactCountVal;
                  }
            }

            throw new CriticalException("contacttype.invalid.enumval", "The contact type count  ["
                        + enumValue + "] is invalid as it does not conform to the ContactCountEnum Enum ");
      }

}

2)      In the example above a HashMap is used as the return value of the Bean, it could also be a List of String if needed.
3)  The Final Validator Code sample is mentioned below:

public class ContactTypeValidator implements ConstraintValidator {

      @Override
      public void initialize(ContactType constraintAnnotation) {

      }

      @Resource(name = "contactTypeMap")
      Map contactTypeMap;


      @Override
      public boolean isValid(String contactType, ConstraintValidatorContext context) {

        
            if (contactType == null)
                  return false;
           
            logger.debug("Contact Type to be Validated ::"+contactType + ", Exists["+(null == contactTypeMap.get(contactType))+"]");

            return (null == contactTypeMap.get(contactType)) ? false : true;
      }

}

Note: Using @Autowired @Qualifier("contactTypeMap") for injecting the Map/List will give the below error as a list or map is injected as a bean list and each element in your List would be considered as a bean by spring. Hence it has been injected using @Resource.


org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.gt.framework.annotation.validator.ContactTypeValidator': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: java.util.Map com.gt.framework.annotation.validator.ContactTypeValidator.contactTypeMap; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.gt.general.entityenum.ContactCountEnum] found for dependency [map with value type com.gt.general.entityenum.ContactCountEnum]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true), @org.springframework.beans.factory.annotation.Qualifier(value=contactTypeMap)}
            at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:289)
            at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1146)