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

org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl Maven / Gradle / Ivy

/*
* 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.internal.metadata.descriptor;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.validation.Constraint;
import javax.validation.ConstraintTarget;
import javax.validation.ConstraintValidator;
import javax.validation.OverridesAttribute;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.ValidationException;
import javax.validation.constraintvalidation.ValidationTarget;
import javax.validation.groups.Default;
import javax.validation.metadata.ConstraintDescriptor;

import org.hibernate.validator.constraints.CompositionType;
import org.hibernate.validator.constraints.ConstraintComposition;
import org.hibernate.validator.internal.metadata.core.ConstraintHelper;
import org.hibernate.validator.internal.metadata.core.ConstraintOrigin;
import org.hibernate.validator.internal.util.ReflectionHelper;
import org.hibernate.validator.internal.util.annotationfactory.AnnotationDescriptor;
import org.hibernate.validator.internal.util.annotationfactory.AnnotationFactory;
import org.hibernate.validator.internal.util.logging.Log;
import org.hibernate.validator.internal.util.logging.LoggerFactory;

import static org.hibernate.validator.constraints.CompositionType.AND;
import static org.hibernate.validator.internal.util.CollectionHelper.newHashMap;
import static org.hibernate.validator.internal.util.CollectionHelper.newHashSet;

/**
 * Describes a single constraint (including it's composing constraints).
 *
 * @author Emmanuel Bernard
 * @author Hardy Ferentschik
 * @author Federico Mancini
 * @author Dag Hovland
 */
public class ConstraintDescriptorImpl implements ConstraintDescriptor, Serializable {

	private static final long serialVersionUID = -2563102960314069246L;
	private static final Log log = LoggerFactory.make();
	private static final int OVERRIDES_PARAMETER_DEFAULT_INDEX = -1;

	/**
	 * A list of annotations which can be ignored when investigating for composing constraints.
	 */
	private static final List NON_COMPOSING_CONSTRAINT_ANNOTATIONS = Arrays.asList(
			Documented.class.getName(),
			Retention.class.getName(),
			Target.class.getName(),
			Constraint.class.getName(),
			ReportAsSingleViolation.class.getName()
	);

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

	/**
	 * The type of the annotation made instance variable, because {@code annotation.annotationType()} is quite expensive.
	 */
	private final Class annotationType;

	/**
	 * The set of classes implementing the validation for this constraint. See also
	 * {@code ConstraintValidator} resolution algorithm.
	 */
	private final List>> constraintValidatorClasses;

	private final List>> matchingConstraintValidatorClasses;

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

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

	/**
	 * The specified payload of the constraint.
	 */
	private final Set> payloads;

	/**
	 * The composing constraints for this constraint.
	 */
	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;

	/**
	 * Describes on which level (TYPE, METHOD, FIELD) the constraint was
	 * defined on.
	 */
	private final ElementType elementType;

	/**
	 * The origin of the constraint. Defined on the actual root class or somewhere in the class hierarchy
	 */
	private final ConstraintOrigin definedOn;

	/**
	 * The type of this constraint.
	 */
	private final ConstraintType constraintType;

	/**
	 * Type indicating how composing constraints should be combined. By default this is set to
	 * {@code ConstraintComposition.CompositionType.AND}.
	 */
	private CompositionType compositionType = AND;

	@SuppressWarnings("unchecked")
	public ConstraintDescriptorImpl(T annotation,
									ConstraintHelper constraintHelper,
									Class implicitGroup,
									ElementType type,
									ConstraintOrigin definedOn,
									Member member) {
		this.annotation = annotation;
		this.annotationType = (Class) this.annotation.annotationType();
		this.elementType = type;
		this.definedOn = definedOn;
		this.isReportAsSingleInvalidConstraint = annotationType.isAnnotationPresent(
				ReportAsSingleViolation.class
		);

		// HV-181 - To avoid any thread visibility 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.constraintValidatorClasses = constraintHelper.getAllValidatorClasses( annotationType );
		List>> crossParameterValidatorClasses = constraintHelper.findValidatorClasses(
				annotationType,
				ValidationTarget.PARAMETERS
		);
		List>> genericValidatorClasses = constraintHelper.findValidatorClasses(
				annotationType,
				ValidationTarget.ANNOTATED_ELEMENT
		);

		if ( crossParameterValidatorClasses.size() > 1 ) {
			throw log.getMultipleCrossParameterValidatorClassesException( annotationType.getName() );
		}

		this.constraintType = determineConstraintType(
				member,
				type,
				!genericValidatorClasses.isEmpty(),
				!crossParameterValidatorClasses.isEmpty()
		);
		this.composingConstraints = parseComposingConstraints( member, constraintHelper );
		validateComposingConstraintTypes();

		if ( constraintType == ConstraintType.GENERIC ) {
			this.matchingConstraintValidatorClasses = Collections.unmodifiableList( genericValidatorClasses );
		}
		else {
			this.matchingConstraintValidatorClasses = Collections.unmodifiableList( crossParameterValidatorClasses );
		}
	}

