1 /*
2  * Hibernate Validator, declare and validate application constraints
3  *
4  * License: Apache License, Version 2.0
5  * See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
6  */

7 package org.hibernate.validator.internal.metadata.aggregated;
8
9 import java.lang.invoke.MethodHandles;
10 import java.lang.reflect.Type;
11 import java.lang.reflect.TypeVariable;
12 import java.util.Collections;
13 import java.util.HashMap;
14 import java.util.Map;
15 import java.util.Map.Entry;
16 import java.util.Objects;
17 import java.util.Set;
18 import java.util.stream.Collectors;
19 import java.util.stream.Stream;
20
21 import javax.validation.GroupSequence;
22
23 import org.hibernate.validator.internal.engine.valueextraction.AnnotatedObject;
24 import org.hibernate.validator.internal.engine.valueextraction.ArrayElement;
25 import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorDescriptor;
26 import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorHelper;
27 import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorManager;
28 import org.hibernate.validator.internal.util.CollectionHelper;
29 import org.hibernate.validator.internal.util.ReflectionHelper;
30 import org.hibernate.validator.internal.util.StringHelper;
31 import org.hibernate.validator.internal.util.TypeVariableBindings;
32 import org.hibernate.validator.internal.util.TypeVariables;
33 import org.hibernate.validator.internal.util.logging.Log;
34 import org.hibernate.validator.internal.util.logging.LoggerFactory;
35 import org.hibernate.validator.internal.util.stereotypes.Immutable;
36
37 /**
38  * A temporary data structure used to build {@link CascadingMetaData}. It is not a builder per se but it's as much as it
39  * gets.
40  *
41  * @author Guillaume Smet
42  */

43 public class CascadingMetaDataBuilder {
44
45     private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );
46
47     private static final CascadingMetaDataBuilder NON_CASCADING =
48             new CascadingMetaDataBuilder( nullnullnullnullfalse, Collections.emptyMap(), Collections.emptyMap() );
49
50     /**
51      * The enclosing type that defines this type parameter.
52      */

53     private final Type enclosingType;
54
55     /**
56      * The type parameter.
57      */

58     private final TypeVariable<?> typeParameter;
59
60     /**
61      * The declared container class: it is the one used in the node of the property path.
62      */

63     private final Class<?> declaredContainerClass;
64
65     /**
66      * The declared type parameter: it is the one used in the node of the property path.
67      */

68     private final TypeVariable<?> declaredTypeParameter;
69
70     /**
71      * Possibly the cascading type parameters corresponding to this type parameter if it is a parameterized type.
72      */

73     @Immutable
74     private final Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData;
75
76     /**
77      * If this type parameter is marked for cascading.
78      */

79     private final boolean cascading;
80
81     /**
82      * Group conversions defined for this type parameter.
83      */

84     @Immutable
85     private final Map<Class<?>, Class<?>> groupConversions;
86
87     /**
88      * Whether any container element (it can be nested) is marked for cascaded validation.
89      */

90     private final boolean hasContainerElementsMarkedForCascading;
91
92     /**
93      * Whether the constrained element has directly or indirectly (via type arguments) group conversions defined.
94      */

