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

org.hibernate.validation.metadata.ConstraintDescriptorImpl Maven / Gradle / Ivy

// $Id: ConstraintDescriptorImpl.java 17263 2009-08-11 18:00:25Z epbernard $
/*
* JBoss, Home of Professional Open Source
* Copyright 2008, Red Hat Middleware LLC, 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.validation.metadata;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
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.Set;
import java.security.AccessController;
import javax.validation.Constraint;
import javax.validation.ConstraintDefinitionException;
import javax.validation.ConstraintPayload;
import javax.validation.ConstraintValidator;
import javax.validation.OverridesAttribute;
import javax.validation.ReportAsSingleViolation;
import javax.validation.ValidationException;
import javax.validation.groups.Default;
import javax.validation.metadata.ConstraintDescriptor;

import org.slf4j.Logger;

import org.hibernate.validation.util.LoggerFactory;
import org.hibernate.validation.util.GetAnnotationParameter;
import org.hibernate.validation.util.GetMethods;
import org.hibernate.validation.util.GetMethod;
import org.hibernate.validation.util.GetDeclaredMethods;
import org.hibernate.validation.util.annotationfactory.AnnotationDescriptor;
import org.hibernate.validation.util.annotationfactory.AnnotationFactory;

/**
 * Describe a single constraint (including it's composing constraints).
 *
 * @author Emmanuel Bernard
 * @author Hardy Ferentschik
 */
public class ConstraintDescriptorImpl implements ConstraintDescriptor {
	private static final Logger log = LoggerFactory.make();
	private static final int OVERRIDES_PARAMETER_DEFAULT_INDEX = -1;
	private static final String GROUPS = "groups";
	private static final String PAYLOAD = "payload";

	/**
	 * The actual constraint annotation.
	 */
	private final T annotation;

	/**
	 * The set of classes implementing the validation for this constraint. See also
	 * ConstraintValidator resolution algorithm.
	 */
	private final List>> constraintValidatorDefinitonClasses;

	/**
	 * The groups for which to apply this constraint.
	 */
	private final Set> groups;

	/**
	 * The constraint parameters as map. The key is the paramter name and the value the
	 * parameter value as specified in the constraint.
	 */
	private final Map attributes;

	private final Set> payloads;

	/**
	 * The composing constraints for this constraints.
	 */
	private final Set> composingConstraints;

	/**
	 * Flag indicating if in case of a composing constraint a single error or multiple errors should be raised.
	 */
	private final boolean isReportAsSingleInvalidConstraint;

	/**
	 * Handle to the builtin constraint implementations.
	 */
	private final ConstraintHelper constraintHelper;

	public ConstraintDescriptorImpl(T annotation, ConstraintHelper constraintHelper, Class implicitGroup) {
		this.annotation = annotation;
		this.constraintHelper = constraintHelper;
		this.isReportAsSingleInvalidConstraint = annotation.annotationType().isAnnotationPresent(
				ReportAsSingleViolation.class
		);

		// HV-181 - To avoid and thread visibilty issues we are building the different data structures in tmp variables and
		// then assign them to the final variables
		this.attributes = buildAnnotationParameterMap( annotation );
		this.groups = buildGroupSet( implicitGroup );
		this.payloads = buildPayloadSet( annotation );
		this.constraintValidatorDefinitonClasses = findConstraintValidatorClasses();
		this.composingConstraints = parseComposingConstraints();
	}

	public ConstraintDescriptorImpl(T annotation, ConstraintHelper constraintHelper) {
		this( annotation, constraintHelper, null );
	}

