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