	public ConstraintDescriptorImpl(Member member,
									T annotation,
									ConstraintHelper constraintHelper,
									ElementType type,
									ConstraintOrigin definedOn) {
		this( annotation, constraintHelper, null, type, definedOn, member );
	}

	@Override
	public T getAnnotation() {
		return annotation;
	}

	public Class getAnnotationType() {
		return annotationType;
	}

	@Override
	public String getMessageTemplate() {
		return (String) getAttributes().get( ConstraintHelper.MESSAGE );
	}

	@Override
	public Set> getGroups() {
		return groups;
	}

	@Override
	public Set> getPayload() {
		return payloads;
	}

	@Override
	public ConstraintTarget getValidationAppliesTo() {
		return (ConstraintTarget) attributes.get( ConstraintHelper.VALIDATION_APPLIES_TO );
	}

	@Override
	public List>> getConstraintValidatorClasses() {
		return constraintValidatorClasses;
	}

	/**
	 * Returns those validators registered with this constraint which apply to
	 * the given constraint type (either generic or cross-parameter).
	 *
	 * @return The validators applying to type of this constraint.
	 */
	public List>> getMatchingConstraintValidatorClasses() {
		return matchingConstraintValidatorClasses;
	}

	@Override
	public Map getAttributes() {
		return attributes;
	}

	@Override
	public Set> getComposingConstraints() {
		return Collections.>unmodifiableSet( composingConstraints );
	}

	public Set> getComposingConstraintImpls() {
		return composingConstraints;
	}

	@Override
	public boolean isReportAsSingleViolation() {
		return isReportAsSingleInvalidConstraint;
	}

	public ElementType getElementType() {
		return elementType;
	}

	public ConstraintOrigin getDefinedOn() {
		return definedOn;
	}

	public ConstraintType getConstraintType() {
		return constraintType;
	}

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

		ConstraintDescriptorImpl that = (ConstraintDescriptorImpl) o;

		if ( annotation != null ? !annotation.equals( that.annotation ) : that.annotation != null ) {
			return false;
		}

