org.hibernate.validator.internal.metadata.core.ConstraintHelper Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source
* Copyright 2010, 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.core;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import javax.validation.Constraint;
import javax.validation.ConstraintTarget;
import javax.validation.ConstraintValidator;
import javax.validation.ValidationException;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import org.hibernate.validator.constraints.ConstraintComposition;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.ModCheck;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.SafeHtml;
import org.hibernate.validator.constraints.ScriptAssert;
import org.hibernate.validator.constraints.URL;
import org.hibernate.validator.internal.constraintvalidators.AssertFalseValidator;
import org.hibernate.validator.internal.constraintvalidators.AssertTrueValidator;
import org.hibernate.validator.internal.constraintvalidators.DecimalMaxValidatorForCharSequence;
import org.hibernate.validator.internal.constraintvalidators.DecimalMaxValidatorForNumber;
import org.hibernate.validator.internal.constraintvalidators.DecimalMinValidatorForCharSequence;
import org.hibernate.validator.internal.constraintvalidators.DecimalMinValidatorForNumber;
import org.hibernate.validator.internal.constraintvalidators.DigitsValidatorForCharSequence;
import org.hibernate.validator.internal.constraintvalidators.DigitsValidatorForNumber;
import org.hibernate.validator.internal.constraintvalidators.EmailValidator;
import org.hibernate.validator.internal.constraintvalidators.FutureValidatorForCalendar;
import org.hibernate.validator.internal.constraintvalidators.FutureValidatorForDate;
import org.hibernate.validator.internal.constraintvalidators.FutureValidatorForReadableInstant;
import org.hibernate.validator.internal.constraintvalidators.FutureValidatorForReadablePartial;
import org.hibernate.validator.internal.constraintvalidators.LengthValidator;
import org.hibernate.validator.internal.constraintvalidators.MaxValidatorForCharSequence;
import org.hibernate.validator.internal.constraintvalidators.MaxValidatorForNumber;
import org.hibernate.validator.internal.constraintvalidators.MinValidatorForCharSequence;
import org.hibernate.validator.internal.constraintvalidators.MinValidatorForNumber;
import org.hibernate.validator.internal.constraintvalidators.ModCheckValidator;
import org.hibernate.validator.internal.constraintvalidators.NotBlankValidator;
import org.hibernate.validator.internal.constraintvalidators.NotNullValidator;
import org.hibernate.validator.internal.constraintvalidators.NullValidator;
import org.hibernate.validator.internal.constraintvalidators.PastValidatorForCalendar;
import org.hibernate.validator.internal.constraintvalidators.PastValidatorForDate;
import org.hibernate.validator.internal.constraintvalidators.PastValidatorForReadableInstant;
import org.hibernate.validator.internal.constraintvalidators.PastValidatorForReadablePartial;
import org.hibernate.validator.internal.constraintvalidators.PatternValidator;
import org.hibernate.validator.internal.constraintvalidators.SafeHtmlValidator;
import org.hibernate.validator.internal.constraintvalidators.ScriptAssertValidator;
import org.hibernate.validator.internal.constraintvalidators.SizeValidatorForArray;
import org.hibernate.validator.internal.constraintvalidators.SizeValidatorForArraysOfBoolean;
import org.hibernate.validator.internal.constraintvalidators.SizeValidatorForArraysOfByte;
import org.hibernate.validator.internal.constraintvalidators.SizeValidatorForArraysOfChar;
import org.hibernate.validator.internal.constraintvalidators.SizeValidatorForArraysOfDouble;
import org.hibernate.validator.internal.constraintvalidators.SizeValidatorForArraysOfFloat;
import org.hibernate.validator.internal.constraintvalidators.SizeValidatorForArraysOfInt;
import org.hibernate.validator.internal.constraintvalidators.SizeValidatorForArraysOfLong;
import org.hibernate.validator.internal.constraintvalidators.SizeValidatorForCharSequence;
import org.hibernate.validator.internal.constraintvalidators.SizeValidatorForCollection;
import org.hibernate.validator.internal.constraintvalidators.SizeValidatorForMap;
import org.hibernate.validator.internal.constraintvalidators.URLValidator;
import org.hibernate.validator.internal.util.Contracts;
import org.hibernate.validator.internal.util.ReflectionHelper;
import org.hibernate.validator.internal.util.logging.Log;
import org.hibernate.validator.internal.util.logging.LoggerFactory;
import static org.hibernate.validator.internal.util.CollectionHelper.newArrayList;
import static org.hibernate.validator.internal.util.CollectionHelper.newConcurrentHashMap;
import static org.hibernate.validator.internal.util.logging.Messages.MESSAGES;
/**
* Keeps track of builtin constraints and their validator implementations, as well as already resolved validator definitions.
*
* @author Hardy Ferentschik
* @author Alaa Nassef
* @author Gunnar Morling
*/
public class ConstraintHelper {
public static final String GROUPS = "groups";
public static final String PAYLOAD = "payload";
public static final String MESSAGE = "message";
public static final String VALIDATION_APPLIES_TO = "validationAppliesTo";
private static final Log log = LoggerFactory.make();
private static final String JODA_TIME_CLASS_NAME = "org.joda.time.ReadableInstant";
private final ConcurrentMap, List>> builtinConstraints = newConcurrentHashMap();
private final ValidatorClassMap validatorClasses = new ValidatorClassMap();
public ConstraintHelper() {
List>> constraintList = newArrayList();
constraintList.add( AssertFalseValidator.class );
builtinConstraints.put( AssertFalse.class, constraintList );
constraintList = newArrayList();
constraintList.add( AssertTrueValidator.class );
builtinConstraints.put( AssertTrue.class, constraintList );
constraintList = newArrayList();
constraintList.add( DecimalMaxValidatorForNumber.class );
constraintList.add( DecimalMaxValidatorForCharSequence.class );
builtinConstraints.put( DecimalMax.class, constraintList );
constraintList = newArrayList();
constraintList.add( DecimalMinValidatorForNumber.class );
constraintList.add( DecimalMinValidatorForCharSequence.class );
builtinConstraints.put( DecimalMin.class, constraintList );
constraintList = newArrayList();
constraintList.add( DigitsValidatorForCharSequence.class );
constraintList.add( DigitsValidatorForNumber.class );
builtinConstraints.put( Digits.class, constraintList );
constraintList = newArrayList();
constraintList.add( FutureValidatorForCalendar.class );
constraintList.add( FutureValidatorForDate.class );
if ( isJodaTimeInClasspath() ) {
constraintList.add( FutureValidatorForReadableInstant.class );
constraintList.add( FutureValidatorForReadablePartial.class );
}
builtinConstraints.put( Future.class, constraintList );
constraintList = newArrayList();
constraintList.add( MaxValidatorForNumber.class );
constraintList.add( MaxValidatorForCharSequence.class );
builtinConstraints.put( Max.class, constraintList );
constraintList = newArrayList();
constraintList.add( MinValidatorForNumber.class );
constraintList.add( MinValidatorForCharSequence.class );
builtinConstraints.put( Min.class, constraintList );
constraintList = newArrayList();
constraintList.add( NotNullValidator.class );
builtinConstraints.put( NotNull.class, constraintList );
constraintList = newArrayList();
constraintList.add( NullValidator.class );
builtinConstraints.put( Null.class, constraintList );
constraintList = newArrayList();
constraintList.add( PastValidatorForCalendar.class );
constraintList.add( PastValidatorForDate.class );
if ( isJodaTimeInClasspath() ) {
constraintList.add( PastValidatorForReadableInstant.class );
constraintList.add( PastValidatorForReadablePartial.class );
}
builtinConstraints.put( Past.class, constraintList );
constraintList = newArrayList();
constraintList.add( PatternValidator.class );
builtinConstraints.put( Pattern.class, constraintList );
constraintList = newArrayList();
constraintList.add( SizeValidatorForCharSequence.class );
constraintList.add( SizeValidatorForCollection.class );
constraintList.add( SizeValidatorForArray.class );
constraintList.add( SizeValidatorForMap.class );
constraintList.add( SizeValidatorForArraysOfBoolean.class );
constraintList.add( SizeValidatorForArraysOfByte.class );
constraintList.add( SizeValidatorForArraysOfChar.class );
constraintList.add( SizeValidatorForArraysOfDouble.class );
constraintList.add( SizeValidatorForArraysOfFloat.class );
constraintList.add( SizeValidatorForArraysOfInt.class );
constraintList.add( SizeValidatorForArraysOfLong.class );
builtinConstraints.put( Size.class, constraintList );
constraintList = newArrayList();
constraintList.add( EmailValidator.class );
builtinConstraints.put( Email.class, constraintList );
constraintList = newArrayList();
constraintList.add( LengthValidator.class );
builtinConstraints.put( Length.class, constraintList );
constraintList = newArrayList();
constraintList.add( ModCheckValidator.class );
builtinConstraints.put( ModCheck.class, constraintList );
constraintList = newArrayList();
constraintList.add( NotBlankValidator.class );
builtinConstraints.put( NotBlank.class, constraintList );
constraintList = newArrayList();
constraintList.add( SafeHtmlValidator.class );
builtinConstraints.put( SafeHtml.class, constraintList );
constraintList = newArrayList();
constraintList.add( ScriptAssertValidator.class );
builtinConstraints.put( ScriptAssert.class, constraintList );
constraintList = newArrayList();
constraintList.add( URLValidator.class );
builtinConstraints.put( URL.class, constraintList );
}
private List>> getBuiltInConstraints(Class annotationClass) {
//safe cause all CV for a given annotation A are CV
@SuppressWarnings("unchecked")
final List>> builtInList = (List>>) builtinConstraints
.get( annotationClass );
if ( builtInList == null || builtInList.size() == 0 ) {
throw log.getUnableToFindAnnotationConstraintsException( annotationClass );
}
return builtInList;
}
private boolean isBuiltinConstraint(Class annotationType) {
return builtinConstraints.containsKey( annotationType );
}
/**
* Returns the constraint validator classes for the given constraint
* annotation type, as retrieved from
*
*
* - {@link Constraint#validatedBy()},
*
- internally registered validators for built-in constraints and
* - XML configuration.
*
*
* The result is cached internally.
*
* @param annotationType The constraint annotation type.
*
* @return The validator classes for the given type.
*/
public List>> getAllValidatorClasses(Class annotationType) {
Contracts.assertNotNull( annotationType, MESSAGES.classCannotBeNull() );
List>> classes = validatorClasses.get( annotationType );
if ( classes == null ) {
classes = getDefaultValidatorClasses( annotationType );
List>> cachedValidatorClasses = validatorClasses.putIfAbsent(
annotationType,
classes
);
if ( cachedValidatorClasses != null ) {
classes = cachedValidatorClasses;
}
}
return Collections.unmodifiableList( classes );
}
/**
* Returns those validator classes for the given constraint annotation
* matching the given target.
*
* @param annotationType The annotation of interest.
* @param validationTarget The target, either annotated element or parameters.
*
* @return A list with matching validator classes.
*/
public List>> findValidatorClasses(Class annotationType, ValidationTarget validationTarget) {
List>> validatorClasses = getAllValidatorClasses( annotationType );
List>> matchingValidatorClasses = newArrayList();
for ( Class> validatorClass : validatorClasses ) {
if ( supportsValidationTarget( validatorClass, validationTarget ) ) {
matchingValidatorClasses.add( validatorClass );
}
}
return matchingValidatorClasses;
}
private boolean supportsValidationTarget(Class> validatorClass, ValidationTarget target) {
SupportedValidationTarget supportedTargetAnnotation = validatorClass.getAnnotation(
SupportedValidationTarget.class
);
//by default constraints target the annotated element
if ( supportedTargetAnnotation == null ) {
return target == ValidationTarget.ANNOTATED_ELEMENT;
}
return Arrays.asList( supportedTargetAnnotation.value() ).contains( target );
}
/**
* Registers the given validator classes with the given constraint
* annotation type.
*
* @param annotationType The constraint annotation type
* @param definitionClasses The validators to register
* @param keepDefaultClasses Whether any default validators should be kept or not
*/
public void putValidatorClasses(Class annotationType,
List>> definitionClasses,
boolean keepDefaultClasses) {
if ( keepDefaultClasses ) {
List>> defaultValidators = getDefaultValidatorClasses(
annotationType
);
for ( Class> defaultValidator : defaultValidators ) {
definitionClasses.add( 0, defaultValidator );
}
}
validatorClasses.put( annotationType, definitionClasses );
}
/**
* Checks whether a given annotation is a multi value constraint or not.
*
* @param annotationType the annotation type to check.
*
* @return {@code true} if the specified annotation is a multi value constraints, {@code false}
* otherwise.
*/
public boolean isMultiValueConstraint(Class annotationType) {
boolean isMultiValueConstraint = false;
final Method method = ReflectionHelper.getMethod( annotationType, "value" );
if ( method != null ) {
Class returnType = method.getReturnType();
if ( returnType.isArray() && returnType.getComponentType().isAnnotation() ) {
@SuppressWarnings("unchecked")
Class componentType = (Class) returnType.getComponentType();
if ( isConstraintAnnotation( componentType ) || isBuiltinConstraint( componentType ) ) {
isMultiValueConstraint = true;
}
else {
isMultiValueConstraint = false;
}
}
}
return isMultiValueConstraint;
}
/**
* Checks whether a given annotation is a multi value constraint and returns the contained constraints if so.
*
* @param annotation the annotation to check.
*
* @return A list of constraint annotations or the empty list if annotation
is not a multi constraint
* annotation.
*/
public List getMultiValueConstraints(A annotation) {
List annotationList = newArrayList();
try {
final Method method = ReflectionHelper.getMethod( annotation.getClass(), "value" );
if ( method != null ) {
Class returnType = method.getReturnType();
if ( returnType.isArray() && returnType.getComponentType().isAnnotation() ) {
Annotation[] annotations = (Annotation[]) method.invoke( annotation );
for ( Annotation a : annotations ) {
Class annotationType = a.annotationType();
if ( isConstraintAnnotation( annotationType ) || isBuiltinConstraint( annotationType ) ) {
annotationList.add( a );
}
}
}
}
}
catch ( IllegalAccessException iae ) {
// ignore
}
catch ( InvocationTargetException ite ) {
// ignore
}
return annotationList;
}
/**
* Checks whether the specified annotation is a valid constraint annotation. A constraint annotation has to
* fulfill the following conditions:
*
* - Must be annotated with {@link Constraint}
*
- Define a message parameter
* - Define a group parameter
* - Define a payload parameter
*
*
* @param annotationType The annotation type to test.
*
* @return {@code true} if the annotation fulfills the above conditions, {@code false} otherwise.
*/
public boolean isConstraintAnnotation(Class annotationType) {
if ( annotationType.getAnnotation( Constraint.class ) == null ) {
return false;
}
assertMessageParameterExists( annotationType );
assertGroupsParameterExists( annotationType );
assertPayloadParameterExists( annotationType );
assertValidationAppliesToParameterSetUpCorrectly( annotationType );
assertNoParameterStartsWithValid( annotationType );
return true;
}
private void assertNoParameterStartsWithValid(Class annotationType) {
final Method[] methods = ReflectionHelper.getDeclaredMethods( annotationType );
for ( Method m : methods ) {
if ( m.getName().startsWith( "valid" ) && !m.getName().equals( VALIDATION_APPLIES_TO ) ) {
throw log.getConstraintParametersCannotStartWithValidException();
}
}
}
private void assertPayloadParameterExists(Class annotationType) {
try {
final Method method = ReflectionHelper.getMethod( annotationType, PAYLOAD );
if ( method == null ) {
throw log.getConstraintWithoutMandatoryParameterException( PAYLOAD, annotationType.getName() );
}
Class[] defaultPayload = (Class[]) method.getDefaultValue();
if ( defaultPayload.length != 0 ) {
throw log.getWrongDefaultValueForPayloadParameterException( annotationType.getName() );
}
}
catch ( ClassCastException e ) {
throw log.getWrongTypeForPayloadParameterException( annotationType.getName(), e );
}
}
private void assertGroupsParameterExists(Class annotationType) {
try {
final Method method = ReflectionHelper.getMethod( annotationType, GROUPS );
if ( method == null ) {
throw log.getConstraintWithoutMandatoryParameterException( GROUPS, annotationType.getName() );
}
Class[] defaultGroups = (Class[]) method.getDefaultValue();
if ( defaultGroups.length != 0 ) {
throw log.getWrongDefaultValueForGroupsParameterException( annotationType.getName() );
}
}
catch ( ClassCastException e ) {
throw log.getWrongTypeForGroupsParameterException( annotationType.getName(), e );
}
}
private void assertMessageParameterExists(Class annotationType) {
final Method method = ReflectionHelper.getMethod( annotationType, MESSAGE );
if ( method == null ) {
throw log.getConstraintWithoutMandatoryParameterException( MESSAGE, annotationType.getName() );
}
if ( method.getReturnType() != String.class ) {
throw log.getWrongTypeForMessageParameterException( annotationType.getName() );
}
}
private void assertValidationAppliesToParameterSetUpCorrectly(Class annotationType) {
boolean hasGenericValidators = !findValidatorClasses(
annotationType,
ValidationTarget.ANNOTATED_ELEMENT
).isEmpty();
boolean hasCrossParameterValidator = !findValidatorClasses(
annotationType,
ValidationTarget.PARAMETERS
).isEmpty();
final Method method = ReflectionHelper.getMethod( annotationType, VALIDATION_APPLIES_TO );
if ( hasGenericValidators && hasCrossParameterValidator ) {
if ( method == null ) {
throw log.getGenericAndCrossParameterConstraintDoesNotDefineValidationAppliesToParameterException(
annotationType.getName()
);
}
if ( method.getReturnType() != ConstraintTarget.class ) {
throw log.getValidationAppliesToParameterMustHaveReturnTypeConstraintTargetException( annotationType.getName() );
}
ConstraintTarget defaultValue = (ConstraintTarget) method.getDefaultValue();
if ( defaultValue != ConstraintTarget.IMPLICIT ) {
throw log.getValidationAppliesToParameterMustHaveDefaultValueImplicitException( annotationType.getName() );
}
}
else if ( method != null ) {
throw log.getValidationAppliesToParameterMustNotBeDefinedForNonGenericAndCrossParameterConstraintException(
annotationType.getName()
);
}
}
public boolean isConstraintComposition(Class annotationType) {
return annotationType == ConstraintComposition.class;
}
private boolean isJodaTimeInClasspath() {
boolean isInClasspath;
try {
ReflectionHelper.loadClass( JODA_TIME_CLASS_NAME, this.getClass() );
isInClasspath = true;
}
catch ( ValidationException e ) {
isInClasspath = false;
}
return isInClasspath;
}
/**
* Returns the default validators for the given constraint type.
*
* @param annotationType The constraint annotation type.
*
* @return A list with the default validators as retrieved from
* {@link Constraint#validatedBy()} or the list of validators for
* built-in constraints.
*/
private List>> getDefaultValidatorClasses(Class annotationType) {
if ( isBuiltinConstraint( annotationType ) ) {
return getBuiltInConstraints( annotationType );
}
else {
@SuppressWarnings("unchecked")
Class>[] validatedBy = (Class>[]) annotationType
.getAnnotation( Constraint.class )
.validatedBy();
return Arrays.asList( validatedBy );
}
}
/**
* A type-safe wrapper around a concurrent map from constraint types to
* associated validator classes. The casts are safe as data is added trough
* the typed API only.
*
* @author Gunnar Morling
*/
@SuppressWarnings("unchecked")
private static class ValidatorClassMap {
private final ConcurrentMap, List>> constraintValidatorClasses = newConcurrentHashMap();
private List>> get(Class annotationType) {
return (List>>) constraintValidatorClasses.get( annotationType );
}
private void put(Class annotationType, List>> validatorClasses) {
constraintValidatorClasses.put( annotationType, validatorClasses );
}
private List>> putIfAbsent(Class annotationType, List>> classes) {
return (List>>) constraintValidatorClasses.putIfAbsent(
annotationType,
classes
);
}
}
}