/*
* JBoss, Home of Professional Open Source
* Copyright 2009, Red Hat, Inc. and/or its affiliates, and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,  
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.hibernate.validator.metadata;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.validation.GroupDefinitionException;
import javax.validation.GroupSequence;
import javax.validation.Valid;
import javax.validation.groups.Default;
import javax.validation.metadata.BeanDescriptor;
import javax.validation.metadata.PropertyDescriptor;

import org.slf4j.Logger;

import org.hibernate.validator.group.DefaultGroupSequenceProvider;
import org.hibernate.validator.group.GroupSequenceProvider;
import org.hibernate.validator.metadata.AggregatedMethodMetaData.Builder;
import org.hibernate.validator.method.metadata.TypeDescriptor;
import org.hibernate.validator.util.LoggerFactory;
import org.hibernate.validator.util.ReflectionHelper;
import org.hibernate.validator.util.privilegedactions.GetDeclaredFields;
import org.hibernate.validator.util.privilegedactions.GetDeclaredMethods;
import org.hibernate.validator.util.privilegedactions.GetMethods;
import org.hibernate.validator.util.privilegedactions.NewInstance;
import org.hibernate.validator.util.privilegedactions.SetAccessibility;

import static org.hibernate.validator.util.CollectionHelper.newArrayList;
import static org.hibernate.validator.util.CollectionHelper.newHashMap;
import static org.hibernate.validator.util.CollectionHelper.newHashSet;
import static org.hibernate.validator.util.ReflectionHelper.computeAllImplementedInterfaces;

/**
 * This class encapsulates all meta data needed for validation. Implementations of {@code Validator} interface can
 * instantiate an instance of this class and delegate the metadata extraction to it.
 *
 * @author Hardy Ferentschik
 * @author Gunnar Morling
 * @author Kevin Pollet - SERLI - (kevin.pollet@serli.com)
 */
public final class BeanMetaDataImpl<T> implements BeanMetaData<T> {

	private static final Logger log = LoggerFactory.make();

	/**
	 * Used as prefix for parameter names, if no explicit names are given.
	 */
	public static final String DEFAULT_PARAMETER_NAME_PREFIX = "arg";

	/**
	 * The root bean class for this meta data.
	 */
	private final Class<T> beanClass;

	/**
	 * Map of all direct constraints which belong to the entity {@code beanClass}. The constraints are mapped to the class
	 * (eg super class or interface) in which they are defined.
	 */
	private final Map<Class<?>, List<BeanMetaConstraint<?>>> metaConstraints = newHashMap();

	/**
	 * Set of all constraints for this bean type (defined on any implemented interfaces or super types)
	 */
	private final Set<BeanMetaConstraint<?>> allMetaConstraints;

	/**
	 * Set of all constraints which are directly defined on the bean or any of the directly implemented interfaces
	 */
	private final Set<BeanMetaConstraint<?>> directMetaConstraints;

	/**
	 * The main element descriptor for {@link #beanClass}.
	 */
	private BeanDescriptorImpl<T> beanDescriptor;

	/**
	 * Contains constrained related meta data for all methods of the type
	 * represented by this bean meta data. Keyed by method, values are an
	 * aggregated view on each method together with all the methods from the
	 * inheritance hierarchy with the same signature.
	 */
	private Map<Method, AggregatedMethodMetaData> methodMetaData;

	/**
	 * Builders used for gathering method meta data. Used only at construction
	 * time.
	 */
	private Set<AggregatedMethodMetaData.Builder> methodMetaDataBuilders = newHashSet();

	/**
	 * List of cascaded members.
	 */
	private Set<Member> cascadedMembers = newHashSet();

	/**
	 * Maps field and method names to their {@code ElementDescriptorImpl}.
	 */
	private Map<String, PropertyDescriptor> propertyDescriptors = newHashMap();

	/**
	 * The default groups sequence for this bean class.
	 */
	private List<Class<?>> defaultGroupSequence = newArrayList();

	/**
	 * The default group sequence provider.
	 *
	 * @see org.hibernate.validator.group.GroupSequenceProvider
	 * @see DefaultGroupSequenceProvider
	 */
	private DefaultGroupSequenceProvider<T> defaultGroupSequenceProvider;

	/**
	 * The class hierarchy for this class starting with the class itself going up the inheritance chain. Interfaces
	 * are not included.
	 */
	private List<Class<?>> classHierarchyWithoutInterfaces;

	/**
	 * Object used to determine whether a given annotation is a constraint. Only used during initialization.
	 */
	private ConstraintHelper constraintHelper;

	/**
	 * A list of all property names in the class (constrained and un-constrained).
	 */
	// Used to avoid ReflectionHelper#containsMember which is slow
	private final Set<String> propertyNames = newHashSet( 30 );

