66
77import java .lang .reflect .ParameterizedType ;
88import java .lang .reflect .Type ;
9- import java .util .Collections ;
109import java .util .HashMap ;
1110import java .util .HashSet ;
1211import java .util .Map ;
1514import org .hibernate .validator .internal .metadata .aggregated .BeanMetaData ;
1615import org .hibernate .validator .internal .metadata .aggregated .CascadingMetaData ;
1716import org .hibernate .validator .internal .metadata .aggregated .ContainerCascadingMetaData ;
17+ import org .hibernate .validator .internal .metadata .aggregated .PotentiallyContainerCascadingMetaData ;
1818import org .hibernate .validator .internal .metadata .facets .Cascadable ;
1919import org .hibernate .validator .internal .properties .Signature ;
2020import org .hibernate .validator .internal .util .CollectionHelper ;
@@ -29,11 +29,11 @@ public class PredefinedScopeProcessedBeansTrackingStrategy implements ProcessedB
2929
3030 public PredefinedScopeProcessedBeansTrackingStrategy (Map <Class <?>, BeanMetaData <?>> rawBeanMetaDataMap ) {
3131 // 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.
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.
3737
3838 this .trackingEnabledForBeans = CollectionHelper .toImmutableMap (
3939 new TrackingEnabledStrategyBuilder ( rawBeanMetaDataMap ).build ()
@@ -45,10 +45,21 @@ public PredefinedScopeProcessedBeansTrackingStrategy(Map<Class<?>, BeanMetaData<
4545 private static class TrackingEnabledStrategyBuilder {
4646 private final Map <Class <?>, BeanMetaData <?>> rawBeanMetaDataMap ;
4747 private final Map <Class <?>, Boolean > classToBeanTrackingEnabled ;
48+ // Map values are a set of subtypes for the key class, including "self" i.e. the "key":
49+ private final Map <Class <?>, Set <Class <?>>> subtypesMap ;
4850
4951 TrackingEnabledStrategyBuilder (Map <Class <?>, BeanMetaData <?>> rawBeanMetaDataMap ) {
5052 this .rawBeanMetaDataMap = rawBeanMetaDataMap ;
5153 this .classToBeanTrackingEnabled = CollectionHelper .newHashMap ( rawBeanMetaDataMap .size () );
54+ this .subtypesMap = CollectionHelper .newHashMap ( rawBeanMetaDataMap .size () );
55+ for ( Class <?> beanClass : rawBeanMetaDataMap .keySet () ) {
56+ for ( Class <?> otherBeanClass : rawBeanMetaDataMap .keySet () ) {
57+ if ( beanClass .isAssignableFrom ( otherBeanClass ) ) {
58+ subtypesMap .computeIfAbsent ( beanClass , k -> new HashSet <>() )
59+ .add ( otherBeanClass );
60+ }
61+ }
62+ }
5263 }
5364
5465 public Map <Class <?>, Boolean > build () {
@@ -95,8 +106,16 @@ public Map<Class<?>, Boolean> build() {
95106 // -----
96107 // A, B, C have cycles; D does not have a cycle.
97108 //
109+ //
110+ // We also need to account for the case when the subtype is used at runtime that may change the cycles:
111+ // 4) A -> B -> C -> D
112+ // And C1 extends C where C1 -> A
113+ // Hence, at runtime we "may" get:
114+ // A -> B -> C1 -> D
115+ // ^ |
116+ // | |
117+ // -----------
98118 private boolean determineTrackingRequired (Class <?> beanClass , Set <Class <?>> beanClassesInPath ) {
99-
100119 final Boolean isBeanTrackingEnabled = classToBeanTrackingEnabled .get ( beanClass );
101120 if ( isBeanTrackingEnabled != null ) {
102121 // It was already determined for beanClass.
@@ -157,36 +176,50 @@ private boolean determineTrackingRequired(Class<?> beanClass, Set<Class<?>> bean
157176
158177 // TODO: is there a more concise way to do this?
159178 private <T > Set <Class <?>> getDirectCascadedBeanClasses (Class <T > beanClass ) {
160- final BeanMetaData <?> beanMetaData = rawBeanMetaDataMap .get ( beanClass );
161-
162- if ( beanMetaData == null || !beanMetaData .hasCascadables () ) {
163- return Collections .emptySet ();
179+ final Set <Class <?>> directCascadedBeanClasses = new HashSet <>();
180+ // At runtime, if we are not looking at the root bean the actual value of a cascadable
181+ // can be either the same `beanClass` or one of its subtypes... since subtypes can potentially add
182+ // more constraints we want to iterate through the subclasses (for which there is some metadata defined)
183+ // and include the info from them too.
184+ Set <Class <?>> classes = subtypesMap .get ( beanClass );
185+ if ( classes == null ) {
186+ // It may be that some bean property without any constraints is marked for cascading validation,
187+ // In that case the metadata entry will be missing from the map, but we
188+ return Set .of ();
164189 }
190+ for ( Class <?> otherBeanClass : classes ) {
191+ final BeanMetaData <?> beanMetaData = rawBeanMetaDataMap .get ( otherBeanClass );
165192
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 );
176- }
177- else {
178- throw new UnsupportedOperationException ( "Only ParameterizedType values of type Class are supported" );
193+ if ( beanMetaData == null || !beanMetaData .hasCascadables () ) {
194+ continue ;
195+ }
196+
197+ for ( Cascadable cascadable : beanMetaData .getCascadables () ) {
198+ final CascadingMetaData cascadingMetaData = cascadable .getCascadingMetaData ();
199+ if ( cascadingMetaData .isContainer () ) {
200+ final ContainerCascadingMetaData containerCascadingMetaData = (ContainerCascadingMetaData ) cascadingMetaData ;
201+ if ( containerCascadingMetaData .getEnclosingType () instanceof ParameterizedType parameterizedType ) {
202+ for ( Type typeArgument : parameterizedType .getActualTypeArguments () ) {
203+ if ( typeArgument instanceof Class ) {
204+ directCascadedBeanClasses .add ( (Class <?>) typeArgument );
205+ }
206+ else {
207+ throw new UnsupportedOperationException ( "Only ParameterizedType values of type Class are supported" );
208+ }
179209 }
180210 }
211+ else {
212+ throw new UnsupportedOperationException ( "Non-parameterized containers are not supported yet." );
213+ }
214+ }
215+ else if ( cascadingMetaData instanceof PotentiallyContainerCascadingMetaData ) {
216+ // if it's a potentially container cascading one ...
181217 }
182218 else {
183- throw new UnsupportedOperationException ( "Non-parameterized containers are not supported yet." );
219+ // TODO: For now, assume non-container Cascadables are always beans. Truee???
220+ directCascadedBeanClasses .add ( (Class <?>) cascadable .getCascadableType () );
184221 }
185222 }
186- else {
187- // TODO: For now, assume non-container Cascadables are always beans. Truee???
188- directCascadedBeanClasses .add ( (Class <?>) cascadable .getCascadableType () );
189- }
190223 }
191224 return directCascadedBeanClasses ;
192225 }
0 commit comments