95     private final boolean hasGroupConversionsOnAnnotatedObjectOrContainerElements;
96
97     public CascadingMetaDataBuilder(Type enclosingType, TypeVariable<?> typeParameter, boolean cascading,
98             Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData, Map<Class<?>, Class<?>> groupConversions) {
99         this( enclosingType, typeParameter,
100                 TypeVariables.getContainerClass( typeParameter ), TypeVariables.getActualTypeParameter( typeParameter ),
101                 cascading, containerElementTypesCascadingMetaData, groupConversions );
102     }
103
104     private CascadingMetaDataBuilder(Type enclosingType, TypeVariable<?> typeParameter, Class<?> declaredContainerClass, TypeVariable<?> declaredTypeParameter,
105             boolean cascading, Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData,
106             Map<Class<?>, Class<?>> groupConversions) {
107         this.enclosingType = enclosingType;
108         this.typeParameter = typeParameter;
109         this.declaredContainerClass = declaredContainerClass;
110         this.declaredTypeParameter = declaredTypeParameter;
111         this.cascading = cascading;
112         this.groupConversions = CollectionHelper.toImmutableMap( groupConversions );
113         this.containerElementTypesCascadingMetaData = CollectionHelper.toImmutableMap( containerElementTypesCascadingMetaData );
114
115         boolean tmpHasContainerElementsMarkedForCascading = false;
116         boolean tmpHasGroupConversionsOnAnnotatedObjectOrContainerElements = !groupConversions.isEmpty();
117         for ( CascadingMetaDataBuilder nestedCascadingTypeParameter : containerElementTypesCascadingMetaData.values() ) {
118             tmpHasContainerElementsMarkedForCascading = tmpHasContainerElementsMarkedForCascading
119                     || nestedCascadingTypeParameter.cascading || nestedCascadingTypeParameter.hasContainerElementsMarkedForCascading;
120             tmpHasGroupConversionsOnAnnotatedObjectOrContainerElements = tmpHasGroupConversionsOnAnnotatedObjectOrContainerElements
121                     || nestedCascadingTypeParameter.hasGroupConversionsOnAnnotatedObjectOrContainerElements;
122         }
123         hasContainerElementsMarkedForCascading = tmpHasContainerElementsMarkedForCascading;
124         hasGroupConversionsOnAnnotatedObjectOrContainerElements = tmpHasGroupConversionsOnAnnotatedObjectOrContainerElements;
125     }
126
127     public static CascadingMetaDataBuilder nonCascading() {
128         return NON_CASCADING;
129     }
130
131     public static CascadingMetaDataBuilder annotatedObject(Type cascadableType, boolean cascading, Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData, Map<Class<?>, Class<?>> groupConversions) {
132         return new CascadingMetaDataBuilder( cascadableType, AnnotatedObject.INSTANCE, cascading, containerElementTypesCascadingMetaData, groupConversions );
133     }
134
135     public TypeVariable<?> getTypeParameter() {
136         return typeParameter;
137     }
138
139     public Type getEnclosingType() {
140         return enclosingType;
141     }
142
143     public Class<?> getDeclaredContainerClass() {
144         return declaredContainerClass;
145     }
146
147     public TypeVariable<?> getDeclaredTypeParameter() {
148         return declaredTypeParameter;
149     }
150
151     public boolean isCascading() {
152         return cascading;
153     }
154
155     public Map<Class<?>, Class<?>> getGroupConversions() {
156         return groupConversions;
157     }
158
159     public boolean hasContainerElementsMarkedForCascading() {
160         return hasContainerElementsMarkedForCascading;
161     }
162
163     public boolean isMarkedForCascadingOnAnnotatedObjectOrContainerElements() {
164         return cascading || hasContainerElementsMarkedForCascading;
165     }
166
167     public boolean hasGroupConversionsOnAnnotatedObjectOrContainerElements() {
168         return hasGroupConversionsOnAnnotatedObjectOrContainerElements;
169     }
170
171     public Map<TypeVariable<?>, CascadingMetaDataBuilder> getContainerElementTypesCascadingMetaData() {
172         return containerElementTypesCascadingMetaData;
173     }
174
175     public CascadingMetaDataBuilder merge(CascadingMetaDataBuilder otherCascadingTypeParameter) {
176         if ( this == NON_CASCADING ) {
177             return otherCascadingTypeParameter;
178         }
179         if ( otherCascadingTypeParameter == NON_CASCADING ) {
180             return this;
181         }
182
183         boolean cascading = this.cascading || otherCascadingTypeParameter.cascading;
184
185         Map<Class<?>, Class<?>> groupConversions = mergeGroupConversion( this.groupConversions, otherCascadingTypeParameter.groupConversions );
186
187         Map<TypeVariable<?>, CascadingMetaDataBuilder> nestedCascadingTypeParameterMap = Stream
188                 .concat( this.containerElementTypesCascadingMetaData.entrySet().stream(),
189                         otherCascadingTypeParameter.containerElementTypesCascadingMetaData.entrySet().stream() )
190                 .collect(
191                         Collectors.toMap( entry -> entry.getKey(), entry -> entry.getValue(), ( value1, value2 ) -> value1.merge( value2 ) ) );
192
193         return new CascadingMetaDataBuilder( this.enclosingType, this.typeParameter, cascading, nestedCascadingTypeParameterMap, groupConversions );
194     }
195
196     public CascadingMetaData build(ValueExtractorManager valueExtractorManager, Object context) {
197         validateGroupConversions( context );
198
199         // In the case the whole object is not annotated as cascading, we don't need to enable
200         // the runtime detection of container so we can return early.
201         if ( !cascading ) {
202             // We have cascading enabled for at least one of the container elements
203             if ( !containerElementTypesCascadingMetaData.isEmpty() && hasContainerElementsMarkedForCascading ) {
204                 return ContainerCascadingMetaData.of( valueExtractorManager, this, context );
205             }
206             // It is not a container or it doesn't have cascading enabled on any container element
207             else {
208                 return NonContainerCascadingMetaData.of( this, context );
209             }
210         }
211
212         // We are now in the case where @Valid is defined on the whole object e.g. @Valid SomeType property;.
213         //
214         // In this case, we try to detect if SomeType is a container i.e. if it has a valid value extractor.
215         //
216         // If SomeType is a container, we will enable cascading validation for this container, if and only if
217         // we have only one compatible value extractor.
218         //
219         // In the special case of a Map, only MapValueExtractor is considered compatible in this case as per
220         // the Bean Validation spec.
221         //
222         // If we find several compatible value extractors, we throw an exception.
223         //
224         // The value extractor returned here is just used to add the proper cascading metadata to the type
225         // argument of the container. Proper value extractor resolution is executed at runtime.
226         Set<ValueExtractorDescriptor> containerDetectionValueExtractorCandidates = valueExtractorManager.getResolver()
227                 .getValueExtractorCandidatesForContainerDetectionOfGlobalCascadedValidation( enclosingType );
228         if ( !containerDetectionValueExtractorCandidates.isEmpty() ) {
229             if ( containerDetectionValueExtractorCandidates.size() > 1 ) {
230                 throw LOG.getUnableToGetMostSpecificValueExtractorDueToSeveralMaximallySpecificValueExtractorsDeclaredException(
231                         ReflectionHelper.getClassFromType( enclosingType ),
232                         ValueExtractorHelper.toValueExtractorClasses( containerDetectionValueExtractorCandidates )
233                 );
234             }
235
236             return ContainerCascadingMetaData.of(
237                     valueExtractorManager,
238                     new CascadingMetaDataBuilder(
239                             enclosingType,
240                             typeParameter,
241                             cascading,
242                             addCascadingMetaDataBasedOnContainerDetection( enclosingType, containerElementTypesCascadingMetaData, groupConversions,
243                                     containerDetectionValueExtractorCandidates.iterator().next() ),
244                             groupConversions
245                     ),
246                     context
247             );
248         }
249
250         // If there are no possible VEs that can be applied right away to a declared type we should check if
251         // there are any VEs that can be potentially applied to our type at runtime. This should cover cases
252         // like @Valid Object object; or @Valid ContainerWithoutRegisteredVE container; where at runtime we can have
253         // object = new ArrayList<>(); or container = new ContainerWithRegisteredVE(); (with ContainerWithRegisteredVE
254         // extends ContainerWithoutRegisteredVE)
255         // so we are looking for VEs such that ValueExtractorDescriptor#getContainerType() is assignable to the declared
256         // type under inspection.
257         Set<ValueExtractorDescriptor> potentialValueExtractorCandidates = valueExtractorManager.getResolver()
258                 .getPotentialValueExtractorCandidatesForCascadedValidation( enclosingType );
259
260         // if such VEs were found we return an instance of PotentiallyContainerCascadingMetaData that will store those potential VEs
261         // and they will be used at runtime to check if any of those could be applied to a runtime type and if PotentiallyContainerCascadingMetaData
262         // should be promoted to ContainerCascadingMetaData or not.
263         if ( !potentialValueExtractorCandidates.isEmpty() ) {
264             return PotentiallyContainerCascadingMetaData.of( this, potentialValueExtractorCandidates, context );
265         }
266
267         // if cascading == false, or none of the above cases matched we just return a non container metadata
268         return NonContainerCascadingMetaData.of( this, context );
269     }
270
271     private void validateGroupConversions(Object context) {
272         // group conversions may only be configured for cascadable elements
273         if ( !cascading && !groupConversions.isEmpty() ) {
274             throw LOG.getGroupConversionOnNonCascadingElementException( context );
275         }
276
277         // group conversions may not be configured using a sequence as source
278         for ( Class<?> group : groupConversions.keySet() ) {
279             if ( group.isAnnotationPresent( GroupSequence.class ) ) {
280                 throw LOG.getGroupConversionForSequenceException( group );
281             }
282         }
283
284         for ( CascadingMetaDataBuilder containerElementCascadingTypeParameter : containerElementTypesCascadingMetaData.values() ) {
285             containerElementCascadingTypeParameter.validateGroupConversions( context );
286         }
287     }
288
289     @Override
290     public String toString() {
291         StringBuilder sb = new StringBuilder();
292         sb.append( getClass().getSimpleName() );
293         sb.append( " [" );
294         sb.append( "enclosingType=" ).append( StringHelper.toShortString( enclosingType ) ).append( ", " );
295         sb.append( "typeParameter=" ).append( typeParameter ).append( ", " );
296         sb.append( "cascading=" ).append( cascading ).append( ", " );
297         sb.append( "groupConversions=" ).append( groupConversions ).append( ", " );
298         sb.append( "containerElementTypesCascadingMetaData=" ).append( containerElementTypesCascadingMetaData );
299         sb.append( "]" );
300         return sb.toString();
301     }
302
303     @Override
304     public int hashCode() {
305         // enclosingType is excluded from the hashCode and equals methods as it will not work for parameterized types
306         // see TypeAnnotationDefinedOnAGenericTypeArgumentTest.constraintOnGenericTypeArgumentOfListReturnValueThrowsException for instance
307         final int prime = 31;
308         int result = 1;
309         result = prime * result + typeParameter.hashCode();
310         result = prime * result + ( cascading ? 1 : 0 );
311         result = prime * result + groupConversions.hashCode();
312         result = prime * result + containerElementTypesCascadingMetaData.hashCode();
313         return result;
314     }
315
316     @Override
317     public boolean equals(Object obj) {
318         // enclosingType is excluded from the hashCode and equals methods as it will not work for parameterized types
319         // see TypeAnnotationDefinedOnAGenericTypeArgumentTest.constraintOnGenericTypeArgumentOfListReturnValueThrowsException for instance
320         if ( this == obj ) {
321             return true;
322         }
323         if ( obj == null ) {
324             return false;
325         }
326         if ( getClass() != obj.getClass() ) {
327             return false;
328         }
329         CascadingMetaDataBuilder other = (CascadingMetaDataBuilder) obj;
330         if ( !typeParameter.equals( other.typeParameter ) ) {
331             return false;
332         }
333         if ( cascading != other.cascading ) {
334             return false;
335         }
336         if ( !groupConversions.equals( other.groupConversions ) ) {
337             return false;
338         }
339         if ( !containerElementTypesCascadingMetaData.equals( other.containerElementTypesCascadingMetaData ) ) {
340             return false;
341         }
342         return true;
343     }
344
345     private static Map<Class<?>, Class<?>> mergeGroupConversion(Map<Class<?>, Class<?>> groupConversions, Map<Class<?>, Class<?>> otherGroupConversions) {
346         if ( groupConversions.isEmpty() && otherGroupConversions.isEmpty() ) {
347             // this is a rather common case so let's optimize it
348             return Collections.emptyMap();
349         }
350
351         Map<Class<?>, Class<?>> mergedGroupConversions = new HashMap<>( groupConversions.size() + otherGroupConversions.size() );
352
353         for ( Entry<Class<?>, Class<?>> otherGroupConversionEntry : otherGroupConversions.entrySet() ) {
354             if ( groupConversions.containsKey( otherGroupConversionEntry.getKey() ) ) {
355                 throw LOG.getMultipleGroupConversionsForSameSourceException(
356                         otherGroupConversionEntry.getKey(),
357                         CollectionHelper.<Class<?>>asSet(
358                                 groupConversions.get( otherGroupConversionEntry.getKey() ),
359                                 otherGroupConversionEntry.getValue() ) );
360             }
361         }
362
363         mergedGroupConversions.putAll( groupConversions );
364         mergedGroupConversions.putAll( otherGroupConversions );
365
366         return mergedGroupConversions;
367     }
368
369     private static Map<TypeVariable<?>, CascadingMetaDataBuilder> addCascadingMetaDataBasedOnContainerDetection(Type cascadableType, Map<TypeVariable<?>,
370             CascadingMetaDataBuilder> containerElementTypesCascadingMetaData, Map<Class<?>, Class<?>> groupConversions,
371              ValueExtractorDescriptor possibleValueExtractor) {
372         Class<?> cascadableClass = ReflectionHelper.getClassFromType( cascadableType );
373         if ( cascadableClass.isArray() ) {
374             // for arrays, we need to add an ArrayElement cascading metadata: it's the only way arrays support cascading at the moment.
375             return addArrayElementCascadingMetaData( cascadableClass, containerElementTypesCascadingMetaData, groupConversions );
376         }
377         else {
378             Map<TypeVariable<?>, CascadingMetaDataBuilder> cascadingMetaData = containerElementTypesCascadingMetaData;
379                 cascadingMetaData = addCascadingMetaData(
380                         cascadableClass,
381                         possibleValueExtractor.getContainerType(),
382                         possibleValueExtractor.getExtractedTypeParameter(),
383                         cascadingMetaData,
384                         groupConversions
385                 );
386             return cascadingMetaData;
387         }
388     }
389
390     private static Map<TypeVariable<?>, CascadingMetaDataBuilder> addCascadingMetaData(final Class<?> enclosingType, Class<?> referenceType,
391             TypeVariable<?> typeParameter, Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData,
392             Map<Class<?>, Class<?>> groupConversions) {
393         // we try to find a corresponding type parameter in the current cascadable type
394         Map<Class<?>, Map<TypeVariable<?>, TypeVariable<?>>> typeVariableBindings = TypeVariableBindings.getTypeVariableBindings( enclosingType );
395         final TypeVariable<?> correspondingTypeParameter = typeVariableBindings.get( referenceType ).entrySet().stream()
396                 .filter( e -> Objects.equals( e.getKey().getGenericDeclaration(), enclosingType ) )
397                 .collect( Collectors.toMap( Map.Entry::getValue, Map.Entry::getKey ) )
398                 .get( typeParameter );
399
400         Class<?> cascadableClass;
401         TypeVariable<?> cascadableTypeParameter;
402         if ( correspondingTypeParameter != null ) {
403             cascadableClass = enclosingType;
404             cascadableTypeParameter = correspondingTypeParameter;
405         }
406         else {
407             // if we can't find one, we default to the reference type (e.g. List.class for instance)
408             cascadableClass = referenceType;
409             cascadableTypeParameter = typeParameter;
410         }
411
412         Map<TypeVariable<?>, CascadingMetaDataBuilder> amendedCascadingMetadata = CollectionHelper.newHashMap( containerElementTypesCascadingMetaData.size() + 1 );
413         amendedCascadingMetadata.putAll( containerElementTypesCascadingMetaData );
414
415         if ( containerElementTypesCascadingMetaData.containsKey( cascadableTypeParameter ) ) {
416             amendedCascadingMetadata.put( cascadableTypeParameter,
417                     makeCascading( containerElementTypesCascadingMetaData.get( cascadableTypeParameter ), groupConversions ) );
418         }
419         else {
420             amendedCascadingMetadata.put( cascadableTypeParameter,
421                     new CascadingMetaDataBuilder( cascadableClass, cascadableTypeParameter, enclosingType, correspondingTypeParameter, true,
422                             Collections.emptyMap(), groupConversions ) );
423         }
424
425         return amendedCascadingMetadata;
426     }
427
428     private static Map<TypeVariable<?>, CascadingMetaDataBuilder> addArrayElementCascadingMetaData(final Class<?> enclosingType,
429             Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData,
430             Map<Class<?>, Class<?>> groupConversions) {
431         Map<TypeVariable<?>, CascadingMetaDataBuilder> amendedCascadingMetadata = CollectionHelper.newHashMap( containerElementTypesCascadingMetaData.size() + 1 );
432         amendedCascadingMetadata.putAll( containerElementTypesCascadingMetaData );
433
434         TypeVariable<?> cascadableTypeParameter = new ArrayElement( enclosingType );
435
436         amendedCascadingMetadata.put( cascadableTypeParameter,
437                 new CascadingMetaDataBuilder( enclosingType, cascadableTypeParameter, true, Collections.emptyMap(), groupConversions ) );
438
439         return amendedCascadingMetadata;
440     }
441
442     private static CascadingMetaDataBuilder makeCascading(CascadingMetaDataBuilder cascadingTypeParameter, Map<Class<?>, Class<?>> groupConversions) {
443         return new CascadingMetaDataBuilder( cascadingTypeParameter.enclosingType, cascadingTypeParameter.typeParameter, true,
444                 cascadingTypeParameter.containerElementTypesCascadingMetaData,
445                 cascadingTypeParameter.groupConversions.isEmpty() ? groupConversions : cascadingTypeParameter.groupConversions );
446     }
447 }
448