	/**
	 * Constructor used for creating the bean meta data using annotations only
	 *
	 * @param beanClass The bean type for which to create the meta data
	 * @param constraintHelper constraint helper
	 * @param beanMetaDataCache the cache of already configured meta data instances
	 */
	public BeanMetaDataImpl(Class<T> beanClass, ConstraintHelper constraintHelper, BeanMetaDataCache beanMetaDataCache) {
		this(
				beanClass,
				constraintHelper,
				new ArrayList<Class<?>>(),
				null,
				new HashMap<Class<?>, List<BeanMetaConstraint<?>>>(),
				new HashSet<AggregatedMethodMetaData>(),
				new HashSet<Member>(),
				new AnnotationIgnores(),
				beanMetaDataCache
		);
	}

	/**
	 * Constructor used when creating a bean meta data instance via the xml or programmatic API. In this case
	 * additional metadata (the already configured constraints, cascaded members, etc) are passed as well.
	 *
	 * @param beanClass The bean type for which to create the meta data
	 * @param constraintHelper constraint helper
	 * @param defaultGroupSequence programmatic/xml configured default group sequence (overrides annotations)
	 * @param defaultGroupSequenceProvider programmatic configured default group sequence provider class (overrides annotations)
	 * @param constraints programmatic/xml configured constraints
	 * @param methodMetaDatas programmatic configured method constraints
	 * @param cascadedMembers programmatic/xml configured cascaded members
	 * @param annotationIgnores in xml configured ignores for annotations
	 * @param beanMetaDataCache the cache of already configured meta data instances
	 */
	public BeanMetaDataImpl(Class<T> beanClass,
							ConstraintHelper constraintHelper,
							List<Class<?>> defaultGroupSequence,
							Class<? extends DefaultGroupSequenceProvider<?>> defaultGroupSequenceProvider,
							Map<Class<?>, List<BeanMetaConstraint<?>>> constraints,
							Set<AggregatedMethodMetaData> methodMetaDatas,
							Set<Member> cascadedMembers,
							AnnotationIgnores annotationIgnores,
							BeanMetaDataCache beanMetaDataCache) {
		this.beanClass = beanClass;
		this.constraintHelper = constraintHelper;
		this.defaultGroupSequenceProvider = null;
		for ( Member member : cascadedMembers ) {
			addCascadedMember( member );
		}

		classHierarchyWithoutInterfaces = ReflectionHelper.computeClassHierarchy( beanClass, false );

		// start the annotation discovery phase (look for annotations in the whole class hierarchy)
		createMetaData( annotationIgnores, beanMetaDataCache );

		// set the default explicitly specified default group sequence after the discovery process is complete
		if ( !defaultGroupSequence.isEmpty() ) {
			setDefaultGroupSequence( defaultGroupSequence );
		}

		// set the default explicitly specified default group sequence provider after the discovery process is complete
		if ( defaultGroupSequenceProvider != null ) {
			this.defaultGroupSequenceProvider = newGroupSequenceProviderInstance( defaultGroupSequenceProvider );
		}

		// validates that programmatic/xml definition of default group sequence or default group sequence provider
		// doesn't introduce illegal default group sequence definition.
		if ( hasDefaultGroupSequenceProvider() && this.defaultGroupSequence.size() > 1 ) {
			throw new GroupDefinitionException(
					"Default group sequence and default group sequence provider cannot be defined at the same time"
			);
		}

		// add the explicitly configured constraints
		for ( Map.Entry<Class<?>, List<BeanMetaConstraint<?>>> entry : constraints.entrySet() ) {
			Class<?> clazz = entry.getKey();

			// will hold the method constraints (getter and non-getter) of the given class keyed by method
			Map<Method, List<MethodMetaConstraint<?>>> constraintsByMethod = newHashMap();

			for ( BeanMetaConstraint<?> constraint : entry.getValue() ) {

				if ( constraint.getDescriptor().getElementType() == ElementType.METHOD ) {

					List<MethodMetaConstraint<?>> constraintsForMethod = constraintsByMethod.get(
							constraint.getLocation().getMember()
					);
					if ( constraintsForMethod == null ) {
						constraintsForMethod = newArrayList();
						constraintsByMethod.put(
								(Method) constraint.getLocation().getMember(), constraintsForMethod
						);
					}

					constraintsForMethod.add( getAsMethodMetaConstraint( constraint ) );
				}
				//register non-method constraints
				else {
					addMetaConstraint( clazz, constraint );
				}
			}

			// register the constraints for each method in methodMetaConstraints. Constraints at getters will also registered in metaConstraints
			for ( Entry<Method, List<MethodMetaConstraint<?>>> methodAndConstraints : constraintsByMethod
					.entrySet() ) {

				MethodMetaData methodMetaData = new MethodMetaData(
						methodAndConstraints.getKey(),
						methodAndConstraints.getValue(),
						cascadedMembers.contains( methodAndConstraints.getKey() )
				);
				addMethodMetaConstraint( clazz, methodMetaData );
			}

		}

		allMetaConstraints = buildAllConstraintSets();
		directMetaConstraints = buildDirectConstraintSets();

		// add the explicitly configured method constraints, here we need to merge the programmatic and discovered
		// metadata built with the "automatic" discovering.
		if ( !methodMetaDatas.isEmpty() ) {
			for ( AggregatedMethodMetaData aggregatedMethodMetaData : methodMetaDatas ) {
				for ( MethodMetaData methodMetaData : aggregatedMethodMetaData.getAllMethodMetaData() ) {
					Method method = methodMetaData.getMethod();
					addMethodMetaConstraint( method.getDeclaringClass(), methodMetaData );
				}
			}
		}

		this.methodMetaData = Collections.unmodifiableMap( buildMethodMetaData() );

		// reset class members we don't need any longer
		this.methodMetaDataBuilders = null;
		this.constraintHelper = null;
	}

