All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hibernate.validator.ClassValidator Maven / Gradle / Ivy

There is a newer version: 3.5.6-Final
Show newest version
//$Id: ClassValidator.java 10650 2006-10-25 01:18:29Z epbernard $
package org.hibernate.validator;

import java.beans.Introspector;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.StringTokenizer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.AssertionFailure;
import org.hibernate.Hibernate;
import org.hibernate.MappingException;
import org.hibernate.validator.interpolator.DefaultMessageInterpolatorAggerator;
import org.hibernate.cfg.annotations.Version;
import org.hibernate.cfg.BinderHelper;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.reflection.Filter;
import org.hibernate.reflection.ReflectionManager;
import org.hibernate.reflection.XClass;
import org.hibernate.reflection.XMember;
import org.hibernate.reflection.XMethod;
import org.hibernate.reflection.XProperty;
import org.hibernate.reflection.java.JavaXFactory;
import org.hibernate.util.IdentitySet;


/**
 * Engine that take a bean and check every expressed annotation restrictions
 *
 * @author Gavin King
 * @author Emmanuel Bernard
 */
public class ClassValidator implements Serializable {
	//TODO Define magic number
	private static Log log = LogFactory.getLog( ClassValidator.class );
	private static final InvalidValue[] EMPTY_INVALID_VALUE_ARRAY = new InvalidValue[]{};
	private static final String DEFAULT_VALIDATOR_MESSAGE = "org.hibernate.validator.resources.DefaultValidatorMessages";
	private static final String VALIDATOR_MESSAGE = "ValidatorMessages";
	private static final Set INDEXABLE_CLASS = new HashSet();

	static {
		INDEXABLE_CLASS.add( Integer.class );
		INDEXABLE_CLASS.add( Long.class );
		INDEXABLE_CLASS.add( String.class );
	}

	static {
		Version.touch(); //touch version
	}

	private final Class beanClass;
	private transient ResourceBundle messageBundle;
	private transient ResourceBundle defaultMessageBundle;
	private transient boolean isUserProvidedResourceBundle;
	private transient ReflectionManager reflectionManager;

	private final transient Map childClassValidators;
	private transient List beanValidators;
	private transient List memberValidators;
	private transient List memberGetters;
	private transient List childGetters;
	private transient DefaultMessageInterpolatorAggerator defaultInterpolator;
	private transient MessageInterpolator userInterpolator;
	private static final Filter GET_ALL_FILTER = new Filter() {
		public boolean returnStatic() {
		return true;
		}

		public boolean returnTransient() {
		return true;
		}
	};

	/**
	 * create the validator engine for this bean type
	 */
	public ClassValidator(Class beanClass) {
		this( beanClass, (ResourceBundle) null );
	}

	/**
	 * create the validator engine for a particular bean class, using a resource bundle
	 * for message rendering on violation
	 */
	public ClassValidator(Class beanClass, ResourceBundle resourceBundle) {
		this( beanClass, resourceBundle, null, new HashMap(), null );
	}

	/**
	 * create the validator engine for a particular bean class, using a custom message interpolator
	 * for message rendering on violation
	 */
	public ClassValidator(Class beanClass, MessageInterpolator interpolator) {
		this( beanClass, null, interpolator, new HashMap(), null );
	}

    /**
     * Not a public API
     */
    public ClassValidator(
			Class beanClass, ResourceBundle resourceBundle, MessageInterpolator interpolator,
            Map childClassValidators, ReflectionManager reflectionManager
    ) {
        this.reflectionManager = reflectionManager != null ? reflectionManager : new JavaXFactory();
        XClass beanXClass = this.reflectionManager.toXClass( beanClass );
		this.beanClass = beanClass;
		this.messageBundle = resourceBundle == null ?
				getDefaultResourceBundle() :
				resourceBundle;
		this.defaultMessageBundle = ResourceBundle.getBundle( DEFAULT_VALIDATOR_MESSAGE );
		this.userInterpolator = interpolator;
		this.childClassValidators = childClassValidators != null ?
                childClassValidators :
                new HashMap();
		initValidator( beanXClass, this.childClassValidators );
	}

