001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.databind.io.xml;
007
008import org.codehaus.stax2.XMLStreamWriter2;
009import org.w3c.dom.Element;
010
011import java.io.IOException;
012import java.util.List;
013
014import javax.xml.namespace.NamespaceContext;
015import javax.xml.stream.XMLStreamException;
016
017import dev.metaschema.core.model.IAnyContent;
018import dev.metaschema.core.model.IAnyInstance;
019import dev.metaschema.core.model.IBoundObject;
020import dev.metaschema.core.qname.IEnhancedQName;
021import dev.metaschema.databind.io.json.DefaultJsonProblemHandler;
022import dev.metaschema.databind.model.IBoundDefinitionModel;
023import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
024import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
025import dev.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
026import dev.metaschema.databind.model.IBoundFieldValue;
027import dev.metaschema.databind.model.IBoundInstanceFlag;
028import dev.metaschema.databind.model.IBoundInstanceModel;
029import dev.metaschema.databind.model.IBoundInstanceModelAny;
030import dev.metaschema.databind.model.IBoundInstanceModelAssembly;
031import dev.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
032import dev.metaschema.databind.model.IBoundInstanceModelFieldComplex;
033import dev.metaschema.databind.model.IBoundInstanceModelFieldScalar;
034import dev.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
035import dev.metaschema.databind.model.IBoundInstanceModelGroupedField;
036import dev.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
037import dev.metaschema.databind.model.IBoundInstanceModelNamed;
038import dev.metaschema.databind.model.info.AbstractModelInstanceWriteHandler;
039import dev.metaschema.databind.model.info.IFeatureComplexItemValueHandler;
040import dev.metaschema.databind.model.info.IItemWriteHandler;
041import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo;
042import edu.umd.cs.findbugs.annotations.NonNull;
043
044/**
045 * Provides support for writing Metaschema-bound Java objects to XML format.
046 * <p>
047 * This class implements the {@link IXmlWritingContext} interface to serialize
048 * bound objects to XML using StAX's {@link XMLStreamWriter2}. It handles flags
049 * as attributes and fields/assemblies as child elements according to the
050 * Metaschema XML serialization rules.
051 *
052 * @see IXmlWritingContext
053 * @see XMLStreamWriter2
054 */
055public class MetaschemaXmlWriter implements IXmlWritingContext {
056  @NonNull
057  private final XMLStreamWriter2 writer;
058
059  /**
060   * Construct a new Module-aware JSON writer.
061   *
062   * @param writer
063   *          the XML stream writer to write with
064   * @see DefaultJsonProblemHandler
065   */
066  public MetaschemaXmlWriter(
067      @NonNull XMLStreamWriter2 writer) {
068    this.writer = writer;
069  }
070
071  @Override
072  public XMLStreamWriter2 getWriter() {
073    return writer;
074  }
075
076  // =====================================
077  // Entry point for top-level-definitions
078  // =====================================
079
080  @Override
081  public void write(
082      @NonNull IBoundDefinitionModelComplex definition,
083      @NonNull IBoundObject item) throws IOException {
084
085    IEnhancedQName qname = definition.getQName();
086
087    definition.writeItem(item, new ItemWriter(qname));
088  }
089
090  @Override
091  public void writeRoot(
092      @NonNull IBoundDefinitionModelAssembly definition,
093      @NonNull IBoundObject item) throws IOException {
094    IEnhancedQName rootEQName = definition.getRootQName();
095    if (rootEQName == null) {
096      throw new IllegalArgumentException(
097          String.format("The assembly definition '%s' does not have a root QName.",
098              definition.getQName()));
099    }
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}