6
6
7
7
import java .lang .reflect .ParameterizedType ;
8
8
import java .lang .reflect .Type ;
9
- import java .util .Collections ;
9
+ import java .lang .reflect .TypeVariable ;
10
+ import java .lang .reflect .WildcardType ;
10
11
import java .util .HashMap ;
11
12
import java .util .HashSet ;
12
13
import java .util .Map ;
13
14
import java .util .Set ;
14
15
16
+ import org .hibernate .validator .internal .engine .valueextraction .ValueExtractorDescriptor ;
15
17
import org .hibernate .validator .internal .metadata .aggregated .BeanMetaData ;
16
18
import org .hibernate .validator .internal .metadata .aggregated .CascadingMetaData ;
17
19
import org .hibernate .validator .internal .metadata .aggregated .ContainerCascadingMetaData ;
20
+ import org .hibernate .validator .internal .metadata .aggregated .PotentiallyContainerCascadingMetaData ;
18
21
import org .hibernate .validator .internal .metadata .facets .Cascadable ;
19
22
import org .hibernate .validator .internal .properties .Signature ;
20
23
import org .hibernate .validator .internal .util .CollectionHelper ;
@@ -29,11 +32,11 @@ public class PredefinedScopeProcessedBeansTrackingStrategy implements ProcessedB
29
32
30
33
public PredefinedScopeProcessedBeansTrackingStrategy (Map <Class <?>, BeanMetaData <?>> rawBeanMetaDataMap ) {
31
34
// 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.
37
40
38
41
this .trackingEnabledForBeans = CollectionHelper .toImmutableMap (
39
42
new TrackingEnabledStrategyBuilder ( rawBeanMetaDataMap ).build ()
@@ -45,10 +48,21 @@ public PredefinedScopeProcessedBeansTrackingStrategy(Map<Class<?>, BeanMetaData<
45
48
private static class TrackingEnabledStrategyBuilder {
46
49
private final Map <Class <?>, BeanMetaData <?>> rawBeanMetaDataMap ;
47
50
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 ;
48
53
49
54
TrackingEnabledStrategyBuilder (Map <Class <?>, BeanMetaData <?>> rawBeanMetaDataMap ) {
50
55
this .rawBeanMetaDataMap = rawBeanMetaDataMap ;
51
56
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
+ }
52
66
}
53
67
54
68
public Map <Class <?>, Boolean > build () {
@@ -95,8 +109,16 @@ public Map<Class<?>, Boolean> build() {
95
109
// -----
96
110
// A, B, C have cycles; D does not have a cycle.
97
111
//
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
+ // -----------
98
121
private boolean determineTrackingRequired (Class <?> beanClass , Set <Class <?>> beanClassesInPath ) {
99
-
100
122
final Boolean isBeanTrackingEnabled = classToBeanTrackingEnabled .get ( beanClass );
101
123
if ( isBeanTrackingEnabled != null ) {
102
124
// It was already determined for beanClass.
@@ -157,38 +179,104 @@ private boolean determineTrackingRequired(Class<?> beanClass, Set<Class<?>> bean
157
179
158
180
// TODO: is there a more concise way to do this?
159
181
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
+ }
161
199
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
+ }
164
219
}
220
+ return directCascadedBeanClasses ;
221
+ }
165
222
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 ) );
176
239
}
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 );
179
243
}
180
244
}
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
+ }
184
249
}
185
250
}
186
251
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
+ }
189
258
}
190
259
}
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
+ }
192
280
}
193
281
194
282
private boolean register (Class <?> beanClass , boolean isBeanTrackingEnabled ) {
0 commit comments