1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.bremersee.xml;
18
19 import static java.lang.reflect.Modifier.isPublic;
20 import static java.lang.reflect.Modifier.isStatic;
21 import static java.lang.reflect.Modifier.isTransient;
22 import static org.springframework.util.ObjectUtils.isEmpty;
23
24 import jakarta.xml.bind.annotation.XmlAccessType;
25 import jakarta.xml.bind.annotation.XmlAccessorType;
26 import jakarta.xml.bind.annotation.XmlAnyAttribute;
27 import jakarta.xml.bind.annotation.XmlAnyElement;
28 import jakarta.xml.bind.annotation.XmlAttachmentRef;
29 import jakarta.xml.bind.annotation.XmlAttribute;
30 import jakarta.xml.bind.annotation.XmlElement;
31 import jakarta.xml.bind.annotation.XmlElementRef;
32 import jakarta.xml.bind.annotation.XmlElementRefs;
33 import jakarta.xml.bind.annotation.XmlElementWrapper;
34 import jakarta.xml.bind.annotation.XmlElements;
35 import jakarta.xml.bind.annotation.XmlID;
36 import jakarta.xml.bind.annotation.XmlIDREF;
37 import jakarta.xml.bind.annotation.XmlList;
38 import jakarta.xml.bind.annotation.XmlMixed;
39 import jakarta.xml.bind.annotation.XmlRootElement;
40 import jakarta.xml.bind.annotation.XmlSeeAlso;
41 import jakarta.xml.bind.annotation.XmlTransient;
42 import jakarta.xml.bind.annotation.XmlType;
43 import java.lang.annotation.Annotation;
44 import java.lang.reflect.AnnotatedElement;
45 import java.lang.reflect.Field;
46 import java.lang.reflect.Method;
47 import java.util.Arrays;
48 import java.util.Collection;
49 import java.util.HashSet;
50 import java.util.Optional;
51 import java.util.Set;
52 import java.util.stream.Collectors;
53 import lombok.AccessLevel;
54 import lombok.EqualsAndHashCode;
55 import lombok.NoArgsConstructor;
56 import lombok.NonNull;
57 import org.springframework.core.ResolvableType;
58 import org.springframework.core.annotation.AnnotationUtils;
59 import org.springframework.util.Assert;
60 import org.springframework.util.ClassUtils;
61 import org.springframework.util.ReflectionUtils;
62 import org.springframework.util.ReflectionUtils.FieldCallback;
63 import org.springframework.util.ReflectionUtils.FieldFilter;
64 import org.springframework.util.ReflectionUtils.MethodCallback;
65 import org.springframework.util.ReflectionUtils.MethodFilter;
66
67
68
69
70
71
72 @NoArgsConstructor(access = AccessLevel.PACKAGE)
73 class JaxbDependenciesResolverImpl implements JaxbDependenciesResolver {
74
75 @SuppressWarnings("unchecked")
76 private static final Class<? extends Annotation>[] EXPLICIT_XML_ANNOTATIONS = new Class[]{
77 XmlAnyAttribute.class,
78 XmlAnyElement.class,
79 XmlAttachmentRef.class,
80 XmlAttribute.class,
81 XmlElement.class,
82 XmlElementRef.class,
83 XmlElementRefs.class,
84 XmlElements.class,
85 XmlElementWrapper.class,
86 XmlID.class,
87 XmlIDREF.class,
88 XmlList.class,
89 XmlMixed.class
90 };
91
92 @Override
93 public Class<?>[] resolveClasses(Object value) {
94 Set<ScanResult> scanResults = new HashSet<>();
95 if (value instanceof Class<?>[] clsArray) {
96 for (Class<?> clazz : clsArray) {
97 resolveClasses(clazz, scanResults);
98 }
99 } else {
100 resolveClasses(value, scanResults);
101 }
102 return ClassUtils.toClassArray(scanResults
103 .stream()
104 .map(ScanResult::getClazz)
105 .collect(Collectors.toSet()));
106 }
107
108 private boolean resolveClasses(Object value, Set<ScanResult> scanResults) {
109 if (isEmpty(value)
110 || stopResolving(ClassUtils.getUserClass(value), value, scanResults)) {
111 return false;
112 }
113 if (value instanceof Class) {
114 resolveClasses((Class<?>) value, scanResults);
115 return true;
116 }
117 if ((value instanceof Collection<?> collection) && !collection.isEmpty()) {
118 for (Object v : collection) {
119 if (!resolveClasses(v, scanResults)) {
120 return true;
121 }
122 }
123 return true;
124 }
125 Class<?> clazz = ClassUtils.getUserClass(value);
126 resolveSuperClasses(clazz, value, scanResults);
127 ReflectionUtils.doWithFields(
128 clazz,
129 new XmlFieldCallback(value, scanResults),
130 new XmlFieldFilter(clazz));
131 ReflectionUtils.doWithMethods(
132 clazz,
133 new XmlMethodCallback(value, scanResults),
134 new XmlMethodFilter(clazz));
135 Optional.ofNullable(AnnotationUtils.findAnnotation(clazz, XmlSeeAlso.class))
136 .ifPresent(seeAlso -> Arrays.stream(seeAlso.value())
137 .forEach(seeAlsoClass -> resolveClasses(seeAlsoClass, scanResults)));
138 return true;
139 }
140
141 private void resolveClasses(Class<?> clazz, Set<ScanResult> scanResults) {
142 if (stopResolving(clazz, null, scanResults)) {
143 return;
144 }
145 resolveSuperClasses(clazz, null, scanResults);
146 ReflectionUtils.doWithFields(
147 clazz,
148 new XmlFieldCallback(null, scanResults),
149 new XmlFieldFilter(clazz));
150 ReflectionUtils.doWithMethods(
151 clazz,
152 new XmlMethodCallback(null, scanResults),
153 new XmlMethodFilter(clazz));
154 Optional.ofNullable(AnnotationUtils.findAnnotation(clazz, XmlSeeAlso.class))
155 .ifPresent(seeAlso -> Arrays.stream(seeAlso.value())
156 .forEach(seeAlsoClass -> resolveClasses(seeAlsoClass, scanResults)));
157 }
158
159 private void resolveSuperClasses(
160 Class<?> clazz,
161 Object source,
162 Set<ScanResult> scanResults) {
163
164 if (!stopResolving(clazz, source, scanResults)) {
165 scanResults.add(new ScanResult(clazz, source));
166 resolveSuperClasses(clazz.getSuperclass(), null, scanResults);
167 }
168 }
169
170 private boolean stopResolving(Class<?> clazz, Object source,
171 Set<ScanResult> scanResults) {
172 return isEmpty(clazz)
173 || (!isAnnotatedWithXml(clazz) && !Collection.class.isAssignableFrom(clazz))
174 || scanResults.contains(new ScanResult(clazz, source));
175 }
176
177 private boolean isAnnotatedWithXml(Class<?> clazz) {
178 return clazz.isAnnotationPresent(XmlRootElement.class)
179 || clazz.isAnnotationPresent(XmlType.class);
180 }
181
182 private void processXmlAnnotations(AnnotatedElement element,
183 Set<ScanResult> scanResults) {
184 processXmlElement(AnnotationUtils.findAnnotation(element, XmlElement.class), scanResults);
185 Optional
186 .ofNullable(AnnotationUtils.findAnnotation(element, XmlElements.class))
187 .map(XmlElements::value)
188 .ifPresent(a -> Arrays.stream(a).forEach(e -> processXmlElement(e, scanResults)));
189
190 processXmlElementRef(AnnotationUtils.findAnnotation(element, XmlElementRef.class), scanResults);
191 Optional
192 .ofNullable(AnnotationUtils.findAnnotation(element, XmlElementRefs.class))
193 .map(XmlElementRefs::value)
194 .ifPresent(a -> Arrays.stream(a).forEach(e -> processXmlElementRef(e, scanResults)));
195 }
196
197 private void processXmlElement(XmlElement annotation, Set<ScanResult> scanResults) {
198 Optional.ofNullable(annotation)
199 .map(XmlElement::type)
200 .filter(type -> !type.equals(XmlElement.DEFAULT.class))
201 .ifPresent(type -> resolveClasses(type, scanResults));
202 }
203
204 private void processXmlElementRef(XmlElementRef annotation,
205 Set<ScanResult> scanResults) {
206 Optional.ofNullable(annotation)
207 .map(XmlElementRef::type)
208 .filter(type -> !type.equals(XmlElementRef.DEFAULT.class))
209 .ifPresent(type -> resolveClasses(type, scanResults));
210 }
211
212 private class XmlFieldCallback implements FieldCallback {
213
214 private final Object value;
215
216 private final Set<ScanResult> scanResults;
217
218
219
220
221
222
223
224 XmlFieldCallback(Object value, Set<ScanResult> scanResults) {
225 this.value = value;
226 this.scanResults = scanResults;
227 }
228
229 @Override
230 public void doWith(@NonNull Field field) throws IllegalArgumentException {
231 ReflectionUtils.makeAccessible(field);
232 processXmlAnnotations(field, scanResults);
233 if (isEmpty(value)) {
234 if (Collection.class.isAssignableFrom(field.getType())) {
235 for (ResolvableType rt : ResolvableType.forField(field).getGenerics()) {
236 resolveClasses(rt.resolve(), scanResults);
237 }
238 } else {
239 resolveClasses(field.getType(), scanResults);
240 }
241 } else {
242 Object fieldValue = ReflectionUtils.getField(field, value);
243 if (!isEmpty(fieldValue)) {
244 if (fieldValue instanceof Collection && ((Collection<?>) fieldValue).isEmpty()) {
245 for (ResolvableType rt : ResolvableType.forField(field).getGenerics()) {
246 resolveClasses(rt.resolve(), scanResults);
247 }
248 } else {
249 resolveClasses(fieldValue, scanResults);
250 }
251 } else {
252 if (Collection.class.isAssignableFrom(field.getType())) {
253 for (ResolvableType rt : ResolvableType.forField(field).getGenerics()) {
254 resolveClasses(rt.resolve(), scanResults);
255 }
256 } else {
257 resolveClasses(field.getType(), scanResults);
258 }
259 }
260 }
261 }
262 }
263
264 private class XmlMethodCallback implements MethodCallback {
265
266 private final Object value;
267
268 private final Set<ScanResult> scanResults;
269
270
271
272
273
274
275
276 XmlMethodCallback(Object value, Set<ScanResult> scanResults) {
277 this.value = value;
278 this.scanResults = scanResults;
279 }
280
281 @Override
282 public void doWith(@NonNull Method method) throws IllegalArgumentException {
283 ReflectionUtils.makeAccessible(method);
284 processXmlAnnotations(method, scanResults);
285 if (isEmpty(value)) {
286 if (Collection.class.isAssignableFrom(method.getReturnType())) {
287 for (ResolvableType rt : ResolvableType.forMethodReturnType(method).getGenerics()) {
288 resolveClasses(rt.resolve(), scanResults);
289 }
290 } else {
291 resolveClasses(method.getReturnType(), scanResults);
292 }
293 } else {
294 Object methodValue = ReflectionUtils.invokeMethod(method, value);
295 if (!isEmpty(methodValue)) {
296 if (methodValue instanceof Collection && ((Collection<?>) methodValue).isEmpty()) {
297 for (ResolvableType rt : ResolvableType
298 .forMethodReturnType(method, ClassUtils.getUserClass(value))
299 .getGenerics()) {
300 resolveClasses(rt.resolve(), scanResults);
301 }
302 } else {
303 resolveClasses(methodValue, scanResults);
304 }
305 } else {
306 resolveClasses(method.getReturnType(), scanResults);
307 }
308 }
309 }
310 }
311
312 private static boolean anyXmlAnnotationPresent(AnnotatedElement element) {
313 return Arrays.stream(EXPLICIT_XML_ANNOTATIONS).anyMatch(element::isAnnotationPresent);
314 }
315
316 @SuppressWarnings("ClassCanBeRecord")
317 private static class XmlFieldFilter implements FieldFilter {
318
319 private final XmlAccessType accessType;
320
321
322
323
324
325
326 XmlFieldFilter(Class<?> clazz) {
327 this.accessType = Optional
328 .ofNullable(AnnotationUtils.findAnnotation(clazz, XmlAccessorType.class))
329 .map(XmlAccessorType::value)
330 .orElseGet(() -> Optional
331 .ofNullable(clazz.getPackage().getAnnotation(XmlAccessorType.class))
332 .map(XmlAccessorType::value)
333 .orElse(XmlAccessType.PUBLIC_MEMBER));
334 }
335
336 @Override
337 public boolean matches(Field field) {
338 int modifiers = field.getModifiers();
339 if (isStatic(modifiers)
340 || isTransient(modifiers)
341 || field.isAnnotationPresent(XmlTransient.class)) {
342 return false;
343 }
344 return switch (accessType) {
345 case FIELD -> true;
346 case PUBLIC_MEMBER -> isPublic(modifiers) || anyXmlAnnotationPresent(field);
347 default -> anyXmlAnnotationPresent(field);
348 };
349 }
350
351 }
352
353 @SuppressWarnings("ClassCanBeRecord")
354 private static class XmlMethodFilter implements MethodFilter {
355
356 private final XmlAccessType accessType;
357
358
359
360
361
362
363 XmlMethodFilter(Class<?> clazz) {
364 this.accessType = Optional
365 .ofNullable(AnnotationUtils.findAnnotation(clazz, XmlAccessorType.class))
366 .map(XmlAccessorType::value)
367 .orElseGet(() -> Optional
368 .ofNullable(clazz.getPackage().getAnnotation(XmlAccessorType.class))
369 .map(XmlAccessorType::value)
370 .orElse(XmlAccessType.PUBLIC_MEMBER));
371 }
372
373 @Override
374 public boolean matches(Method method) {
375 int modifiers = method.getModifiers();
376 if (isStatic(modifiers)
377 || !(method.getName().startsWith("get") || method.getName().startsWith("is"))
378 || method.getParameterCount() > 0
379 || method.getReturnType().equals(Void.class)
380 || method.isAnnotationPresent(XmlTransient.class)) {
381 return false;
382 }
383 return switch (accessType) {
384 case PROPERTY -> true;
385 case PUBLIC_MEMBER -> isPublic(modifiers) || anyXmlAnnotationPresent(method);
386 default -> anyXmlAnnotationPresent(method);
387 };
388 }
389 }
390
391 @EqualsAndHashCode
392 @SuppressWarnings("ClassCanBeRecord")
393 private static class ScanResult {
394
395 private final Class<?> clazz;
396
397 private final Object source;
398
399
400
401
402
403
404
405 ScanResult(Class<?> clazz, Object source) {
406 Assert.notNull(clazz, "Class must be present.");
407 this.clazz = clazz;
408 this.source = source;
409 }
410
411
412
413
414
415
416 Class<?> getClazz() {
417 return clazz;
418 }
419 }
420
421 }