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