66
77import java .lang .reflect .ParameterizedType ;
88import java .lang .reflect .Type ;
9- import java .util .Collections ;
9+ import java .lang .reflect .TypeVariable ;
10+ import java .lang .reflect .WildcardType ;
1011import java .util .HashMap ;
1112import java .util .HashSet ;
1213import java .util .Map ;
1314import java .util .Set ;
1415
16+ import org .hibernate .validator .internal .engine .valueextraction .ValueExtractorDescriptor ;
1517import org .hibernate .validator .internal .metadata .aggregated .BeanMetaData ;
1618import org .hibernate .validator .internal .metadata .aggregated .CascadingMetaData ;
1719import org .hibernate .validator .internal .metadata .aggregated .ContainerCascadingMetaData ;
20+ import org .hibernate .validator .internal .metadata .aggregated .PotentiallyContainerCascadingMetaData ;
1821import org .hibernate .validator .internal .metadata .facets .Cascadable ;
1922import org .hibernate .validator .internal .properties .Signature ;
2023import org .hibernate .validator .internal .util .CollectionHelper ;
@@ -29,11 +32,11 @@ public class PredefinedScopeProcessedBeansTrackingStrategy implements ProcessedB
2932
3033 public PredefinedScopeProcessedBeansTrackingStrategy (Map <Class <?>, BeanMetaData <?>> rawBeanMetaDataMap ) {
3134 // TODO: build the maps from the information inside the beanMetaDataManager
32- // There is a good chance we will need a structure with the whole hierarchy of constraint classes.
33- // That's something we could add to PredefinedScopeBeanMetaDataManager, as we are already doing similar things
34- // there (see the ClassHierarchyHelper.getHierarchy call).
35- // In the predefined scope case, we will have the whole hierarchy of constrained classes passed to
36- // PredefinedScopeBeanMetaDataManager.
35+ // There is a good chance we will need a structure with the whole hierarchy of constraint classes.
36+ // That's something we could add to PredefinedScopeBeanMetaDataManager, as we are already doing similar things
37+ // there (see the ClassHierarchyHelper.getHierarchy call).
38+ // In the predefined scope case, we will have the whole hierarchy of constrained classes passed to
39+ // PredefinedScopeBeanMetaDataManager.
3740
3841 this .trackingEnabledForBeans = CollectionHelper .toImmutableMap (
3942 new TrackingEnabledStrategyBuilder ( rawBeanMetaDataMap ).build ()
@@ -45,10 +48,21 @@ public PredefinedScopeProcessedBeansTrackingStrategy(Map<Class<?>, BeanMetaData<
4548 private static class TrackingEnabledStrategyBuilder {
4649 private final Map <Class <?>, BeanMetaData <?>> rawBeanMetaDataMap ;
4750 private final Map <Class <?>, Boolean > classToBeanTrackingEnabled ;
51+ // Map values are a set of subtypes for the key class, including "self" i.e. the "key":
52+ private final Map <Class <?>, Set <Class <?>>> subtypesMap ;
4853
4954 TrackingEnabledStrategyBuilder (Map <Class <?>, BeanMetaData <?>> rawBeanMetaDataMap ) {
5055 this .rawBeanMetaDataMap = rawBeanMetaDataMap ;
5156 this .classToBeanTrackingEnabled = CollectionHelper .newHashMap ( rawBeanMetaDataMap .size () );
57+ this .subtypesMap = CollectionHelper .newHashMap ( rawBeanMetaDataMap .size () );
58+ for ( Class <?> beanClass : rawBeanMetaDataMap .keySet () ) {
59+ for ( Class <?> otherBeanClass : rawBeanMetaDataMap .keySet () ) {
60+ if ( beanClass .isAssignableFrom ( otherBeanClass ) ) {
61+ subtypesMap .computeIfAbsent ( beanClass , k -> new HashSet <>() )
62+ .add ( otherBeanClass );
63+ }
64+ }
65+ }
5266 }
5367
5468 public Map <Class <?>, Boolean > build () {
@@ -95,8 +109,16 @@ public Map<Class<?>, Boolean> build() {
95109 // -----
96110 // A, B, C have cycles; D does not have a cycle.
97111 //
112+ //
113+ // We also need to account for the case when the subtype is used at runtime that may change the cycles:
114+ // 4) A -> B -> C -> D
115+ // And C1 extends C where C1 -> A
116+ // Hence, at runtime we "may" get:
117+ // A -> B -> C1 -> D
118+ // ^ |
119+ // | |
120+ // -----------
98121 private boolean determineTrackingRequired (Class <?> beanClass , Set <Class <?>> beanClassesInPath ) {
99-
100122 final Boolean isBeanTrackingEnabled = classToBeanTrackingEnabled .get ( beanClass );
101123 if ( isBeanTrackingEnabled != null ) {
102124 // It was already determined for beanClass.
@@ -157,38 +179,104 @@ private boolean determineTrackingRequired(Class<?> beanClass, Set<Class<?>> bean
157179
158180 // TODO: is there a more concise way to do this?
159181 private <T > Set <Class <?>> getDirectCascadedBeanClasses (Class <T > beanClass ) {
160- final BeanMetaData <?> beanMetaData = rawBeanMetaDataMap .get ( beanClass );
182+ final Set <Class <?>> directCascadedBeanClasses = new HashSet <>();
183+ // At runtime, if we are not looking at the root bean the actual value of a cascadable
184+ // can be either the same `beanClass` or one of its subtypes... since subtypes can potentially add
185+ // more constraints we want to iterate through the subclasses (for which there is some metadata defined)
186+ // and include the info from them too.
187+ Set <Class <?>> classes = subtypesMap .get ( beanClass );
188+ if ( classes == null ) {
189+ // It may be that some bean property without any constraints is marked for cascading validation,
190+ // In that case the metadata entry will be missing from the map:
191+ return Set .of ();
192+ }
193+ for ( Class <?> otherBeanClass : classes ) {
194+ final BeanMetaData <?> beanMetaData = rawBeanMetaDataMap .get ( otherBeanClass );
195+
196+ if ( beanMetaData == null || !beanMetaData .hasCascadables () ) {
197+ continue ;
198+ }
161199
162- if ( beanMetaData == null || !beanMetaData .hasCascadables () ) {
163- return Collections .emptySet ();
200+ for ( Cascadable cascadable : beanMetaData .getCascadables () ) {
201+ final CascadingMetaData cascadingMetaData = cascadable .getCascadingMetaData ();
202+ if ( cascadingMetaData .isContainer () ) {
203+ final ContainerCascadingMetaData containerCascadingMetaData = cascadingMetaData .as ( ContainerCascadingMetaData .class );
204+ processContainerCascadingMetaData ( containerCascadingMetaData , directCascadedBeanClasses );
205+ }
206+ else if ( cascadingMetaData instanceof PotentiallyContainerCascadingMetaData potentiallyContainerCascadingMetaData ) {
207+ // if it's a potentially container cascading one, we are "in trouble" as thing can be "almost anything".
208+ // TODO: would it be enough to just take the type as defined ?
209+ // directCascadedBeanClasses.add( (Class<?>) cascadable.getCascadableType() );
210+ //
211+ // TODO: or be much more cautious and just assume that it can be "anything":
212+ directCascadedBeanClasses .add ( Object .class );
213+ }
214+ else {
215+ // TODO: For now, assume non-container Cascadables are always beans. Truee???
216+ directCascadedBeanClasses .add ( typeToClassToProcess ( cascadable .getCascadableType () ) );
217+ }
218+ }
164219 }
220+ return directCascadedBeanClasses ;
221+ }
165222
166- final Set <Class <?>> directCascadedBeanClasses = new HashSet <>();
167- for ( Cascadable cascadable : beanMetaData .getCascadables () ) {
168- final CascadingMetaData cascadingMetaData = cascadable .getCascadingMetaData ();
169- if ( cascadingMetaData .isContainer () ) {
170- final ContainerCascadingMetaData containerCascadingMetaData = (ContainerCascadingMetaData ) cascadingMetaData ;
171- if ( containerCascadingMetaData .getEnclosingType () instanceof ParameterizedType ) {
172- ParameterizedType parameterizedType = (ParameterizedType ) containerCascadingMetaData .getEnclosingType ();
173- for ( Type typeArgument : parameterizedType .getActualTypeArguments () ) {
174- if ( typeArgument instanceof Class ) {
175- directCascadedBeanClasses .add ( (Class <?>) typeArgument );
223+ private static void processContainerCascadingMetaData (ContainerCascadingMetaData metaData , Set <Class <?>> directCascadedBeanClasses ) {
224+ if ( metaData .isCascading () ) {
225+ if ( metaData .getDeclaredTypeParameterIndex () != null ) {
226+ if ( metaData .getEnclosingType () instanceof ParameterizedType parameterizedType ) {
227+ Type typeArgument = parameterizedType .getActualTypeArguments ()[metaData .getDeclaredTypeParameterIndex ()];
228+ if ( typeArgument instanceof Class <?> typeArgumentClass ) {
229+ directCascadedBeanClasses .add ( typeArgumentClass );
230+ }
231+ else if ( typeArgument instanceof TypeVariable <?> typeVariable ) {
232+ for ( Type bound : typeVariable .getBounds () ) {
233+ directCascadedBeanClasses .add ( typeToClassToProcess ( bound ) );
234+ }
235+ }
236+ else if ( typeArgument instanceof WildcardType wildcard ) {
237+ for ( Type bound : wildcard .getUpperBounds () ) {
238+ directCascadedBeanClasses .add ( typeToClassToProcess ( bound ) );
176239 }
177- else {
178- throw new UnsupportedOperationException ( "Only ParameterizedType values of type Class are supported" );
240+ if ( wildcard .getLowerBounds ().length != 0 ) {
241+ // if it's a lower bound ? super smth ... it doesn't matter anymore since it can contain anything so go with object ?
242+ directCascadedBeanClasses .add ( Object .class );
179243 }
180244 }
181- }
182- else {
183- throw new UnsupportedOperationException ( "Non-parameterized containers are not supported yet." );
245+ else {
246+ // TODO: instead of failing, add an Object.class and assume it can be anything ?
247+ throw new UnsupportedOperationException ( typeArgument .getClass ().getSimpleName () + " type argument values are not supported." );
248+ }
184249 }
185250 }
186251 else {
187- // TODO: For now, assume non-container Cascadables are always beans. Truee???
188- directCascadedBeanClasses .add ( (Class <?>) cascadable .getCascadableType () );
252+ // If we do not have the type arguments then we can go though the value extractors,
253+ // as they are required to define the `@ExtractedValue(type = ???)` ...
254+ // this way we should get the type we want:
255+ for ( ValueExtractorDescriptor valueExtractorCandidate : metaData .getValueExtractorCandidates () ) {
256+ valueExtractorCandidate .getExtractedType ().ifPresent ( directCascadedBeanClasses ::add );
257+ }
189258 }
190259 }
191- return directCascadedBeanClasses ;
260+
261+ if ( metaData .getEnclosingType () instanceof ParameterizedType parameterizedType ) {
262+ for ( ContainerCascadingMetaData sub : metaData .getContainerElementTypesCascadingMetaData () ) {
263+ processContainerCascadingMetaData ( sub , directCascadedBeanClasses );
264+ }
265+ }
266+ }
267+
268+ private static Class <?> typeToClassToProcess (Type type ) {
269+ if ( type instanceof Class <?> cascadableClass ) {
270+ return cascadableClass ;
271+ }
272+ else if ( type instanceof ParameterizedType parameterizedType ) {
273+ return typeToClassToProcess ( parameterizedType .getRawType () );
274+ }
275+ else {
276+ // TODO: instead of failing, add an Object.class and assume it can be anything ?
277+ // return Object.class;
278+ throw new UnsupportedOperationException ( type .getClass ().getSimpleName () + " type values are not supported." );
279+ }
192280 }
193281
194282 private boolean register (Class <?> beanClass , boolean isBeanTrackingEnabled ) {
0 commit comments