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}