package org.hibernate.validator.internal.metadata.aggregated;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.validation.GroupSequence;
import org.hibernate.validator.internal.engine.valueextraction.AnnotatedObject;
import org.hibernate.validator.internal.engine.valueextraction.ArrayElement;
import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorDescriptor;
import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorHelper;
import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorManager;
import org.hibernate.validator.internal.util.CollectionHelper;
import org.hibernate.validator.internal.util.ReflectionHelper;
import org.hibernate.validator.internal.util.StringHelper;
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;
public class CascadingMetaDataBuilder {
private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );
private static final CascadingMetaDataBuilder NON_CASCADING =
new CascadingMetaDataBuilder( null, null, null, null, false, Collections.emptyMap(), Collections.emptyMap() );
private final Type enclosingType;
private final TypeVariable<?> typeParameter;
private final Class<?> declaredContainerClass;
private final TypeVariable<?> declaredTypeParameter;
@Immutable
private final Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData;
private final boolean cascading;
@Immutable
private final Map<Class<?>, Class<?>> groupConversions;
private final boolean hasContainerElementsMarkedForCascading;
private final boolean hasGroupConversionsOnAnnotatedObjectOrContainerElements;
public CascadingMetaDataBuilder(Type enclosingType, TypeVariable<?> typeParameter, boolean cascading,
Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData, Map<Class<?>, Class<?>> groupConversions) {
this( enclosingType, typeParameter,
TypeVariables.getContainerClass( typeParameter ), TypeVariables.getActualTypeParameter( typeParameter ),
cascading, containerElementTypesCascadingMetaData, groupConversions );
}
private CascadingMetaDataBuilder(Type enclosingType, TypeVariable<?> typeParameter, Class<?> declaredContainerClass, TypeVariable<?> declaredTypeParameter,
boolean cascading, Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData,
Map<Class<?>, Class<?>> groupConversions) {
this.enclosingType = enclosingType;
this.typeParameter = typeParameter;
this.declaredContainerClass = declaredContainerClass;
this.declaredTypeParameter = declaredTypeParameter;
this.cascading = cascading;
this.groupConversions = CollectionHelper.toImmutableMap( groupConversions );
this.containerElementTypesCascadingMetaData = CollectionHelper.toImmutableMap( containerElementTypesCascadingMetaData );
boolean tmpHasContainerElementsMarkedForCascading = false;
boolean tmpHasGroupConversionsOnAnnotatedObjectOrContainerElements = !groupConversions.isEmpty();
for ( CascadingMetaDataBuilder nestedCascadingTypeParameter : containerElementTypesCascadingMetaData.values() ) {
tmpHasContainerElementsMarkedForCascading = tmpHasContainerElementsMarkedForCascading
|| nestedCascadingTypeParameter.cascading || nestedCascadingTypeParameter.hasContainerElementsMarkedForCascading;
tmpHasGroupConversionsOnAnnotatedObjectOrContainerElements = tmpHasGroupConversionsOnAnnotatedObjectOrContainerElements
|| nestedCascadingTypeParameter.hasGroupConversionsOnAnnotatedObjectOrContainerElements;
}
hasContainerElementsMarkedForCascading = tmpHasContainerElementsMarkedForCascading;
hasGroupConversionsOnAnnotatedObjectOrContainerElements = tmpHasGroupConversionsOnAnnotatedObjectOrContainerElements;
}
public static CascadingMetaDataBuilder nonCascading() {
return NON_CASCADING;
}
public static CascadingMetaDataBuilder annotatedObject(Type cascadableType, boolean cascading, Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData, Map<Class<?>, Class<?>> groupConversions) {
return new CascadingMetaDataBuilder( cascadableType, AnnotatedObject.INSTANCE, cascading, containerElementTypesCascadingMetaData, groupConversions );
}
public TypeVariable<?> getTypeParameter() {
return typeParameter;
}
public Type getEnclosingType() {
return enclosingType;
}
public Class<?> getDeclaredContainerClass() {
return declaredContainerClass;
}
public TypeVariable<?> getDeclaredTypeParameter() {
return declaredTypeParameter;
}
public boolean isCascading() {
return cascading;
}
public Map<Class<?>, Class<?>> getGroupConversions() {
return groupConversions;
}
public boolean hasContainerElementsMarkedForCascading() {
return hasContainerElementsMarkedForCascading;
}
public boolean isMarkedForCascadingOnAnnotatedObjectOrContainerElements() {
return cascading || hasContainerElementsMarkedForCascading;
}
public boolean hasGroupConversionsOnAnnotatedObjectOrContainerElements() {
return hasGroupConversionsOnAnnotatedObjectOrContainerElements;
}
public Map<TypeVariable<?>, CascadingMetaDataBuilder> getContainerElementTypesCascadingMetaData() {
return containerElementTypesCascadingMetaData;
}
public CascadingMetaDataBuilder merge(CascadingMetaDataBuilder otherCascadingTypeParameter) {
if ( this == NON_CASCADING ) {
return otherCascadingTypeParameter;
}
if ( otherCascadingTypeParameter == NON_CASCADING ) {
return this;
}
boolean cascading = this.cascading || otherCascadingTypeParameter.cascading;
Map<Class<?>, Class<?>> groupConversions = mergeGroupConversion( this.groupConversions, otherCascadingTypeParameter.groupConversions );
Map<TypeVariable<?>, CascadingMetaDataBuilder> nestedCascadingTypeParameterMap = Stream
.concat( this.containerElementTypesCascadingMetaData.entrySet().stream(),
otherCascadingTypeParameter.containerElementTypesCascadingMetaData.entrySet().stream() )
.collect(
Collectors.toMap( entry -> entry.getKey(), entry -> entry.getValue(), ( value1, value2 ) -> value1.merge( value2 ) ) );
return new CascadingMetaDataBuilder( this.enclosingType, this.typeParameter, cascading, nestedCascadingTypeParameterMap, groupConversions );
}
public CascadingMetaData (ValueExtractorManager valueExtractorManager, Object context) {
validateGroupConversions( context );
if ( !cascading ) {
if ( !containerElementTypesCascadingMetaData.isEmpty() && hasContainerElementsMarkedForCascading ) {
return ContainerCascadingMetaData.of( valueExtractorManager, this, context );
}
else {
return NonContainerCascadingMetaData.of( this, context );
}
}
Set<ValueExtractorDescriptor> containerDetectionValueExtractorCandidates = valueExtractorManager.getResolver()
.getValueExtractorCandidatesForContainerDetectionOfGlobalCascadedValidation( enclosingType );
if ( !containerDetectionValueExtractorCandidates.isEmpty() ) {
if ( containerDetectionValueExtractorCandidates.size() > 1 ) {
throw LOG.getUnableToGetMostSpecificValueExtractorDueToSeveralMaximallySpecificValueExtractorsDeclaredException(
ReflectionHelper.getClassFromType( enclosingType ),
ValueExtractorHelper.toValueExtractorClasses( containerDetectionValueExtractorCandidates )
);
}
return ContainerCascadingMetaData.of(
valueExtractorManager,
new CascadingMetaDataBuilder(
enclosingType,
typeParameter,
cascading,
addCascadingMetaDataBasedOnContainerDetection( enclosingType, containerElementTypesCascadingMetaData, groupConversions,
containerDetectionValueExtractorCandidates.iterator().next() ),
groupConversions
),
context
);
}
Set<ValueExtractorDescriptor> potentialValueExtractorCandidates = valueExtractorManager.getResolver()
.getPotentialValueExtractorCandidatesForCascadedValidation( enclosingType );
if ( !potentialValueExtractorCandidates.isEmpty() ) {
return PotentiallyContainerCascadingMetaData.of( this, potentialValueExtractorCandidates, context );
}
return NonContainerCascadingMetaData.of( this, context );
}
private void validateGroupConversions(Object context) {
if ( !cascading && !groupConversions.isEmpty() ) {
throw LOG.getGroupConversionOnNonCascadingElementException( context );
}
for ( Class<?> group : groupConversions.keySet() ) {
if ( group.isAnnotationPresent( GroupSequence.class ) ) {
throw LOG.getGroupConversionForSequenceException( group );
}
}
for ( CascadingMetaDataBuilder containerElementCascadingTypeParameter : containerElementTypesCascadingMetaData.values() ) {
containerElementCascadingTypeParameter.validateGroupConversions( context );
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append( getClass().getSimpleName() );
sb.append( " [" );
sb.append( "enclosingType=" ).append( StringHelper.toShortString( enclosingType ) ).append( ", " );
sb.append( "typeParameter=" ).append( typeParameter ).append( ", " );
sb.append( "cascading=" ).append( cascading ).append( ", " );
sb.append( "groupConversions=" ).append( groupConversions ).append( ", " );
sb.append( "containerElementTypesCascadingMetaData=" ).append( containerElementTypesCascadingMetaData );
sb.append( "]" );
return sb.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + typeParameter.hashCode();
result = prime * result + ( cascading ? 1 : 0 );
result = prime * result + groupConversions.hashCode();
result = prime * result + containerElementTypesCascadingMetaData.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if ( this == obj ) {
return true;
}
if ( obj == null ) {
return false;
}
if ( getClass() != obj.getClass() ) {
return false;
}
CascadingMetaDataBuilder other = (CascadingMetaDataBuilder) obj;
if ( !typeParameter.equals( other.typeParameter ) ) {
return false;
}
if ( cascading != other.cascading ) {
return false;
}
if ( !groupConversions.equals( other.groupConversions ) ) {
return false;
}
if ( !containerElementTypesCascadingMetaData.equals( other.containerElementTypesCascadingMetaData ) ) {
return false;
}
return true;
}
private static Map<Class<?>, Class<?>> mergeGroupConversion(Map<Class<?>, Class<?>> groupConversions, Map<Class<?>, Class<?>> otherGroupConversions) {
if ( groupConversions.isEmpty() && otherGroupConversions.isEmpty() ) {
return Collections.emptyMap();
}
Map<Class<?>, Class<?>> mergedGroupConversions = new HashMap<>( groupConversions.size() + otherGroupConversions.size() );
for ( Entry<Class<?>, Class<?>> otherGroupConversionEntry : otherGroupConversions.entrySet() ) {
if ( groupConversions.containsKey( otherGroupConversionEntry.getKey() ) ) {
throw LOG.getMultipleGroupConversionsForSameSourceException(
otherGroupConversionEntry.getKey(),
CollectionHelper.<Class<?>>asSet(
groupConversions.get( otherGroupConversionEntry.getKey() ),
otherGroupConversionEntry.getValue() ) );
}
}
mergedGroupConversions.putAll( groupConversions );
mergedGroupConversions.putAll( otherGroupConversions );
return mergedGroupConversions;
}
private static Map<TypeVariable<?>, CascadingMetaDataBuilder> (Type cascadableType, Map<TypeVariable<?>,
CascadingMetaDataBuilder> containerElementTypesCascadingMetaData, Map<Class<?>, Class<?>> groupConversions,
ValueExtractorDescriptor possibleValueExtractor) {
Class<?> cascadableClass = ReflectionHelper.getClassFromType( cascadableType );
if ( cascadableClass.isArray() ) {
return addArrayElementCascadingMetaData( cascadableClass, containerElementTypesCascadingMetaData, groupConversions );
}
else {
Map<TypeVariable<?>, CascadingMetaDataBuilder> cascadingMetaData = containerElementTypesCascadingMetaData;
cascadingMetaData = addCascadingMetaData(
cascadableClass,
possibleValueExtractor.getContainerType(),
possibleValueExtractor.getExtractedTypeParameter(),
cascadingMetaData,
groupConversions
);
return cascadingMetaData;
}
}
private static Map<TypeVariable<?>, CascadingMetaDataBuilder> addCascadingMetaData(final Class<?> enclosingType, Class<?> referenceType,
TypeVariable<?> typeParameter, Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData,
Map<Class<?>, Class<?>> groupConversions) {
Map<Class<?>, Map<TypeVariable<?>, TypeVariable<?>>> typeVariableBindings = TypeVariableBindings.getTypeVariableBindings( enclosingType );
final TypeVariable<?> correspondingTypeParameter = typeVariableBindings.get( referenceType ).entrySet().stream()
.filter( e -> Objects.equals( e.getKey().getGenericDeclaration(), enclosingType ) )
.collect( Collectors.toMap( Map.Entry::getValue, Map.Entry::getKey ) )
.get( typeParameter );
Class<?> cascadableClass;
TypeVariable<?> cascadableTypeParameter;
if ( correspondingTypeParameter != null ) {
cascadableClass = enclosingType;
cascadableTypeParameter = correspondingTypeParameter;
}
else {
cascadableClass = referenceType;
cascadableTypeParameter = typeParameter;
}
Map<TypeVariable<?>, CascadingMetaDataBuilder> amendedCascadingMetadata = CollectionHelper.newHashMap( containerElementTypesCascadingMetaData.size() + 1 );
amendedCascadingMetadata.putAll( containerElementTypesCascadingMetaData );
if ( containerElementTypesCascadingMetaData.containsKey( cascadableTypeParameter ) ) {
amendedCascadingMetadata.put( cascadableTypeParameter,
makeCascading( containerElementTypesCascadingMetaData.get( cascadableTypeParameter ), groupConversions ) );
}
else {
amendedCascadingMetadata.put( cascadableTypeParameter,
new CascadingMetaDataBuilder( cascadableClass, cascadableTypeParameter, enclosingType, correspondingTypeParameter, true,
Collections.emptyMap(), groupConversions ) );
}
return amendedCascadingMetadata;
}
private static Map<TypeVariable<?>, CascadingMetaDataBuilder> addArrayElementCascadingMetaData(final Class<?> enclosingType,
Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData,
Map<Class<?>, Class<?>> groupConversions) {
Map<TypeVariable<?>, CascadingMetaDataBuilder> amendedCascadingMetadata = CollectionHelper.newHashMap( containerElementTypesCascadingMetaData.size() + 1 );
amendedCascadingMetadata.putAll( containerElementTypesCascadingMetaData );
TypeVariable<?> cascadableTypeParameter = new ArrayElement( enclosingType );
amendedCascadingMetadata.put( cascadableTypeParameter,
new CascadingMetaDataBuilder( enclosingType, cascadableTypeParameter, true, Collections.emptyMap(), groupConversions ) );
return amendedCascadingMetadata;
}
private static CascadingMetaDataBuilder makeCascading(CascadingMetaDataBuilder cascadingTypeParameter, Map<Class<?>, Class<?>> groupConversions) {
return new CascadingMetaDataBuilder( cascadingTypeParameter.enclosingType, cascadingTypeParameter.typeParameter, true,
cascadingTypeParameter.containerElementTypesCascadingMetaData,
cascadingTypeParameter.groupConversions.isEmpty() ? groupConversions : cascadingTypeParameter.groupConversions );
}
}