001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.databind.io.xml;
007
008import gov.nist.secauto.metaschema.core.model.IBoundObject;
009import gov.nist.secauto.metaschema.core.util.ObjectUtils;
010import gov.nist.secauto.metaschema.databind.io.json.DefaultJsonProblemHandler;
011import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModel;
012import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
013import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
014import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
015import gov.nist.secauto.metaschema.databind.model.IBoundFieldValue;
016import gov.nist.secauto.metaschema.databind.model.IBoundInstanceFlag;
017import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModel;
018import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelAssembly;
019import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
020import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldComplex;
021import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldScalar;
022import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
023import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedField;
024import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
025import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelNamed;
026import gov.nist.secauto.metaschema.databind.model.info.AbstractModelInstanceWriteHandler;
027import gov.nist.secauto.metaschema.databind.model.info.IFeatureComplexItemValueHandler;
028import gov.nist.secauto.metaschema.databind.model.info.IItemWriteHandler;
029import gov.nist.secauto.metaschema.databind.model.info.IModelInstanceCollectionInfo;
030
031import org.codehaus.stax2.XMLStreamWriter2;
032
033import java.io.IOException;
034
035import javax.xml.namespace.NamespaceContext;
036import javax.xml.namespace.QName;
037import javax.xml.stream.XMLStreamException;
038
039import edu.umd.cs.findbugs.annotations.NonNull;
040
041public class MetaschemaXmlWriter implements IXmlWritingContext {
042  @NonNull
043  private final XMLStreamWriter2 writer;
044
045  /**
046   * Construct a new Module-aware JSON writer.
047   *
048   * @param writer
049   *          the XML stream writer to write with
050   * @see DefaultJsonProblemHandler
051   */
052  public MetaschemaXmlWriter(
053      @NonNull XMLStreamWriter2 writer) {
054    this.writer = writer;
055  }
056
057  @Override
058  public XMLStreamWriter2 getWriter() {
059    return writer;
060  }
061
062  // =====================================
063  // Entry point for top-level-definitions
064  // =====================================
065
066  @Override
067  public void write(
068      @NonNull IBoundDefinitionModelComplex definition,
069      @NonNull IBoundObject item) throws IOException {
070
071    QName qname = definition.getXmlQName();
072
073    definition.writeItem(item, new ItemWriter(qname));
074  }
075
076  @Override
077  public void writeRoot(
078      @NonNull IBoundDefinitionModelAssembly definition,
079      @NonNull IBoundObject item) throws IOException {
080    definition.writeItem(item, new ItemWriter(ObjectUtils.requireNonNull(definition.getRootXmlQName())));
081  }
082
083  // ================
084  // Instance writers
085  // ================
086
087  private <T> void writeModelInstance(
088      @NonNull IBoundInstanceModel<T> instance,
089      @NonNull Object parentItem,
090      @NonNull ItemWriter itemWriter) throws IOException {
091    Object value = instance.getValue(parentItem);
092    if (value == null) {
093      return;
094    }
095
096    // this if is not strictly needed, since isEmpty will return false on a null
097    // value
098    // checking null here potentially avoids the expensive operation of
099    // 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}