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