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 org.springframework.util.ObjectUtils.isEmpty;
20  
21  import jakarta.xml.bind.JAXBContext;
22  import jakarta.xml.bind.JAXBException;
23  import jakarta.xml.bind.Marshaller;
24  import jakarta.xml.bind.Unmarshaller;
25  import jakarta.xml.bind.ValidationEventHandler;
26  import jakarta.xml.bind.annotation.adapters.XmlAdapter;
27  import jakarta.xml.bind.attachment.AttachmentMarshaller;
28  import jakarta.xml.bind.attachment.AttachmentUnmarshaller;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.Collection;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Objects;
35  import java.util.Optional;
36  import java.util.Set;
37  import java.util.concurrent.ConcurrentHashMap;
38  import java.util.stream.Collectors;
39  import javax.xml.transform.Source;
40  import javax.xml.validation.Schema;
41  import lombok.ToString;
42  import org.springframework.util.ClassUtils;
43  
44  /**
45   * The jaxb context builder.
46   *
47   * @author Christian Bremer
48   */
49  @ToString
50  class JaxbContextBuilderImpl implements JaxbContextBuilder {
51  
52    /**
53     * Key is package name, value is jaxb data set.
54     */
55    private final Map<Object, JaxbContextData> jaxbContextDataMap = new ConcurrentHashMap<>();
56  
57    private final Map<JaxbContextDetails, Schema> schemaCache = new ConcurrentHashMap<>();
58  
59    private final Map<JaxbContextDetails, JAXBContext> jaxbContextCache
60        = new ConcurrentHashMap<>();
61  
62    private JaxbDependenciesResolver dependenciesResolver = DEFAULT_DEPENDENCIES_RESOLVER;
63  
64    private SchemaBuilder schemaBuilder = SchemaBuilder.newInstance();
65  
66    private ClassLoader classLoader;
67  
68    private boolean formattedOutput = true;
69  
70    private SchemaMode schemaMode = SchemaMode.NEVER;
71  
72    private List<XmlAdapter<?, ?>> xmlAdapters;
73  
74    private AttachmentMarshaller attachmentMarshaller;
75  
76    private AttachmentUnmarshaller attachmentUnmarshaller;
77  
78    private ValidationEventHandler validationEventHandler;
79  
80    /**
81     * Instantiates a new jaxb context builder.
82     */
83    JaxbContextBuilderImpl() {
84    }
85  
86    private void clearCache() {
87      schemaCache.clear();
88      jaxbContextCache.clear();
89    }
90  
91    @Override
92    public JaxbContextBuilder copy() {
93      JaxbContextBuilderImpl copy = new JaxbContextBuilderImpl();
94      copy.dependenciesResolver = dependenciesResolver;
95      copy.schemaMode = schemaMode;
96      copy.schemaCache.putAll(schemaCache);
97      copy.attachmentMarshaller = attachmentMarshaller;
98      copy.attachmentUnmarshaller = attachmentUnmarshaller;
99      copy.classLoader = classLoader;
100     copy.formattedOutput = formattedOutput;
101     copy.jaxbContextCache.putAll(jaxbContextCache);
102     copy.jaxbContextDataMap.putAll(jaxbContextDataMap);
103     copy.schemaBuilder = schemaBuilder.copy();
104     copy.validationEventHandler = validationEventHandler;
105     if (!isEmpty(xmlAdapters)) {
106       copy.xmlAdapters = new ArrayList<>(xmlAdapters);
107     }
108     return copy;
109   }
110 
111   @Override
112   public JaxbContextBuilder withSchemaMode(SchemaMode schemaMode) {
113     if (!isEmpty(schemaMode)) {
114       this.schemaMode = schemaMode;
115     }
116     return this;
117   }
118 
119   @Override
120   public JaxbContextBuilder withSchemaBuilder(SchemaBuilder schemaBuilder) {
121     if (!isEmpty(schemaBuilder)) {
122       this.schemaBuilder = schemaBuilder;
123     }
124     return this;
125   }
126 
127   @Override
128   public JaxbContextBuilder withDependenciesResolver(JaxbDependenciesResolver resolver) {
129     if ((isEmpty(dependenciesResolver) && !isEmpty(resolver))
130         || (!isEmpty(dependenciesResolver) && isEmpty(resolver))
131         || (!isEmpty(dependenciesResolver) && !ClassUtils.getUserClass(dependenciesResolver)
132         .equals(ClassUtils.getUserClass(resolver)))) {
133       clearCache();
134     }
135     this.dependenciesResolver = resolver;
136     return this;
137   }
138 
139   @Override
140   public JaxbContextBuilder withContextClassLoader(ClassLoader classLoader) {
141     this.classLoader = classLoader;
142     return this;
143   }
144 
145   @Override
146   public JaxbContextBuilder withFormattedOutput(boolean formattedOutput) {
147     this.formattedOutput = formattedOutput;
148     return this;
149   }
150 
151   @Override
152   public JaxbContextBuilder withXmlAdapters(
153       Collection<? extends XmlAdapter<?, ?>> xmlAdapters) {
154 
155     if (isEmpty(xmlAdapters)) {
156       this.xmlAdapters = null;
157     } else {
158       this.xmlAdapters = xmlAdapters
159           .stream()
160           .filter(Objects::nonNull)
161           .collect(Collectors.toList());
162     }
163     return this;
164   }
165 
166   @Override
167   public JaxbContextBuilder withAttachmentMarshaller(
168       AttachmentMarshaller attachmentMarshaller) {
169     this.attachmentMarshaller = attachmentMarshaller;
170     return this;
171   }
172 
173   @Override
174   public JaxbContextBuilder withAttachmentUnmarshaller(
175       AttachmentUnmarshaller attachmentUnmarshaller) {
176     this.attachmentUnmarshaller = attachmentUnmarshaller;
177     return this;
178   }
179 
180   @Override
181   public JaxbContextBuilder withValidationEventHandler(
182       ValidationEventHandler validationEventHandler) {
183     this.validationEventHandler = validationEventHandler;
184     return this;
185   }
186 
187   @Override
188   public JaxbContextBuilder add(JaxbContextMember data) {
189     return Optional.ofNullable(data)
190         .map(JaxbContextData::new)
191         .map(d -> {
192           clearCache();
193           jaxbContextDataMap.put(d.getKey(), d);
194           return this;
195         })
196         .orElse(this);
197   }
198 
199   @Override
200   public Unmarshaller buildUnmarshaller(Class<?>... classes) {
201     if (!isEmpty(classes)) {
202       Class<?>[] jaxbClasses = isEmpty(dependenciesResolver)
203           ? classes
204           : dependenciesResolver.resolveClasses(classes);
205       Arrays.stream(jaxbClasses)
206           .map(JaxbContextData::new)
207           .forEach(data -> jaxbContextDataMap.computeIfAbsent(data.getKey(), key -> {
208             clearCache();
209             return data;
210           }));
211     }
212     JaxbContextWrapper jaxbContext = computeJaxbContext(null);
213     SchemaMode mode = jaxbContext.getSchemaMode();
214     if (mode == SchemaMode.ALWAYS
215         || mode == SchemaMode.UNMARSHAL
216         || mode == SchemaMode.EXTERNAL_XSD
217         && !isEmpty(jaxbContext.getDetails().getSchemaLocation())) {
218       jaxbContext.setSchema(computeSchema(jaxbContext));
219     }
220     try {
221       return jaxbContext.createUnmarshaller();
222 
223     } catch (JAXBException e) {
224       throw new JaxbRuntimeException(e);
225     }
226   }
227 
228   @Override
229   public Marshaller buildMarshaller(Object value) {
230     JaxbContextWrapper jaxbContext = computeJaxbContext(value);
231     SchemaMode mode = jaxbContext.getSchemaMode();
232     if ((mode == SchemaMode.ALWAYS
233         || mode == SchemaMode.MARSHAL
234         || mode == SchemaMode.EXTERNAL_XSD)
235         && !isEmpty(jaxbContext.getDetails().getSchemaLocation())) {
236       jaxbContext.setSchema(computeSchema(jaxbContext));
237     }
238     try {
239       return jaxbContext.createMarshaller();
240     } catch (JAXBException e) {
241       throw new JaxbRuntimeException(e);
242     }
243   }
244 
245   @Override
246   public JaxbContextBuilder initJaxbContext() {
247     if (!jaxbContextDataMap.isEmpty()) {
248       buildJaxbContext(null);
249     }
250     return this;
251   }
252 
253   @Override
254   public JaxbContextWrapper buildJaxbContext(Object value) {
255     JaxbContextWrapper wrapper = computeJaxbContext(value);
256     SchemaMode mode = wrapper.getSchemaMode();
257     if ((mode == SchemaMode.ALWAYS
258         || mode == SchemaMode.MARSHAL
259         || mode == SchemaMode.UNMARSHAL
260         || mode == SchemaMode.EXTERNAL_XSD)
261         && !isEmpty(wrapper.getDetails().getSchemaLocation())) {
262       wrapper.setSchema(computeSchema(wrapper));
263     }
264     return wrapper;
265   }
266 
267   @Override
268   public Schema buildSchema(Object value) {
269     return computeSchema(value);
270   }
271 
272   private JaxbContextDetails buildDetails() {
273     return jaxbContextDataMap.values().stream()
274         .collect(JaxbContextDetails.contextDataCollector());
275   }
276 
277   private JaxbContextDetails buildDetails(Object value) {
278     if (isEmpty(value)) {
279       return buildDetails();
280     }
281     if (value instanceof Class<?>) {
282       return buildDetails(new Class<?>[]{(Class<?>) value});
283     }
284     Class<?>[] classes;
285     if (value instanceof Class<?>[] clsArray) {
286       classes = isEmpty(dependenciesResolver)
287           ? clsArray
288           : dependenciesResolver.resolveClasses(value);
289     } else {
290       classes = isEmpty(dependenciesResolver)
291           ? new Class<?>[]{value.getClass()}
292           : dependenciesResolver.resolveClasses(value);
293     }
294     return Arrays.stream(classes)
295         .map(JaxbContextData::new)
296         .map(data -> jaxbContextDataMap.computeIfAbsent(data.getKey(), key -> {
297           clearCache();
298           return data;
299         }))
300         .collect(JaxbContextDetails.contextDataCollector());
301   }
302 
303   private JaxbContextWrapper computeJaxbContext(Object value) {
304     JaxbContextDetails details = buildDetails(value);
305     JAXBContext jaxbContext = jaxbContextCache.computeIfAbsent(details, key -> {
306       try {
307         return isEmpty(classLoader)
308             ? JAXBContext.newInstance(key.getClasses())
309             : JAXBContext.newInstance(key.getClasses(classLoader));
310 
311       } catch (Exception e) {
312         throw new JaxbRuntimeException(
313             String.format("Creating jaxb context failed with builder context: %s", key),
314             e);
315       }
316     });
317     JaxbContextWrapper wrapper = new JaxbContextWrapper(jaxbContext, details);
318     wrapper.setAttachmentMarshaller(attachmentMarshaller);
319     wrapper.setAttachmentUnmarshaller(attachmentUnmarshaller);
320     wrapper.setFormattedOutput(formattedOutput);
321     wrapper.setValidationEventHandler(validationEventHandler);
322     wrapper.setXmlAdapters(xmlAdapters);
323     wrapper.setSchemaMode(schemaMode);
324     return wrapper;
325   }
326 
327   private Schema computeSchema(Object value) {
328     return computeSchema(computeJaxbContext(value));
329   }
330 
331   private Schema computeSchema(JaxbContextWrapper jaxbContext) {
332     JaxbContextDetails details = jaxbContext.getDetails();
333     return schemaCache.computeIfAbsent(details, key -> {
334       SchemaSourcesResolver sourcesResolver = new SchemaSourcesResolver();
335       try {
336         jaxbContext.generateSchema(sourcesResolver);
337       } catch (Exception e) {
338         throw new JaxbRuntimeException(e);
339       }
340       List<Source> sources = new ArrayList<>(
341           sourcesResolver.toSources(key.getNameSpaces()));
342       Set<String> locations = key.getSchemaLocations();
343       sources.addAll(schemaBuilder.fetchSchemaSources(locations));
344       return schemaBuilder.buildSchema(sources);
345     });
346   }
347 
348 }