	private Set> buildPayloadSet(T annotation) {
		Set> payloadSet = new HashSet>();
		Class[] payloadFromAnnotation;
		try {
			//TODO be extra safe and make sure this is an array of ConstraintPayload
			GetAnnotationParameter action = GetAnnotationParameter.action( annotation, PAYLOAD, Class[].class );
			if (System.getSecurityManager() != null) {
				payloadFromAnnotation = AccessController.doPrivileged( action );
			}
			else {
				payloadFromAnnotation = action.run();
			}
		}
		catch ( ValidationException e ) {
			//ignore people not defining payloads
			payloadFromAnnotation = null;
		}
		if ( payloadFromAnnotation != null ) {
			payloadSet.addAll( Arrays.asList( payloadFromAnnotation ) );
		}
		return Collections.unmodifiableSet( payloadSet );
	}

	private Set> buildGroupSet(Class implicitGroup) {
		Set> groupSet = new HashSet>();
		final Class[] groupsFromAnnotation;
		GetAnnotationParameter action = GetAnnotationParameter.action( annotation, GROUPS, Class[].class );
		if (System.getSecurityManager() != null) {
			groupsFromAnnotation = AccessController.doPrivileged( action );
		}
		else {
			groupsFromAnnotation = action.run();
		}
		if ( groupsFromAnnotation.length == 0 ) {
			groupSet.add( Default.class );
		}
		else {
			groupSet.addAll( Arrays.asList( groupsFromAnnotation ) );
		}

		// if the constraint is part of the Default group it is automatically part of the implicit group as well
		if ( implicitGroup != null && groupSet.contains( Default.class ) ) {
			groupSet.add( implicitGroup );
		}
		return Collections.unmodifiableSet( groupSet );
	}

	private List>> findConstraintValidatorClasses() {
		final Class annotationType = getAnnotationType();
		final List>> constraintValidatorClasses = new ArrayList>>();
		if ( constraintHelper.containsConstraintValidatorDefinition( annotationType ) ) {
			for ( Class> validator : constraintHelper
					.getConstraintValidatorDefinition( annotationType ) ) {
				constraintValidatorClasses.add( validator );
			}
			return Collections.unmodifiableList( constraintValidatorClasses );
		}

		List>> constraintDefinitonClasses = new ArrayList>>();
		if ( constraintHelper.isBuiltinConstraint( annotation.annotationType() ) ) {
			constraintDefinitonClasses.addAll( constraintHelper.getBuiltInConstraints( annotationType ) );
		}
		else {
			Class>[] validatedBy = annotationType
					.getAnnotation( Constraint.class )
					.validatedBy();
			constraintDefinitonClasses.addAll( Arrays.asList( validatedBy ) );
		}

		constraintHelper.addConstraintValidatorDefinition(
				annotation.annotationType(), constraintDefinitonClasses
		);

		for ( Class> validator : constraintDefinitonClasses ) {
			@SuppressWarnings("unchecked")
			Class> safeValidator = ( Class> ) validator;
			constraintValidatorClasses.add( safeValidator );
		}
		return Collections.unmodifiableList( constraintValidatorClasses );
	}

	@SuppressWarnings("unchecked")
	private Class getAnnotationType() {
		return ( Class ) annotation.annotationType();
	}

	public T getAnnotation() {
		return annotation;
	}

	public Set> getGroups() {
		return groups;
	}

	public Set> getPayload() {
		return payloads;
	}

	public List>> getConstraintValidatorClasses() {
		return constraintValidatorDefinitonClasses;
	}

	public Map getAttributes() {
		return attributes;
	}

	public Set> getComposingConstraints() {
		return composingConstraints;
	}

	public boolean isReportAsSingleViolation() {
		return isReportAsSingleInvalidConstraint;
	}

	@Override
	public String toString() {
		return "ConstraintDescriptorImpl{" +
				"annotation=" + annotation +
				", constraintValidatorDefinitonClasses=" + constraintValidatorDefinitonClasses.toString() +
				", groups=" + groups +
				", attributes=" + attributes +
				", composingConstraints=" + composingConstraints +
				", isReportAsSingleInvalidConstraint=" + isReportAsSingleInvalidConstraint +
				'}';
	}

