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