	public Class<T> getBeanClass() {
		return beanClass;
	}

	public BeanDescriptor getBeanDescriptor() {
		return beanDescriptor;
	}

	public TypeDescriptor getTypeDescriptor() {
		return beanDescriptor;
	}

	public Set<Member> getCascadedMembers() {
		return Collections.unmodifiableSet( cascadedMembers );
	}

	public Map<Class<?>, List<BeanMetaConstraint<?>>> getMetaConstraintsAsMap() {
		return Collections.unmodifiableMap( metaConstraints );
	}

	public Set<BeanMetaConstraint<?>> getMetaConstraints() {
		return allMetaConstraints;
	}

	public Set<BeanMetaConstraint<?>> getDirectMetaConstraints() {
		return directMetaConstraints;
	}

	public AggregatedMethodMetaData getMetaDataFor(Method method) {
		return methodMetaData.get( method );
	}

	public Set<AggregatedMethodMetaData> getAllMethodMetaData() {
		return new HashSet<AggregatedMethodMetaData>( methodMetaData.values() );
	}

	public PropertyDescriptor getPropertyDescriptor(String property) {
		return propertyDescriptors.get( property );
	}

	public boolean isPropertyPresent(String name) {
		return propertyNames.contains( name );
	}

	public List<Class<?>> getDefaultGroupSequence(T beanState) {
		if ( hasDefaultGroupSequenceProvider() ) {
			List<Class<?>> providerDefaultGroupSequence = defaultGroupSequenceProvider.getValidationGroups( beanState );
			return getValidDefaultGroupSequence( providerDefaultGroupSequence );
		}

		return Collections.unmodifiableList( defaultGroupSequence );
	}

	public boolean defaultGroupSequenceIsRedefined() {
		return defaultGroupSequence.size() > 1 || hasDefaultGroupSequenceProvider();
	}

	private boolean hasDefaultGroupSequenceProvider() {
		return defaultGroupSequenceProvider != null;
	}

	public Set<PropertyDescriptor> getConstrainedProperties() {
		return Collections.unmodifiableSet( new HashSet<PropertyDescriptor>( propertyDescriptors.values() ) );
	}

	private Set<BeanMetaConstraint<?>> buildAllConstraintSets() {
		Set<BeanMetaConstraint<?>> constraints = newHashSet();
		for ( List<BeanMetaConstraint<?>> list : metaConstraints.values() ) {
			constraints.addAll( list );
		}
		return Collections.unmodifiableSet( constraints );
	}

	private Set<BeanMetaConstraint<?>> buildDirectConstraintSets() {
		Set<BeanMetaConstraint<?>> constraints = newHashSet();
		// collect all constraints directly defined in this bean
		if ( metaConstraints.get( beanClass ) != null ) {
			constraints.addAll( metaConstraints.get( beanClass ) );
		}
		Set<Class<?>> classAndInterfaces = computeAllImplementedInterfaces( beanClass );
		for ( Class<?> clazz : classAndInterfaces ) {
			if ( metaConstraints.get( clazz ) != null ) {
				constraints.addAll( metaConstraints.get( clazz ) );
			}
		}
		return Collections.unmodifiableSet( constraints );
	}

