1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.io.xml;
7   
8   import org.codehaus.stax2.XMLStreamWriter2;
9   
10  import java.io.IOException;
11  
12  import javax.xml.namespace.NamespaceContext;
13  import javax.xml.stream.XMLStreamException;
14  
15  import dev.metaschema.core.model.IBoundObject;
16  import dev.metaschema.core.qname.IEnhancedQName;
17  import dev.metaschema.databind.io.json.DefaultJsonProblemHandler;
18  import dev.metaschema.databind.model.IBoundDefinitionModel;
19  import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
20  import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
21  import dev.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
22  import dev.metaschema.databind.model.IBoundFieldValue;
23  import dev.metaschema.databind.model.IBoundInstanceFlag;
24  import dev.metaschema.databind.model.IBoundInstanceModel;
25  import dev.metaschema.databind.model.IBoundInstanceModelAssembly;
26  import dev.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
27  import dev.metaschema.databind.model.IBoundInstanceModelFieldComplex;
28  import dev.metaschema.databind.model.IBoundInstanceModelFieldScalar;
29  import dev.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
30  import dev.metaschema.databind.model.IBoundInstanceModelGroupedField;
31  import dev.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
32  import dev.metaschema.databind.model.IBoundInstanceModelNamed;
33  import dev.metaschema.databind.model.info.AbstractModelInstanceWriteHandler;
34  import dev.metaschema.databind.model.info.IFeatureComplexItemValueHandler;
35  import dev.metaschema.databind.model.info.IItemWriteHandler;
36  import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo;
37  import edu.umd.cs.findbugs.annotations.NonNull;
38  
39  /**
40   * Provides support for writing Metaschema-bound Java objects to XML format.
41   * <p>
42   * This class implements the {@link IXmlWritingContext} interface to serialize
43   * bound objects to XML using StAX's {@link XMLStreamWriter2}. It handles flags
44   * as attributes and fields/assemblies as child elements according to the
45   * Metaschema XML serialization rules.
46   *
47   * @see IXmlWritingContext
48   * @see XMLStreamWriter2
49   */
50  public class MetaschemaXmlWriter implements IXmlWritingContext {
51    @NonNull
52    private final XMLStreamWriter2 writer;
53  
54    /**
55     * Construct a new Module-aware JSON writer.
56     *
57     * @param writer
58     *          the XML stream writer to write with
59     * @see DefaultJsonProblemHandler
60     */
61    public MetaschemaXmlWriter(
62        @NonNull XMLStreamWriter2 writer) {
63      this.writer = writer;
64    }
65  
66    @Override
67    public XMLStreamWriter2 getWriter() {
68      return writer;
69    }
70  
71    // =====================================
72    // Entry point for top-level-definitions
73    // =====================================
74  
75    @Override
76    public void write(
77        @NonNull IBoundDefinitionModelComplex definition,
78        @NonNull IBoundObject item) throws IOException {
79  
80      IEnhancedQName qname = definition.getQName();
81  
82      definition.writeItem(item, new ItemWriter(qname));
83    }
84  
85    @Override
86    public void writeRoot(
87        @NonNull IBoundDefinitionModelAssembly definition,
88        @NonNull IBoundObject item) throws IOException {
89      IEnhancedQName rootEQName = definition.getRootQName();
90      if (rootEQName == null) {
91        throw new IllegalArgumentException(
92            String.format("The assembly definition '%s' does not have a root QName.",
93                definition.getQName()));
94      }
95  
96      definition.writeItem(item, new ItemWriter(rootEQName));
97    }
98  
99    // ================
100   // Instance writers
101   // ================
102 
103   private <T> void writeModelInstance(
104       @NonNull IBoundInstanceModel<T> instance,
105       @NonNull Object parentItem,
106       @NonNull ItemWriter itemWriter) throws IOException {
107     Object value = instance.getValue(parentItem);
108     if (value == null) {
109       return;
110     }
111 
112     // this if is not strictly needed, since isEmpty will return false on a null
113     // value
114     // checking null here potentially avoids the expensive operation of
115     // instantiating
116     IModelInstanceCollectionInfo<T> collectionInfo = instance.getCollectionInfo();
117     if (!collectionInfo.isEmpty(value)) {
118       IEnhancedQName currentQName = itemWriter.getObjectQName();
119       IEnhancedQName groupAsEQName = instance.getEffectiveXmlGroupAsQName();
120       try {
121         if (groupAsEQName != null) {
122           // write the grouping element
123           writer.writeStartElement(groupAsEQName.getNamespace(), groupAsEQName.getLocalName());
124           currentQName = groupAsEQName;
125         }
126 
127         collectionInfo.writeItems(
128             new ModelInstanceWriteHandler<>(instance, new ItemWriter(currentQName)),
129             value);
130 
131         if (groupAsEQName != null) {
132           writer.writeEndElement();
133         }
134       } catch (XMLStreamException ex) {
135         throw new IOException(ex);
136       }
137     }
138   }
139 
140   private static class ModelInstanceWriteHandler<ITEM>
141       extends AbstractModelInstanceWriteHandler<ITEM> {
142     @NonNull
143     private final ItemWriter itemWriter;
144 
145     public ModelInstanceWriteHandler(
146         @NonNull IBoundInstanceModel<ITEM> instance,
147         @NonNull ItemWriter itemWriter) {
148       super(instance);
149       this.itemWriter = itemWriter;
150     }
151 
152     @Override
153     public void writeItem(ITEM item) throws IOException {
154       IBoundInstanceModel<ITEM> instance = getInstance();
155       instance.writeItem(item, itemWriter);
156     }
157   }
158 
159   private class ItemWriter
160       extends AbstractItemWriter {
161 
162     public ItemWriter(@NonNull IEnhancedQName qname) {
163       super(qname);
164     }
165 
166     private <T extends IBoundInstanceModelNamed<IBoundObject> & IFeatureComplexItemValueHandler> void writeFlags(
167         @NonNull IBoundObject parentItem,
168         @NonNull T instance) throws IOException {
169       writeFlags(parentItem, instance.getDefinition());
170     }
171 
172     private <T extends IBoundInstanceModelGroupedNamed & IFeatureComplexItemValueHandler> void writeFlags(
173         @NonNull IBoundObject parentItem,
174         @NonNull T instance) throws IOException {
175       writeFlags(parentItem, instance.getDefinition());
176     }
177 
178     private void writeFlags(
179         @NonNull IBoundObject parentItem,
180         @NonNull IBoundDefinitionModel<?> definition) throws IOException {
181       for (IBoundInstanceFlag flag : definition.getFlagInstances()) {
182         assert flag != null;
183 
184         Object value = flag.getValue(parentItem);
185         if (value != null) {
186           writeItemFlag(value, flag);
187         }
188       }
189     }
190 
191     private <T extends IBoundInstanceModelAssembly & IFeatureComplexItemValueHandler> void writeAssemblyModel(
192         @NonNull IBoundObject parentItem,
193         @NonNull T instance) throws IOException {
194       writeAssemblyModel(parentItem, instance.getDefinition());
195     }
196 
197     private <T extends IBoundInstanceModelGroupedAssembly & IFeatureComplexItemValueHandler> void writeAssemblyModel(
198         @NonNull IBoundObject parentItem,
199         @NonNull T instance) throws IOException {
200       writeAssemblyModel(parentItem, instance.getDefinition());
201     }
202 
203     private void writeAssemblyModel(
204         @NonNull IBoundObject parentItem,
205         @NonNull IBoundDefinitionModelAssembly definition) throws IOException {
206       for (IBoundInstanceModel<?> modelInstance : definition.getModelInstances()) {
207         assert modelInstance != null;
208         writeModelInstance(modelInstance, parentItem, this);
209       }
210     }
211 
212     private void writeFieldValue(
213         @NonNull IBoundObject parentItem,
214         @NonNull IBoundInstanceModelFieldComplex instance) throws IOException {
215       writeFieldValue(parentItem, instance.getDefinition());
216     }
217 
218     private void writeFieldValue(
219         @NonNull IBoundObject parentItem,
220         @NonNull IBoundInstanceModelGroupedField instance) throws IOException {
221       writeFieldValue(parentItem, instance.getDefinition());
222     }
223 
224     private void writeFieldValue(
225         @NonNull IBoundObject parentItem,
226         @NonNull IBoundDefinitionModelFieldComplex definition) throws IOException {
227       definition.getFieldValue().writeItem(parentItem, this);
228     }
229 
230     private <T extends IFeatureComplexItemValueHandler & IBoundInstanceModelNamed<IBoundObject>> void writeModelObject(
231         @NonNull T instance,
232         @NonNull IBoundObject parentItem,
233         @NonNull ObjectWriter<T> propertyWriter) throws IOException {
234       try {
235         IEnhancedQName wrapperQName = instance.getQName();
236         writer.writeStartElement(wrapperQName.getNamespace(), wrapperQName.getLocalName());
237 
238         propertyWriter.accept(parentItem, instance);
239 
240         writer.writeEndElement();
241       } catch (XMLStreamException ex) {
242         throw new IOException(ex);
243       }
244     }
245 
246     private <T extends IFeatureComplexItemValueHandler & IBoundInstanceModelGroupedNamed> void writeGroupedModelObject(
247         @NonNull T instance,
248         @NonNull IBoundObject parentItem,
249         @NonNull ObjectWriter<T> propertyWriter) throws IOException {
250       try {
251         IEnhancedQName wrapperQName = instance.getQName();
252         writer.writeStartElement(wrapperQName.getNamespace(), wrapperQName.getLocalName());
253 
254         propertyWriter.accept(parentItem, instance);
255 
256         writer.writeEndElement();
257       } catch (XMLStreamException ex) {
258         throw new IOException(ex);
259       }
260     }
261 
262     private <T extends IFeatureComplexItemValueHandler & IBoundDefinitionModelComplex> void writeDefinitionObject(
263         @NonNull T definition,
264         @NonNull IBoundObject parentItem,
265         @NonNull ObjectWriter<T> propertyWriter) throws IOException {
266 
267       try {
268         IEnhancedQName qname = getObjectQName();
269         NamespaceContext nsContext = writer.getNamespaceContext();
270         String prefix = nsContext.getPrefix(qname.getNamespace());
271         if (prefix == null) {
272           prefix = "";
273         }
274 
275         writer.writeStartElement(prefix, qname.getLocalName(), qname.getNamespace());
276 
277         propertyWriter.accept(parentItem, definition);
278 
279         writer.writeEndElement();
280       } catch (XMLStreamException ex) {
281         throw new IOException(ex);
282       }
283     }
284 
285     @Override
286     public void writeItemFlag(Object item, IBoundInstanceFlag instance) throws IOException {
287       String itemString;
288       try {
289         itemString = instance.getJavaTypeAdapter().asString(item);
290       } catch (IllegalArgumentException ex) {
291         throw new IOException(ex);
292       }
293       IEnhancedQName name = instance.getQName();
294       try {
295         if (name.getNamespace().isEmpty()) {
296           writer.writeAttribute(name.getLocalName(), itemString);
297         } else {
298           writer.writeAttribute(name.getNamespace(), name.getLocalName(), itemString);
299         }
300       } catch (XMLStreamException ex) {
301         throw new IOException(ex);
302       }
303     }
304 
305     @Override
306     public void writeItemField(Object item, IBoundInstanceModelFieldScalar instance) throws IOException {
307       try {
308         if (instance.isEffectiveValueWrappedInXml()) {
309           IEnhancedQName wrapperQName = instance.getQName();
310           writer.writeStartElement(wrapperQName.getNamespace(), wrapperQName.getLocalName());
311           instance.getJavaTypeAdapter().writeXmlValue(item, wrapperQName, writer);
312           writer.writeEndElement();
313         } else {
314           instance.getJavaTypeAdapter().writeXmlValue(item, getObjectQName(), writer);
315         }
316       } catch (XMLStreamException ex) {
317         throw new IOException(ex);
318       }
319     }
320 
321     @Override
322     public void writeItemField(IBoundObject item, IBoundInstanceModelFieldComplex instance) throws IOException {
323       ItemWriter itemWriter = new ItemWriter(instance.getQName());
324       writeModelObject(
325           instance,
326           item,
327           ((ObjectWriter<IBoundInstanceModelFieldComplex>) this::writeFlags)
328               .andThen(itemWriter::writeFieldValue));
329     }
330 
331     @Override
332     public void writeItemField(IBoundObject item, IBoundInstanceModelGroupedField instance) throws IOException {
333       ItemWriter itemWriter = new ItemWriter(instance.getQName());
334       writeGroupedModelObject(
335           instance,
336           item,
337           ((ObjectWriter<IBoundInstanceModelGroupedField>) this::writeFlags)
338               .andThen(itemWriter::writeFieldValue));
339     }
340 
341     @Override
342     public void writeItemField(IBoundObject item, IBoundDefinitionModelFieldComplex definition) throws IOException {
343       ItemWriter itemWriter = new ItemWriter(definition.getQName());
344       writeDefinitionObject(
345           definition,
346           item,
347           ((ObjectWriter<IBoundDefinitionModelFieldComplex>) this::writeFlags)
348               .andThen(itemWriter::writeFieldValue));
349     }
350 
351     @Override
352     public void writeItemFieldValue(Object parentItem, IBoundFieldValue fieldValue) throws IOException {
353       Object item = fieldValue.getValue(parentItem);
354       if (item != null) {
355         fieldValue.getJavaTypeAdapter().writeXmlValue(item, getObjectQName(), writer);
356       }
357     }
358 
359     @Override
360     public void writeItemAssembly(IBoundObject item, IBoundInstanceModelAssembly instance) throws IOException {
361       ItemWriter itemWriter = new ItemWriter(instance.getQName());
362       writeModelObject(
363           instance,
364           item,
365           ((ObjectWriter<IBoundInstanceModelAssembly>) this::writeFlags)
366               .andThen(itemWriter::writeAssemblyModel));
367     }
368 
369     @Override
370     public void writeItemAssembly(IBoundObject item, IBoundInstanceModelGroupedAssembly instance) throws IOException {
371       ItemWriter itemWriter = new ItemWriter(instance.getQName());
372       writeGroupedModelObject(
373           instance,
374           item,
375           ((ObjectWriter<IBoundInstanceModelGroupedAssembly>) this::writeFlags)
376               .andThen(itemWriter::writeAssemblyModel));
377     }
378 
379     @Override
380     public void writeItemAssembly(IBoundObject item, IBoundDefinitionModelAssembly definition) throws IOException {
381       // this is a special case where we are writing a top-level, potentially root,
382       // element. Need to take the object qname passed in
383       writeDefinitionObject(
384           definition,
385           item,
386           ((ObjectWriter<IBoundDefinitionModelAssembly>) this::writeFlags)
387               .andThen(this::writeAssemblyModel));
388     }
389 
390     @Override
391     public void writeChoiceGroupItem(IBoundObject item, IBoundInstanceModelChoiceGroup instance) throws IOException {
392       IBoundInstanceModelGroupedNamed actualInstance = instance.getItemInstance(item);
393       assert actualInstance != null;
394       actualInstance.writeItem(item, this);
395     }
396   }
397 
398   private abstract static class AbstractItemWriter implements IItemWriteHandler {
399     @NonNull
400     private final IEnhancedQName objectQName;
401 
402     protected AbstractItemWriter(@NonNull IEnhancedQName qname) {
403       this.objectQName = qname;
404     }
405 
406     /**
407      * Get the qualified name of the item's parent.
408      *
409      * @return the qualified name
410      */
411     @NonNull
412     protected IEnhancedQName getObjectQName() {
413       return objectQName;
414     }
415   }
416 }