Skip to content

Commit 997c626

Browse files
committed
HV-1831 Adjust the cascading cycle detection algorithm
Signed-off-by: marko-bekhta <[email protected]>
1 parent 6cdc297 commit 997c626

File tree

8 files changed

+403
-49
lines changed

8 files changed

+403
-49
lines changed

engine/src/main/java/org/hibernate/validator/internal/engine/tracking/PredefinedScopeProcessedBeansTrackingStrategy.java

Lines changed: 116 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
66

77
import java.lang.reflect.ParameterizedType;
88
import java.lang.reflect.Type;
9-
import java.util.Collections;
9+
import java.lang.reflect.TypeVariable;
10+
import java.lang.reflect.WildcardType;
1011
import java.util.HashMap;
1112
import java.util.HashSet;
1213
import java.util.Map;
1314
import java.util.Set;
1415

16+
import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorDescriptor;
1517
import org.hibernate.validator.internal.metadata.aggregated.BeanMetaData;
1618
import org.hibernate.validator.internal.metadata.aggregated.CascadingMetaData;
1719
import org.hibernate.validator.internal.metadata.aggregated.ContainerCascadingMetaData;
20+
import org.hibernate.validator.internal.metadata.aggregated.PotentiallyContainerCascadingMetaData;
1821
import org.hibernate.validator.internal.metadata.facets.Cascadable;
1922
import org.hibernate.validator.internal.properties.Signature;
2023
import 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) {

engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles2Test.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44
*/
55
package org.hibernate.validator.test.internal.engine.tracking;
66

7+
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertThat;
8+
79
import java.util.List;
10+
import java.util.Set;
811

912
import jakarta.validation.Valid;
1013
import jakarta.validation.Validator;
1114
import jakarta.validation.constraints.NotNull;
1215

1316
import org.hibernate.validator.testutils.ValidatorUtil;
1417

18+
import org.testng.annotations.DataProvider;
1519
import org.testng.annotations.Test;
1620

1721
/**
@@ -23,11 +27,30 @@
2327
*/
2428
public class ProcessedBeansTrackingCycles2Test {
2529

26-
@Test
27-
public void testSerializeHibernateEmail() throws Exception {
28-
Validator validator = ValidatorUtil.getValidator();
30+
@DataProvider(name = "validators")
31+
public Object[][] createValidators() {
32+
return new Object[][] {
33+
{ ValidatorUtil.getValidator() },
34+
{ ValidatorUtil.getPredefinedValidator( Set.of( Parent.class, Child.class ) ) }
35+
};
36+
}
37+
38+
@Test(dataProvider = "validators")
39+
public void testNoCycle(Validator validator) throws Exception {
40+
Parent parent = new Parent();
41+
parent.property = "";
42+
assertThat( validator.validate( parent ) ).isEmpty();
43+
}
2944

30-
validator.validate( new Parent() );
45+
@Test(dataProvider = "validators")
46+
public void testCycle(Validator validator) throws Exception {
47+
Parent parent = new Parent();
48+
parent.property = "";
49+
Child child = new Child();
50+
child.property = "";
51+
child.parent = parent;
52+
parent.children = List.of( child );
53+
assertThat( validator.validate( parent ) ).isEmpty();
3154
}
3255

3356
private static class Parent {

engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles3Test.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44
*/
55
package org.hibernate.validator.test.internal.engine.tracking;
66

7+
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertThat;
8+
79
import java.util.List;
810
import java.util.Map;
11+
import java.util.Set;
912

1013
import jakarta.validation.Valid;
1114
import jakarta.validation.Validator;
1215
import jakarta.validation.constraints.NotNull;
1316

1417
import org.hibernate.validator.testutils.ValidatorUtil;
1518

19+
import org.testng.annotations.DataProvider;
1620
import org.testng.annotations.Test;
1721

1822
/**
@@ -24,11 +28,23 @@
2428
*/
2529
public class ProcessedBeansTrackingCycles3Test {
2630

27-
@Test
28-
public void testSerializeHibernateEmail() throws Exception {
29-
Validator validator = ValidatorUtil.getValidator();
31+
@DataProvider(name = "validators")
32+
public Object[][] createValidators() {
33+
return new Object[][] {
34+
{ ValidatorUtil.getValidator() },
35+
{ ValidatorUtil.getPredefinedValidator( Set.of( Parent.class, Child.class ) ) }
36+
};
37+
}
3038

31-
validator.validate( new Parent() );
39+
@Test(dataProvider = "validators")
40+
public void testCycle(Validator validator) throws Exception {
41+
Parent parent = new Parent();
42+
parent.property = "";
43+
Child child = new Child();
44+
child.property = "";
45+
child.parent = parent;
46+
parent.children = Map.of( "1", List.of( child ) );
47+
assertThat( validator.validate( parent ) ).isEmpty();
3248
}
3349

3450
private static class Parent {

engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles4Test.java

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44
*/
55
package org.hibernate.validator.test.internal.engine.tracking;
66

7+
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertThat;
8+
79
import java.util.List;
810
import java.util.Map;
11+
import java.util.Set;
912

1013
import jakarta.validation.Valid;
1114
import jakarta.validation.Validator;
1215
import jakarta.validation.constraints.NotNull;
1316

1417
import org.hibernate.validator.testutils.ValidatorUtil;
1518

19+
import org.testng.annotations.DataProvider;
1620
import org.testng.annotations.Test;
1721

1822
/**
@@ -24,27 +28,58 @@
2428
*/
2529
public class ProcessedBeansTrackingCycles4Test {
2630

27-
@Test
28-
public void testSerializeHibernateEmail() throws Exception {
29-
Validator validator = ValidatorUtil.getValidator();
31+
@DataProvider(name = "validators")
32+
public Object[][] createValidators() {
33+
return new Object[][] {
34+
{ ValidatorUtil.getValidator() },
35+
{ ValidatorUtil.getPredefinedValidator( Set.of( Parent.class, Child.class, ParentSuper.class ) ) }
36+
};
37+
}
38+
39+
@Test(dataProvider = "validators")
40+
public void testCycle(Validator validator) throws Exception {
41+
Parent parent = new Parent();
42+
parent.property = "";
43+
Child child = new Child();
44+
child.property = "";
45+
child.parent = parent;
46+
parent.children = Map.of( "1", List.of( child ) );
47+
assertThat( validator.validate( parent ) ).isEmpty();
48+
49+
ParentSuper parent2 = new ParentSuper();
50+
parent2.property = "";
51+
Child child2 = new Child();
52+
child2.property = "";
53+
child2.parent = parent2;
54+
parent2.children = Map.of( "1", List.of( child2 ) );
55+
assertThat( validator.validate( parent2 ) ).isEmpty();
56+
}
57+
58+
private static class Parent extends BaseParent {
3059

31-
validator.validate( new Parent() );
60+
Map<String, List<@Valid ? extends Child>> children;
3261
}
3362

34-
private static class Parent {
63+
private static class ParentSuper extends BaseParent {
64+
65+
66+
Map<String, List<@Valid ? super Child>> children;
67+
}
68+
69+
private static class BaseParent {
3570

3671
@NotNull
37-
private String property;
72+
String property;
3873

39-
private Map<String, List<@Valid ? extends Child>> children;
4074
}
4175

4276
private static class Child {
4377

4478
@NotNull
45-
private String property;
79+
String property;
4680

4781
@Valid
48-
private Parent parent;
82+
BaseParent parent;
4983
}
84+
5085
}

0 commit comments

Comments
 (0)