	/**
	 * Builds up the method meta data for this type by invoking each builder in
	 * {@link #methodMetaDataBuilders}.
	 */
	private Map<Method, AggregatedMethodMetaData> buildMethodMetaData() {

		Map<Method, AggregatedMethodMetaData> theValue = newHashMap();

		for ( AggregatedMethodMetaData.Builder oneBuilder : methodMetaDataBuilders ) {

			AggregatedMethodMetaData aggregatedMethodMetaData = oneBuilder.build();

			//register the aggregated meta data for each underlying method for a quick
			//read access
			for ( MethodMetaData oneMethodMetaData : aggregatedMethodMetaData.getAllMethodMetaData() ) {
				theValue.put( oneMethodMetaData.getMethod(), aggregatedMethodMetaData );
			}
		}

		return theValue;
	}

	private void setDefaultGroupSequence(List<Class<?>> groupSequence) {
		defaultGroupSequence = getValidDefaultGroupSequence( groupSequence );
	}

	private List<Class<?>> getValidDefaultGroupSequence(List<Class<?>> groupSequence) {
		List<Class<?>> validDefaultGroupSequence = new ArrayList<Class<?>>();

		boolean groupSequenceContainsDefault = false;
		if ( groupSequence != null ) {
			for ( Class<?> group : groupSequence ) {
				if ( group.getName().equals( beanClass.getName() ) ) {
					validDefaultGroupSequence.add( Default.class );
					groupSequenceContainsDefault = true;
				}
				else if ( group.getName().equals( Default.class.getName() ) ) {
					throw new GroupDefinitionException( "'Default.class' cannot appear in default group sequence list." );
				}
				else {
					validDefaultGroupSequence.add( group );
				}
			}
		}
		if ( !groupSequenceContainsDefault ) {
			throw new GroupDefinitionException( beanClass.getName() + " must be part of the redefined default group sequence." );
		}
		if ( log.isTraceEnabled() ) {
			log.trace(
					"Members of the default group sequence for bean {} are: {}",
					beanClass.getName(),
					validDefaultGroupSequence
			);
		}

		return validDefaultGroupSequence;
	}

	private void addMetaConstraint(Class<?> clazz, BeanMetaConstraint<?> metaConstraint) {
		// first we add the meta constraint to our meta constraint map
		List<BeanMetaConstraint<?>> constraintList;
		if ( !metaConstraints.containsKey( clazz ) ) {
			constraintList = new ArrayList<BeanMetaConstraint<?>>();
			metaConstraints.put( clazz, constraintList );
		}
		else {
			constraintList = metaConstraints.get( clazz );
		}
		constraintList.add( metaConstraint );

		// but we also have to update the descriptors exposing the BV metadata API
		if ( metaConstraint.getElementType() == ElementType.TYPE ) {
			beanDescriptor.addConstraintDescriptor( metaConstraint.getDescriptor() );
		}
		else {
			PropertyDescriptorImpl propertyDescriptor = (PropertyDescriptorImpl) propertyDescriptors.get(
					metaConstraint.getLocation().getPropertyName()
			);
			if ( propertyDescriptor == null ) {
				Member member = metaConstraint.getLocation().getMember();
				propertyDescriptor = addPropertyDescriptorForMember( member, isValidAnnotationPresent( member ) );
			}
			propertyDescriptor.addConstraintDescriptor( metaConstraint.getDescriptor() );
		}
	}

	private void addMethodMetaConstraint(Class<?> clazz, MethodMetaData methodMetaData) {
		addToBuilder( methodMetaData );

		if ( ReflectionHelper.isGetterMethod( methodMetaData.getMethod() ) ) {

			addToPropertyNameList( methodMetaData.getMethod() );

			for ( MethodMetaConstraint<?> metaConstraint : methodMetaData ) {

				addMetaConstraint( clazz, getAsBeanMetaConstraint( metaConstraint, methodMetaData.getMethod() ) );
			}

			if ( methodMetaData.isCascading() ) {
				addCascadedMember( methodMetaData.getMethod() );
			}
		}
	}

	/**
	 * <p>
	 * Adds the given method meta data to the aggregated method meta data
	 * builder for this method signature. In case no aggregation builder exists
	 * yet for this method signature, one will be created with the given method
	 * meta data as base entry.
	 * </p>
	 * <p>
	 * As the type hierarchy is traversed bottom-up when building the bean meta
	 * data for a given type, it is ensured that for each method the aggregated
	 * method meta data is based on the lowest implementation of this method in
	 * the hierarchy.
	 * </p>
	 *
	 * @param metaData The method meta data to add.
	 */
	private void addToBuilder(MethodMetaData metaData) {

		// add the given meta data to an existing aggregation builder ...
		for ( Builder oneBuilder : methodMetaDataBuilders ) {

			if ( oneBuilder.accepts( metaData ) ) {
				oneBuilder.addMetaData( metaData );
				return;
			}
		}

		// ... or create a new builder
		Builder newBuilder = new AggregatedMethodMetaData.Builder( metaData );
		methodMetaDataBuilders.add( newBuilder );
	}