		return true;
	}

	@Override
	public int hashCode() {
		return annotation != null ? annotation.hashCode() : 0;
	}

	@Override
	public String toString() {
		final StringBuilder sb = new StringBuilder();
		sb.append( "ConstraintDescriptorImpl" );
		sb.append( "{annotation=" ).append( annotationType.getName() );
		sb.append( ", payloads=" ).append( payloads );
		sb.append( ", hasComposingConstraints=" ).append( composingConstraints.isEmpty() );
		sb.append( ", isReportAsSingleInvalidConstraint=" ).append( isReportAsSingleInvalidConstraint );
		sb.append( ", elementType=" ).append( elementType );
		sb.append( ", definedOn=" ).append( definedOn );
		sb.append( ", groups=" ).append( groups );
		sb.append( ", attributes=" ).append( attributes );
		sb.append( ", constraintType=" ).append( constraintType );
		sb.append( '}' );
		return sb.toString();
	}

	/**
	 * Determines the type of this constraint. The following rules apply in
	 * descending order:
	 * 
    *
  • If {@code validationAppliesTo()} is set to either * {@link ConstraintTarget#RETURN_VALUE} or * {@link ConstraintTarget#PARAMETERS}, this value will be considered.
  • *
  • Otherwise, if the constraint is either purely generic or purely * cross-parameter as per its validators, that value will be considered.
  • *
  • Otherwise, if the constraint is not on an executable, it is * considered generic.
  • *
  • Otherwise, the type will be determined based on exclusive existence * of parameters and return value.
  • *
  • If that also is not possible, determination fails (i.e. the user must * specify the target explicitly).
  • *
* * @param member The annotated member * @param elementType The type of the annotated element * @param hasGenericValidators Whether the constraint has at least one generic validator or * not * @param hasCrossParameterValidator Whether the constraint has a cross-parameter validator * * @return The type of this constraint */ private ConstraintType determineConstraintType(Member member, ElementType elementType, boolean hasGenericValidators, boolean hasCrossParameterValidator) { ConstraintTarget constraintTarget = (ConstraintTarget) attributes.get( ConstraintHelper.VALIDATION_APPLIES_TO ); ConstraintType constraintType; boolean isExecutable = isExecutable( elementType ); //target explicitly set to RETURN_VALUE if ( constraintTarget == ConstraintTarget.RETURN_VALUE ) { if ( !isExecutable ) { throw log.getParametersOrReturnValueConstraintTargetGivenAtNonExecutableException( annotationType.getName(), ConstraintTarget.RETURN_VALUE ); } constraintType = ConstraintType.GENERIC; } //target explicitly set to PARAMETERS else if ( constraintTarget == ConstraintTarget.PARAMETERS ) { if ( !isExecutable ) { throw log.getParametersOrReturnValueConstraintTargetGivenAtNonExecutableException( annotationType.getName(), ConstraintTarget.PARAMETERS ); } constraintType = ConstraintType.CROSS_PARAMETER; } //target set to IMPLICIT or not set at all else { //try to derive the type from the existing validators if ( hasGenericValidators && !hasCrossParameterValidator ) { constraintType = ConstraintType.GENERIC; } else if ( !hasGenericValidators && hasCrossParameterValidator ) { constraintType = ConstraintType.CROSS_PARAMETER; } else if ( !isExecutable ) { constraintType = ConstraintType.GENERIC; } //try to derive from existence of parameters/return value else { boolean hasParameters = hasParameters( member ); boolean hasReturnValue = hasReturnValue( member ); if ( !hasParameters && hasReturnValue ) { constraintType = ConstraintType.GENERIC; } else if ( hasParameters && !hasReturnValue ) { constraintType = ConstraintType.CROSS_PARAMETER; } // Now we are out of luck else { throw log.getImplicitConstraintTargetInAmbiguousConfigurationException( annotationType.getName() ); } } } if ( constraintType == ConstraintType.CROSS_PARAMETER ) { validateCrossParameterConstraintType( member, hasCrossParameterValidator ); } return constraintType; } private void validateCrossParameterConstraintType(Member member, boolean hasCrossParameterValidator) { if ( !hasCrossParameterValidator ) { throw log.getCrossParameterConstraintHasNoValidatorException( annotationType.getName() ); } else if ( member == null ) { throw log.getCrossParameterConstraintOnClassException( annotationType.getName() ); } else if ( member instanceof Field ) { throw log.getCrossParameterConstraintOnFieldException( annotationType.getName(), member.toString() ); } else if ( !hasParameters( member ) ) { throw log.getCrossParameterConstraintOnMethodWithoutParametersException( annotationType.getName(), member.toString() ); } } /** * Asserts that this constraint and all its composing constraints share the * same constraint type (generic or cross-parameter). */ private void validateComposingConstraintTypes() { for ( ConstraintDescriptorImpl composingConstraint : composingConstraints ) { if ( composingConstraint.constraintType != constraintType ) { throw log.getComposedAndComposingConstraintsHaveDifferentTypesException( annotationType.getName(), composingConstraint.annotationType.getName(), constraintType, composingConstraint.constraintType ); } } } private boolean hasParameters(Member member) { boolean hasParameters = false; if ( member instanceof Constructor ) { Constructor constructor = (Constructor) member; hasParameters = constructor.getParameterTypes().length > 0; } else if ( member instanceof Method ) { Method method = (Method) member; hasParameters = method.getParameterTypes().length > 0; } return hasParameters; } private boolean hasReturnValue(Member member) { boolean hasReturnValue; if ( member instanceof Constructor ) { hasReturnValue = true; } else if ( member instanceof Method ) { Method method = (Method) member; hasReturnValue = method.getGenericReturnType() != void.class; } else { // field or type hasReturnValue = false; } return hasReturnValue; } private boolean isExecutable(ElementType elementType) { return elementType == ElementType.METHOD || elementType == ElementType.CONSTRUCTOR; } @SuppressWarnings("unchecked") private Set> buildPayloadSet(T annotation) { Set> payloadSet = newHashSet(); Class[] payloadFromAnnotation; try { //TODO be extra safe and make sure this is an array of Payload payloadFromAnnotation = ReflectionHelper.getAnnotationParameter( annotation, ConstraintHelper.PAYLOAD, Class[].class ); } 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 = newHashSet(); final Class[] groupsFromAnnotation = ReflectionHelper.getAnnotationParameter( annotation, ConstraintHelper.GROUPS, Class[].class ); 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 Map buildAnnotationParameterMap(Annotation annotation) { final Method[] declaredMethods = ReflectionHelper.getDeclaredMethods( annotation.annotationType() ); Map parameters = newHashMap( declaredMethods.length ); for ( Method m : declaredMethods ) { try { parameters.put( m.getName(), m.invoke( annotation ) ); } catch ( IllegalAccessException e ) { throw log.getUnableToReadAnnotationAttributesException( annotation.getClass(), e ); } catch ( InvocationTargetException e ) { throw log.getUnableToReadAnnotationAttributesException( 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 log.getUnableToRetrieveAnnotationParameterValueException( e ); } catch ( InvocationTargetException e ) { throw log.getUnableToRetrieveAnnotationParameterValueException( e ); } return value; } private Map> parseOverrideParameters() { Map> overrideParameters = newHashMap(); final Method[] methods = ReflectionHelper.getDeclaredMethods( annotationType ); 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 = newHashMap(); overrideParameters.put( wrapper, map ); } map.put( overridesAttribute.name(), value ); } } private void ensureAttributeIsOverridable(Method m, OverridesAttribute overridesAttribute) { final Method method = ReflectionHelper.getMethod( overridesAttribute.constraint(), overridesAttribute.name() ); if ( method == null ) { throw log.getOverriddenConstraintAttributeNotFoundException( overridesAttribute.name() ); } Class returnTypeOfOverriddenConstraint = method.getReturnType(); if ( !returnTypeOfOverriddenConstraint.equals( m.getReturnType() ) ) { throw log.getWrongAttributeTypeForOverriddenConstraintException( returnTypeOfOverriddenConstraint.getName(), m.getReturnType() ); } } private Set> parseComposingConstraints(Member member, ConstraintHelper constraintHelper) { Set> composingConstraintsSet = newHashSet(); Map> overrideParameters = parseOverrideParameters(); for ( Annotation declaredAnnotation : annotationType.getDeclaredAnnotations() ) { Class declaredAnnotationType = declaredAnnotation.annotationType(); if ( NON_COMPOSING_CONSTRAINT_ANNOTATIONS.contains( declaredAnnotationType.getName() ) ) { // ignore the usual suspects which will be in almost any constraint, but are no composing constraint continue; } //If there is a @ConstraintCompositionType annotation, set its value as the local compositionType field if ( constraintHelper.isConstraintComposition( declaredAnnotationType ) ) { this.setCompositionType( ( (ConstraintComposition) declaredAnnotation ).value() ); if ( log.isDebugEnabled() ) { log.debugf( "Adding Bool %s.", declaredAnnotationType.getName() ); } continue; } if ( constraintHelper.isConstraintAnnotation( declaredAnnotationType ) ) { ConstraintDescriptorImpl descriptor = createComposingConstraintDescriptor( member, declaredAnnotation, overrideParameters, OVERRIDES_PARAMETER_DEFAULT_INDEX, constraintHelper ); composingConstraintsSet.add( descriptor ); log.debugf( "Adding composing constraint: %s.", descriptor ); } else if ( constraintHelper.isMultiValueConstraint( declaredAnnotationType ) ) { List multiValueConstraints = constraintHelper.getMultiValueConstraints( declaredAnnotation ); int index = 0; for ( Annotation constraintAnnotation : multiValueConstraints ) { ConstraintDescriptorImpl descriptor = createComposingConstraintDescriptor( member, constraintAnnotation, overrideParameters, index, constraintHelper ); composingConstraintsSet.add( descriptor ); log.debugf( "Adding composing constraint: %s.", descriptor ); index++; } } } return Collections.unmodifiableSet( composingConstraintsSet ); } private ConstraintDescriptorImpl createComposingConstraintDescriptor( Member member, U declaredAnnotation, Map> overrideParameters, int index, ConstraintHelper constraintHelper) { @SuppressWarnings("unchecked") final Class annotationType = (Class) declaredAnnotation.annotationType(); return createComposingConstraintDescriptor( member, overrideParameters, index, declaredAnnotation, annotationType, constraintHelper ); } private ConstraintDescriptorImpl createComposingConstraintDescriptor( Member member, Map> overrideParameters, int index, U constraintAnnotation, Class annotationType, ConstraintHelper constraintHelper) { // 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() ); } } //propagate inherited attributes to composing constraints annotationDescriptor.setValue( ConstraintHelper.GROUPS, groups.toArray( new Class[groups.size()] ) ); annotationDescriptor.setValue( ConstraintHelper.PAYLOAD, payloads.toArray( new Class[payloads.size()] ) ); if ( annotationDescriptor.getElements().containsKey( ConstraintHelper.VALIDATION_APPLIES_TO ) ) { annotationDescriptor.setValue( ConstraintHelper.VALIDATION_APPLIES_TO, getValidationAppliesTo() ); } U annotationProxy = AnnotationFactory.create( annotationDescriptor ); return new ConstraintDescriptorImpl( member, annotationProxy, constraintHelper, elementType, definedOn ); } /** * @param compositionType the compositionType to set */ private void setCompositionType(CompositionType compositionType) { this.compositionType = compositionType; } /** * @return the compositionType */ public CompositionType getCompositionType() { return compositionType; } /** * A wrapper class to keep track for which composing 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 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; } } /** * The type of a constraint. */ public enum ConstraintType { /** * A non cross parameter constraint. */ GENERIC, /** * A cross parameter constraint. */ CROSS_PARAMETER } }