ANN-828 Use Bean Validation for DDL generation

git-svn-id: https://svn.jboss.org/repos/hibernate/core/trunk@16516 1b8cb986-b30d-0410-93ca-fae66ebed9b2
This commit is contained in:
Emmanuel Bernard 2009-05-06 15:52:26 +00:00
parent e8dfecb5f7
commit a7930671d9
19 changed files with 1239 additions and 90 deletions

View File

@ -396,6 +396,11 @@ public class AnnotationConfiguration extends Configuration {
}
}
}
applyDDLOnBeanValidation( (Collection<PersistentClass>) classes.values(), getProperties() );
}
private void applyDDLOnBeanValidation(Collection<PersistentClass> persistentClasses, Properties properties) {
BeanValidationActivator.applyDDL( persistentClasses, properties );
}
/**

View File

@ -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<ValidationMode> 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<PersistentClass> persistentClasses, Properties properties) {
Set<ValidationMode> 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<ValidationMode> getModes(Object modeProperty) {
Set<ValidationMode> modes = new HashSet<ValidationMode>(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() );

View File

@ -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<Operation, Class<?>[]> groupsPerOperation = new HashMap<Operation, Class<?>[]>(3);
private ConcurrentHashMap<EntityPersister, Set<String>> associationsPerEntityPersister =
new ConcurrentHashMap<EntityPersister, Set<String>>();
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<Class<?>> groupsList = new ArrayList<Class<?>>(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 <T> void validate(T object, EntityMode mode, Operation operation) {
private <T> 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<ConstraintViolation<?>>
throw new ConstraintViolationException(
"Invalid object at " + operation.getName() + " time for groups " + toString( groups ),
(Set<ConstraintViolation>) unsafeViolations);
(Set<ConstraintViolation<?>>) 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;
}
}
}

View File

@ -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<Operation, Class<?>[]> groupsPerOperation = new HashMap<Operation, Class<?>[]>(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<Class<?>> groupsList = new ArrayList<Class<?>>(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;
}
}
}

View File

@ -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<String> associations;
public HibernateTraversableResolver(
EntityPersister persister,
ConcurrentHashMap<EntityPersister, Set<String>> associationsPerEntityPersister,
SessionFactoryImplementor factory) {
this.associations = associationsPerEntityPersister.get( persister );
if (this.associations == null) {
this.associations = new HashSet<String>();
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);
}
}

View File

@ -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<Object, Object> properties) {
public static void applyDDL(Collection<PersistentClass> persistentClasses, Properties properties) {
ValidatorFactory factory = getValidatorFactory( properties );
Class<?>[] groupsArray = new GroupsPerOperation( properties ).get( GroupsPerOperation.Operation.DDL );
Set<Class<?>> groups = new HashSet<Class<?>>( 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<Class<?>> 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<ConstraintDescriptor<?>> constraintDescriptors,
Property property,
PropertyDescriptor propertyDesc,
Set<Class<?>> 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<Min> minConstraint = (ConstraintDescriptor<Min>) 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<Max> maxConstraint = (ConstraintDescriptor<Max>) 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<Column> iter = (Iterator<Column>) 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<Digits> digitsConstraint = (ConstraintDescriptor<Digits>) 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<Size> sizeConstraint = (ConstraintDescriptor<Size>) 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<Object, Object> properties) {
ValidatorFactory factory = null;
if ( properties != null ) {
Object unsafeProperty = properties.get( FACTORY_PROPERTY );

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
};
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
};
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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<DisplayConnector> connectors = new HashSet<DisplayConnector>();
private Set<Color> displayColors = new HashSet<Color>();
@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<DisplayConnector> getConnectors() {
return connectors;
}
public void setConnectors(Set<DisplayConnector> connectors) {
this.connectors = connectors;
}
@ManyToMany(cascade = CascadeType.PERSIST)
public Set<Color> getDisplayColors() {
return displayColors;
}
public void setDisplayColors(Set<Color> displayColors) {
this.displayColors = displayColors;
}
}

View File

@ -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;
}
}

View File

@ -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;
}