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