/*
* 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<T extends Annotation> implements ConstraintDescriptor<T>, 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<String> 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<T> annotationType;
/**
* The set of classes implementing the validation for this constraint. See also
* {@code ConstraintValidator} resolution algorithm.
*/
private final List<Class<? extends ConstraintValidator<T, ?>>> constraintValidatorClasses;
private final List<Class<? extends ConstraintValidator<T, ?>>> matchingConstraintValidatorClasses;
/**
* The groups for which to apply this constraint.
*/
private final Set<Class<?>> 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<String, Object> attributes;
/**
* The specified payload of the constraint.
*/
private final Set<Class<? extends Payload>> payloads;
/**
* The composing constraints for this constraint.
*/
private final Set<ConstraintDescriptorImpl<?>> 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 (<code>TYPE</code>, <code>METHOD</code>, <code>FIELD</code>) 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<T>) 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<Class<? extends ConstraintValidator<T, ?>>> crossParameterValidatorClasses = constraintHelper.findValidatorClasses(
annotationType,
ValidationTarget.PARAMETERS
);
List<Class<? extends ConstraintValidator<T, ?>>> 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<T> getAnnotationType() {
return annotationType;
}
@Override
public String getMessageTemplate() {
return (String) getAttributes().get( ConstraintHelper.MESSAGE );
}
@Override
public Set<Class<?>> getGroups() {
return groups;
}
@Override
public Set<Class<? extends Payload>> getPayload() {
return payloads;
}
@Override
public ConstraintTarget getValidationAppliesTo() {
return (ConstraintTarget) attributes.get( ConstraintHelper.VALIDATION_APPLIES_TO );
}
@Override
public List<Class<? extends ConstraintValidator<T, ?>>> 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<Class<? extends ConstraintValidator<T, ?>>> getMatchingConstraintValidatorClasses() {
return matchingConstraintValidatorClasses;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public Set<ConstraintDescriptor<?>> getComposingConstraints() {
return Collections.<ConstraintDescriptor<?>>unmodifiableSet( composingConstraints );
}
public Set<ConstraintDescriptorImpl<?>> 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:
* <ul>
* <li>If {@code validationAppliesTo()} is set to either
* {@link ConstraintTarget#RETURN_VALUE} or
* {@link ConstraintTarget#PARAMETERS}, this value will be considered.</li>
* <li>Otherwise, if the constraint is either purely generic or purely
* cross-parameter as per its validators, that value will be considered.</li>
* <li>Otherwise, if the constraint is not on an executable, it is
* considered generic.</li>
* <li>Otherwise, the type will be determined based on exclusive existence
* of parameters and return value.</li>
* <li>If that also is not possible, determination fails (i.e. the user must
* specify the target explicitly).</li>
* </ul>
*
* @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<Class<? extends Payload>> buildPayloadSet(T annotation) {
Set<Class<? extends Payload>> payloadSet = newHashSet();
Class<Payload>[] 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<Class<?>> buildGroupSet(Class<?> implicitGroup) {
Set<Class<?>> 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<String, Object> buildAnnotationParameterMap(Annotation annotation) {
final Method[] declaredMethods = ReflectionHelper.getDeclaredMethods( annotation.annotationType() );
Map<String, Object> 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<ClassIndexWrapper, Map<String, Object>> parseOverrideParameters() {
Map<ClassIndexWrapper, Map<String, Object>> 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<ClassIndexWrapper, Map<String, Object>> 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<String, Object> 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<ConstraintDescriptorImpl<?>> parseComposingConstraints(Member member, ConstraintHelper constraintHelper) {
Set<ConstraintDescriptorImpl<?>> composingConstraintsSet = newHashSet();
Map<ClassIndexWrapper, Map<String, Object>> overrideParameters = parseOverrideParameters();
for ( Annotation declaredAnnotation : annotationType.getDeclaredAnnotations() ) {
Class<? extends Annotation> 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<Annotation> 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 <U extends Annotation> ConstraintDescriptorImpl<U> createComposingConstraintDescriptor(
Member member,
U declaredAnnotation,
Map<ClassIndexWrapper, Map<String, Object>> overrideParameters,
int index,
ConstraintHelper constraintHelper) {
@SuppressWarnings("unchecked")
final Class<U> annotationType = (Class<U>) declaredAnnotation.annotationType();
return createComposingConstraintDescriptor(
member,
overrideParameters,
index,
declaredAnnotation,
annotationType,
constraintHelper
);
}
private <U extends Annotation> ConstraintDescriptorImpl<U> createComposingConstraintDescriptor(
Member member,
Map<ClassIndexWrapper, Map<String, Object>> overrideParameters,
int index,
U constraintAnnotation,
Class<U> annotationType,
ConstraintHelper constraintHelper) {
// use a annotation proxy
AnnotationDescriptor<U> annotationDescriptor = new AnnotationDescriptor<U>(
annotationType, buildAnnotationParameterMap( constraintAnnotation )
);
// get the right override parameters
Map<String, Object> overrides = overrideParameters.get(
new ClassIndexWrapper(
annotationType, index
)
);
if ( overrides != null ) {
for ( Map.Entry<String, Object> 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<U>(
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
}
}