	@SuppressWarnings("unchecked")
	protected ClassValidator(
			XClass beanXClass, ResourceBundle resourceBundle, MessageInterpolator userInterpolator,
			Map childClassValidators, ReflectionManager reflectionManager
	) {
		this.beanClass = reflectionManager.toClass( beanXClass );
		this.messageBundle = resourceBundle == null ?
				getDefaultResourceBundle() :
				resourceBundle;
		this.defaultMessageBundle = ResourceBundle.getBundle( DEFAULT_VALIDATOR_MESSAGE );
		this.userInterpolator = userInterpolator;
		this.childClassValidators = childClassValidators;
		initValidator( beanXClass, childClassValidators );
	}

	private ResourceBundle getDefaultResourceBundle() {
		ResourceBundle rb;
		try {
			//use context class loader as a first citizen
			ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
			if ( contextClassLoader == null ) {
				throw new MissingResourceException( "No context classloader", null, VALIDATOR_MESSAGE );
			}
			rb = ResourceBundle.getBundle(
					VALIDATOR_MESSAGE,
					Locale.getDefault(),
					contextClassLoader
			);
		}
		catch (MissingResourceException e) {
			log.trace( "ResourceBundle " + VALIDATOR_MESSAGE + " not found in thread context classloader" );
			//then use the Validator Framework classloader
			try {
				rb = ResourceBundle.getBundle(
						VALIDATOR_MESSAGE,
						Locale.getDefault(),
						this.getClass().getClassLoader()
				);
			}
			catch (MissingResourceException ee) {
				log.debug(
						"ResourceBundle ValidatorMessages not found in Validator classloader. Delegate to " + DEFAULT_VALIDATOR_MESSAGE
				);
				//the user did not override the default ValidatorMessages
				rb = null;
			}
		}
		isUserProvidedResourceBundle = true;
		return rb;
	}

	private void initValidator(
			XClass xClass, Map childClassValidators
	) {
		beanValidators = new ArrayList();
		memberValidators = new ArrayList();
		memberGetters = new ArrayList();
		childGetters = new ArrayList();
		defaultInterpolator = new DefaultMessageInterpolatorAggerator();
		defaultInterpolator.initialize( messageBundle, defaultMessageBundle );

		//build the class hierarchy to look for members in
		childClassValidators.put( xClass, this );
		Collection classes = new HashSet();
		addSuperClassesAndInterfaces( xClass, classes );
		for ( XClass currentClass : classes ) {
			Annotation[] classAnnotations = currentClass.getAnnotations();
			for ( int i = 0; i < classAnnotations.length ; i++ ) {
				Annotation classAnnotation = classAnnotations[i];
				Validator beanValidator = createValidator( classAnnotation );
				if ( beanValidator != null ) beanValidators.add( beanValidator );
			}
		}

		//Check on all selected classes
		for ( XClass currClass : classes ) {
			List methods = currClass.getDeclaredMethods();
			for ( XMethod method : methods ) {
				createMemberValidator( method );
				createChildValidator( method );
			}

			List fields = currClass.getDeclaredProperties(
					"field", GET_ALL_FILTER
			);
			for ( XProperty field : fields ) {
				createMemberValidator( field );
				createChildValidator( field );
			}
		}
	}

	private void addSuperClassesAndInterfaces(XClass clazz, Collection classes) {
		for ( XClass currClass = clazz; currClass != null ; currClass = currClass.getSuperclass() ) {
			if ( ! classes.add( currClass ) ) return;
			XClass[] interfaces = currClass.getInterfaces();
			for ( XClass interf : interfaces ) {
				addSuperClassesAndInterfaces( interf, classes );
			}
		}
	}

	@SuppressWarnings("unchecked")
	private void createChildValidator( XMember member) {
		if ( member.isAnnotationPresent( Valid.class ) ) {
			setAccessible( member );
			childGetters.add( member );
			XClass clazz;
			if ( member.isCollection() || member.isArray() ) {
				clazz = member.getElementClass();
			}
			else {
				clazz = member.getType();
			}
			if ( !childClassValidators.containsKey( clazz ) ) {
				new ClassValidator( clazz, messageBundle, userInterpolator, childClassValidators, reflectionManager );
			}
		}
	}

