|
15 | 15 |
|
16 | 16 | import java.util.Collections; |
17 | 17 | import java.util.HashMap; |
| 18 | +import java.util.HashSet; |
18 | 19 | import java.util.Map; |
| 20 | +import java.util.Set; |
19 | 21 | import java.util.stream.Stream; |
20 | 22 |
|
21 | 23 | @EqualsAndHashCode |
@@ -73,31 +75,51 @@ public static OutboundAccessControlForCustomClass fromClassAnnotationsOf(Object |
73 | 75 | } |
74 | 76 |
|
75 | 77 | private static Map<String, OutboundAccessControlForCustomClass> extractNestedRecursively(Class<?> clazz) { |
| 78 | + return extractNestedRecursively(clazz, new HashSet<>()); |
| 79 | + } |
| 80 | + |
| 81 | + private static Map<String, OutboundAccessControlForCustomClass> extractNestedRecursively(Class<?> clazz, |
| 82 | + Set<Class<?>> visited) { |
76 | 83 | Map<String, OutboundAccessControlForCustomClass> result = new HashMap<>(); |
77 | 84 |
|
78 | | - Map<String, Class<?>> fields = ReflectionUtils.fetchFieldTypes(clazz); |
79 | | - |
80 | | - fields.forEach((fieldName, fieldClass) -> { |
81 | | - if (!fieldClass.getPackageName().startsWith("java.")) { |
82 | | - AccessControlModel classLevelAccessControl |
83 | | - = AccessControlModel.fromClassAnnotation(fieldClass); |
84 | | - Map<String, AccessControlModel> fieldLevelAccessControl |
85 | | - = AccessControlModel.fromFieldsAnnotations(fieldClass); |
86 | | - Map<String, OutboundAccessControlForCustomClass> nested |
87 | | - = extractNestedRecursively(fieldClass); |
88 | | - if (classLevelAccessControl != null |
89 | | - || MapUtils.isNotEmpty(fieldLevelAccessControl) |
90 | | - || MapUtils.isNotEmpty(nested)) { |
91 | | - result.put( |
92 | | - fieldName, |
93 | | - OutboundAccessControlForCustomClass.builder() |
94 | | - .classLevel(classLevelAccessControl) |
95 | | - .fieldLevel(fieldLevelAccessControl) |
96 | | - .nested(nested).build() |
97 | | - ); |
| 85 | + // Guard against self-referential / cyclic class graphs (e.g. a tree node referencing its own |
| 86 | + // type) that would otherwise recurse until the stack overflows. Scoped to the current branch |
| 87 | + // so diamond-shaped graphs are still fully explored. |
| 88 | + if (!visited.add(clazz)) { |
| 89 | + return result; |
| 90 | + } |
| 91 | + try { |
| 92 | + Map<String, Class<?>> fields = ReflectionUtils.fetchFieldTypes(clazz); |
| 93 | + |
| 94 | + fields.forEach((fieldName, fieldClass) -> { |
| 95 | + if (!ReflectionUtils.isJdkType(fieldClass)) { |
| 96 | + AccessControlModel classLevelAccessControl |
| 97 | + = AccessControlModel.fromClassAnnotation(fieldClass); |
| 98 | + Map<String, AccessControlModel> fieldLevelAccessControl |
| 99 | + = AccessControlModel.fromFieldsAnnotations(fieldClass); |
| 100 | + // Enums are value types: their compiler-generated constant fields are static and |
| 101 | + // self-referential (each constant is a field typed as the enum itself), so recursing |
| 102 | + // into them never terminates. Capture the enum's own class/field-level access control |
| 103 | + // but do not descend into its constants. |
| 104 | + Map<String, OutboundAccessControlForCustomClass> nested = fieldClass.isEnum() |
| 105 | + ? Collections.emptyMap() |
| 106 | + : extractNestedRecursively(fieldClass, visited); |
| 107 | + if (classLevelAccessControl != null |
| 108 | + || MapUtils.isNotEmpty(fieldLevelAccessControl) |
| 109 | + || MapUtils.isNotEmpty(nested)) { |
| 110 | + result.put( |
| 111 | + fieldName, |
| 112 | + OutboundAccessControlForCustomClass.builder() |
| 113 | + .classLevel(classLevelAccessControl) |
| 114 | + .fieldLevel(fieldLevelAccessControl) |
| 115 | + .nested(nested).build() |
| 116 | + ); |
| 117 | + } |
98 | 118 | } |
99 | | - } |
100 | | - }); |
| 119 | + }); |
| 120 | + } finally { |
| 121 | + visited.remove(clazz); |
| 122 | + } |
101 | 123 |
|
102 | 124 | return result; |
103 | 125 | } |
|
0 commit comments