org.hibernate.validator.internal.engine.valueextraction.ValueExtractorResolver Maven / Gradle / Ivy
Show all versions of bean-validator Show documentation
/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or .
*/
package org.hibernate.validator.internal.engine.valueextraction;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import javax.validation.ConstraintDeclarationException;
import javax.validation.valueextraction.ValueExtractor;
import org.hibernate.validator.internal.metadata.aggregated.CascadingMetaDataBuilder;
import org.hibernate.validator.internal.metadata.aggregated.ContainerCascadingMetaData;
import org.hibernate.validator.internal.metadata.aggregated.PotentiallyContainerCascadingMetaData;
import org.hibernate.validator.internal.util.CollectionHelper;
import org.hibernate.validator.internal.util.Contracts;
import org.hibernate.validator.internal.util.ReflectionHelper;
import org.hibernate.validator.internal.util.TypeHelper;
import org.hibernate.validator.internal.util.TypeVariableBindings;
import org.hibernate.validator.internal.util.TypeVariables;
import org.hibernate.validator.internal.util.logging.Log;
import org.hibernate.validator.internal.util.logging.LoggerFactory;
import org.hibernate.validator.internal.util.stereotypes.Immutable;
/**
* Contains resolving algorithms for {@link ValueExtractor}s, and caches for these
* extractors based on container types.
*
* @author Gunnar Morling
* @author Guillaume Smet
* @author Marko Bekhta
*/
public class ValueExtractorResolver {
private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );
private static final Object NON_CONTAINER_VALUE = new Object();
@Immutable
private final Set registeredValueExtractors;
private final ConcurrentHashMap> possibleValueExtractorsByRuntimeTypeAndTypeParameter = new ConcurrentHashMap<>();
private final ConcurrentHashMap, Set> possibleValueExtractorsByRuntimeType = new ConcurrentHashMap<>();
private final ConcurrentHashMap, Object> nonContainerTypes = new ConcurrentHashMap<>();
ValueExtractorResolver(Set valueExtractors) {
this.registeredValueExtractors = CollectionHelper.toImmutableSet( valueExtractors );
}
/**
* Used to find all the maximally specific value extractors based on a declared type in the case of value unwrapping.
*
* There might be several of them as there might be several type parameters.
*
* Used for container element constraints.
*/
public Set getMaximallySpecificValueExtractors(Class declaredType) {
return getRuntimeCompliantValueExtractors( declaredType, registeredValueExtractors );
}
/**
* Used to find the maximally specific and container element compliant value extractor based on the declared type
* and the type parameter.
*
* Used for container element constraints.
*
* @throws ConstraintDeclarationException if more than 2 maximally specific container-element-compliant value extractors are found
*/
public ValueExtractorDescriptor getMaximallySpecificAndContainerElementCompliantValueExtractor(Class declaredType, TypeVariable typeParameter) {
return getUniqueValueExtractorOrThrowException(
declaredType,
getRuntimeAndContainerElementCompliantValueExtractorsFromPossibleCandidates( declaredType, typeParameter, declaredType, registeredValueExtractors )
);
}
/**
* Used to find the maximally specific and container element compliant value extractor based on the runtime type.
*
* The maximally specific one is chosen among the candidates passed to this method.
*
* Used for cascading validation.
*
* @see ValueExtractorResolver#getMaximallySpecificAndRuntimeContainerElementCompliantValueExtractor(Type,
* TypeVariable, Class, Collection)
* @throws ConstraintDeclarationException if more than 2 maximally specific container-element-compliant value extractors are found
*/
public ValueExtractorDescriptor getMaximallySpecificAndRuntimeContainerElementCompliantValueExtractor(Type declaredType, TypeVariable typeParameter,
Class runtimeType, Collection valueExtractorCandidates) {
Contracts.assertNotEmpty( valueExtractorCandidates, "Value extractor candidates cannot be empty" );
if ( valueExtractorCandidates.size() == 1 ) {
return valueExtractorCandidates.iterator().next();
}
else {
return getUniqueValueExtractorOrThrowException(
runtimeType,
getRuntimeAndContainerElementCompliantValueExtractorsFromPossibleCandidates(
declaredType, typeParameter, runtimeType, valueExtractorCandidates
)
);
}
}
/**
* Used to determine if the passed runtime type is a container and if so return a corresponding maximally specific
* value extractor.
*
* Obviously, it only works if there's only one value extractor corresponding to the runtime type as we don't
* precise any type parameter.
*
* There is a special case: when the passed type is assignable to a {@link Map}, the {@link MapValueExtractor} will
* be returned. This is required by the Bean Validation specification.
*
* Used for cascading validation when the {@code @Valid} annotation is placed on the whole container.
*
* @throws ConstraintDeclarationException if more than 2 maximally specific container-element-compliant value extractors are found
*/
public ValueExtractorDescriptor getMaximallySpecificValueExtractorForAllContainerElements(Class runtimeType, Set potentialValueExtractorDescriptors) {
// if it's a Map assignable type, it gets a special treatment to conform to the Bean Validation specification
if ( TypeHelper.isAssignable( Map.class, runtimeType ) ) {
return MapValueExtractor.DESCRIPTOR;
}
return getUniqueValueExtractorOrThrowException( runtimeType, getRuntimeCompliantValueExtractors( runtimeType, potentialValueExtractorDescriptors ) );
}
/**
* Used to determine the value extractor candidates valid for a declared type and type variable.
*
* The effective value extractor will be narrowed from these candidates using the runtime type.
*
* Used to optimize the choice of the value extractor in the case of cascading validation.
*/
public Set getValueExtractorCandidatesForCascadedValidation(Type declaredType, TypeVariable typeParameter) {
Set valueExtractorDescriptors = new HashSet<>();
valueExtractorDescriptors.addAll( getRuntimeAndContainerElementCompliantValueExtractorsFromPossibleCandidates( declaredType, typeParameter,
TypeHelper.getErasedReferenceType( declaredType ), registeredValueExtractors
) );
valueExtractorDescriptors.addAll( getPotentiallyRuntimeTypeCompliantAndContainerElementCompliantValueExtractors( declaredType, typeParameter ) );
return CollectionHelper.toImmutableSet( valueExtractorDescriptors );
}
/**
* Used to determine the possible value extractors that can be applied to a declared type.
*
* Used when building cascading metadata in {@link CascadingMetaDataBuilder} to decide if it should be promoted to
* {@link ContainerCascadingMetaData} with cascaded constrained type arguments.
*
* An example could be when we need to upgrade BV 1.1 style {@code @Valid private List list;}
* to {@code private List<@Valid SomeBean> list;}
*
* Searches only for maximally specific value extractors based on a type.
*
* Types that are assignable to {@link Map} are handled as a special case - key value extractor is ignored for them.
*/
public Set getValueExtractorCandidatesForContainerDetectionOfGlobalCascadedValidation(Type enclosingType) {
// if it's a Map assignable type, it gets a special treatment to conform to the Bean Validation specification
boolean mapAssignable = TypeHelper.isAssignable( Map.class, enclosingType );
Class enclosingClass = ReflectionHelper.getClassFromType( enclosingType );
return getRuntimeCompliantValueExtractors( enclosingClass, registeredValueExtractors )
.stream()
.filter( ved -> !mapAssignable || !ved.equals( MapKeyExtractor.DESCRIPTOR ) )
.collect( Collectors.collectingAndThen( Collectors.toSet(), CollectionHelper::toImmutableSet ) );
}
/**
* Used to determine the value extractors which potentially could be applied to the runtime type of a given declared type.
*
* An example could be when there's a declaration like {@code private PotentiallyContainerAtRuntime<@Valid Bean>;} and there's
* no value extractor present for {@code PotentiallyContainerAtRuntime} but there's one available for
* {@code Container extends PotentiallyContainerAtRuntime}.
*
* Returned set of extractors is used to determine if at runtime a value extractor can be applied to a runtime type,
* and if {@link PotentiallyContainerCascadingMetaData} should be promoted to {@link ContainerCascadingMetaData}.
*
* @return a set of {@link ValueExtractorDescriptor}s that possibly might be applied to a {@code declaredType}
* at a runtime.
*/
public Set getPotentialValueExtractorCandidatesForCascadedValidation(Type declaredType) {
return registeredValueExtractors
.stream()
.filter( e -> TypeHelper.isAssignable( declaredType, e.getContainerType() ) )
.collect( Collectors.collectingAndThen( Collectors.toSet(), CollectionHelper::toImmutableSet ) );
}
public void clear() {
nonContainerTypes.clear();
possibleValueExtractorsByRuntimeType.clear();
possibleValueExtractorsByRuntimeTypeAndTypeParameter.clear();
}
/**
* Returns the set of potentially type-compliant and container-element-compliant value extractors or an empty set if none was found.
*
* A value extractor is potentially runtime type compliant if it might be compliant for any runtime type that matches the declared type.
*/
private Set getPotentiallyRuntimeTypeCompliantAndContainerElementCompliantValueExtractors(Type declaredType,
TypeVariable typeParameter) {
boolean isInternal = TypeVariables.isInternal( typeParameter );
Type erasedDeclaredType = TypeHelper.getErasedReferenceType( declaredType );
Set typeCompatibleExtractors = registeredValueExtractors
.stream()
.filter( e -> TypeHelper.isAssignable( erasedDeclaredType, e.getContainerType() ) )
.collect( Collectors.toSet() );
Set containerElementCompliantExtractors = new HashSet<>();
for ( ValueExtractorDescriptor extractorDescriptor : typeCompatibleExtractors ) {
TypeVariable typeParameterBoundToExtractorType;
if ( !isInternal ) {
Map, Map, TypeVariable>> allBindings =
TypeVariableBindings.getTypeVariableBindings( extractorDescriptor.getContainerType() );
Map, TypeVariable> bindingsForExtractorType = allBindings.get( erasedDeclaredType );
typeParameterBoundToExtractorType = bind( extractorDescriptor.getExtractedTypeParameter(), bindingsForExtractorType );
}
else {
typeParameterBoundToExtractorType = typeParameter;
}
if ( Objects.equals( typeParameter, typeParameterBoundToExtractorType ) ) {
containerElementCompliantExtractors.add( extractorDescriptor );
}
}
return containerElementCompliantExtractors;
}
private ValueExtractorDescriptor getUniqueValueExtractorOrThrowException(Class runtimeType,
Set maximallySpecificContainerElementCompliantValueExtractors) {
if ( maximallySpecificContainerElementCompliantValueExtractors.size() == 1 ) {
return maximallySpecificContainerElementCompliantValueExtractors.iterator().next();
}
else if ( maximallySpecificContainerElementCompliantValueExtractors.isEmpty() ) {
return null;
}
else {
throw LOG.getUnableToGetMostSpecificValueExtractorDueToSeveralMaximallySpecificValueExtractorsDeclaredException( runtimeType,
ValueExtractorHelper.toValueExtractorClasses( maximallySpecificContainerElementCompliantValueExtractors )
);
}
}
private Set getMaximallySpecificValueExtractors(Set possibleValueExtractors) {
Set valueExtractorDescriptors = CollectionHelper.newHashSet( possibleValueExtractors.size() );
for ( ValueExtractorDescriptor descriptor : possibleValueExtractors ) {
if ( valueExtractorDescriptors.isEmpty() ) {
valueExtractorDescriptors.add( descriptor );
continue;
}
Iterator candidatesIterator = valueExtractorDescriptors.iterator();
boolean isNewRoot = true;
while ( candidatesIterator.hasNext() ) {
ValueExtractorDescriptor candidate = candidatesIterator.next();
// we consider the strictly more specific value extractor so 2 value extractors for the same container
// type should throw an error in the end if no other more specific value extractor is found.
if ( candidate.getContainerType().equals( descriptor.getContainerType() ) ) {
continue;
}
if ( TypeHelper.isAssignable( candidate.getContainerType(), descriptor.getContainerType() ) ) {
candidatesIterator.remove();
}
else if ( TypeHelper.isAssignable( descriptor.getContainerType(), candidate.getContainerType() ) ) {
isNewRoot = false;
}
}
if ( isNewRoot ) {
valueExtractorDescriptors.add( descriptor );
}
}
return valueExtractorDescriptors;
}
/**
* @return a set of runtime compliant value extractors based on a runtime type. If there are no available value extractors
* an empty set will be returned which means the type is not a container.
*/
private Set getRuntimeCompliantValueExtractors(Class runtimeType, Set potentialValueExtractorDescriptors) {
if ( nonContainerTypes.contains( runtimeType ) ) {
return Collections.emptySet();
}
Set valueExtractorDescriptors = possibleValueExtractorsByRuntimeType.get( runtimeType );
if ( valueExtractorDescriptors == null ) {
//otherwise we just look for maximally specific extractors for the runtime type
Set possibleValueExtractors = potentialValueExtractorDescriptors
.stream()
.filter( e -> TypeHelper.isAssignable( e.getContainerType(), runtimeType ) )
.collect( Collectors.toSet() );
valueExtractorDescriptors = getMaximallySpecificValueExtractors( possibleValueExtractors );
}
if ( valueExtractorDescriptors.isEmpty() ) {
nonContainerTypes.put( runtimeType, NON_CONTAINER_VALUE );
}
else {
Set extractorDescriptorsToCache = CollectionHelper.toImmutableSet( valueExtractorDescriptors );
possibleValueExtractorsByRuntimeType.put( runtimeType, extractorDescriptorsToCache );
return extractorDescriptorsToCache;
}
return valueExtractorDescriptors;
}
private Set getRuntimeAndContainerElementCompliantValueExtractorsFromPossibleCandidates(Type declaredType,
TypeVariable typeParameter, Class runtimeType, Collection valueExtractorCandidates) {
if ( nonContainerTypes.contains( runtimeType ) ) {
return Collections.emptySet();
}
ValueExtractorCacheKey cacheKey = new ValueExtractorCacheKey( runtimeType, typeParameter );
Set valueExtractorDescriptors = possibleValueExtractorsByRuntimeTypeAndTypeParameter.get( cacheKey );
if ( valueExtractorDescriptors == null ) {
boolean isInternal = TypeVariables.isInternal( typeParameter );
Class erasedDeclaredType = TypeHelper.getErasedReferenceType( declaredType );
Set possibleValueExtractors = valueExtractorCandidates
.stream()
.filter( e -> TypeHelper.isAssignable( e.getContainerType(), runtimeType ) )
.filter( extractorDescriptor ->
checkValueExtractorTypeCompatibility(
typeParameter, isInternal, erasedDeclaredType, extractorDescriptor
)
).collect( Collectors.toSet() );
valueExtractorDescriptors = getMaximallySpecificValueExtractors( possibleValueExtractors );
if ( valueExtractorDescriptors.isEmpty() ) {
nonContainerTypes.put( runtimeType, NON_CONTAINER_VALUE );
}
else {
Set extractorDescriptorsToCache = CollectionHelper.toImmutableSet( valueExtractorDescriptors );
possibleValueExtractorsByRuntimeTypeAndTypeParameter.put( cacheKey, extractorDescriptorsToCache );
return extractorDescriptorsToCache;
}
}
return valueExtractorDescriptors;
}
private boolean checkValueExtractorTypeCompatibility(TypeVariable typeParameter, boolean isInternal, Class erasedDeclaredType,
ValueExtractorDescriptor extractorDescriptor) {
return TypeHelper.isAssignable( extractorDescriptor.getContainerType(), erasedDeclaredType )
? validateValueExtractorCompatibility( isInternal, erasedDeclaredType, extractorDescriptor.getContainerType(), typeParameter,
extractorDescriptor.getExtractedTypeParameter()
)
: validateValueExtractorCompatibility( isInternal, extractorDescriptor.getContainerType(), erasedDeclaredType,
extractorDescriptor.getExtractedTypeParameter(), typeParameter
);
}
private boolean validateValueExtractorCompatibility(boolean isInternal,
Class typeForBinding,
Class typeToBind,
TypeVariable typeParameterForBinding,
TypeVariable typeParameterToCompare) {
TypeVariable typeParameterBoundToExtractorType;
if ( !isInternal ) {
Map, Map, TypeVariable>> allBindings =
TypeVariableBindings.getTypeVariableBindings( typeForBinding );
Map, TypeVariable> bindingsForExtractorType = allBindings.get( typeToBind );
typeParameterBoundToExtractorType = bind( typeParameterForBinding, bindingsForExtractorType );
}
else {
typeParameterBoundToExtractorType = typeParameterForBinding;
}
return Objects.equals( typeParameterToCompare, typeParameterBoundToExtractorType );
}
private TypeVariable bind(TypeVariable typeParameter, Map, TypeVariable> bindings) {
return bindings != null ? bindings.get( typeParameter ) : null;
}
private static class ValueExtractorCacheKey {
// These properties are not final on purpose, it's faster when they are not
private Class type;
private TypeVariable typeParameter;
private int hashCode;
ValueExtractorCacheKey(Class type, TypeVariable typeParameter) {
this.type = type;
this.typeParameter = typeParameter;
this.hashCode = buildHashCode();
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null ) {
return false;
}
// We don't check the class as an optimization, the keys of the map are ValueExtractorCacheKey anyway
ValueExtractorCacheKey that = (ValueExtractorCacheKey) o;
return Objects.equals( this.type, that.type ) &&
Objects.equals( this.typeParameter, that.typeParameter );
}
@Override
public int hashCode() {
return hashCode;
}
private int buildHashCode() {
int result = this.type.hashCode();
result = 31 * result + ( this.typeParameter != null ? this.typeParameter.hashCode() : 0 );
return result;
}
}
}