001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.databind.io.xml;
007
008import com.ctc.wstx.api.WstxOutputProperties;
009import com.ctc.wstx.stax.WstxOutputFactory;
010
011import org.codehaus.stax2.XMLOutputFactory2;
012import org.codehaus.stax2.XMLStreamWriter2;
013
014import java.io.IOException;
015import java.io.Writer;
016
017import javax.xml.stream.XMLOutputFactory;
018import javax.xml.stream.XMLStreamException;
019
020import dev.metaschema.core.configuration.IMutableConfiguration;
021import dev.metaschema.core.model.IBoundObject;
022import dev.metaschema.core.util.ObjectUtils;
023import dev.metaschema.databind.io.AbstractSerializer;
024import dev.metaschema.databind.io.SerializationFeature;
025import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
026import edu.umd.cs.findbugs.annotations.NonNull;
027import nl.talsmasoftware.lazy4j.Lazy;
028
029/**
030 * Provides support for serializing bound Java objects to XML format based on a
031 * Metaschema module definition.
032 * <p>
033 * This serializer uses StAX's {@link XMLStreamWriter2} to produce XML output
034 * that conforms to the Metaschema-defined data structure.
035 *
036 * @param <CLASS>
037 *          the Java type of the bound object to be serialized
038 */
039public class DefaultXmlSerializer<CLASS extends IBoundObject>
040    extends AbstractSerializer<CLASS> {
041  private Lazy<XMLOutputFactory2> factory;
042
043  /**
044   * Construct a new XML serializer based on the top-level assembly indicated by
045   * the provided {@code classBinding}.
046   *
047   * @param definition
048   *          the bound Module assembly definition that describes the data to
049   *          serialize
050   */
051  public DefaultXmlSerializer(@NonNull IBoundDefinitionModelAssembly definition) {
052    super(definition);
053    resetFactory();
054  }
055
056  /**
057   * Resets the XML output factory to use a freshly created instance.
058   * <p>
059   * This method is called when the serializer configuration changes to ensure the
060   * factory reflects the current settings.
061   */
062  protected final void resetFactory() {
063    this.factory = Lazy.of(this::newFactoryInstance);
064  }
065
066  @Override
067  protected void configurationChanged(IMutableConfiguration<SerializationFeature<?>> config) {
068    super.configurationChanged(config);
069    resetFactory();
070  }
071
072  /**
073   * Get a JSON factory instance.
074   * <p>
075   * This method can be used by sub-classes to create a customized factory
076   * instance.
077   *
078   * @return the factory
079   */
080  @NonNull
081  protected XMLOutputFactory2 newFactoryInstance() {
082    XMLOutputFactory2 retval = (XMLOutputFactory2) XMLOutputFactory.newInstance();
083    assert retval instanceof WstxOutputFactory;
084    retval.configureForSpeed();
085    retval.setProperty(WstxOutputProperties.P_USE_DOUBLE_QUOTES_IN_XML_DECL, true);
086    retval.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
087    return retval;
088  }
089
090  /**
091   * Get the configured XML output factory used to create {@link XMLStreamWriter2}
092   * instances.
093   *
094   * @return the factory
095   */
096  @NonNull
097  protected final XMLOutputFactory2 getXMLOutputFactory() {
098    return ObjectUtils.notNull(factory.get());
099  }
100
101  /**
102   * Create a new stream writer using the provided writer.
103   *
104   * @param writer
105   *          the writer to use for output
106   * @return the stream writer created by the output factory
107   * @throws IOException
108   *           if an error occurred while creating the writer
109   */
110  @NonNull
111  protected final XMLStreamWriter2 newXMLStreamWriter(@NonNull Writer writer) throws IOException {
112    try {
113      return ObjectUtils.notNull((XMLStreamWriter2) getXMLOutputFactory().createXMLStreamWriter(writer));
114    } catch (XMLStreamException ex) {
115      throw new IOException(ex);
116    }
117  }
118
119  @Override
120  public void serialize(IBoundObject data, Writer writer) throws IOException {
121    XMLStreamWriter2 streamWriter = newXMLStreamWriter(writer);
122    IOException caughtException = null;
123    IBoundDefinitionModelAssembly definition = getDefinition();
124
125    MetaschemaXmlWriter xmlGenerator = new MetaschemaXmlWriter(streamWriter);
126
127    boolean serializeRoot = get(SerializationFeature.SERIALIZE_ROOT);
128    try {
129      if (serializeRoot) {
130        streamWriter.writeStartDocument("UTF-8", "1.0");
131        xmlGenerator.writeRoot(definition, data);
132      } else {
133        xmlGenerator.write(definition, data);
134      }
135
136      streamWriter.flush();
137
138      if (serializeRoot) {
139        streamWriter.writeEndDocument();
140      }
141    } catch (XMLStreamException ex) {
142      caughtException = new IOException(ex);
143      throw caughtException;
144    } finally { // NOPMD - exception handling is needed
145      try {
146        streamWriter.close();
147      } catch (XMLStreamException ex) {
148        if (caughtException == null) {
149          throw new IOException(ex);
150        }
151        caughtException.addSuppressed(ex);
152        throw caughtException;
153      }
154    }
155  }
156}