	private Map buildAnnotationParameterMap(Annotation annotation) {
		GetDeclaredMethods action = GetDeclaredMethods.action( annotation.annotationType() );
		final Method[] declaredMethods;
		if ( System.getSecurityManager() != null ) {
			declaredMethods = AccessController.doPrivileged( action );
		}
		else {
			declaredMethods = action.run();
		}
		Map parameters = new HashMap( declaredMethods.length );
		for ( Method m : declaredMethods ) {
			try {
				parameters.put( m.getName(), m.invoke( annotation ) );
			}
			catch ( IllegalAccessException e ) {
				throw new ValidationException( "Unable to read annotation attributes: " + annotation.getClass(), e );
			}
			catch ( InvocationTargetException e ) {
				throw new ValidationException( "Unable to read annotation attributes: " + annotation.getClass(), e );
			}
		}
		return Collections.unmodifiableMap( parameters );
	}

	private Object getMethodValue(Annotation annotation, Method m) {
		Object value;
		try {
			value = m.invoke( annotation );
		}
		// should never happen
		catch ( IllegalAccessException e ) {
			throw new ValidationException( "Unable to retrieve annotation parameter value." );
		}
		catch ( InvocationTargetException e ) {
			throw new ValidationException( "Unable to retrieve annotation parameter value." );
		}
		return value;
	}

	private Map> parseOverrideParameters() {
		Map> overrideParameters = new HashMap>();
		final Method[] methods;
		final GetMethods getMethods = GetMethods.action( annotation.annotationType() );
		if ( System.getSecurityManager() != null ) {
			methods = AccessController.doPrivileged( getMethods );
		}
		else {
			methods = getMethods.run();
		}

		for ( Method m : methods ) {
			if ( m.getAnnotation( OverridesAttribute.class ) != null ) {
				addOverrideAttributes(
						overrideParameters, m, m.getAnnotation( OverridesAttribute.class )
				);
			}
			else if ( m.getAnnotation( OverridesAttribute.List.class ) != null ) {
				addOverrideAttributes(
						overrideParameters,
						m,
						m.getAnnotation( OverridesAttribute.List.class ).value()
				);
			}
		}
		return overrideParameters;
	}

	private void addOverrideAttributes(Map> overrideParameters, Method m, OverridesAttribute... attributes) {

		Object value = getMethodValue( annotation, m );
		for ( OverridesAttribute overridesAttribute : attributes ) {
			ensureAttributeIsOverridable( m, overridesAttribute );

			ClassIndexWrapper wrapper = new ClassIndexWrapper(
					overridesAttribute.constraint(), overridesAttribute.constraintIndex()
			);
			Map map = overrideParameters.get( wrapper );
			if ( map == null ) {
				map = new HashMap();
				overrideParameters.put( wrapper, map );
			}
			map.put( overridesAttribute.name(), value );
		}
	}

	private void ensureAttributeIsOverridable(Method m, OverridesAttribute overridesAttribute) {
		final GetMethod getMethod = GetMethod.action( overridesAttribute.constraint(), overridesAttribute.name() );
		final Method method;
		if ( System.getSecurityManager() != null ) {
			method = AccessController.doPrivileged( getMethod );
		}
		else {
			method = getMethod.run();
		}
		if (method == null) {
			throw new ConstraintDefinitionException(
					"Overriden constraint does not define an attribute with name " + overridesAttribute.name()
			);
		}
		Class returnTypeOfOverridenConstraint = method.getReturnType();
		if ( !returnTypeOfOverridenConstraint.equals( m.getReturnType() ) ) {
			String message = "The overiding type of a composite constraint must be identical to the overwridden one. Expected " + returnTypeOfOverridenConstraint
					.getName() + " found " + m.getReturnType();
			throw new ConstraintDefinitionException( message );
		}
	}