	private void createMemberValidator(XMember member) {
		boolean validatorPresent = false;
		Annotation[] memberAnnotations = member.getAnnotations();
		for ( int j = 0; j < memberAnnotations.length ; j++ ) {
			Annotation methodAnnotation = memberAnnotations[j];
			Validator propertyValidator = createValidator( methodAnnotation );
			if ( propertyValidator != null ) {
				memberValidators.add( propertyValidator );
				setAccessible( member );
				memberGetters.add( member );
				validatorPresent = true;
			}
		}
		if ( validatorPresent && !member.isTypeResolved() ) {
			log.warn( "Original type of property " + member + " is unbound and has been approximated." );
		}
	}

	private static void setAccessible(XMember member) {
		if ( !Modifier.isPublic( member.getModifiers() ) ) {
			member.setAccessible( true );
		}
	}

	@SuppressWarnings("unchecked")
	private Validator createValidator(Annotation annotation) {
		try {
			ValidatorClass validatorClass = annotation.annotationType().getAnnotation( ValidatorClass.class );
			if ( validatorClass == null ) {
				return null;
			}
			Validator beanValidator = validatorClass.value().newInstance();
			beanValidator.initialize( annotation );
			defaultInterpolator.addInterpolator( annotation, beanValidator );
			return beanValidator;
		}
		catch (Exception e) {
			throw new IllegalArgumentException( "could not instantiate ClassValidator", e );
		}
	}

	public boolean hasValidationRules() {
		return beanValidators.size() != 0 || memberValidators.size() != 0;
	}

	/**
	 * apply constraints on a bean instance and return all the failures.
	 * if bean is null, an empty array is returned
	 */
	public InvalidValue[] getInvalidValues(T bean) {
		return this.getInvalidValues( bean, new IdentitySet() );
	}

	/**
	 * apply constraints on a bean instance and return all the failures.
	 * if bean is null, an empty array is returned
	 */
	@SuppressWarnings("unchecked")
	protected InvalidValue[] getInvalidValues(T bean, Set circularityState) {
		if ( bean == null || circularityState.contains( bean ) ) {
			return EMPTY_INVALID_VALUE_ARRAY; //Avoid circularity
		}
		else {
			circularityState.add( bean );
		}

		if ( !beanClass.isInstance( bean ) ) {
			throw new IllegalArgumentException( "not an instance of: " + bean.getClass() );
		}

		List results = new ArrayList();

		for ( int i = 0; i < beanValidators.size() ; i++ ) {
			Validator validator = beanValidators.get( i );
			if ( !validator.isValid( bean ) ) {
				results.add( new InvalidValue( interpolate(validator), beanClass, null, bean, bean ) );
			}
		}

		for ( int i = 0; i < memberValidators.size() ; i++ ) {
			XMember getter = memberGetters.get( i );
			if ( Hibernate.isPropertyInitialized( bean, getPropertyName( getter ) ) ) {
				Object value = getMemberValue( bean, getter );
				Validator validator = memberValidators.get( i );
				if ( !validator.isValid( value ) ) {
					String propertyName = getPropertyName( getter );
					results.add( new InvalidValue( interpolate(validator), beanClass, propertyName, value, bean ) );
				}
			}
		}

		for ( int i = 0; i < childGetters.size() ; i++ ) {
			XMember getter = childGetters.get( i );
			if ( Hibernate.isPropertyInitialized( bean, getPropertyName( getter ) ) ) {
				Object value = getMemberValue( bean, getter );
				if ( value != null && Hibernate.isInitialized( value ) ) {
					String propertyName = getPropertyName( getter );
					if ( getter.isCollection() ) {
						int index = 0;
						boolean isIterable = value instanceof Iterable;
						Map map = ! isIterable ? (Map) value : null;
						Iterable elements = isIterable ?
								(Iterable) value :
								map.keySet();
						for ( Object element : elements ) {
							Object actualElement = isIterable ? element : map.get( element );
							if ( actualElement == null ) {
								index++;
								continue;
							}
							InvalidValue[] invalidValues = getClassValidator( actualElement )
									.getInvalidValues( actualElement, circularityState );

							String indexedPropName = MessageFormat.format(
									"{0}[{1}]",
									propertyName,
									INDEXABLE_CLASS.contains( element.getClass() ) ?
											( "'" + element + "'" ) :
											index
							);
							index++;

							for ( InvalidValue invalidValue : invalidValues ) {
								invalidValue.addParentBean( bean, indexedPropName );
								results.add( invalidValue );
							}
						}
					}
					if ( getter.isArray() ) {
						int index = 0;
						for ( Object element : (Object[]) value ) {
							if ( element == null ) {
								index++;
								continue;
							}
							InvalidValue[] invalidValues = getClassValidator( element )
									.getInvalidValues( element, circularityState );

							String indexedPropName = MessageFormat.format(
									"{0}[{1}]",
									propertyName,
									index
							);
							index++;

							for ( InvalidValue invalidValue : invalidValues ) {
								invalidValue.addParentBean( bean, indexedPropName );
								results.add( invalidValue );
							}
						}
					}
					else {
						InvalidValue[] invalidValues = getClassValidator( value )
								.getInvalidValues( value, circularityState );
						for ( InvalidValue invalidValue : invalidValues ) {
							invalidValue.addParentBean( bean, propertyName );
							results.add( invalidValue );
						}
					}
				}
			}
		}

		return results.toArray( new InvalidValue[results.size()] );
	}

