7
7
package org .hibernate .validator .internal .engine .tracking ;
8
8
9
9
import java .lang .reflect .Executable ;
10
+ import java .util .Collections ;
10
11
import java .util .HashMap ;
12
+ import java .util .HashSet ;
11
13
import java .util .Map ;
14
+ import java .util .Set ;
12
15
13
16
import org .hibernate .validator .internal .metadata .PredefinedScopeBeanMetaDataManager ;
17
+ import org .hibernate .validator .internal .metadata .aggregated .BeanMetaData ;
18
+ import org .hibernate .validator .internal .metadata .aggregated .CascadingMetaData ;
19
+ import org .hibernate .validator .internal .metadata .facets .Cascadable ;
14
20
import org .hibernate .validator .internal .util .CollectionHelper ;
15
21
16
22
public class PredefinedScopeProcessedBeansTrackingStrategy implements ProcessedBeansTrackingStrategy {
@@ -29,18 +35,163 @@ public PredefinedScopeProcessedBeansTrackingStrategy(PredefinedScopeBeanMetaData
29
35
// In the predefined scope case, we will have the whole hierarchy of constrained classes passed to
30
36
// PredefinedScopeBeanMetaDataManager.
31
37
32
- this .trackingEnabledForBeans = CollectionHelper .toImmutableMap ( new HashMap <>() );
38
+ this .trackingEnabledForBeans = CollectionHelper .toImmutableMap (
39
+ new TrackingEnabledStrategyBuilder ( beanMetaDataManager ).build ()
40
+ );
33
41
this .trackingEnabledForReturnValues = CollectionHelper .toImmutableMap ( new HashMap <>() );
34
42
this .trackingEnabledForParameters = CollectionHelper .toImmutableMap ( new HashMap <>() );
35
43
}
36
44
45
+ private static class TrackingEnabledStrategyBuilder {
46
+ private final PredefinedScopeBeanMetaDataManager beanMetaDataManager ;
47
+ private final Map <Class <?>, Boolean > classToBeanTrackingEnabled ;
48
+
49
+ TrackingEnabledStrategyBuilder (PredefinedScopeBeanMetaDataManager beanMetaDataManager ) {
50
+ this .beanMetaDataManager = beanMetaDataManager ;
51
+ this .classToBeanTrackingEnabled = new HashMap <>( beanMetaDataManager .getBeanMetaData ().size () );
52
+ }
53
+
54
+ public Map <Class <?>, Boolean > build () {
55
+ final Set <Class <?>> beanClassesInPath = new HashSet <>();
56
+ for (BeanMetaData <?> beanMetadata : beanMetaDataManager .getBeanMetaData () ) {
57
+ determineTrackingRequired ( beanMetadata .getBeanClass (), beanClassesInPath );
58
+ if ( !beanClassesInPath .isEmpty () ) {
59
+ throw new IllegalStateException ( "beanClassesInPath not empty" );
60
+ }
61
+ }
62
+ return classToBeanTrackingEnabled ;
63
+ }
64
+
65
+ // Do a depth-first search for cycles along paths of cascaded bean classes.
66
+ // The algorithm stops due to one of the following:
67
+ // 1) The bean class was previously put in classToBeanTrackingEnabled
68
+ // (i.e., the bean class was already determined to either have a cycle,
69
+ // or not have a cycle).
70
+ // 2) A cycle is found. In this case, all bean classes in the particular path,
71
+ // starting from beanClass up to first bean class that causes a cycle, will
72
+ // be registered in classToBeanTrackingEnabled with a value of true.
73
+ // Once a cycle is found, no further bean classes are examined. Those bean
74
+ // classes that were examined in the process that are found to not have a
75
+ // cycle are registered in classToBeanTrackingEnabled with a value of false.
76
+ // 3) No cycle is found. In this case, all bean classes in the tree will be
77
+ // registered in classToBeanTrackingEnabled with a value of false.
78
+ //
79
+ // Examples: An arrow, ->, indicates a cascading constraint from a bean class.
80
+ //
81
+ // 1) A -> B
82
+ // | ^
83
+ // | |
84
+ // ----
85
+ // A, B have no cycles. A has 2 paths to B, but there are no cycles, because there is no path from B to A.
86
+ //
87
+ // 2) A <-
88
+ // | |
89
+ // ---
90
+ // A has a cycle to itself.
91
+ //
92
+ // 3) A -> B -> C -> D
93
+ // ^ |
94
+ // | |
95
+ // -----
96
+ // A, B, C have cycles; D does not have a cycle.
97
+ //
98
+ private boolean determineTrackingRequired (Class <?> beanClass , Set <Class <?>> beanClassesInPath ) {
99
+
100
+ final Boolean isBeanTrackingEnabled = classToBeanTrackingEnabled .get ( beanClass );
101
+ if ( isBeanTrackingEnabled != null ) {
102
+ // It was already determined for beanClass.
103
+ return isBeanTrackingEnabled ;
104
+ }
105
+
106
+ // Add beanClass to the path.
107
+ // We don't care about the order of the bean classes in
108
+ // beanClassesInPath. We only care about detecting a duplicate,
109
+ // which indicates a cycle. If no cycle is found in beanClass,
110
+ // it will be removed below.
111
+ if ( !beanClassesInPath .add ( beanClass ) ) {
112
+ // The bean was already present in the path being examined.
113
+ // That means that there is cycle involving beanClass.
114
+ // Enable tracking for all elements in beanClassesInPath
115
+ for ( Class <?> dependency : beanClassesInPath ) {
116
+ register ( dependency , true );
117
+ }
118
+ beanClassesInPath .clear ();
119
+ return true ;
120
+ }
121
+
122
+ // Now check the cascaded bean classes.
123
+ for ( Class <?> directCascadedBeanClass : getDirectCascadedBeanClasses ( beanClass ) ) {
124
+ // Check to see if tracking has already been determined for directCascadedBeanClass
125
+ Boolean isSubBeanTrackingEnabled = classToBeanTrackingEnabled .get ( directCascadedBeanClass );
126
+ if ( isSubBeanTrackingEnabled != null ) {
127
+ if ( isSubBeanTrackingEnabled ) {
128
+ // We already know that directCascadedBeanClass has a cycle.
129
+ // That means that all elements in beanClassesInPath
130
+ // will have a cycle.
131
+ for ( Class <?> dependency : beanClassesInPath ) {
132
+ register ( dependency , true );
133
+ }
134
+ // No point in checking any others in this loop.
135
+ beanClassesInPath .clear ();
136
+ return true ;
137
+ }
138
+ else {
139
+ // We already know that directCascadedBeanClass is not involved in
140
+ // any cycles, so move on to the next iteration.
141
+ continue ;
142
+ }
143
+ }
144
+ if ( determineTrackingRequired ( directCascadedBeanClass , beanClassesInPath ) ) {
145
+ // A cycle was found. No point in checking any others in this loop.
146
+ // beanClassesInPath should have already been cleared.
147
+ assert beanClassesInPath .isEmpty ();
148
+ return true ;
149
+ }
150
+ // directCascadedBeanClass does not have a cycle.
151
+ // directCascadedBeanClass would have already been removed by the
152
+ // call to #determineTrackingRequired above
153
+ }
154
+ beanClassesInPath .remove ( beanClass );
155
+ return register ( beanClass , false );
156
+ }
157
+
158
+ // TODO: is there a more concise way to do this?
159
+ private <T > Set <Class <?>> getDirectCascadedBeanClasses (Class <T > beanClass ) {
160
+ final BeanMetaData <T > beanMetaData = beanMetaDataManager .getBeanMetaData ( beanClass );
161
+ if ( beanMetaData .hasCascadables () ) {
162
+ final Set <Class <?>> directCascadedBeanClasses = new HashSet <>();
163
+ for ( Cascadable cascadable : beanMetaData .getCascadables () ) {
164
+ final CascadingMetaData cascadingMetaData = cascadable .getCascadingMetaData ();
165
+ if ( cascadingMetaData .isContainer () ) {
166
+ throw new UnsupportedOperationException ( "Containers are not supported yet." );
167
+ }
168
+ else {
169
+ // TODO: For now, assume non-container Cascadables are always beans. Truee???
170
+ directCascadedBeanClasses .add ( (Class <?>) cascadable .getCascadableType () );
171
+ }
172
+ }
173
+ return directCascadedBeanClasses ;
174
+ }
175
+ else {
176
+ return Collections .emptySet ();
177
+ }
178
+ }
179
+
180
+ private boolean register (Class <?> beanClass , boolean isBeanTrackingEnabled ) {
181
+ if ( classToBeanTrackingEnabled .put ( beanClass , isBeanTrackingEnabled ) != null ) {
182
+ throw new IllegalStateException ( beanClass .getName () + " registered more than once." );
183
+ }
184
+ return isBeanTrackingEnabled ;
185
+ }
186
+ }
187
+
37
188
@ Override
38
189
public boolean isEnabledForBean (Class <?> rootBeanClass , boolean hasCascadables ) {
39
190
if ( !hasCascadables ) {
40
191
return false ;
41
192
}
42
193
43
- return trackingEnabledForBeans .getOrDefault ( rootBeanClass , true );
194
+ return trackingEnabledForBeans .get ( rootBeanClass );
44
195
}
45
196
46
197
@ Override
0 commit comments