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 ;
1312import java .util .Set ;
1413
14+ import org .hibernate .validator .internal .engine .valueextraction .ValueExtractorDescriptor ;
1515import org .hibernate .validator .internal .metadata .aggregated .BeanMetaData ;
1616import org .hibernate .validator .internal .metadata .aggregated .CascadingMetaData ;
1717import org .hibernate .validator .internal .metadata .aggregated .ContainerCascadingMetaData ;
18+ import org .hibernate .validator .internal .metadata .aggregated .PotentiallyContainerCascadingMetaData ;
1819import org .hibernate .validator .internal .metadata .facets .Cascadable ;
1920import org .hibernate .validator .internal .properties .Signature ;
2021import org .hibernate .validator .internal .util .CollectionHelper ;
@@ -29,11 +30,11 @@ public class PredefinedScopeProcessedBeansTrackingStrategy implements ProcessedB
2930
3031 public PredefinedScopeProcessedBeansTrackingStrategy (Map <Class <?>, BeanMetaData <?>> rawBeanMetaDataMap ) {
3132 // 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.
33+ // There is a good chance we will need a structure with the whole hierarchy of constraint classes.
34+ // That's something we could add to PredefinedScopeBeanMetaDataManager, as we are already doing similar things
35+ // there (see the ClassHierarchyHelper.getHierarchy call).
36+ // In the predefined scope case, we will have the whole hierarchy of constrained classes passed to
37+ // PredefinedScopeBeanMetaDataManager.
3738
3839 this .trackingEnabledForBeans = CollectionHelper .toImmutableMap (
3940 new TrackingEnabledStrategyBuilder ( rawBeanMetaDataMap ).build ()
@@ -45,10 +46,21 @@ public PredefinedScopeProcessedBeansTrackingStrategy(Map<Class<?>, BeanMetaData<
4546 private static class TrackingEnabledStrategyBuilder {
4647 private final Map <Class <?>, BeanMetaData <?>> rawBeanMetaDataMap ;
4748 private final Map <Class <?>, Boolean > classToBeanTrackingEnabled ;
49+ // Map values are a set of subtypes for the key class, including "self" i.e. the "key":
50+ private final Map <Class <?>, Set <Class <?>>> subtypesMap ;
4851
4952 TrackingEnabledStrategyBuilder (Map <Class <?>, BeanMetaData <?>> rawBeanMetaDataMap ) {
5053 this .rawBeanMetaDataMap = rawBeanMetaDataMap ;
5154 this .classToBeanTrackingEnabled = CollectionHelper .newHashMap ( rawBeanMetaDataMap .size () );
55+ this .subtypesMap = CollectionHelper .newHashMap ( rawBeanMetaDataMap .size () );
56+ for ( Class <?> beanClass : rawBeanMetaDataMap .keySet () ) {
57+ for ( Class <?> otherBeanClass : rawBeanMetaDataMap .keySet () ) {
58+ if ( beanClass .isAssignableFrom ( otherBeanClass ) ) {
59+ subtypesMap .computeIfAbsent ( beanClass , k -> new HashSet <>() )
60+ .add ( otherBeanClass );
61+ }
62+ }
63+ }
5264 }
5365
5466 public Map <Class <?>, Boolean > build () {
@@ -95,8 +107,16 @@ public Map<Class<?>, Boolean> build() {
95107 // -----
96108 // A, B, C have cycles; D does not have a cycle.
97109 //
110+ //
111+ // We also need to account for the case when the subtype is used at runtime that may change the cycles:
112+ // 4) A -> B -> C -> D
113+ // And C1 extends C where C1 -> A
114+ // Hence, at runtime we "may" get:
115+ // A -> B -> C1 -> D
116+ // ^ |
117+ // | |
118+ // -----------
98119 private boolean determineTrackingRequired (Class <?> beanClass , Set <Class <?>> beanClassesInPath ) {
99-
100120 final Boolean isBeanTrackingEnabled = classToBeanTrackingEnabled .get ( beanClass );
101121 if ( isBeanTrackingEnabled != null ) {
102122 // It was already determined for beanClass.
@@ -157,36 +177,59 @@ private boolean determineTrackingRequired(Class<?> beanClass, Set<Class<?>> bean
157177
158178 // TODO: is there a more concise way to do this?
159179 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 ();
180+ final Set <Class <?>> directCascadedBeanClasses = new HashSet <>();
181+ // At runtime, if we are not looking at the root bean the actual value of a cascadable
182+ // can be either the same `beanClass` or one of its subtypes... since subtypes can potentially add
183+ // more constraints we want to iterate through the subclasses (for which there is some metadata defined)
184+ // and include the info from them too.
185+ Set <Class <?>> classes = subtypesMap .get ( beanClass );
186+ if ( classes == null ) {
187+ // It may be that some bean property without any constraints is marked for cascading validation,
188+ // In that case the metadata entry will be missing from the map, but we
189+ return Set .of ();
164190 }
191+ for ( Class <?> otherBeanClass : classes ) {
192+ final BeanMetaData <?> beanMetaData = rawBeanMetaDataMap .get ( otherBeanClass );
165193
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 );
194+ if ( beanMetaData == null || !beanMetaData .hasCascadables () ) {
195+ continue ;
196+ }
197+
198+ for ( Cascadable cascadable : beanMetaData .getCascadables () ) {
199+ final CascadingMetaData cascadingMetaData = cascadable .getCascadingMetaData ();
200+ if ( cascadingMetaData .isContainer () ) {
201+ final ContainerCascadingMetaData containerCascadingMetaData = (ContainerCascadingMetaData ) cascadingMetaData ;
202+ if ( containerCascadingMetaData .getEnclosingType () instanceof ParameterizedType parameterizedType ) {
203+ for ( Type typeArgument : parameterizedType .getActualTypeArguments () ) {
204+ if ( typeArgument instanceof Class <?> typeArgumentClass ) {
205+ directCascadedBeanClasses .add ( typeArgumentClass );
206+ }
207+ else {
208+ throw new UnsupportedOperationException ( "Only ParameterizedType values of type Class are supported" );
209+ }
176210 }
177- else {
178- throw new UnsupportedOperationException ( "Only ParameterizedType values of type Class are supported" );
211+ }
212+ else {
213+ // If we do not have the type arguments then we can go though the value extractors,
214+ // as they are required to define the `@ExtractedValue(type = ???)` ...
215+ // this way we should get the type we want:
216+ for ( ValueExtractorDescriptor valueExtractorCandidate : containerCascadingMetaData .getValueExtractorCandidates () ) {
217+ valueExtractorCandidate .getExtractedType ().ifPresent ( directCascadedBeanClasses ::add );
179218 }
180219 }
181220 }
221+ else if ( cascadingMetaData instanceof PotentiallyContainerCascadingMetaData potentiallyContainerCascadingMetaData ) {
222+ // if it's a potentially container cascading one, we are "in trouble" as thing can be "almost anything".
223+ // TODO: would it be enough to just take the type as defined ?
224+ directCascadedBeanClasses .add ( (Class <?>) cascadable .getCascadableType () );
225+ // TODO: or be much more cautious and just assume that it can be "anything":
226+ directCascadedBeanClasses .add ( Object .class );
227+ }
182228 else {
183- throw new UnsupportedOperationException ( "Non-parameterized containers are not supported yet." );
229+ // TODO: For now, assume non-container Cascadables are always beans. Truee???
230+ directCascadedBeanClasses .add ( (Class <?>) cascadable .getCascadableType () );
184231 }
185232 }
186- else {
187- // TODO: For now, assume non-container Cascadables are always beans. Truee???
188- directCascadedBeanClasses .add ( (Class <?>) cascadable .getCascadableType () );
189- }
190233 }
191234 return directCascadedBeanClasses ;
192235 }
0 commit comments