	private Set> parseComposingConstraints() {
		Set> composingConstraintsSet = new HashSet>();
		Map> overrideParameters = parseOverrideParameters();

		for ( Annotation declaredAnnotation : annotation.annotationType().getDeclaredAnnotations() ) {
			if ( constraintHelper.isConstraintAnnotation( declaredAnnotation )
					|| constraintHelper.isBuiltinConstraint( declaredAnnotation.annotationType() ) ) {
				ConstraintDescriptorImpl descriptor = createComposingConstraintDescriptor(
						declaredAnnotation, overrideParameters, OVERRIDES_PARAMETER_DEFAULT_INDEX
				);
				composingConstraintsSet.add( descriptor );
				log.debug( "Adding composing constraint: " + descriptor );
			}
			else if ( constraintHelper.isMultiValueConstraint( declaredAnnotation ) ) {
				List multiValueConstraints = constraintHelper.getMultiValueConstraints( declaredAnnotation );
				int index = 0;
				for ( Annotation constraintAnnotation : multiValueConstraints ) {
					ConstraintDescriptorImpl descriptor = createComposingConstraintDescriptor(
							constraintAnnotation, overrideParameters, index
					);
					composingConstraintsSet.add( descriptor );
					log.debug( "Adding composing constraint: " + descriptor );
					index++;
				}
			}
		}
		return Collections.unmodifiableSet( composingConstraintsSet );
	}

	private  ConstraintDescriptorImpl createComposingConstraintDescriptor(U declaredAnnotation, Map> overrideParameters, int index) {
		//TODO don't quite understand this warning
		//TODO assuming U.getClass() returns Class
		@SuppressWarnings("unchecked")
		final Class annotationType = ( Class ) declaredAnnotation.annotationType();
		return createComposingConstraintDescriptor(
				overrideParameters,
				index,
				declaredAnnotation,
				annotationType
		);
	}

	private  ConstraintDescriptorImpl createComposingConstraintDescriptor(Map> overrideParameters, int index, U constraintAnnotation, Class annotationType) {
		// use a annotation proxy
		AnnotationDescriptor annotationDescriptor = new AnnotationDescriptor(
				annotationType, buildAnnotationParameterMap( constraintAnnotation )
		);

		// get the right override parameters
		Map overrides = overrideParameters.get(
				new ClassIndexWrapper(
						annotationType, index
				)
		);
		if ( overrides != null ) {
			for ( Map.Entry entry : overrides.entrySet() ) {
				annotationDescriptor.setValue( entry.getKey(), entry.getValue() );
			}
		}

		// groups get inherited from the parent
		annotationDescriptor.setValue( GROUPS, groups.toArray( new Class[groups.size()]) );

		U annotationProxy = AnnotationFactory.create( annotationDescriptor );
		return new ConstraintDescriptorImpl(
				annotationProxy, constraintHelper
		);
	}

	/**
	 * A wrapper class to keep track for which compposing constraints (class and index) a given attribute override applies to.
	 */
	private class ClassIndexWrapper {
		final Class clazz;
		final int index;

		ClassIndexWrapper(Class clazz, int index) {
			this.clazz = clazz;
			this.index = index;
		}

		@Override
		@SuppressWarnings("SimplifiableIfStatement")
		public boolean equals(Object o) {
			if ( this == o ) {
				return true;
			}
			if ( o == null || getClass() != o.getClass() ) {
				return false;
			}

			@SuppressWarnings("unchecked") // safe due to the check above
					ClassIndexWrapper that = ( ClassIndexWrapper ) o;

			if ( index != that.index ) {
				return false;
			}
			if ( clazz != null && !clazz.equals( that.clazz ) ) {
				return false;
			}
			if ( clazz == null && that.clazz != null ) {
				return false;
			}

			return true;
		}

		@Override
		public int hashCode() {
			int result = clazz != null ? clazz.hashCode() : 0;
			result = 31 * result + index;
			return result;
		}
	}
}