diff --git a/annotations/pom.xml b/annotations/pom.xml index 33624706c7..d6c9976ecf 100644 --- a/annotations/pom.xml +++ b/annotations/pom.xml @@ -71,7 +71,16 @@ cglib test - + + javax.validation + validation-api + true + + + org.hibernate + hibernate-validator + test + @@ -97,10 +106,20 @@ 3.4.GA - cglib - cglib - 2.2 - + cglib + cglib + 2.2 + + + org.hibernate + hibernate-validator + 4.0.0.Beta1 + + + javax.validation + validation-api + 1.0.CR2 + diff --git a/annotations/src/main/java/org/hibernate/cfg/AnnotationConfiguration.java b/annotations/src/main/java/org/hibernate/cfg/AnnotationConfiguration.java index 485a97c985..e8723f5531 100644 --- a/annotations/src/main/java/org/hibernate/cfg/AnnotationConfiguration.java +++ b/annotations/src/main/java/org/hibernate/cfg/AnnotationConfiguration.java @@ -69,6 +69,7 @@ import org.hibernate.annotations.common.reflection.XClass; import org.hibernate.annotations.common.reflection.java.JavaReflectionManager; import org.hibernate.cfg.annotations.Version; import org.hibernate.cfg.annotations.reflection.JPAMetadataProvider; +import org.hibernate.cfg.beanvalidation.BeanValidationActivator; import org.hibernate.engine.NamedQueryDefinition; import org.hibernate.engine.NamedSQLQueryDefinition; import org.hibernate.engine.ResultSetMappingDefinition; @@ -800,6 +801,13 @@ public class AnnotationConfiguration extends Configuration { } public SessionFactory buildSessionFactory() throws HibernateException { + enableLegacyHibernateValidator(); + enableBeanValidation(); + enableHibernateSearch(); + return super.buildSessionFactory(); + } + + private void enableLegacyHibernateValidator() { //add validator events if the jar is available boolean enableValidatorListeners = !"false".equalsIgnoreCase( getProperty( "hibernate.validator.autoregister_listeners" ) ); Class validateEventListenerClass = null; @@ -868,10 +876,10 @@ public class AnnotationConfiguration extends Configuration { } } } - - enableHibernateSearch(); - - return super.buildSessionFactory(); + } + + private void enableBeanValidation() { + BeanValidationActivator.activateBeanValidation( getEventListeners(), getProperties() ); } /** diff --git a/annotations/src/main/java/org/hibernate/cfg/beanvalidation/BeanValidationActivator.java b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/BeanValidationActivator.java new file mode 100644 index 0000000000..df37cb2469 --- /dev/null +++ b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/BeanValidationActivator.java @@ -0,0 +1,87 @@ +package org.hibernate.cfg.beanvalidation; + +import java.util.Map; +import java.util.Properties; +import java.lang.reflect.Method; +import java.lang.reflect.InvocationTargetException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.hibernate.util.ReflectHelper; +import org.hibernate.HibernateException; +import org.hibernate.AssertionFailure; +import org.hibernate.event.EventListeners; + +/** + * This class has no hard depenmdency on Bean Validation APIs + * It must uses reflectione very time BV is required. + * @author Emmanuel Bernard + */ +public class BeanValidationActivator { + + private static final String BV_DISCOVERY_CLASS = "javax.validation.Validation"; + private static final String TYPE_SAFE_ACTIVATOR_CLASS = "org.hibernate.cfg.beanvalidation.TypeSafeActivator"; + private static final String TYPE_SAFE_ACTIVATOR_METHOD = "activateBeanValidation"; + private static final String MODE_PROPERTY = "javax.persistence.validation.mode"; + + public static void activateBeanValidation(EventListeners eventListeners, Properties properties) { + ValidationMode mode = ValidationMode.getMode( properties.get( MODE_PROPERTY ) ); + if (mode == ValidationMode.NONE) return; + try { + //load Validation + ReflectHelper.classForName( BV_DISCOVERY_CLASS, BeanValidationActivator.class ); + } + catch ( ClassNotFoundException e ) { + + if (mode == ValidationMode.CALLBACK) { + throw new HibernateException( "Bean Validation not available in the class path but required in " + MODE_PROPERTY ); + } + else if (mode == ValidationMode.AUTO) { + //nothing to activate + return; + } + else { + throw new AssertionFailure( "Unexpected ValidationMode: " + mode ); + } + } + try { + Class activator = ReflectHelper.classForName( TYPE_SAFE_ACTIVATOR_CLASS, BeanValidationActivator.class ); + Method buildDefaultValidatorFactory = + activator.getMethod( TYPE_SAFE_ACTIVATOR_METHOD, EventListeners.class, Properties.class ); + buildDefaultValidatorFactory.invoke( null, eventListeners, properties ); + } + catch ( NoSuchMethodException e ) { + throw new HibernateException( "Unable to get the default Bean Validation factory", e); + } + catch ( IllegalAccessException e ) { + throw new HibernateException( "Unable to get the default Bean Validation factory", e); + } + catch ( InvocationTargetException e ) { + throw new HibernateException( "Unable to get the default Bean Validation factory", e); + } + catch ( ClassNotFoundException e ) { + throw new HibernateException( "Unable to get the default Bean Validation factory", e); + } + } + + private static enum ValidationMode { + AUTO, + CALLBACK, + NONE; + + public static ValidationMode getMode(Object modeProperty) { + if (modeProperty == null) { + return AUTO; + } + else { + try { + return valueOf( modeProperty.toString().toUpperCase() ); + } + catch ( IllegalArgumentException e ) { + throw new HibernateException( "Unknown validation mode in " + MODE_PROPERTY + ": " + modeProperty.toString() ); + } + } + } + } +} diff --git a/annotations/src/main/java/org/hibernate/cfg/beanvalidation/BeanValidationEventListener.java b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/BeanValidationEventListener.java new file mode 100644 index 0000000000..0d2ad49e50 --- /dev/null +++ b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/BeanValidationEventListener.java @@ -0,0 +1,156 @@ +package org.hibernate.cfg.beanvalidation; + +import java.util.Set; +import java.util.Properties; +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import javax.validation.ValidatorFactory; +import javax.validation.ConstraintViolation; +import javax.validation.TraversableResolver; +import javax.validation.Validator; +import javax.validation.ConstraintViolationException; +import javax.validation.groups.Default; + +import org.hibernate.event.PreInsertEventListener; +import org.hibernate.event.PreUpdateEventListener; +import org.hibernate.event.PreDeleteEventListener; +import org.hibernate.event.PreInsertEvent; +import org.hibernate.event.PreUpdateEvent; +import org.hibernate.event.PreDeleteEvent; +import org.hibernate.EntityMode; +import org.hibernate.HibernateException; +import org.hibernate.util.ReflectHelper; + +/** + * @author Emmanuel Bernard + */ +//FIXME review exception model +public class BeanValidationEventListener implements + PreInsertEventListener, PreUpdateEventListener, PreDeleteEventListener { + private static final String JPA_GROUP_PREFIX = "javax.persistence.validation.group."; + private static final Class[] DEFAULT_GROUPS = new Class[] { Default.class }; + private static final Class[] EMPTY_GROUPS = new Class[] { }; + + private ValidatorFactory factory; + private TraversableResolver tr; + private Map[]> groupsPerOperation = new HashMap[]>(3); + + + public BeanValidationEventListener(ValidatorFactory factory, Properties properties) { + this.factory = factory; + setGroupsForOperation( Operation.INSERT, properties ); + setGroupsForOperation( Operation.UPDATE, properties ); + setGroupsForOperation( Operation.DELETE, properties ); + } + + private void setGroupsForOperation(Operation operation, Properties properties) { + Object property = properties.get( JPA_GROUP_PREFIX + operation.getGroupPropertyName() ); + + Class[] groups; + if ( property == null ) { + groups = operation == Operation.DELETE ? EMPTY_GROUPS : DEFAULT_GROUPS; + } + else { + if ( property instanceof String ) { + String stringProperty = (String) property; + String[] groupNames = stringProperty.split( "," ); + if ( groupNames.length == 1 && groupNames[0].equals( "" ) ) { + groups = EMPTY_GROUPS; + } + else { + List> groupsList = new ArrayList>(groupNames.length); + for (String groupName : groupNames) { + String cleanedGroupName = groupName.trim(); + if ( cleanedGroupName.length() > 0) { + try { + groupsList.add( ReflectHelper.classForName( cleanedGroupName ) ); + } + catch ( ClassNotFoundException e ) { + throw new HibernateException( "Unable to load class " + cleanedGroupName, e ); + } + } + + } + groups = groupsList.toArray( new Class[groupsList.size()] ); + } + } + else if ( property instanceof Class[] ) { + groups = (Class[]) property; + } + else { + //null is bad and excluded by instanceof => exception is raised + throw new HibernateException( JPA_GROUP_PREFIX + operation.getGroupPropertyName() + " is of unknown type: String or Class[] only"); + } + } + groupsPerOperation.put( operation, groups ); + } + + public boolean onPreInsert(PreInsertEvent event) { + validate( event.getEntity(), event.getSession().getEntityMode(), Operation.INSERT ); + return false; + } + + public boolean onPreUpdate(PreUpdateEvent event) { + validate( event.getEntity(), event.getSession().getEntityMode(), Operation.UPDATE ); + return false; + } + + public boolean onPreDelete(PreDeleteEvent event) { + validate( event.getEntity(), event.getSession().getEntityMode(), Operation.DELETE ); + return false; + } + + private void validate(T object, EntityMode mode, Operation operation) { + if ( object == null || mode != EntityMode.POJO ) return; + Validator validator = factory.usingContext() + //.traversableResolver( tr ) + .getValidator(); + final Class[] groups = groupsPerOperation.get( operation ); + if ( groups.length > 0 ) { + final Set> constraintViolations = + validator.validate( object, groups ); + //FIXME CV should no longer be generics + Object unsafeViolations = constraintViolations; + if (constraintViolations.size() > 0 ) { + //FIXME add Set> + throw new ConstraintViolationException( + "Invalid object at " + operation.getName() + " time for groups " + toString( groups ), + (Set) unsafeViolations); + } + } + } + + private String toString(Class[] groups) { + StringBuilder toString = new StringBuilder( "["); + for ( Class group : groups ) { + toString.append( group.getName() ).append( ", " ); + } + toString.append( "]" ); + return toString.toString(); + } + + private static enum Operation { + INSERT("persist", "pre-persist"), + UPDATE("update", "pre-update"), + DELETE("remove", "pre-remove"); + + private String exposedName; + private String groupPropertyName; + + Operation(String exposedName, String groupProperty) { + this.exposedName = exposedName; + this.groupPropertyName = groupProperty; + } + + public String getName() { + return exposedName; + } + + public String getGroupPropertyName() { + return groupPropertyName; + } + } + +} diff --git a/annotations/src/main/java/org/hibernate/cfg/beanvalidation/TypeSafeActivator.java b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/TypeSafeActivator.java new file mode 100644 index 0000000000..61bc99a26d --- /dev/null +++ b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/TypeSafeActivator.java @@ -0,0 +1,79 @@ +package org.hibernate.cfg.beanvalidation; + +import java.util.Map; +import java.util.Arrays; +import java.util.Properties; +import javax.validation.ValidatorFactory; +import javax.validation.Validation; + +import org.hibernate.HibernateException; +import org.hibernate.event.EventListeners; +import org.hibernate.event.PreInsertEventListener; +import org.hibernate.event.PreUpdateEventListener; +import org.hibernate.event.PreDeleteEventListener; + +/** + * @author Emmanuel Bernard + */ +class TypeSafeActivator { + + private static final String FACTORY_PROPERTY = "javax.persistence.validation.factory"; + + public static void activateBeanValidation(EventListeners eventListeners, Properties properties) { + ValidatorFactory factory = getValidatorFactory( properties ); + BeanValidationEventListener beanValidationEventListener = new BeanValidationEventListener( factory, properties ); + + { + PreInsertEventListener[] listeners = eventListeners.getPreInsertEventListeners(); + int length = listeners.length + 1; + PreInsertEventListener[] newListeners = new PreInsertEventListener[length]; + System.arraycopy( listeners, 0, newListeners, 0, length - 1 ); + newListeners[length - 1] = beanValidationEventListener; + eventListeners.setPreInsertEventListeners( newListeners ); + } + + { + PreUpdateEventListener[] listeners = eventListeners.getPreUpdateEventListeners(); + int length = listeners.length + 1; + PreUpdateEventListener[] newListeners = new PreUpdateEventListener[length]; + System.arraycopy( listeners, 0, newListeners, 0, length - 1 ); + newListeners[length - 1] = beanValidationEventListener; + eventListeners.setPreUpdateEventListeners( newListeners ); + } + + { + PreDeleteEventListener[] listeners = eventListeners.getPreDeleteEventListeners(); + int length = listeners.length + 1; + PreDeleteEventListener[] newListeners = new PreDeleteEventListener[length]; + System.arraycopy( listeners, 0, newListeners, 0, length - 1 ); + newListeners[length - 1] = beanValidationEventListener; + eventListeners.setPreDeleteEventListeners( newListeners ); + } + } + + static ValidatorFactory getValidatorFactory(Map properties) { + ValidatorFactory factory = null; + if ( properties != null ) { + Object unsafeProperty = properties.get( FACTORY_PROPERTY ); + if (unsafeProperty != null) { + try { + factory = ValidatorFactory.class.cast( unsafeProperty ); + } + catch ( ClassCastException e ) { + throw new HibernateException( "Property " + FACTORY_PROPERTY + + " should containt an object of type " + ValidatorFactory.class.getName() ); + } + } + } + if (factory == null) { + try { + factory = Validation.buildDefaultValidatorFactory(); + } + catch ( Exception e ) { + throw new HibernateException( "Unable to build the default ValidatorFactory", e); + } + } + return factory; + } + +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/BeanValidationAutoTest.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/BeanValidationAutoTest.java new file mode 100644 index 0000000000..5f4aaadb5c --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/BeanValidationAutoTest.java @@ -0,0 +1,36 @@ +package org.hibernate.test.annotations.beanvalidation; + +import java.math.BigDecimal; +import javax.validation.ConstraintViolationException; + +import org.hibernate.Session; +import org.hibernate.Transaction; +import org.hibernate.test.annotations.TestCase; + +/** + * @author Emmanuel Bernard + */ +public class BeanValidationAutoTest extends TestCase { + public void testListeners() { + CupHolder ch = new CupHolder(); + ch.setRadius( new BigDecimal( "12" ) ); + Session s = openSession( ); + Transaction tx = s.beginTransaction(); + try { + s.persist( ch ); + s.flush(); + fail("invalid object should not be persisted"); + } + catch ( ConstraintViolationException e ) { + assertEquals( 1, e.getConstraintViolations().size() ); + } + tx.rollback(); + s.close(); + } + + protected Class[] getMappings() { + return new Class[] { + CupHolder.class + }; + } +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/BeanValidationDisabledTest.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/BeanValidationDisabledTest.java new file mode 100644 index 0000000000..1521b6c944 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/BeanValidationDisabledTest.java @@ -0,0 +1,42 @@ +package org.hibernate.test.annotations.beanvalidation; + +import java.math.BigDecimal; +import javax.validation.ConstraintViolationException; + +import org.hibernate.Session; +import org.hibernate.Transaction; +import org.hibernate.cfg.Configuration; +import org.hibernate.test.annotations.TestCase; + +/** + * @author Emmanuel Bernard + */ +public class BeanValidationDisabledTest extends TestCase { + public void testListeners() { + CupHolder ch = new CupHolder(); + ch.setRadius( new BigDecimal( "12" ) ); + Session s = openSession( ); + Transaction tx = s.beginTransaction(); + try { + s.persist( ch ); + s.flush(); + } + catch ( ConstraintViolationException e ) { + fail("invalid object should not be validated"); + } + tx.rollback(); + s.close(); + } + + @Override + protected void configure(Configuration cfg) { + super.configure( cfg ); + cfg.setProperty( "javax.persistence.validation.mode", "none" ); + } + + protected Class[] getMappings() { + return new Class[] { + CupHolder.class + }; + } +} \ No newline at end of file diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/BeanValidationGroupsTest.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/BeanValidationGroupsTest.java new file mode 100644 index 0000000000..21af462e3e --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/BeanValidationGroupsTest.java @@ -0,0 +1,68 @@ +package org.hibernate.test.annotations.beanvalidation; + +import java.math.BigDecimal; +import javax.validation.ConstraintViolationException; +import javax.validation.constraints.NotNull; +import javax.validation.groups.Default; + +import org.hibernate.Session; +import org.hibernate.Transaction; +import org.hibernate.cfg.Configuration; +import org.hibernate.cfg.annotations.reflection.XMLContext; +import org.hibernate.test.annotations.TestCase; + +/** + * @author Emmanuel Bernard + */ +public class BeanValidationGroupsTest extends TestCase { + public void testListeners() { + CupHolder ch = new CupHolder(); + ch.setRadius( new BigDecimal( "12" ) ); + Session s = openSession( ); + Transaction tx = s.beginTransaction(); + try { + s.persist( ch ); + s.flush(); + } + catch ( ConstraintViolationException e ) { + fail("invalid object should not be validated"); + } + try { + ch.setRadius( null ); + s.flush(); + } + catch ( ConstraintViolationException e ) { + fail("invalid object should not be validated"); + } + try { + s.delete( ch ); + s.flush(); + fail("invalid object should not be persisted"); + } + catch ( ConstraintViolationException e ) { + assertEquals( 1, e.getConstraintViolations().size() ); + assertEquals( NotNull.class, + e.getConstraintViolations().iterator().next().getConstraintDescriptor().getAnnotation().annotationType() + ); + } + tx.rollback(); + s.close(); + } + + @Override + protected void configure(Configuration cfg) { + super.configure( cfg ); + cfg.setProperty( "javax.persistence.validation.group.pre-persist", + "" ); + cfg.setProperty( "javax.persistence.validation.group.pre-update", + "" ); + cfg.setProperty( "javax.persistence.validation.group.pre-remove", + Default.class.getName() + ", " + Strict.class.getName() ); + } + + protected Class[] getMappings() { + return new Class[] { + CupHolder.class + }; + } +} \ No newline at end of file diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/CupHolder.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/CupHolder.java new file mode 100644 index 0000000000..0cce754fd2 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/CupHolder.java @@ -0,0 +1,37 @@ +package org.hibernate.test.annotations.beanvalidation; + +import java.math.BigDecimal; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.validation.constraints.Max; +import javax.validation.constraints.NotNull; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class CupHolder { + @Id + @GeneratedValue + private Integer id; + private BigDecimal radius; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Max( value = 10, message = "Radius way out") + @NotNull(groups = Strict.class) + public BigDecimal getRadius() { + return radius; + } + + public void setRadius(BigDecimal radius) { + this.radius = radius; + } +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Strict.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Strict.java new file mode 100644 index 0000000000..2704c4d277 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Strict.java @@ -0,0 +1,7 @@ +package org.hibernate.test.annotations.beanvalidation; + +/** + * @author Emmanuel Bernard + */ +public interface Strict { +}