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;
009
010import java.io.IOException;
011
012import javax.xml.namespace.NamespaceContext;
013import javax.xml.stream.XMLStreamException;
014
015import dev.metaschema.core.model.IBoundObject;
016import dev.metaschema.core.qname.IEnhancedQName;
017import dev.metaschema.databind.io.json.DefaultJsonProblemHandler;
018import dev.metaschema.databind.model.IBoundDefinitionModel;
019import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
020import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
021import dev.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
022import dev.metaschema.databind.model.IBoundFieldValue;
023import dev.metaschema.databind.model.IBoundInstanceFlag;
024import dev.metaschema.databind.model.IBoundInstanceModel;
025import dev.metaschema.databind.model.IBoundInstanceModelAssembly;
026import dev.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
027import dev.metaschema.databind.model.IBoundInstanceModelFieldComplex;
028import dev.metaschema.databind.model.IBoundInstanceModelFieldScalar;
029import dev.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
030import dev.metaschema.databind.model.IBoundInstanceModelGroupedField;
031import dev.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
032import dev.metaschema.databind.model.IBoundInstanceModelNamed;
033import dev.metaschema.databind.model.info.AbstractModelInstanceWriteHandler;
034import dev.metaschema.databind.model.info.IFeatureComplexItemValueHandler;
035import dev.metaschema.databind.model.info.IItemWriteHandler;
036import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo;
037import edu.umd.cs.findbugs.annotations.NonNull;
038
039/**
040 * Provides support for writing Metaschema-bound Java objects to XML format.
041 * <p>
042 * This class implements the {@link IXmlWritingContext} interface to serialize
043 * bound objects to XML using StAX's {@link XMLStreamWriter2}. It handles flags
044 * as attributes and fields/assemblies as child elements according to the
045 * Metaschema XML serialization rules.
046 *
047 * @see IXmlWritingContext
048 * @see XMLStreamWriter2
049 */
050public class MetaschemaXmlWriter implements IXmlWritingContext {
051  @NonNull
052  private final XMLStreamWriter2 writer;
053
054  /**
055   * Construct a new Module-aware JSON writer.
056   *
057   * @param writer
058   *          the XML stream writer to write with
059   * @see DefaultJsonProblemHandler
060   */
061  public MetaschemaXmlWriter(
062      @NonNull XMLStreamWriter2 writer) {
063    this.writer = writer;
064  }
065
066  @Override
067  public XMLStreamWriter2 getWriter() {
068    return writer;
069  }
070
071  // =====================================
072  // Entry point for top-level-definitions
073  // =====================================
074
075  @Override
076  public void write(
077      @NonNull IBoundDefinitionModelComplex definition,
078      @NonNull IBoundObject item) throws IOException {
079
080    IEnhancedQName qname = definition.getQName();
081
082    definition.writeItem(item, new ItemWriter(qname));
083  }
084
085  @Override
086  public void writeRoot(
087      @NonNull IBoundDefinitionModelAssembly definition,
088      @NonNull IBoundObject item) throws IOException {
089    IEnhancedQName rootEQName = definition.getRootQName();
090    if (rootEQName == null) {
091      throw new IllegalArgumentException(
092          String.format("The assembly definition '%s' does not have a root QName.",
093              definition.getQName()));
094    }
095
096    definition.writeItem(item, new ItemWriter(rootEQName));
097  }
098
099  // ================
100  // Instance writers
101  // ================
102
103  private <T> void writeModelInstance(
104      @NonNull IBoundInstanceModel<T> instance,
105      @NonNull Object parentItem,
106      @NonNull ItemWriter itemWriter) throws IOException {
107    Object value = instance.getValue(parentItem);
108    if (value == null) {
109      return;
110    }
111
112    // this if is not strictly needed, since isEmpty will return false on a null
113    // value
114    // checking null here potentially avoids the expensive operation of
115    // instantiating
116    IModelInstanceCollectionInfo<T> collectionInfo = instance.getCollectionInfo();
117    if (!collectionInfo.isEmpty(value)) {
118      IEnhancedQName currentQName = itemWriter.getObjectQName();
119      IEnhancedQName groupAsEQName = instance.getEffectiveXmlGroupAsQName();
120      try {
121        if (groupAsEQName != null) {
122          // write the grouping element
123          writer.writeStartElement(groupAsEQName.getNamespace(), groupAsEQName.getLocalName());
124          currentQName = groupAsEQName;
125        }
126
127        collectionInfo.writeItems(
128            new ModelInstanceWriteHandler<>(instance, new ItemWriter(currentQName)),
129            value);
130
131        if (groupAsEQName != null) {
132          writer.writeEndElement();
133        }
134      } catch (XMLStreamException ex) {
135        throw new IOException(ex);
136      }
137    }
138  }
139
140  private static class ModelInstanceWriteHandler<ITEM>
141      extends AbstractModelInstanceWriteHandler<ITEM> {
142    @NonNull
143    private final ItemWriter itemWriter;
144
145    public ModelInstanceWriteHandler(
146        @NonNull IBoundInstanceModel<ITEM> instance,
147        @NonNull ItemWriter itemWriter) {
148      super(instance);
149      this.itemWriter = itemWriter;
150    }
151
152    @Override
153    public void writeItem(ITEM item) throws IOException {
154      IBoundInstanceModel<ITEM> instance = getInstance();
155      instance.writeItem(item, itemWriter);
156    }
157  }
158
159  private class ItemWriter
160      extends AbstractItemWriter {
161
162    public ItemWriter(@NonNull IEnhancedQName qname) {
163      super(qname);
164    }
165
166    private <T extends IBoundInstanceModelNamed<IBoundObject> & IFeatureComplexItemValueHandler> void writeFlags(
167        @NonNull IBoundObject parentItem,
168        @NonNull T instance) throws IOException {
169      writeFlags(parentItem, instance.getDefinition());
170    }
171
172    private <T extends IBoundInstanceModelGroupedNamed & IFeatureComplexItemValueHandler> void writeFlags(
173        @NonNull IBoundObject parentItem,
174        @NonNull T instance) throws IOException {
175      writeFlags(parentItem, instance.getDefinition());
176    }
177
178    private void writeFlags(
179        @NonNull IBoundObject parentItem,
180        @NonNull IBoundDefinitionModel<?> definition) throws IOException {
181      for (IBoundInstanceFlag flag : definition.getFlagInstances()) {
182        assert flag != null;
183
184        Object value = flag.getValue(parentItem);
185        if (value != null) {
186          writeItemFlag(value, flag);
187        }
188      }
189    }
190
191    private <T extends IBoundInstanceModelAssembly & IFeatureComplexItemValueHandler> void writeAssemblyModel(
192        @NonNull IBoundObject parentItem,
193        @NonNull T instance) throws IOException {
194      writeAssemblyModel(parentItem, instance.getDefinition());
195    }
196
197    private <T extends IBoundInstanceModelGroupedAssembly & IFeatureComplexItemValueHandler> void writeAssemblyModel(
198        @NonNull IBoundObject parentItem,
199        @NonNull T instance) throws IOException {
200      writeAssemblyModel(parentItem, instance.getDefinition());
201    }
202
203    private void writeAssemblyModel(
204        @NonNull IBoundObject parentItem,
205        @NonNull IBoundDefinitionModelAssembly definition) throws IOException {
206      for (IBoundInstanceModel<?> modelInstance : definition.getModelInstances()) {
207        assert modelInstance != null;
208        writeModelInstance(modelInstance, parentItem, this);
209      }
210    }
211
212    private void writeFieldValue(
213        @NonNull IBoundObject parentItem,
214        @NonNull IBoundInstanceModelFieldComplex instance) throws IOException {
215      writeFieldValue(parentItem, instance.getDefinition());
216    }
217
218    private void writeFieldValue(
219        @NonNull IBoundObject parentItem,
220        @NonNull IBoundInstanceModelGroupedField instance) throws IOException {
221      writeFieldValue(parentItem, instance.getDefinition());
222    }
223
224    private void writeFieldValue(
225        @NonNull IBoundObject parentItem,
226        @NonNull IBoundDefinitionModelFieldComplex definition) throws IOException {
227      definition.getFieldValue().writeItem(parentItem, this);
228    }
229
230    private <T extends IFeatureComplexItemValueHandler & IBoundInstanceModelNamed<IBoundObject>> void writeModelObject(
231        @NonNull T instance,
232        @NonNull IBoundObject parentItem,
233        @NonNull ObjectWriter<T> propertyWriter) throws IOException {
234      try {
235        IEnhancedQName wrapperQName = instance.getQName();
236        writer.writeStartElement(wrapperQName.getNamespace(), wrapperQName.getLocalName());
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 & IBoundInstanceModelGroupedNamed> void writeGroupedModelObject(
247        @NonNull T instance,
248        @NonNull IBoundObject parentItem,
249        @NonNull ObjectWriter<T> propertyWriter) throws IOException {
250      try {
251        IEnhancedQName wrapperQName = instance.getQName();
252        writer.writeStartElement(wrapperQName.getNamespace(), wrapperQName.getLocalName());
253
254        propertyWriter.accept(parentItem, instance);
255
256        writer.writeEndElement();
257      } catch (XMLStreamException ex) {
258        throw new IOException(ex);
259      }
260    }
261
262    private <T extends IFeatureComplexItemValueHandler & IBoundDefinitionModelComplex> void writeDefinitionObject(
263        @NonNull T definition,
264        @NonNull IBoundObject parentItem,
265        @NonNull ObjectWriter<T> propertyWriter) throws IOException {
266
267      try {
268        IEnhancedQName qname = getObjectQName();
269        NamespaceContext nsContext = writer.getNamespaceContext();
270        String prefix = nsContext.getPrefix(qname.getNamespace());
271        if (prefix == null) {
272          prefix = "";
273        }
274
275        writer.writeStartElement(prefix, qname.getLocalName(), qname.getNamespace());
276
277        propertyWriter.accept(parentItem, definition);
278
279        writer.writeEndElement();
280      } catch (XMLStreamException ex) {
281        throw new IOException(ex);
282      }
283    }
284
285    @Override
286    public void writeItemFlag(Object item, IBoundInstanceFlag instance) throws IOException {
287      String itemString;
288      try {
289        itemString = instance.getJavaTypeAdapter().asString(item);
290      } catch (IllegalArgumentException ex) {
291        throw new IOException(ex);
292      }
293      IEnhancedQName name = instance.getQName();
294      try {
295        if (name.getNamespace().isEmpty()) {
296          writer.writeAttribute(name.getLocalName(), itemString);
297        } else {
298          writer.writeAttribute(name.getNamespace(), name.getLocalName(), itemString);
299        }
300      } catch (XMLStreamException ex) {
301        throw new IOException(ex);
302      }
303    }
304
305    @Override
306    public void writeItemField(Object item, IBoundInstanceModelFieldScalar instance) throws IOException {
307      try {
308        if (instance.isEffectiveValueWrappedInXml()) {
309          IEnhancedQName wrapperQName = instance.getQName();
310          writer.writeStartElement(wrapperQName.getNamespace(), wrapperQName.getLocalName());
311          instance.getJavaTypeAdapter().writeXmlValue(item, wrapperQName, writer);
312          writer.writeEndElement();
313        } else {
314          instance.getJavaTypeAdapter().writeXmlValue(item, getObjectQName(), writer);
315        }
316      } catch (XMLStreamException ex) {
317        throw new IOException(ex);
318      }
319    }
320
321    @Override
322    public void writeItemField(IBoundObject item, IBoundInstanceModelFieldComplex instance) throws IOException {
323      ItemWriter itemWriter = new ItemWriter(instance.getQName());
324      writeModelObject(
325          instance,
326          item,
327          ((ObjectWriter<IBoundInstanceModelFieldComplex>) this::writeFlags)
328              .andThen(itemWriter::writeFieldValue));
329    }
330
331    @Override
332    public void writeItemField(IBoundObject item, IBoundInstanceModelGroupedField instance) throws IOException {
333      ItemWriter itemWriter = new ItemWriter(instance.getQName());
334      writeGroupedModelObject(
335          instance,
336          item,
337          ((ObjectWriter<IBoundInstanceModelGroupedField>) this::writeFlags)
338              .andThen(itemWriter::writeFieldValue));
339    }
340
341    @Override
342    public void writeItemField(IBoundObject item, IBoundDefinitionModelFieldComplex definition) throws IOException {
343      ItemWriter itemWriter = new ItemWriter(definition.getQName());
344      writeDefinitionObject(
345          definition,
346          item,
347          ((ObjectWriter<IBoundDefinitionModelFieldComplex>) this::writeFlags)
348              .andThen(itemWriter::writeFieldValue));
349    }
350
351    @Override
352    public void writeItemFieldValue(Object parentItem, IBoundFieldValue fieldValue) throws IOException {
353      Object item = fieldValue.getValue(parentItem);
354      if (item != null) {
355        fieldValue.getJavaTypeAdapter().writeXmlValue(item, getObjectQName(), writer);
356      }
357    }
358
359    @Override
360    public void writeItemAssembly(IBoundObject item, IBoundInstanceModelAssembly instance) throws IOException {
361      ItemWriter itemWriter = new ItemWriter(instance.getQName());
362      writeModelObject(
363          instance,
364          item,
365          ((ObjectWriter<IBoundInstanceModelAssembly>) this::writeFlags)
366              .andThen(itemWriter::writeAssemblyModel));
367    }
368
369    @Override
370    public void writeItemAssembly(IBoundObject item, IBoundInstanceModelGroupedAssembly instance) throws IOException {
371      ItemWriter itemWriter = new ItemWriter(instance.getQName());
372      writeGroupedModelObject(
373          instance,
374          item,
375          ((ObjectWriter<IBoundInstanceModelGroupedAssembly>) this::writeFlags)
376              .andThen(itemWriter::writeAssemblyModel));
377    }
378
379    @Override
380    public void writeItemAssembly(IBoundObject item, IBoundDefinitionModelAssembly definition) throws IOException {
381      // this is a special case where we are writing a top-level, potentially root,
382      // element. Need to take the object qname passed in
383      writeDefinitionObject(
384          definition,
385          item,
386          ((ObjectWriter<IBoundDefinitionModelAssembly>) this::writeFlags)
387              .andThen(this::writeAssemblyModel));
388    }
389
390    @Override
391    public void writeChoiceGroupItem(IBoundObject item, IBoundInstanceModelChoiceGroup instance) throws IOException {
392      IBoundInstanceModelGroupedNamed actualInstance = instance.getItemInstance(item);
393      assert actualInstance != null;
394      actualInstance.writeItem(item, this);
395    }
396  }
397
398  private abstract static class AbstractItemWriter implements IItemWriteHandler {
399    @NonNull
400    private final IEnhancedQName objectQName;
401
402    protected AbstractItemWriter(@NonNull IEnhancedQName qname) {
403      this.objectQName = qname;
404    }
405
406    /**
407     * Get the qualified name of the item's parent.
408     *
409     * @return the qualified name
410     */
411    @NonNull
412    protected IEnhancedQName getObjectQName() {
413      return objectQName;
414    }
415  }
416}