View Javadoc
1   /*
2   * Copyright 2020-2026  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.assertj.core.api.Assertions.assertThat;
20  import static org.mockito.Mockito.mock;
21  
22  import jakarta.xml.bind.Marshaller;
23  import jakarta.xml.bind.ValidationEventHandler;
24  import jakarta.xml.bind.attachment.AttachmentMarshaller;
25  import jakarta.xml.bind.attachment.AttachmentUnmarshaller;
26  import java.io.StringReader;
27  import java.io.StringWriter;
28  import java.time.Duration;
29  import java.time.OffsetDateTime;
30  import java.time.format.DateTimeFormatter;
31  import java.util.Arrays;
32  import java.util.List;
33  import java.util.ServiceLoader;
34  import org.assertj.core.api.InstanceOfAssertFactories;
35  import org.assertj.core.api.SoftAssertions;
36  import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
37  import org.bremersee.xml.adapter.DateXmlAdapter;
38  import org.bremersee.xml.adapter.DurationXmlAdapter;
39  import org.bremersee.xml.adapter.EpochMilliXmlAdapter;
40  import org.bremersee.xml.adapter.OffsetDateTimeXmlAdapter;
41  import org.bremersee.xml.model1.Person;
42  import org.bremersee.xml.model2.Vehicle;
43  import org.bremersee.xml.model3.Company;
44  import org.bremersee.xml.model4.Address;
45  import org.bremersee.xml.model5.StartEnd;
46  import org.bremersee.xml.model6.StandaloneModel;
47  import org.bremersee.xml.model7a.Fender;
48  import org.bremersee.xml.model7a.ObjectFactory;
49  import org.bremersee.xml.model7b.DirtBikeReseller;
50  import org.bremersee.xml.model7b.MountainBike;
51  import org.bremersee.xml.model7b.RacingBike;
52  import org.bremersee.xml.model7b.RacingReseller;
53  import org.bremersee.xml.model7b.SportBikes;
54  import org.bremersee.xml.model7c.Carrier;
55  import org.bremersee.xml.model8.AnyElementList;
56  import org.junit.jupiter.api.Test;
57  import org.junit.jupiter.api.extension.ExtendWith;
58  import org.w3c.dom.Element;
59  
60  /**
61   * The jaxb context builder test.
62   *
63   * @author Christian Bremer
64   */
65  @ExtendWith({SoftAssertionsExtension.class})
66  class JaxbContextBuilderTest {
67  
68    /**
69     * The xml we want to write and read.
70     *
71     * <pre>
72     * <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
73     * <ns2:MountainBike xsi:schemaLocation="http://bremersee.org/xmlschemas/common-xml-test-model-7a http://bremersee.github.io/xmlschemas/common-xml-test-model-7a.xsd http://bremersee.org/xmlschemas/common-xml-test-model-7b http://bremersee.github.io/xmlschemas/common-xml-test-model-7b.xsd" xmlns="http://bremersee.org/xmlschemas/common-xml-test-model-7a" xmlns:ns2="http://bremersee.org/xmlschemas/common-xml-test-model-7b" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
74     *     <producer xsi:type="ns2:sportBikesType">
75     *         <name>Sport Bikes</name>
76     *         <ns2:Reseller xsi:type="ns2:dirtBikeResellerType">
77     *             <name>Dirt Bikes</name>
78     *         </ns2:Reseller>
79     *         <ns2:Reseller xsi:type="ns2:racingResellerType">
80     *             <name>Racing Fun</name>
81     *         </ns2:Reseller>
82     *     </producer>
83     *     <color>Red</color>
84     *     <extraParts>
85     *         <ns2:Carrier xsi:schemaLocation="http://bremersee.org/xmlschemas/common-xml-test-model-7a http://bremersee.github.io/xmlschemas/common-xml-test-model-7a.xsd http://bremersee.org/xmlschemas/common-xml-test-model-7c http://bremersee.github.io/xmlschemas/common-xml-test-model-7c.xsd" xmlns:ns2="http://bremersee.org/xmlschemas/common-xml-test-model-7c">
86     *             <partNumber>123456789</partNumber>
87     *             <ns2:capacity>15 kg</ns2:capacity>
88     *         </ns2:Carrier>
89     *     </extraParts>
90     *     <ns2:seatHeight>60</ns2:seatHeight>
91     * </ns2:MountainBike>
92     * </pre>
93     */
94    private static final String XML1 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
95        + "<ns2:MountainBike "
96        + "xsi:schemaLocation=\"http://bremersee.org/xmlschemas/common-xml-test-model-7a "
97        + "http://bremersee.github.io/xmlschemas/common-xml-test-model-7a.xsd "
98        + "http://bremersee.org/xmlschemas/common-xml-test-model-7b "
99        + "http://bremersee.github.io/xmlschemas/common-xml-test-model-7b.xsd\" "
100       + "xmlns=\"http://bremersee.org/xmlschemas/common-xml-test-model-7a\" "
101       + "xmlns:ns2=\"http://bremersee.org/xmlschemas/common-xml-test-model-7b\" "
102       + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
103       + "<producer xsi:type=\"ns2:sportBikesType\">"
104       + "<name>Sport Bikes</name>"
105       + "<ns2:Reseller xsi:type=\"ns2:dirtBikeResellerType\">"
106       + "<name>Dirt Bikes</name></ns2:Reseller>"
107       + "<ns2:Reseller xsi:type=\"ns2:racingResellerType\">"
108       + "<name>Racing Fun</name></ns2:Reseller>"
109       + "</producer><color>Red</color><extraParts>"
110       + "<ns2:Carrier "
111       + "xsi:schemaLocation=\"http://bremersee.org/xmlschemas/common-xml-test-model-7a "
112       + "http://bremersee.github.io/xmlschemas/common-xml-test-model-7a.xsd "
113       + "http://bremersee.org/xmlschemas/common-xml-test-model-7c "
114       + "http://bremersee.github.io/xmlschemas/common-xml-test-model-7c.xsd\" "
115       + "xmlns:ns2=\"http://bremersee.org/xmlschemas/common-xml-test-model-7c\">"
116       + "<partNumber>123456789</partNumber>"
117       + "<ns2:capacity>15 kg</ns2:capacity>"
118       + "</ns2:Carrier>"
119       + "</extraParts>"
120       + "<ns2:seatHeight>60</ns2:seatHeight></ns2:MountainBike>";
121 
122   /**
123    * Read and write element.
124    *
125    * @param softly the soft assertions
126    * @throws Exception the exception
127    */
128   @Test
129   void readAndWriteElement(SoftAssertions softly) throws Exception {
130 
131     Carrier carrier = new Carrier();
132     carrier.setPartNumber("123456789");
133     carrier.setCapacity("15 kg");
134 
135     DirtBikeReseller r0 = new DirtBikeReseller();
136     r0.setName("Dirt Bikes");
137     RacingReseller r1 = new RacingReseller();
138     r1.setName("Racing Fun");
139 
140     SportBikes sportBikes = new SportBikes();
141     sportBikes.setName("Sport Bikes");
142     sportBikes.getChain().add(r0);
143     sportBikes.getChain().add(r1);
144 
145     JaxbContextBuilder builder = JaxbContextBuilder
146         .newInstance()
147         .withDependenciesResolver(new JaxbDependenciesResolverImpl())
148         .withXmlAdapters(Arrays.asList(new OffsetDateTimeXmlAdapter(), new DurationXmlAdapter()))
149         .copy()
150         .withFormattedOutput(false)
151         .withSchemaMode(SchemaMode.MARSHAL)
152         .withSchemaBuilder(SchemaBuilder.newInstance())
153         .withContextClassLoader(Thread.currentThread().getContextClassLoader())
154         .addAll(Arrays.asList(
155             new JaxbContextData(ObjectFactory.class.getPackage()),
156             new JaxbContextData(org.bremersee.xml.model7b.ObjectFactory.class.getPackage()),
157             new JaxbContextData(org.bremersee.xml.model7c.ObjectFactory.class.getPackage())))
158         .process(() -> List.of(new JaxbContextData(StandaloneModel.class)))
159         .processAll(ServiceLoader.load(JaxbContextDataProvider.class));
160 
161     MountainBike model = new MountainBike();
162     model.setSeatHeight(60);
163     model.setColor("Red");
164     model.setProducer(sportBikes);
165 
166     Element carrierElement = XmlDocumentBuilder.newInstance()
167         .buildDocument(carrier, builder.buildMarshaller(carrier))
168         .getDocumentElement();
169     model.getExtraParts().add(carrierElement);
170 
171     MountainBike actualModel = (MountainBike) builder
172         .copy()
173         .withDependenciesResolver(null)
174         .buildUnmarshaller(MountainBike.class)
175         .unmarshal(new StringReader(XML1));
176     softly.assertThat(actualModel)
177         .isEqualTo(model);
178     softly.assertThat(actualModel.getExtraParts())
179         .map(elem -> builder
180             .buildUnmarshaller().unmarshal(elem))
181         .containsExactly(carrier);
182 
183     StringWriter sw = new StringWriter();
184     builder.buildMarshaller(model).marshal(model, sw);
185     String actualXml = sw.toString();
186     softly.assertThat(actualXml)
187         .isEqualTo(XML1);
188   }
189 
190   /**
191    * Write and read any elements.
192    *
193    * @throws Exception the exception
194    */
195   @Test
196   void writeAndReadAnyElements() throws Exception {
197     OffsetDateTime start = OffsetDateTime
198         .parse("2000-01-16T12:00:00.000Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME);
199     AnyElementList list = new AnyElementList(List.of(
200 
201         // Company has no namespace.
202         new Company("UnitTest"),
203 
204         // Address has no namespace, but StartEnd has one.
205         new Address("Surefire", "123", new StartEnd(start, null, null)),
206 
207         // StandaloneModel has a namespace, too.
208         new StandaloneModel("assert")
209     ));
210 
211     JaxbContextBuilder builder = JaxbContextBuilder
212         .newInstance();
213     Marshaller marshaller = builder.buildMarshaller(list);
214     StringWriter sw = new StringWriter();
215     marshaller.marshal(list, sw);
216 
217     String xml = sw.toString();
218 
219     AnyElementList actual = (AnyElementList) builder
220         .buildUnmarshaller().unmarshal(new StringReader(xml));
221     assertThat(actual)
222         .isEqualTo(list);
223   }
224 
225   /**
226    * Write and read date time.
227    *
228    * @param softly the soft assertions
229    * @throws Exception the exception
230    */
231   @Test
232   void writeAndReadDateTime(SoftAssertions softly) throws Exception {
233 
234     JaxbContextBuilder builder = JaxbContextBuilder
235         .newInstance()
236         .initJaxbContext()
237         .withFormattedOutput(true)
238         .withSchemaMode(SchemaMode.ALWAYS)
239         .addAll(List.of(new JaxbContextData(Address.class)).iterator())
240         .initJaxbContext();
241 
242     softly.assertThat(builder.canMarshal(Address.class))
243         .isTrue();
244     softly.assertThat(builder.canUnmarshal(Address.class))
245         .isTrue();
246 
247     StartEnd startEnd = new StartEnd();
248     startEnd.setStart(OffsetDateTime
249         .parse("2000-01-16T12:00:00Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME));
250     startEnd.setEnd(OffsetDateTime
251         .parse("2000-01-20T12:00:00Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME));
252     startEnd.setDuration(Duration.ofDays(4));
253 
254     Address model = new Address();
255     model.setStreet("Casparstreet");
256     model.setStreetNumber("1234");
257     model.setStartEnd(startEnd);
258 
259     StringWriter sw = new StringWriter();
260     builder.buildMarshaller().marshal(model, sw);
261     String xml = sw.toString();
262 
263     Address actual = (Address) builder.buildUnmarshaller(Address.class, Company.class)
264         .unmarshal(new StringReader(xml));
265     softly.assertThat(actual)
266         .isEqualTo(model);
267   }
268 
269   /**
270    * Write and read date time with empty context.
271    *
272    * @param softly the soft assertions
273    * @throws Exception the exception
274    */
275   @Test
276   void writeAndReadDateTimeWithEmptyContext(SoftAssertions softly) throws Exception {
277 
278     StartEnd startEnd = new StartEnd();
279     startEnd.setStart(OffsetDateTime
280         .parse("2000-01-16T12:00:00Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME));
281     startEnd.setEnd(OffsetDateTime
282         .parse("2000-01-20T12:00:00Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME));
283 
284     Address model = new Address();
285     model.setStreet("Casparstreet");
286     model.setStreetNumber("1234");
287     model.setStartEnd(startEnd);
288 
289     JaxbContextBuilder builder = JaxbContextBuilder
290         .newInstance()
291         .withSchemaMode(SchemaMode.UNMARSHAL);
292 
293     StringWriter sw = new StringWriter();
294     builder.buildMarshaller(new Class[]{Address.class, Company.class}).marshal(model, sw);
295     String xml = sw.toString();
296 
297     Address actual = (Address) builder.buildUnmarshaller(Address.class)
298         .unmarshal(new StringReader(xml));
299     softly.assertThat(actual)
300         .isEqualTo(model);
301 
302     JaxbContextBuilder jaxbContextBuilder = JaxbContextBuilder.newInstance()
303         .withXmlAdapters(null)
304         .withDependenciesResolver(null);
305     sw = new StringWriter();
306     jaxbContextBuilder.buildMarshaller(model).marshal(model, sw);
307     xml = sw.toString();
308 
309     actual = (Address) builder.buildUnmarshaller(Address.class)
310         .unmarshal(new StringReader(xml));
311     softly.assertThat(actual)
312         .isEqualTo(model);
313   }
314 
315   /**
316    * Build jaxb context.
317    *
318    * @param softly the soft assertions
319    * @throws Exception the exception
320    */
321   @Test
322   void buildJaxbContext(SoftAssertions softly) throws Exception {
323     JaxbContextWrapper ctx = JaxbContextBuilder.newInstance().copy()
324         .withFormattedOutput(true)
325         .withSchemaMode(SchemaMode.EXTERNAL_XSD)
326         .withAttachmentMarshaller(mock(AttachmentMarshaller.class))
327         .withAttachmentUnmarshaller(mock(AttachmentUnmarshaller.class))
328         .withValidationEventHandler(mock(ValidationEventHandler.class))
329         .withXmlAdapters(Arrays.asList(
330             new EpochMilliXmlAdapter(),
331             new DateXmlAdapter()))
332         .add(new JaxbContextData(RacingBike.class))
333         .buildJaxbContext();
334 
335     softly.assertThat(ctx)
336         .extracting(JaxbContextWrapper::createJAXBIntrospector)
337         .isNotNull();
338     softly.assertThat(ctx)
339         .extracting(JaxbContextWrapper::getAttachmentMarshaller)
340         .isNotNull();
341     softly.assertThat(ctx)
342         .extracting(JaxbContextWrapper::getAttachmentUnmarshaller)
343         .isNotNull();
344     softly.assertThat(ctx)
345         .extracting(JaxbContextWrapper::getXmlAdapters)
346         .isNotNull();
347     softly.assertThat(ctx)
348         .extracting(JaxbContextWrapper::getSchema)
349         .isNotNull();
350     softly.assertThat(ctx)
351         .extracting(JaxbContextWrapper::getDetails)
352         .isNotNull();
353     softly.assertThat(ctx)
354         .extracting(JaxbContextWrapper::getValidationEventHandler)
355         .isNotNull();
356     softly.assertThat(ctx)
357         .extracting(JaxbContextWrapper::isFormattedOutput, InstanceOfAssertFactories.BOOLEAN)
358         .isTrue();
359     softly.assertThat(ctx)
360         .extracting(JaxbContextWrapper::getSchemaMode)
361         .isEqualTo(SchemaMode.EXTERNAL_XSD);
362 
363     softly.assertThat(ctx.createMarshaller())
364         .isNotNull();
365     softly.assertThat(ctx.createUnmarshaller())
366         .isNotNull();
367   }
368 
369   /**
370    * Build unmarshaller.
371    *
372    * @param softly the soft assertions
373    */
374   @Test
375   void buildUnmarshaller(SoftAssertions softly) {
376     JaxbContextBuilder builder = JaxbContextBuilder.newInstance()
377         .withSchemaMode(SchemaMode.EXTERNAL_XSD);
378     softly.assertThat(builder.buildUnmarshaller(Fender.class)).isNotNull();
379     softly.assertThat(builder.buildUnmarshaller(StandaloneModel.class)).isNotNull();
380   }
381 
382   /**
383    * Build marshaller.
384    *
385    * @param softly the soft assertions
386    */
387   @Test
388   void buildMarshaller(SoftAssertions softly) {
389     JaxbContextBuilder builder = JaxbContextBuilder.newInstance()
390         .withSchemaMode(SchemaMode.EXTERNAL_XSD);
391     softly.assertThat(builder.buildMarshaller(new Fender())).isNotNull();
392     softly.assertThat(builder.buildMarshaller(StandaloneModel.class)).isNotNull();
393     softly.assertThat(builder
394             .copy()
395             .withDependenciesResolver(null)
396             .buildMarshaller(new Class<?>[]{Person.class, Vehicle.class}))
397         .isNotNull();
398   }
399 
400   /**
401    * Build schema.
402    *
403    * @param softly the soft assertions
404    */
405   @Test
406   void buildSchema(SoftAssertions softly) {
407     JaxbContextBuilder builder = JaxbContextBuilder.newInstance()
408         .withSchemaMode(SchemaMode.EXTERNAL_XSD)
409         .add(new JaxbContextData(Person.class));
410     softly.assertThat(builder.buildSchema()).isNotNull();
411     softly.assertThat(builder.buildSchema(new Fender())).isNotNull();
412   }
413 
414 }