	private void addCascadedMember(Member member) {
		cascadedMembers.add( member );
		addPropertyDescriptorForMember( member, true );
	}

	public List<Class<?>> getClassHierarchy() {
		return classHierarchyWithoutInterfaces;
	}

	/**
	 * Create bean descriptor, find all classes/subclasses/interfaces which have to be taken in consideration
	 * for this validator and create meta data.
	 *
	 * @param annotationIgnores Data structure keeping track on which annotation should be ignored
	 * @param beanMetaDataCache The bean meta data cache
	 */
	private void createMetaData(AnnotationIgnores annotationIgnores, BeanMetaDataCache beanMetaDataCache) {
		beanDescriptor = new BeanDescriptorImpl<T>( this );
		initDefaultGroupSequence();
		for ( Class<?> current : ReflectionHelper.computeClassHierarchy( beanClass, true ) ) {
			initClass( current, annotationIgnores, beanMetaDataCache );
		}
	}

	private void initClass(Class<?> clazz, AnnotationIgnores annotationIgnores, BeanMetaDataCache beanMetaDataCache) {
		initClassConstraints( clazz, annotationIgnores, beanMetaDataCache );
		initMethodConstraints( clazz, annotationIgnores, beanMetaDataCache );
		initFieldConstraints( clazz, annotationIgnores, beanMetaDataCache );
	}

	/**
	 * Checks whether there is a default group sequence defined for this class.
	 * See HV-113.
	 */
	private void initDefaultGroupSequence() {
		List<Class<?>> groupSequence = new ArrayList<Class<?>>();
		GroupSequenceProvider groupSequenceProviderAnnotation = beanClass.getAnnotation( GroupSequenceProvider.class );
		GroupSequence groupSequenceAnnotation = beanClass.getAnnotation( GroupSequence.class );

		if ( groupSequenceAnnotation != null && groupSequenceProviderAnnotation != null ) {
			throw new GroupDefinitionException(
					"GroupSequence and GroupSequenceProvider annotations cannot be used at the same time"
			);
		}

		if ( groupSequenceProviderAnnotation != null ) {
			defaultGroupSequenceProvider = newGroupSequenceProviderInstance( groupSequenceProviderAnnotation.value() );
		}
		else if ( groupSequenceAnnotation != null ) {
			groupSequence.addAll( Arrays.asList( groupSequenceAnnotation.value() ) );
			setDefaultGroupSequence( groupSequence );
		}
		else {
			groupSequence.add( beanClass );
			setDefaultGroupSequence( groupSequence );
		}
	}

	private void initFieldConstraints(Class<?> clazz, AnnotationIgnores annotationIgnores, BeanMetaDataCache beanMetaDataCache) {
		final Field[] fields = run( GetDeclaredFields.action( clazz ) );
		for ( Field field : fields ) {
			addToPropertyNameList( field );

			// HV-172
			if ( Modifier.isStatic( field.getModifiers() ) ) {
				continue;
			}

			if ( annotationIgnores.isIgnoreAnnotations( field ) ) {
				continue;
			}

			// HV-262
			BeanMetaDataImpl<?> cachedMetaData = beanMetaDataCache.getBeanMetaData( clazz );
			boolean metaDataCached = cachedMetaData != null;
			List<ConstraintDescriptorImpl<?>> fieldMetaData;
			if ( metaDataCached && cachedMetaData.getMetaConstraintsAsMap().get( clazz ) != null ) {
				fieldMetaData = new ArrayList<ConstraintDescriptorImpl<?>>();
				for ( BeanMetaConstraint<?> metaConstraint : cachedMetaData.getMetaConstraintsAsMap()
						.get( clazz ) ) {
					ConstraintDescriptorImpl<?> descriptor = metaConstraint.getDescriptor();
					if ( descriptor.getElementType() == ElementType.FIELD
							&& metaConstraint.getLocation()
							.getPropertyName()
							.equals( ReflectionHelper.getPropertyName( field ) ) ) {
						fieldMetaData.add( descriptor );
					}
				}
			}
			else {
				fieldMetaData = findConstraints( field, ElementType.FIELD );
			}

			for ( ConstraintDescriptorImpl<?> constraintDescription : fieldMetaData ) {
				BeanMetaConstraint<?> metaConstraint = createBeanMetaConstraint( clazz, field, constraintDescription );
				addMetaConstraint( clazz, metaConstraint );
			}

			// HV-433 Make sure the field is marked as cascaded in case it was configured via xml/programmatic API or
			// it hosts the @Valid annotation
			boolean isCascadedField = metaDataCached && cachedMetaData.getCascadedMembers().contains( field );
			if ( isCascadedField || field.isAnnotationPresent( Valid.class ) ) {
				addCascadedMember( field );
			}
		}
	}

