diff --git a/annotations/src/main/java/org/hibernate/cfg/AnnotationConfiguration.java b/annotations/src/main/java/org/hibernate/cfg/AnnotationConfiguration.java index e8723f5531..84c71f633a 100644 --- a/annotations/src/main/java/org/hibernate/cfg/AnnotationConfiguration.java +++ b/annotations/src/main/java/org/hibernate/cfg/AnnotationConfiguration.java @@ -396,6 +396,11 @@ public class AnnotationConfiguration extends Configuration { } } } + applyDDLOnBeanValidation( (Collection) classes.values(), getProperties() ); + } + + private void applyDDLOnBeanValidation(Collection persistentClasses, Properties properties) { + BeanValidationActivator.applyDDL( persistentClasses, properties ); } /** diff --git a/annotations/src/main/java/org/hibernate/cfg/beanvalidation/BeanValidationActivator.java b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/BeanValidationActivator.java index df37cb2469..0f136e4ffe 100644 --- a/annotations/src/main/java/org/hibernate/cfg/beanvalidation/BeanValidationActivator.java +++ b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/BeanValidationActivator.java @@ -2,6 +2,9 @@ package org.hibernate.cfg.beanvalidation; import java.util.Map; import java.util.Properties; +import java.util.Set; +import java.util.HashSet; +import java.util.Collection; import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; @@ -11,6 +14,8 @@ import org.slf4j.LoggerFactory; import org.hibernate.util.ReflectHelper; import org.hibernate.HibernateException; import org.hibernate.AssertionFailure; +import org.hibernate.cfg.Environment; +import org.hibernate.mapping.PersistentClass; import org.hibernate.event.EventListeners; /** @@ -22,34 +27,73 @@ 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_DDL_METHOD = "applyDDL"; 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; + Set modes = ValidationMode.getModes( properties.get( MODE_PROPERTY ) ); + if ( modes.contains( ValidationMode.NONE ) ) return; + //desactivate not-null tracking at the core level when Bean Validation is on unless the user really ask for it + if ( properties.getProperty( Environment.CHECK_NULLABILITY ) == null ) { + properties.setProperty( Environment.CHECK_NULLABILITY, "false" ); + } + try { //load Validation ReflectHelper.classForName( BV_DISCOVERY_CLASS, BeanValidationActivator.class ); } catch ( ClassNotFoundException e ) { - if (mode == ValidationMode.CALLBACK) { + if ( modes.contains( ValidationMode.CALLBACK ) ) { throw new HibernateException( "Bean Validation not available in the class path but required in " + MODE_PROPERTY ); } - else if (mode == ValidationMode.AUTO) { + else if (modes.contains( 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 = + Method activateBeanValidation = activator.getMethod( TYPE_SAFE_ACTIVATOR_METHOD, EventListeners.class, Properties.class ); - buildDefaultValidatorFactory.invoke( null, eventListeners, properties ); + activateBeanValidation.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); + } + } + + public static void applyDDL(Collection persistentClasses, Properties properties) { + Set modes = ValidationMode.getModes( properties.get( MODE_PROPERTY ) ); + if ( ! ( modes.contains( ValidationMode.DDL ) || modes.contains( ValidationMode.AUTO ) ) ) return; + try { + //load Validation + ReflectHelper.classForName( BV_DISCOVERY_CLASS, BeanValidationActivator.class ); + } + catch ( ClassNotFoundException e ) { + if ( modes.contains( ValidationMode.DDL ) ) { + throw new HibernateException( "Bean Validation not available in the class path but required in " + MODE_PROPERTY ); + } + else if (modes.contains( ValidationMode.AUTO ) ) { + //nothing to activate + return; + } + } + try { + Class activator = ReflectHelper.classForName( TYPE_SAFE_ACTIVATOR_CLASS, BeanValidationActivator.class ); + Method applyDDL = + activator.getMethod( TYPE_SAFE_DDL_METHOD, Collection.class, Properties.class ); + applyDDL.invoke( null, persistentClasses, properties ); } catch ( NoSuchMethodException e ) { throw new HibernateException( "Unable to get the default Bean Validation factory", e); @@ -68,15 +112,37 @@ public class BeanValidationActivator { private static enum ValidationMode { AUTO, CALLBACK, - NONE; + NONE, + DDL; - public static ValidationMode getMode(Object modeProperty) { + public static Set getModes(Object modeProperty) { + Set modes = new HashSet(3); if (modeProperty == null) { + modes.add(ValidationMode.AUTO); + } + else { + final String[] modesInString = modeProperty.toString().split( "," ); + for ( String modeInString : modesInString ) { + modes.add( getMode(modeInString) ); + } + } + if ( modes.size() > 1 && ( modes.contains( ValidationMode.AUTO ) || modes.contains( ValidationMode.NONE ) ) ) { + StringBuilder message = new StringBuilder( "Incompatible validation modes mixed: " ); + for (ValidationMode mode : modes) { + message.append( mode ).append( ", " ); + } + throw new HibernateException( message.substring( 0, message.length() - 2 ) ); + } + return modes; + } + + private static ValidationMode getMode(String modeProperty) { + if (modeProperty == null || modeProperty.length() == 0) { return AUTO; } else { try { - return valueOf( modeProperty.toString().toUpperCase() ); + return valueOf( modeProperty.trim().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 index 0d2ad49e50..1bf64614d9 100644 --- a/annotations/src/main/java/org/hibernate/cfg/beanvalidation/BeanValidationEventListener.java +++ b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/BeanValidationEventListener.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.HashMap; import java.util.List; import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; import javax.validation.ValidatorFactory; import javax.validation.ConstraintViolation; import javax.validation.TraversableResolver; @@ -21,6 +22,8 @@ import org.hibernate.event.PreUpdateEvent; import org.hibernate.event.PreDeleteEvent; import org.hibernate.EntityMode; import org.hibernate.HibernateException; +import org.hibernate.engine.SessionFactoryImplementor; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.util.ReflectHelper; /** @@ -29,83 +32,45 @@ import org.hibernate.util.ReflectHelper; //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); + private ConcurrentHashMap> associationsPerEntityPersister = + new ConcurrentHashMap>(); + private GroupsPerOperation groupsPerOperation; public BeanValidationEventListener(ValidatorFactory factory, Properties properties) { this.factory = factory; - setGroupsForOperation( Operation.INSERT, properties ); - setGroupsForOperation( Operation.UPDATE, properties ); - setGroupsForOperation( Operation.DELETE, properties ); + groupsPerOperation = new GroupsPerOperation(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 ); + validate( event.getEntity(), event.getSession().getEntityMode(), event.getPersister(), + event.getSession().getFactory(), GroupsPerOperation.Operation.INSERT ); return false; } public boolean onPreUpdate(PreUpdateEvent event) { - validate( event.getEntity(), event.getSession().getEntityMode(), Operation.UPDATE ); + validate( event.getEntity(), event.getSession().getEntityMode(), event.getPersister(), + event.getSession().getFactory(), GroupsPerOperation.Operation.UPDATE ); return false; } public boolean onPreDelete(PreDeleteEvent event) { - validate( event.getEntity(), event.getSession().getEntityMode(), Operation.DELETE ); + validate( event.getEntity(), event.getSession().getEntityMode(), event.getPersister(), + event.getSession().getFactory(), GroupsPerOperation.Operation.DELETE ); return false; } - private void validate(T object, EntityMode mode, Operation operation) { + private void validate(T object, EntityMode mode, EntityPersister persister, + SessionFactoryImplementor sessionFactory, GroupsPerOperation.Operation operation) { if ( object == null || mode != EntityMode.POJO ) return; + TraversableResolver tr = new HibernateTraversableResolver( persister, associationsPerEntityPersister, sessionFactory ); Validator validator = factory.usingContext() - //.traversableResolver( tr ) + .traversableResolver( tr ) .getValidator(); final Class[] groups = groupsPerOperation.get( operation ); if ( groups.length > 0 ) { @@ -117,7 +82,7 @@ public class BeanValidationEventListener implements //FIXME add Set> throw new ConstraintViolationException( "Invalid object at " + operation.getName() + " time for groups " + toString( groups ), - (Set) unsafeViolations); + (Set>) unsafeViolations); } } } @@ -131,26 +96,6 @@ public class BeanValidationEventListener implements 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/GroupsPerOperation.java b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/GroupsPerOperation.java new file mode 100644 index 0000000000..1023e224c3 --- /dev/null +++ b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/GroupsPerOperation.java @@ -0,0 +1,101 @@ +package org.hibernate.cfg.beanvalidation; + +import java.util.Properties; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import javax.validation.groups.Default; + +import org.hibernate.util.ReflectHelper; +import org.hibernate.HibernateException; + +/** + * @author Emmanuel Bernard + */ +public class GroupsPerOperation { + + private static final String JPA_GROUP_PREFIX = "javax.persistence.validation.group."; + private static final String HIBERNATE_GROUP_PREFIX = "org.hibernate.validator.group."; + private static final Class[] DEFAULT_GROUPS = new Class[] { Default.class }; + private static final Class[] EMPTY_GROUPS = new Class[] { }; + + private Map[]> groupsPerOperation = new HashMap[]>(4); + + public GroupsPerOperation(Properties properties) { + setGroupsForOperation( Operation.INSERT, properties ); + setGroupsForOperation( Operation.UPDATE, properties ); + setGroupsForOperation( Operation.DELETE, properties ); + setGroupsForOperation( Operation.DDL, properties ); + } + + private void setGroupsForOperation(Operation operation, Properties properties) { + Object property = properties.get( 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 Class[] get(Operation operation) { + return groupsPerOperation.get( operation ); + } + + public static enum Operation { + INSERT("persist", JPA_GROUP_PREFIX + "pre-persist"), + UPDATE("update", JPA_GROUP_PREFIX + "pre-update"), + DELETE("remove", JPA_GROUP_PREFIX + "pre-remove"), + DDL("ddl", HIBERNATE_GROUP_PREFIX + "ddl"); + + + 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/HibernateTraversableResolver.java b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/HibernateTraversableResolver.java new file mode 100644 index 0000000000..8a3ad553b4 --- /dev/null +++ b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/HibernateTraversableResolver.java @@ -0,0 +1,92 @@ +package org.hibernate.cfg.beanvalidation; + +import java.lang.annotation.ElementType; +import java.util.Set; +import java.util.HashSet; +import java.util.concurrent.ConcurrentHashMap; +import javax.validation.TraversableResolver; + +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.Hibernate; +import org.hibernate.engine.SessionFactoryImplementor; +import org.hibernate.type.Type; +import org.hibernate.type.ComponentType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.AbstractComponentType; + +/** + * @author Emmanuel Bernard + */ +public class HibernateTraversableResolver implements TraversableResolver { + private Set associations; + + public HibernateTraversableResolver( + EntityPersister persister, + ConcurrentHashMap> associationsPerEntityPersister, + SessionFactoryImplementor factory) { + this.associations = associationsPerEntityPersister.get( persister ); + if (this.associations == null) { + this.associations = new HashSet(); + addAssociationsToTheSetForAllProperties( persister.getPropertyNames(), persister.getPropertyTypes(), "", factory ); + associationsPerEntityPersister.put( persister, associations ); + } + } + + private void addAssociationsToTheSetForAllProperties(String[] names, Type[] types, String prefix, SessionFactoryImplementor factory) { + final int length = names.length; + for( int index = 0 ; index < length; index++ ) { + addAssociationsToTheSetForOneProperty( names[index], types[index], prefix, factory ); + } + } + + private void addAssociationsToTheSetForOneProperty(String name, Type type, String prefix, SessionFactoryImplementor factory) { + + if ( type.isCollectionType() ) { + CollectionType collType = (CollectionType) type; + Type assocType = collType.getElementType( factory ); + addAssociationsToTheSetForOneProperty(name, assocType, prefix, factory); + } + //ToOne association + else if ( type.isEntityType() || type.isAnyType() ) { + associations.add( prefix + name ); + } else if ( type.isComponentType() ) { + AbstractComponentType componentType = (AbstractComponentType) type; + addAssociationsToTheSetForAllProperties( + componentType.getPropertyNames(), + componentType.getSubtypes(), + (prefix.equals( "" ) ? name : prefix + name) + ".", + factory); + } + } + + private String getCleanPathWoBraket(String traversableProperty, String pathToTraversableObject) { + String path = pathToTraversableObject.equals( "" ) ? + traversableProperty : + pathToTraversableObject + "." + traversableProperty; + String[] paths = path.split( "\\[.*\\]" ); + path = ""; + for (String subpath : paths) { + path += subpath; + } + return path; + } + + public boolean isReachable(Object traversableObject, + String traversableProperty, + Class rootBeanType, + String pathToTraversableObject, + ElementType elementType) { + //lazy, don't load + return Hibernate.isInitialized( traversableObject ) + && Hibernate.isPropertyInitialized( traversableObject, traversableProperty ); + } + + public boolean isCascadable(Object traversableObject, + String traversableProperty, + Class rootBeanType, + String pathToTraversableObject, + ElementType elementType) { + String path = getCleanPathWoBraket( traversableProperty, pathToTraversableObject ); + return ! associations.contains(path); + } +} diff --git a/annotations/src/main/java/org/hibernate/cfg/beanvalidation/TypeSafeActivator.java b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/TypeSafeActivator.java index 61bc99a26d..f35ae05941 100644 --- a/annotations/src/main/java/org/hibernate/cfg/beanvalidation/TypeSafeActivator.java +++ b/annotations/src/main/java/org/hibernate/cfg/beanvalidation/TypeSafeActivator.java @@ -1,22 +1,49 @@ package org.hibernate.cfg.beanvalidation; -import java.util.Map; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; import java.util.Properties; -import javax.validation.ValidatorFactory; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.Collection; +import javax.validation.BeanDescriptor; +import javax.validation.ConstraintDescriptor; +import javax.validation.PropertyDescriptor; import javax.validation.Validation; +import javax.validation.ValidatorFactory; +import javax.validation.constraints.Digits; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; +import org.hibernate.MappingException; import org.hibernate.event.EventListeners; +import org.hibernate.event.PreDeleteEventListener; import org.hibernate.event.PreInsertEventListener; import org.hibernate.event.PreUpdateEventListener; -import org.hibernate.event.PreDeleteEventListener; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.Component; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.mapping.SingleTableSubclass; +import org.hibernate.util.ReflectHelper; /** * @author Emmanuel Bernard */ class TypeSafeActivator { + private static final Logger logger = LoggerFactory.getLogger( TypeSafeActivator.class ); + private static final String FACTORY_PROPERTY = "javax.persistence.validation.factory"; public static void activateBeanValidation(EventListeners eventListeners, Properties properties) { @@ -51,7 +78,209 @@ class TypeSafeActivator { } } - static ValidatorFactory getValidatorFactory(Map properties) { + public static void applyDDL(Collection persistentClasses, Properties properties) { + ValidatorFactory factory = getValidatorFactory( properties ); + Class[] groupsArray = new GroupsPerOperation( properties ).get( GroupsPerOperation.Operation.DDL ); + Set> groups = new HashSet>( Arrays.asList( groupsArray ) ); + + for ( PersistentClass persistentClass : persistentClasses ) { + final String className = persistentClass.getClassName(); + + if ( className == null || className.length() == 0) continue; + Class clazz; + try { + clazz = ReflectHelper.classForName( className, TypeSafeActivator.class ); + } + catch ( ClassNotFoundException e ) { + throw new AssertionFailure( "Entity class not found", e); + } + + try { + applyDDL( "", persistentClass, clazz, factory, groups, true ); + } + catch (Exception e) { + logger.warn( "Unable to apply constraints on DDL for " + className, e ); + } + } + } + + private static void applyDDL(String prefix, + PersistentClass persistentClass, + Class clazz, + ValidatorFactory factory, + Set> groups, + boolean activateNotNull) { + final BeanDescriptor descriptor = factory.getValidator().getConstraintsForClass( clazz ); + //no bean level constraints can be applied, go to the properties + + for ( PropertyDescriptor propertyDesc : descriptor.getConstrainedProperties() ) { + Property property = findPropertyByName( persistentClass, prefix + propertyDesc.getPropertyName() ); + boolean hasNotNull = false; + if ( property != null ) { + hasNotNull = applyConstraints( propertyDesc.getConstraintDescriptors(), property, propertyDesc, groups, activateNotNull ); + if ( property.isComposite() && propertyDesc.isCascaded() ) { + Class componentClass = ( ( Component ) property.getValue() ).getComponentClass(); + + /* + * we can apply not null if the upper component let's us activate not null + * and if the property is not null. + * Otherwise, all sub columns should be left nullable + */ + final boolean canSetNotNullOnColumns = activateNotNull && hasNotNull; + applyDDL( prefix + propertyDesc.getPropertyName() + ".", + persistentClass, componentClass, factory, groups, + canSetNotNullOnColumns + ); + } + //FIXME add collection of components + } + } + } + + private static boolean applyConstraints(Set> constraintDescriptors, + Property property, + PropertyDescriptor propertyDesc, + Set> groups, boolean canApplyNotNull) { + boolean hasNotNull = false; + for (ConstraintDescriptor descriptor : constraintDescriptors) { + if ( groups != null && Collections.disjoint( descriptor.getGroups(), groups) ) continue; + + if ( canApplyNotNull ) { + hasNotNull = hasNotNull || applyNotNull( property, descriptor ); + } + applyDigits( property, descriptor ); + applySize( property, descriptor, propertyDesc ); + applyMin( property, descriptor ); + applyMax( property, descriptor ); + + //pass an empty set as composing constraints inherit the main constraint and thus are matching already + hasNotNull = hasNotNull || applyConstraints( + descriptor.getComposingConstraints(), + property, propertyDesc, null, + canApplyNotNull ); + } + return hasNotNull; + } + + private static void applyMin(Property property, ConstraintDescriptor descriptor) { + if ( Min.class.equals( descriptor.getAnnotation().annotationType() ) ) { + @SuppressWarnings( "unchecked" ) + ConstraintDescriptor minConstraint = (ConstraintDescriptor) descriptor; + long min = minConstraint.getAnnotation().value(); + Column col = (Column) property.getColumnIterator().next(); + col.setCheckConstraint( col.getName() + ">=" + min ); + } + } + + private static void applyMax(Property property, ConstraintDescriptor descriptor) { + if ( Max.class.equals( descriptor.getAnnotation().annotationType() ) ) { + @SuppressWarnings( "unchecked" ) + ConstraintDescriptor maxConstraint = (ConstraintDescriptor) descriptor; + long max = maxConstraint.getAnnotation().value(); + Column col = (Column) property.getColumnIterator().next(); + col.setCheckConstraint( col.getName() + "<=" + max ); + } + } + + private static boolean applyNotNull(Property property, ConstraintDescriptor descriptor) { + boolean hasNotNull = false; + if ( NotNull.class.equals( descriptor.getAnnotation().annotationType() ) ) { + if ( ! ( property.getPersistentClass() instanceof SingleTableSubclass ) ) { + //single table should not be forced to null + if ( !property.isComposite() ) { //composite should not add not-null on all columns + @SuppressWarnings( "unchecked" ) + Iterator iter = (Iterator) property.getColumnIterator(); + while ( iter.hasNext() ) { + iter.next().setNullable( false ); + hasNotNull = true; + } + } + } + hasNotNull = true; + } + return hasNotNull; + } + + private static void applyDigits(Property property, ConstraintDescriptor descriptor) { + if ( Digits.class.equals( descriptor.getAnnotation().annotationType() ) ) { + @SuppressWarnings( "unchecked" ) + ConstraintDescriptor digitsConstraint = (ConstraintDescriptor) descriptor; + int integerDigits = digitsConstraint.getAnnotation().integer(); + int fractionalDigits = digitsConstraint.getAnnotation().fraction(); + Column col = (Column) property.getColumnIterator().next(); + col.setPrecision( integerDigits + fractionalDigits ); + col.setScale( fractionalDigits ); + } + } + + private static void applySize(Property property, ConstraintDescriptor descriptor, PropertyDescriptor propertyDesc) { + if ( Size.class.equals( descriptor.getAnnotation().annotationType() ) + && String.class.equals( propertyDesc.getType() ) ) { + @SuppressWarnings( "unchecked" ) + ConstraintDescriptor sizeConstraint = (ConstraintDescriptor) descriptor; + int max = sizeConstraint.getAnnotation().max(); + Column col = (Column) property.getColumnIterator().next(); + if ( max < Integer.MAX_VALUE ) col.setLength( max ); + } + } + + /** + * Retrieve the property by path in a recursive way, including IndetifierProperty in the loop + * If propertyName is null or empty, the IdentifierProperty is returned + */ + private static Property findPropertyByName(PersistentClass associatedClass, String propertyName) { + Property property = null; + Property idProperty = associatedClass.getIdentifierProperty(); + String idName = idProperty != null ? idProperty.getName() : null; + try { + if ( propertyName == null + || propertyName.length() == 0 + || propertyName.equals( idName ) ) { + //default to id + property = idProperty; + } + else { + if ( propertyName.indexOf( idName + "." ) == 0 ) { + property = idProperty; + propertyName = propertyName.substring( idName.length() + 1 ); + } + StringTokenizer st = new StringTokenizer( propertyName, ".", false ); + while ( st.hasMoreElements() ) { + String element = (String) st.nextElement(); + if ( property == null ) { + property = associatedClass.getProperty( element ); + } + else { + if ( ! property.isComposite() ) return null; + property = ( ( Component ) property.getValue() ).getProperty( element ); + } + } + } + } + catch ( MappingException e) { + try { + //if we do not find it try to check the identifier mapper + if ( associatedClass.getIdentifierMapper() == null ) return null; + StringTokenizer st = new StringTokenizer( propertyName, ".", false ); + while ( st.hasMoreElements() ) { + String element = (String) st.nextElement(); + if ( property == null ) { + property = associatedClass.getIdentifierMapper().getProperty( element ); + } + else { + if ( ! property.isComposite() ) return null; + property = ( (Component) property.getValue() ).getProperty( element ); + } + } + } + catch (MappingException ee) { + return null; + } + } + return property; + } + + private static ValidatorFactory getValidatorFactory(Map properties) { ValidatorFactory factory = null; if ( properties != null ) { Object unsafeProperty = properties.get( FACTORY_PROPERTY ); diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Address.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Address.java new file mode 100644 index 0000000000..d9102304f4 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Address.java @@ -0,0 +1,103 @@ +package org.hibernate.test.annotations.beanvalidation; + +import javax.persistence.Entity; +import javax.persistence.Transient; +import javax.persistence.Id; +import javax.validation.constraints.Size; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Min; +import javax.validation.constraints.Max; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.AssertTrue; + +@Entity +public class Address { + @NotNull + public static String blacklistedZipCode; + + private String line1; + private String line2; + private String zip; + private String state; + @Size(max = 20) + @NotNull + private String country; + private long id; + private boolean internalValid = true; + @Min(-2) @Max(value=50) + public int floor; + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + @NotNull + public String getLine1() { + return line1; + } + + public void setLine1(String line1) { + this.line1 = line1; + } + + public String getLine2() { + return line2; + } + + public void setLine2(String line2) { + this.line2 = line2; + } + + @Size(max = 3) + @NotNull + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + @Size(max = 5) + @Pattern(regexp = "[0-9]+") + @NotNull + public String getZip() { + return zip; + } + + public void setZip(String zip) { + this.zip = zip; + } + + @AssertTrue + @Transient + public boolean isValid() { + return true; + } + + @AssertTrue + @Transient + private boolean isInternalValid() { + return internalValid; + } + + public void setInternalValid(boolean internalValid) { + this.internalValid = internalValid; + } + + @Id + @Min(1) + @Max(2000) + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Button.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Button.java new file mode 100644 index 0000000000..34546f39c3 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Button.java @@ -0,0 +1,34 @@ +package org.hibernate.test.annotations.beanvalidation; + +import javax.persistence.Embeddable; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Max; + +/** + * @author Emmanuel Bernard + */ +@Embeddable +public class Button { + + private String name; + + private Integer size; + + @NotNull + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Max( 10 ) + public Integer getSize() { + return size; + } + + public void setSize(Integer size) { + this.size = size; + } +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Color.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Color.java new file mode 100644 index 0000000000..b2236ca2cd --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Color.java @@ -0,0 +1,33 @@ +package org.hibernate.test.annotations.beanvalidation; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.GeneratedValue; +import javax.validation.constraints.NotNull; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class Color { + private Integer id; + private String name; + + @Id @GeneratedValue + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @NotNull + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/DDLTest.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/DDLTest.java new file mode 100644 index 0000000000..4bb4bd03c1 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/DDLTest.java @@ -0,0 +1,60 @@ +package org.hibernate.test.annotations.beanvalidation; + +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.Property; +import org.hibernate.test.annotations.TestCase; + +/** + * @author Emmanuel Bernard + */ +public class DDLTest extends TestCase { + + public void testBasicDDL() { + PersistentClass classMapping = getCfg().getClassMapping( Address.class.getName() ); + //new ClassValidator( Address.class, ResourceBundle.getBundle("messages", Locale.ENGLISH) ).apply( classMapping ); + Column stateColumn = (Column) classMapping.getProperty( "state" ).getColumnIterator().next(); + assertEquals( stateColumn.getLength(), 3 ); + Column zipColumn = (Column) classMapping.getProperty( "zip" ).getColumnIterator().next(); + assertEquals( zipColumn.getLength(), 5 ); + assertFalse( zipColumn.isNullable() ); + } + + public void testApplyOnIdColumn() throws Exception { + PersistentClass classMapping = getCfg().getClassMapping( Tv.class.getName() ); + Column serialColumn = (Column) classMapping.getIdentifierProperty().getColumnIterator().next(); + assertEquals( "Vaidator annotation not applied on ids", 2, serialColumn.getLength() ); + } + + public void testApplyOnManyToOne() throws Exception { + PersistentClass classMapping = getCfg().getClassMapping( TvOwner.class.getName() ); + Column serialColumn = (Column) classMapping.getProperty( "tv" ).getColumnIterator().next(); + assertEquals( "Validator annotations not applied on associations", false, serialColumn.isNullable() ); + } + + public void testSingleTableAvoidNotNull() throws Exception { + PersistentClass classMapping = getCfg().getClassMapping( Rock.class.getName() ); + Column serialColumn = (Column) classMapping.getProperty( "bit" ).getColumnIterator().next(); + assertTrue( "Notnull should not be applised on single tables", serialColumn.isNullable() ); + } + + public void testNotNullOnlyAppliedIfEmbeddedIsNotNullItself() throws Exception { + PersistentClass classMapping = getCfg().getClassMapping( Tv.class.getName() ); + Property property = classMapping.getProperty( "tuner.frequency" ); + Column serialColumn = (Column) property.getColumnIterator().next(); + assertEquals( "Validator annotations are applied on tunner as it is @NotNull", false, serialColumn.isNullable() ); + + property = classMapping.getProperty( "recorder.time" ); + serialColumn = (Column) property.getColumnIterator().next(); + assertEquals( "Validator annotations are applied on tunner as it is @NotNull", true, serialColumn.isNullable() ); + } + + protected Class[] getMappings() { + return new Class[] { + Address.class, + Tv.class, + TvOwner.class, + Rock.class + }; + } +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Display.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Display.java new file mode 100644 index 0000000000..823610edea --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Display.java @@ -0,0 +1,35 @@ +package org.hibernate.test.annotations.beanvalidation; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.GeneratedValue; +import javax.validation.constraints.NotNull; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class Display { + private Integer id; + + private String brand; + + @Id + @GeneratedValue + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @NotNull + public String getBrand() { + return brand; + } + + public void setBrand(String brand) { + this.brand = brand; + } +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/DisplayConnector.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/DisplayConnector.java new file mode 100644 index 0000000000..5fb02f7d05 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/DisplayConnector.java @@ -0,0 +1,38 @@ +package org.hibernate.test.annotations.beanvalidation; + +import javax.persistence.Embeddable; +import javax.persistence.OneToOne; +import javax.persistence.CascadeType; +import javax.persistence.ManyToOne; +import javax.validation.Valid; +import javax.validation.constraints.Min; + + +/** + * @author Emmanuel Bernard + */ +@Embeddable +public class DisplayConnector { + + private int number; + private Display display; + + @Min(1) + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + @ManyToOne(cascade = CascadeType.PERSIST) + @Valid + public Display getDisplay() { + return display; + } + + public void setDisplay(Display display) { + this.display = display; + } +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/HibernateTraversableResolverTest.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/HibernateTraversableResolverTest.java new file mode 100644 index 0000000000..ea49303fb9 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/HibernateTraversableResolverTest.java @@ -0,0 +1,191 @@ +package org.hibernate.test.annotations.beanvalidation; + +import java.math.BigDecimal; +import javax.validation.ConstraintViolationException; +import javax.validation.ConstraintViolation; + +import org.hibernate.test.annotations.TestCase; +import org.hibernate.Session; +import org.hibernate.Transaction; +import org.hibernate.cfg.Configuration; + +/** + * @author Emmanuel Bernard + */ +public class HibernateTraversableResolverTest extends TestCase { + public void testNonLazyAssocFieldWithConstraintsFailureExpected() { + Session s = openSession( ); + Transaction tx = s.beginTransaction(); + + Screen screen = new Screen(); + screen.setPowerSupply( null ); + try { + s.persist( screen ); + s.flush(); + fail("@NotNull on a non lazy association is not evaluated"); + } + catch ( ConstraintViolationException e ) { + assertEquals( 1, e.getConstraintViolations().size() ); + } + + tx.rollback(); + s.close(); + } + + public void testEmbedded() { + Session s = openSession( ); + Transaction tx = s.beginTransaction(); + + Screen screen = new Screen(); + PowerSupply ps = new PowerSupply(); + screen.setPowerSupply( ps ); + Button button = new Button(); + button.setName( null ); + button.setSize( 3 ); + screen.setStopButton( button ); + try { + s.persist( screen ); + s.flush(); + fail("@NotNull on empedded property is not evaluated"); + } + catch ( ConstraintViolationException e ) { + assertEquals( 1, e.getConstraintViolations().size() ); + ConstraintViolation cv = e.getConstraintViolations().iterator().next(); + assertEquals( Screen.class, cv.getRootBeanClass() ); + assertEquals( "stopButton.name", cv.getPropertyPath() ); + } + + tx.rollback(); + s.close(); + } + + public void testToOneAssocNotValidated() { + Session s = openSession( ); + Transaction tx = s.beginTransaction(); + + Screen screen = new Screen(); + PowerSupply ps = new PowerSupply(); + ps.setPosition( "1" ); + ps.setPower( new BigDecimal( 350 ) ); + screen.setPowerSupply( ps ); + try { + s.persist( screen ); + s.flush(); + fail("Associated objects should not be validated"); + } + catch ( ConstraintViolationException e ) { + assertEquals( 1, e.getConstraintViolations().size() ); + final ConstraintViolation constraintViolation = e.getConstraintViolations().iterator().next(); + assertEquals( PowerSupply.class, constraintViolation.getRootBeanClass() ); + } + + tx.rollback(); + s.close(); + } + + public void testCollectionAssocNotValidated() { + Session s = openSession( ); + Transaction tx = s.beginTransaction(); + + Screen screen = new Screen(); + screen.setStopButton( new Button() ); + screen.getStopButton().setName( "STOOOOOP" ); + PowerSupply ps = new PowerSupply(); + screen.setPowerSupply( ps ); + Color c = new Color(); + c.setName( "Blue" ); + s.persist( c ); + c.setName( null ); + screen.getDisplayColors().add( c ); + try { + s.persist( screen ); + s.flush(); + fail("Associated objects should not be validated"); + } + catch ( ConstraintViolationException e ) { + assertEquals( 1, e.getConstraintViolations().size() ); + final ConstraintViolation constraintViolation = e.getConstraintViolations().iterator().next(); + assertEquals( Color.class, constraintViolation.getRootBeanClass() ); + } + + tx.rollback(); + s.close(); + } + + public void testEmbeddedCollection() { + Session s = openSession( ); + Transaction tx = s.beginTransaction(); + + Screen screen = new Screen(); + PowerSupply ps = new PowerSupply(); + screen.setPowerSupply( ps ); + DisplayConnector conn = new DisplayConnector(); + conn.setNumber( 0 ); + screen.getConnectors().add( conn ); + try { + s.persist( screen ); + s.flush(); + fail("Collection of embedded objects should be validated"); + } + catch ( ConstraintViolationException e ) { + assertEquals( 1, e.getConstraintViolations().size() ); + final ConstraintViolation constraintViolation = e.getConstraintViolations().iterator().next(); + assertEquals( Screen.class, constraintViolation.getRootBeanClass() ); + // connectors[0] should be connectors expect failure when bug is fixed in HV + assertEquals( "connectors[0].number", constraintViolation.getPropertyPath() ); + } + + tx.rollback(); + s.close(); + } + + public void testAssocInEmbeddedNotValidated() { + Session s = openSession( ); + Transaction tx = s.beginTransaction(); + + Screen screen = new Screen(); + screen.setStopButton( new Button() ); + screen.getStopButton().setName( "STOOOOOP" ); + PowerSupply ps = new PowerSupply(); + screen.setPowerSupply( ps ); + DisplayConnector conn = new DisplayConnector(); + conn.setNumber( 1 ); + screen.getConnectors().add( conn ); + final Display display = new Display(); + display.setBrand( "dell" ); + conn.setDisplay( display ); + s.persist( display ); + s.flush(); + try { + display.setBrand( null ); + s.persist( screen ); + s.flush(); + fail("Collection of embedded objects should be validated"); + } + catch ( ConstraintViolationException e ) { + assertEquals( 1, e.getConstraintViolations().size() ); + final ConstraintViolation constraintViolation = e.getConstraintViolations().iterator().next(); + assertEquals( Display.class, constraintViolation.getRootBeanClass() ); + } + + tx.rollback(); + s.close(); + } + + @Override + protected void configure(Configuration cfg) { + super.configure( cfg ); + cfg.setProperty( "hibernate.validator.autoregister_listeners", "false" ); + } + + protected Class[] getMappings() { + return new Class[] { + Button.class, + Color.class, + Display.class, + DisplayConnector.class, + PowerSupply.class, + Screen.class + }; + } +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Music.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Music.java new file mode 100644 index 0000000000..4bc84d94d8 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Music.java @@ -0,0 +1,13 @@ +package org.hibernate.test.annotations.beanvalidation; + +import javax.persistence.Entity; +import javax.persistence.Id; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class Music { + @Id + public String name; +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/PowerSupply.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/PowerSupply.java new file mode 100644 index 0000000000..589ebeee33 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/PowerSupply.java @@ -0,0 +1,46 @@ +package org.hibernate.test.annotations.beanvalidation; + +import java.math.BigDecimal; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.GeneratedValue; +import javax.persistence.Column; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class PowerSupply { + private Integer id; + private BigDecimal power; + private String position; + + @Id @GeneratedValue + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Min(100) @Max(250) + public BigDecimal getPower() { + return power; + } + + public void setPower(BigDecimal power) { + this.power = power; + } + + @Column(name="fld_pos") + public String getPosition() { + return position; + } + + public void setPosition(String position) { + this.position = position; + } +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Rock.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Rock.java new file mode 100644 index 0000000000..c7186f8a6e --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Rock.java @@ -0,0 +1,13 @@ +package org.hibernate.test.annotations.beanvalidation; + +import javax.persistence.Entity; +import javax.validation.constraints.NotNull; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class Rock extends Music { + @NotNull + public Integer bit; +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Screen.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Screen.java new file mode 100644 index 0000000000..6123c70df5 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Screen.java @@ -0,0 +1,74 @@ +package org.hibernate.test.annotations.beanvalidation; + +import java.util.Set; +import java.util.HashSet; +import javax.persistence.Entity; +import javax.persistence.ManyToOne; +import javax.persistence.ManyToMany; +import javax.persistence.Id; +import javax.persistence.GeneratedValue; +import javax.persistence.CascadeType; +import javax.validation.constraints.NotNull; +import javax.validation.Valid; + +import org.hibernate.annotations.CollectionOfElements; +import org.hibernate.annotations.Cascade; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class Screen { + private Integer id; + private Button stopButton; + private PowerSupply powerSupply; + private Set connectors = new HashSet(); + private Set displayColors = new HashSet(); + + @Id @GeneratedValue + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + //@NotNull + @Valid + public Button getStopButton() { + return stopButton; + } + + public void setStopButton(Button stopButton) { + this.stopButton = stopButton; + } + + @ManyToOne(cascade = CascadeType.PERSIST) @Valid + @NotNull + public PowerSupply getPowerSupply() { + return powerSupply; + } + + public void setPowerSupply(PowerSupply powerSupply) { + this.powerSupply = powerSupply; + } + + @CollectionOfElements @Valid + public Set getConnectors() { + return connectors; + } + + public void setConnectors(Set connectors) { + this.connectors = connectors; + } + + @ManyToMany(cascade = CascadeType.PERSIST) + public Set getDisplayColors() { + return displayColors; + } + + public void setDisplayColors(Set displayColors) { + this.displayColors = displayColors; + } +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Tv.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Tv.java new file mode 100644 index 0000000000..e92e9cd2c1 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/Tv.java @@ -0,0 +1,50 @@ +package org.hibernate.test.annotations.beanvalidation; + +import java.math.BigInteger; +import java.math.BigDecimal; +import java.util.Date; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Embeddable; +import javax.validation.constraints.Future; +import javax.validation.constraints.Min; +import javax.validation.constraints.Size; +import javax.validation.constraints.NotNull; +import javax.validation.Valid; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class Tv { + + @Id + @Size(max = 2) + public String serial; + public int size; + @Size(max = 2) + public String name; + @Future + public Date expDate; + @Size(min = 0) + public String description; + @Min(1000) + public BigInteger lifetime; + @NotNull @Valid + public Tuner tuner; + @Valid + public Recorder recorder; + + @Embeddable + public static class Tuner { + @NotNull + public String frequency; + } + + @Embeddable + public static class Recorder { + @NotNull + public BigDecimal time; + } + +} diff --git a/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/TvOwner.java b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/TvOwner.java new file mode 100644 index 0000000000..c221193bf7 --- /dev/null +++ b/annotations/src/test/java/org/hibernate/test/annotations/beanvalidation/TvOwner.java @@ -0,0 +1,21 @@ +package org.hibernate.test.annotations.beanvalidation; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.GeneratedValue; +import javax.persistence.ManyToOne; +import javax.validation.constraints.NotNull; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class TvOwner { + @Id + @GeneratedValue + public Integer id; + + @ManyToOne + @NotNull + public Tv tv; +} \ No newline at end of file