/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package org.hibernate.validator.internal.metadata.cascading;
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.stream.Collectors;
import java.util.stream.Stream;
import org.hibernate.validator.internal.engine.cascading.AnnotatedObject;
import org.hibernate.validator.internal.engine.cascading.ArrayElement;
import org.hibernate.validator.internal.util.CollectionHelper;
import org.hibernate.validator.internal.util.StringHelper;
import org.hibernate.validator.internal.util.logging.Log;
import org.hibernate.validator.internal.util.logging.LoggerFactory;
import org.hibernate.validator.internal.util.stereotypes.Immutable;
/**
* A type parameter that is marked for cascaded validation and/or has one or more nested type parameters marked for
* cascaded validation.
*
* @author Guillaume Smet
*/
public class CascadingTypeParameter {
private static final Log LOG = LoggerFactory.make();
private static final CascadingTypeParameter NON_CASCADING =
new CascadingTypeParameter( null, null, false, Collections.emptyMap(), Collections.emptyMap() );
/**
* The enclosing type that defines this type parameter.
*/
private final Type enclosingType;
/**
* The type parameter.
*/
private final TypeVariable<?> typeParameter;
/**
* Possibly the cascading type parameters corresponding to this type parameter if it is a parameterized type.
*/
@Immutable
private final Map<TypeVariable<?>, CascadingTypeParameter> containerElementTypesCascadingMetaData;
/**
* If this type parameter is marked for cascading.
*/
private final boolean cascading;
/**
* Group conversions defined for this type parameter.
*/
@Immutable
private final Map<Class<?>, Class<?>> groupConversions;
/**
* Whether the constrained element is directly or indirectly (via type arguments) marked for cascaded validation.
*/
private final boolean markedForCascadingOnElementOrContainerElements;
/**
* Whether the constrained element has directly or indirectly (via type arguments) group conversions defined.
*/
private final boolean hasGroupConversionsOnElementOrContainerElements;
public CascadingTypeParameter(Type enclosingType, TypeVariable<?> typeParameter, boolean cascading,
Map<TypeVariable<?>, CascadingTypeParameter> containerElementTypesCascadingMetaData, Map<Class<?>, Class<?>> groupConversions) {
this.enclosingType = enclosingType;
this.typeParameter = typeParameter;
this.cascading = cascading;
this.groupConversions = CollectionHelper.toImmutableMap( groupConversions );
this.containerElementTypesCascadingMetaData = CollectionHelper.toImmutableMap( containerElementTypesCascadingMetaData );
boolean tmpMarkedForCascadingOnElementOrContainerElements = cascading;
boolean tmpHasGroupConversionsOnElementOrContainerElements = !groupConversions.isEmpty();
for ( CascadingTypeParameter nestedCascadingTypeParameter : containerElementTypesCascadingMetaData.values() ) {
tmpMarkedForCascadingOnElementOrContainerElements = tmpMarkedForCascadingOnElementOrContainerElements
|| nestedCascadingTypeParameter.markedForCascadingOnElementOrContainerElements;
tmpHasGroupConversionsOnElementOrContainerElements = tmpHasGroupConversionsOnElementOrContainerElements
|| nestedCascadingTypeParameter.hasGroupConversionsOnElementOrContainerElements;
}
markedForCascadingOnElementOrContainerElements = tmpMarkedForCascadingOnElementOrContainerElements;
hasGroupConversionsOnElementOrContainerElements = tmpHasGroupConversionsOnElementOrContainerElements;
}
public static CascadingTypeParameter nonCascading() {
return NON_CASCADING;
}
public static CascadingTypeParameter annotatedObject(Type cascadableType, boolean cascading,
Map<TypeVariable<?>, CascadingTypeParameter> containerElementTypesCascadingMetaData, Map<Class<?>, Class<?>> groupConversions) {
return new CascadingTypeParameter( cascadableType, AnnotatedObject.INSTANCE, cascading, containerElementTypesCascadingMetaData, groupConversions );
}
public static CascadingTypeParameter arrayElement(Type cascadableType, boolean cascading,
Map<TypeVariable<?>, CascadingTypeParameter> containerElementTypesCascadingMetaData, Map<Class<?>, Class<?>> groupConversions) {
return new CascadingTypeParameter( cascadableType, new ArrayElement( cascadableType ), cascading,
containerElementTypesCascadingMetaData, groupConversions );
}
public TypeVariable<?> getTypeParameter() {
return typeParameter;
}
public Type getEnclosingType() {
return enclosingType;
}
public boolean isCascading() {
return cascading;
}
public Map<Class<?>, Class<?>> getGroupConversions() {
return groupConversions;
}
public boolean isMarkedForCascadingOnElementOrContainerElements() {
return markedForCascadingOnElementOrContainerElements;
}
public boolean hasGroupConversionsOnElementOrContainerElements() {
return hasGroupConversionsOnElementOrContainerElements;
}
public Map<TypeVariable<?>, CascadingTypeParameter> getContainerElementTypesCascadingMetaData() {
return containerElementTypesCascadingMetaData;
}
public CascadingTypeParameter merge(CascadingTypeParameter 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<?>, CascadingTypeParameter> 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 CascadingTypeParameter( this.enclosingType, this.typeParameter, cascading, nestedCascadingTypeParameterMap, groupConversions );
}
@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() {
// enclosingType is excluded from the hashCode and equals methods as it will not work for parameterized types
// see TypeAnnotationDefinedOnAGenericTypeArgumentTest.constraintOnGenericTypeArgumentOfListReturnValueThrowsException for instance
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) {
// enclosingType is excluded from the hashCode and equals methods as it will not work for parameterized types
// see TypeAnnotationDefinedOnAGenericTypeArgumentTest.constraintOnGenericTypeArgumentOfListReturnValueThrowsException for instance
if ( this == obj ) {
return true;
}
if ( obj == null ) {
return false;
}
if ( getClass() != obj.getClass() ) {
return false;
}
CascadingTypeParameter other = (CascadingTypeParameter) 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() ) {
// this is a rather common case so let's optimize it
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;
}
}