	private void addToPropertyNameList(Member member) {
		String name = ReflectionHelper.getPropertyName( member );
		if ( name != null ) {
			propertyNames.add( name );
		}
	}

	private void initMethodConstraints(Class<?> clazz, AnnotationIgnores annotationIgnores, BeanMetaDataCache beanMetaDataCache) {
		final Method[] declaredMethods = run( GetDeclaredMethods.action( clazz ) );

		for ( Method method : declaredMethods ) {

			// HV-172; ignoring synthetic methods (inserted by the compiler), as they can't have any constraints
			// anyway and possibly hide the actual method with the same signature in the built meta model
			if ( Modifier.isStatic( method.getModifiers() ) || annotationIgnores.isIgnoreAnnotations( method ) || method
					.isSynthetic() ) {
				continue;
			}

			// try to get meta data from cache, otherwise retrieve it from the class
			MethodMetaData methodMetaData = getFromCache( clazz, method, beanMetaDataCache );

			if ( methodMetaData == null ) {
				methodMetaData = findMethodMetaData( method );
			}

			addMethodMetaConstraint( clazz, methodMetaData );
		}
	}

	private MethodMetaData getFromCache(Class<?> clazz, Method method, BeanMetaDataCache beanMetaDataCache) {
		BeanMetaDataImpl<?> cachedBeanMetaData = beanMetaDataCache.getBeanMetaData( clazz );

		if ( cachedBeanMetaData != null ) {
			AggregatedMethodMetaData cachedMethodMetaData = cachedBeanMetaData.getMetaDataFor( method );

			if ( cachedMethodMetaData != null ) {
				return cachedMethodMetaData.getSingleMetaDataFor( method );
			}
		}

		return null;
	}

	private PropertyDescriptorImpl addPropertyDescriptorForMember(Member member, boolean isCascaded) {
		String name = ReflectionHelper.getPropertyName( member );
		PropertyDescriptorImpl propertyDescriptor = (PropertyDescriptorImpl) propertyDescriptors.get(
				name
		);
		if ( propertyDescriptor == null ) {
			propertyDescriptor = new PropertyDescriptorImpl(
					ReflectionHelper.getType( member ),
					isCascaded,
					name,
					this
			);
			propertyDescriptors.put( name, propertyDescriptor );
		}
		return propertyDescriptor;
	}

	private boolean isValidAnnotationPresent(Member member) {
		return ( (AnnotatedElement) member ).isAnnotationPresent( Valid.class );
	}

	private void initClassConstraints(Class<?> clazz, AnnotationIgnores annotationIgnores, BeanMetaDataCache beanMetaDataCache) {
		if ( annotationIgnores.isIgnoreAnnotations( clazz ) ) {
			return;
		}

		// HV-262
		BeanMetaDataImpl<?> cachedMetaData = beanMetaDataCache.getBeanMetaData( clazz );
		List<ConstraintDescriptorImpl<?>> classMetaData;
		if ( cachedMetaData != null && cachedMetaData.getMetaConstraintsAsMap().get( clazz ) != null ) {
			classMetaData = new ArrayList<ConstraintDescriptorImpl<?>>();
			for ( MetaConstraint<?> metaConstraint : cachedMetaData.getMetaConstraintsAsMap().get( clazz ) ) {
				ConstraintDescriptorImpl<?> descriptor = metaConstraint.getDescriptor();
				if ( descriptor.getElementType() == ElementType.TYPE ) {
					classMetaData.add( descriptor );
				}
			}
		}
		else {
			classMetaData = findClassLevelConstraints( clazz );
		}

		for ( ConstraintDescriptorImpl<?> constraintDescription : classMetaData ) {
			BeanMetaConstraint<?> metaConstraint = createBeanMetaConstraint( clazz, null, constraintDescription );
			addMetaConstraint( clazz, metaConstraint );
		}
	}

	private <A extends Annotation> BeanMetaConstraint<?> createBeanMetaConstraint(Class<?> declaringClass, Member m, ConstraintDescriptorImpl<A> descriptor) {
		return new BeanMetaConstraint<A>( descriptor, declaringClass, m );
	}

	private <A extends Annotation> MethodMetaConstraint<A> createParameterMetaConstraint(Method method, int parameterIndex, ConstraintDescriptorImpl<A> descriptor) {
		return new MethodMetaConstraint<A>( descriptor, method, parameterIndex );
	}

	private <A extends Annotation> MethodMetaConstraint<A> createReturnValueMetaConstraint(Method method, ConstraintDescriptorImpl<A> descriptor) {
		return new MethodMetaConstraint<A>( descriptor, method );
	}