	private String interpolate(Validator validator) {
		String message = defaultInterpolator.getAnnotationMessage( validator );
		if (userInterpolator != null) {
			return userInterpolator.interpolate( message, validator, defaultInterpolator );
		}
		else {
			return defaultInterpolator.interpolate( message, validator, null);
		}
	}

	@SuppressWarnings("unchecked")
	private ClassValidator getClassValidator(Object value) {
		Class clazz = value.getClass();
		ClassValidator validator = childClassValidators.get( reflectionManager.toXClass( clazz ) );
		if ( validator == null ) { //handles polymorphism
			validator = new ClassValidator( clazz );
		}
		return validator;
	}

	/**
	 * Apply constraints of a particular property on a bean instance and return all the failures.
	 * Note this is not recursive.
	 */
	//TODO should it be recursive?
	public InvalidValue[] getInvalidValues(T bean, String propertyName) {
		List results = new ArrayList();

		for ( int i = 0; i < memberValidators.size() ; i++ ) {
			XMember getter = memberGetters.get( i );
			if ( getPropertyName( getter ).equals( propertyName ) ) {
				Object value = getMemberValue( bean, getter );
				Validator validator = memberValidators.get( i );
				if ( !validator.isValid( value ) ) {
					results.add( new InvalidValue( interpolate(validator), beanClass, propertyName, value, bean ) );
				}
			}
		}

		return results.toArray( new InvalidValue[results.size()] );
	}

	/**
	 * Apply constraints of a particular property value of a bean type and return all the failures.
	 * The InvalidValue objects returns return null for InvalidValue#getBean() and InvalidValue#getRootBean()
	 * Note this is not recursive.
	 */
	//TODO should it be recursive?
	public InvalidValue[] getPotentialInvalidValues(String propertyName, Object value) {
		List results = new ArrayList();

		for ( int i = 0; i < memberValidators.size() ; i++ ) {
			XMember getter = memberGetters.get( i );
			if ( getPropertyName( getter ).equals( propertyName ) ) {
				Validator validator = memberValidators.get( i );
				if ( !validator.isValid( value ) ) {
					results.add( new InvalidValue( interpolate(validator), beanClass, propertyName, value, null ) );
				}
			}
		}

		return results.toArray( new InvalidValue[results.size()] );
	}

	private Object getMemberValue(T bean, XMember getter) {
		Object value;
		try {
			value = getter.invoke( bean );
		}
		catch (Exception e) {
			throw new IllegalStateException( "Could not get property value", e );
		}
		return value;
	}

	public String getPropertyName(XMember member) {
		//Do no try to cache the result in a map, it's actually much slower (2.x time)
		String propertyName;
		if ( XProperty.class.isAssignableFrom( member.getClass() ) ) {
			propertyName = member.getName();
		}
		else if ( XMethod.class.isAssignableFrom( member.getClass() ) ) {
			propertyName = member.getName();
			if ( propertyName.startsWith( "is" ) ) {
				propertyName = Introspector.decapitalize( propertyName.substring( 2 ) );
			}
			else if ( propertyName.startsWith( "get" ) ) {
				propertyName = Introspector.decapitalize( propertyName.substring( 3 ) );
			}
			//do nothing for non getter method, in case someone want to validate a PO Method
		}
		else {
			throw new AssertionFailure( "Unexpected member: " + member.getClass().getName() );
		}
		return propertyName;
	}

