View Javadoc
1   /*
2    * Copyright 2020-2022  the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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   * The jaxb dependencies resolver implementation.
69   *
70   * @author Christian Bremer
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      * Instantiates a new xml field callback.
220      *
221      * @param value the value
222      * @param scanResults the scan results
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      * Instantiates a new xml method callback.
272      *
273      * @param value the value
274      * @param scanResults the scan results
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      * Instantiates a new xml field filter.
323      *
324      * @param clazz the class
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); // PROPERTY, NONE
348       };
349     }
350 
351   }
352 
353   @SuppressWarnings("ClassCanBeRecord")
354   private static class XmlMethodFilter implements MethodFilter {
355 
356     private final XmlAccessType accessType;
357 
358     /**
359      * Instantiates a new xml method filter.
360      *
361      * @param clazz the class
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      * Instantiates a new scan result.
401      *
402      * @param clazz the class
403      * @param source the source
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      * Gets clazz.
413      *
414      * @return the class
415      */
416     Class<?> getClazz() {
417       return clazz;
418     }
419   }
420 
421 }