	/**
	 * Examines the given annotation to see whether it is a single- or multi-valued constraint annotation.
	 *
	 * @param clazz the class we are currently processing
	 * @param annotation The annotation to examine
	 * @param type the element type on which the annotation/constraint is placed on
	 *
	 * @return A list of constraint descriptors or the empty list in case <code>annotation</code> is neither a
	 *         single nor multi-valued annotation.
	 */
	private <A extends Annotation> List<ConstraintDescriptorImpl<?>> findConstraintAnnotations(Class<?> clazz, A annotation, ElementType type) {
		List<ConstraintDescriptorImpl<?>> constraintDescriptors = new ArrayList<ConstraintDescriptorImpl<?>>();

		List<Annotation> constraints = new ArrayList<Annotation>();
		Class<? extends Annotation> annotationType = annotation.annotationType();
		if ( constraintHelper.isConstraintAnnotation( annotationType )
				|| constraintHelper.isBuiltinConstraint( annotationType ) ) {
			constraints.add( annotation );
		}
		else if ( constraintHelper.isMultiValueConstraint( annotationType ) ) {
			constraints.addAll( constraintHelper.getMultiValueConstraints( annotation ) );
		}

		for ( Annotation constraint : constraints ) {
			final ConstraintDescriptorImpl<?> constraintDescriptor = buildConstraintDescriptor(
					clazz, constraint, type
			);
			constraintDescriptors.add( constraintDescriptor );
		}
		return constraintDescriptors;
	}

	private <A extends Annotation> ConstraintDescriptorImpl<A> buildConstraintDescriptor(Class<?> clazz, A annotation, ElementType type) {
		ConstraintDescriptorImpl<A> constraintDescriptor;
		ConstraintOrigin definedIn = determineOrigin( clazz );
		if ( clazz.isInterface() && !clazz.equals( beanClass ) ) {
			constraintDescriptor = new ConstraintDescriptorImpl<A>(
					annotation, constraintHelper, clazz, type, definedIn
			);
		}
		else {
			constraintDescriptor = new ConstraintDescriptorImpl<A>( annotation, constraintHelper, type, definedIn );
		}
		return constraintDescriptor;
	}

	/**
	 * Finds all constraint annotations defined for the given class and returns them in a list of
	 * constraint descriptors.
	 *
	 * @param beanClass The class to check for constraints annotations.
	 *
	 * @return A list of constraint descriptors for all constraint specified on the given class.
	 */
	private List<ConstraintDescriptorImpl<?>> findClassLevelConstraints(Class<?> beanClass) {
		List<ConstraintDescriptorImpl<?>> metaData = new ArrayList<ConstraintDescriptorImpl<?>>();
		for ( Annotation annotation : beanClass.getAnnotations() ) {
			metaData.addAll( findConstraintAnnotations( beanClass, annotation, ElementType.TYPE ) );
		}
		return metaData;
	}

	/**
	 * Finds all constraint annotations defined for the given field/method and returns them in a list of
	 * constraint descriptors.
	 *
	 * @param member The fields or method to check for constraints annotations.
	 * @param type The element type the constraint/annotation is placed on.
	 *
	 * @return A list of constraint descriptors for all constraint specified for the given field or method.
	 */
	private List<ConstraintDescriptorImpl<?>> findConstraints(Member member, ElementType type) {
		assert member instanceof Field || member instanceof Method;

		List<ConstraintDescriptorImpl<?>> metaData = new ArrayList<ConstraintDescriptorImpl<?>>();
		for ( Annotation annotation : ( (AnnotatedElement) member ).getAnnotations() ) {
			metaData.addAll( findConstraintAnnotations( member.getDeclaringClass(), annotation, type ) );
		}

		return metaData;
	}

	/**
	 * Finds all constraint annotations defined for the given method.
	 *
	 * @param method The method to check for constraints annotations.
	 *
	 * @return A meta data object describing the constraints specified for the
	 *         given method.
	 */
	private MethodMetaData findMethodMetaData(Method method) {

		List<ParameterMetaData> parameterConstraints = getParameterMetaData( method );
		boolean isCascading = isValidAnnotationPresent( method ) || cascadedMembers.contains( method );
		List<MethodMetaConstraint<?>> constraints =
				convertToMetaConstraints( findConstraints( method, ElementType.METHOD ), method );

		return new MethodMetaData( method, parameterConstraints, constraints, isCascading );
	}