	/** @deprecated */
	private String replace(String message, Annotation parameters) {
		StringTokenizer tokens = new StringTokenizer( message, "#{}", true );
		StringBuilder buf = new StringBuilder( 30 );
		boolean escaped = false;
		boolean el = false;
		while ( tokens.hasMoreTokens() ) {
			String token = tokens.nextToken();
			if ( !escaped && "#".equals( token ) ) {
				el = true;
			}
			if ( !el && "{".equals( token ) ) {
				escaped = true;
			}
			else if ( escaped && "}".equals( token ) ) {
				escaped = false;
			}
			else if ( !escaped ) {
				if ( "{".equals( token ) ) el = false;
				buf.append( token );
			}
			else {
				Method member;
				try {
					member = parameters.getClass().getMethod( token, (Class[]) null );
				}
				catch (NoSuchMethodException nsfme) {
					member = null;
				}
				if ( member != null ) {
					try {
						buf.append( member.invoke( parameters ) );
					}
					catch (Exception e) {
						throw new IllegalArgumentException( "could not render message", e );
					}
				}
				else {
					String string = null;
					try {
						string = messageBundle != null ? messageBundle.getString( token ) : null;
					}
					catch( MissingResourceException e ) {
						//give a second chance with the default resource bundle
					}
					if (string == null) {
						try {
							string = defaultMessageBundle.getString( token );
						}
						catch( MissingResourceException e) {
							throw new MissingResourceException(
									"Can't find resource in validator bundles, key " + token,
                                    defaultMessageBundle.getClass().getName(),
                                    token
							);
						}
					}
					if ( string != null ) buf.append( replace( string, parameters ) );
				}
			}
		}
		return buf.toString();
	}

	/**
	 * apply the registred constraints rules on the hibernate metadata (to be applied on DB schema...)
	 *
	 * @param persistentClass hibernate metadata
	 */
	public void apply(PersistentClass persistentClass) {

		for ( Validator validator : beanValidators ) {
			if ( validator instanceof PersistentClassConstraint ) {
				( (PersistentClassConstraint) validator ).apply( persistentClass );
			}
		}

		Iterator validators = memberValidators.iterator();
		Iterator getters = memberGetters.iterator();
		while ( validators.hasNext() ) {
			Validator validator = validators.next();
			String propertyName = getPropertyName( getters.next() );
			if ( validator instanceof PropertyConstraint ) {
				try {
					Property property = BinderHelper.findPropertyByName(persistentClass, propertyName);
					if (property != null) {
						( (PropertyConstraint) validator ).apply( property );
					}
				}
				catch (MappingException pnfe) {
					//do nothing
				}
			}
		}

	}

	public void assertValid(T bean) {
		InvalidValue[] values = getInvalidValues( bean );
		if ( values.length > 0 ) {
			throw new InvalidStateException( values );
		}
	}

	private void writeObject(ObjectOutputStream oos) throws IOException {
		ResourceBundle rb = messageBundle;
		MessageInterpolator interpolator = this.userInterpolator;
		if ( rb != null && ! ( rb instanceof Serializable ) ) {
			messageBundle = null;
			if ( ! isUserProvidedResourceBundle ) {
				log.warn(
						"Serializing a ClassValidator with a non serializable ResourceBundle: ResourceBundle ignored"
				);
			}
		}
		if (interpolator != null && ! (interpolator instanceof Serializable) ) {
			userInterpolator = null;
			log.warn( "Serializing a non serializable MessageInterpolator" );
		}
		oos.defaultWriteObject();
		oos.writeObject( messageBundle );
		oos.writeObject( userInterpolator );
		messageBundle = rb;
		userInterpolator = interpolator;
	}

	private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
		ois.defaultReadObject();
		ResourceBundle rb = (ResourceBundle) ois.readObject();
		if ( rb == null ) rb = getDefaultResourceBundle();
		this.messageBundle = rb;
		this.userInterpolator = (MessageInterpolator) ois.readObject();
		this.defaultMessageBundle = ResourceBundle.getBundle( DEFAULT_VALIDATOR_MESSAGE );
		reflectionManager = new JavaXFactory();
		initValidator( reflectionManager.toXClass( beanClass ), new HashMap() );
	}
}