	private List<MethodMetaConstraint<?>> convertToMetaConstraints(List<ConstraintDescriptorImpl<?>> constraintsDescriptors, Method method) {

		List<MethodMetaConstraint<?>> constraints = newArrayList();

		for ( ConstraintDescriptorImpl<?> oneDescriptor : constraintsDescriptors ) {
			constraints.add( createReturnValueMetaConstraint( method, oneDescriptor ) );
		}

		return constraints;
	}

	/**
	 * Retrieves constraint related meta data for the parameters of the given
	 * method.
	 *
	 * @param method The method of interest.
	 *
	 * @return A list with parameter meta data for the given method.
	 */
	private List<ParameterMetaData> getParameterMetaData(Method method) {
		List<ParameterMetaData> metaData = newArrayList();

		int i = 0;
		Class<?>[] parameterTypes = method.getParameterTypes();

		for ( Annotation[] annotationsOfOneParameter : method.getParameterAnnotations() ) {

			boolean parameterIsCascading = false;
			String parameterName = DEFAULT_PARAMETER_NAME_PREFIX + i;
			List<MethodMetaConstraint<?>> constraintsOfOneParameter = newArrayList();

			for ( Annotation oneAnnotation : annotationsOfOneParameter ) {

				//1. collect constraints if this annotation is a constraint annotation
				List<ConstraintDescriptorImpl<?>> constraints = findConstraintAnnotations(
						method.getDeclaringClass(), oneAnnotation, ElementType.PARAMETER
				);
				for ( ConstraintDescriptorImpl<?> constraintDescriptorImpl : constraints ) {
					constraintsOfOneParameter.add(
							createParameterMetaConstraint(
									method, i, constraintDescriptorImpl
							)
					);
				}

				//2. mark parameter as cascading if this annotation is the @Valid annotation
				if ( oneAnnotation.annotationType().equals( Valid.class ) ) {
					parameterIsCascading = true;
				}
			}

			metaData.add(
					new ParameterMetaData(
							i, parameterTypes[i], parameterName, constraintsOfOneParameter, parameterIsCascading
					)
			);
			i++;
		}

		return metaData;
	}

	private <A extends Annotation> MethodMetaConstraint<A> getAsMethodMetaConstraint(BeanMetaConstraint<A> beanMetaConstraint) {
		return new MethodMetaConstraint<A>(
				beanMetaConstraint.getDescriptor(), (Method) beanMetaConstraint.getLocation().getMember()
		);
	}

	private <A extends Annotation> BeanMetaConstraint<A> getAsBeanMetaConstraint(MethodMetaConstraint<A> methodMetaConstraint, Method method) {
		return new BeanMetaConstraint<A>(
				methodMetaConstraint.getDescriptor(), methodMetaConstraint.getLocation().getBeanClass(), method
		);
	}

	private ConstraintOrigin determineOrigin(Class<?> clazz) {
		if ( clazz.equals( beanClass ) ) {
			return ConstraintOrigin.DEFINED_LOCALLY;
		}
		else {
			return ConstraintOrigin.DEFINED_IN_HIERARCHY;
		}
	}

	@SuppressWarnings("unchecked")
	private <U extends DefaultGroupSequenceProvider<?>> DefaultGroupSequenceProvider<T> newGroupSequenceProviderInstance(Class<U> providerClass) {
		Method[] providerMethods = run( GetMethods.action( providerClass ) );
		for ( Method method : providerMethods ) {
			Class<?>[] paramTypes = method.getParameterTypes();
			if ( "getValidationGroups".equals( method.getName() ) && !method.isBridge()
					&& paramTypes.length == 1 && paramTypes[0].isAssignableFrom( beanClass ) ) {

				return (DefaultGroupSequenceProvider<T>) run(
						NewInstance.action(
								providerClass, "the default group sequence provider"
						)
				);
			}
		}

		throw new GroupDefinitionException(
				"The default group sequence provider defined for " + beanClass.getName() + " has the wrong type"
		);
	}

	@Override
	public String toString() {
		final StringBuilder sb = new StringBuilder();
		sb.append( "BeanMetaDataImpl" );
		sb.append( "{beanClass=" ).append( beanClass.getSimpleName() );
		sb.append( ", constraintCount=" ).append( getMetaConstraints().size() );
		sb.append( ", cascadedMemberCount=" ).append( cascadedMembers.size() );
		sb.append( ", defaultGroupSequence=" ).append( getDefaultGroupSequence( null ) );
		sb.append( '}' );
		return sb.toString();
	}

	/**
	 * Runs the given privileged action, using a privileged block if required.
	 * <p>
	 * <b>NOTE:</b> This must never be changed into a publicly available method to avoid execution of arbitrary
	 * privileged actions within HV's protection domain.
	 */
	private static <T> T run(PrivilegedAction<T> action) {
		return System.getSecurityManager() != null ? AccessController.doPrivileged( action ) : action.run();
	}
}
