001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.schemagen.xml;
007
008import com.ctc.wstx.stax.WstxOutputFactory;
009
010import org.codehaus.stax2.XMLOutputFactory2;
011import org.codehaus.stax2.XMLStreamWriter2;
012import org.eclipse.jdt.annotation.Owning;
013
014import java.io.Writer;
015import java.util.HashMap;
016import java.util.List;
017import java.util.Map;
018
019import javax.xml.namespace.QName;
020import javax.xml.stream.XMLOutputFactory;
021import javax.xml.stream.XMLStreamException;
022
023import dev.metaschema.core.configuration.IConfiguration;
024import dev.metaschema.core.datatype.markup.MarkupMultiline;
025import dev.metaschema.core.model.IAssemblyDefinition;
026import dev.metaschema.core.model.IModule;
027import dev.metaschema.core.qname.IEnhancedQName;
028import dev.metaschema.core.util.AutoCloser;
029import dev.metaschema.core.util.ObjectUtils;
030import dev.metaschema.schemagen.AbstractSchemaGenerator;
031import dev.metaschema.schemagen.SchemaGenerationException;
032import dev.metaschema.schemagen.SchemaGenerationFeature;
033import dev.metaschema.schemagen.xml.impl.IndentingXMLStreamWriter2;
034import dev.metaschema.schemagen.xml.impl.XmlDatatypeManager;
035import dev.metaschema.schemagen.xml.impl.XmlGenerationState;
036import dev.metaschema.schemagen.xml.impl.schematype.IXmlType;
037import edu.umd.cs.findbugs.annotations.NonNull;
038import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
039
040/**
041 * Generates XML Schema (XSD) documents from Metaschema modules.
042 * <p>
043 * This generator produces W3C XML Schema documents that validate XML instances
044 * conforming to the Metaschema module definitions.
045 */
046public class XmlSchemaGenerator
047    extends AbstractSchemaGenerator<
048        AutoCloser<XMLStreamWriter2, SchemaGenerationException>,
049        XmlDatatypeManager,
050        XmlGenerationState> {
051  // private static final Logger LOGGER =
052  // LogManager.getLogger(XmlSchemaGenerator.class);
053
054  /** The namespace prefix for XML Schema elements. */
055  @NonNull
056  public static final String PREFIX_XML_SCHEMA = XmlDatatypeManager.PREFIX_XML_SCHEMA;
057  /** The XML Schema namespace URI. */
058  @NonNull
059  public static final String NS_XML_SCHEMA = XmlDatatypeManager.NS_XML_SCHEMA;
060  @NonNull
061  private static final String PREFIX_XML_SCHEMA_VERSIONING = "vs";
062  @NonNull
063  private static final String NS_XML_SCHEMA_VERSIONING = "http://www.w3.org/2007/XMLSchema-versioning";
064  /** The XHTML namespace URI used for documentation content. */
065  @NonNull
066  public static final String NS_XHTML = XmlDatatypeManager.NS_XHTML;
067
068  @NonNull
069  private final XMLOutputFactory2 xmlOutputFactory;
070
071  /**
072   * Creates and configures a default XML output factory for schema generation.
073   *
074   * @return a configured XML output factory
075   */
076  @NonNull
077  private static XMLOutputFactory2 defaultXMLOutputFactory() {
078    WstxOutputFactory xmlOutputFactory = new WstxOutputFactory();
079    xmlOutputFactory.configureForSpeed();
080    xmlOutputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
081    return xmlOutputFactory;
082  }
083
084  /**
085   * Constructs a new XML schema generator using the default XML output factory.
086   */
087  public XmlSchemaGenerator() {
088    this(defaultXMLOutputFactory());
089  }
090
091  /**
092   * Constructs a new XML schema generator using the specified XML output factory.
093   *
094   * @param xmlOutputFactory
095   *          the XML output factory to use for creating XML writers
096   */
097  @SuppressFBWarnings("EI_EXPOSE_REP2")
098  public XmlSchemaGenerator(@NonNull XMLOutputFactory2 xmlOutputFactory) {
099    this.xmlOutputFactory = xmlOutputFactory;
100  }
101
102  /**
103   * Retrieves the XML output factory used by this generator.
104   *
105   * @return the XML output factory
106   */
107  protected XMLOutputFactory2 getXmlOutputFactory() {
108    return xmlOutputFactory;
109  }
110
111  @Override
112  @Owning
113  protected AutoCloser<XMLStreamWriter2, SchemaGenerationException> newWriter(
114      Writer out) {
115    XMLStreamWriter2 writer;
116    try {
117      XMLStreamWriter2 baseWriter
118          = ObjectUtils.notNull((XMLStreamWriter2) getXmlOutputFactory().createXMLStreamWriter(out));
119      writer = new IndentingXMLStreamWriter2(baseWriter);
120    } catch (XMLStreamException ex) {
121      throw new SchemaGenerationException(ex);
122    }
123    return AutoCloser.autoClose(writer, t -> {
124      try {
125        t.close();
126      } catch (XMLStreamException ex) {
127        throw new SchemaGenerationException(ex);
128      }
129    });
130  }
131
132  @Override
133  protected XmlGenerationState newGenerationState(
134      IModule module,
135      AutoCloser<XMLStreamWriter2, SchemaGenerationException> schemaWriter,
136      IConfiguration<SchemaGenerationFeature<?>> configuration) {
137    return new XmlGenerationState(module, schemaWriter, configuration);
138  }
139
140  @Override
141  protected void generateSchema(XmlGenerationState state) {
142
143    try {
144      String targetNS = state.getDefaultNS();
145
146      // analyze all definitions
147      Map<String, String> prefixToNamespaceMap = new HashMap<>(); // NOPMD concurrency not needed
148      final List<IAssemblyDefinition> rootAssemblyDefinitions = analyzeDefinitions(
149          state,
150          (entry, definition) -> {
151            assert entry != null;
152            assert definition != null;
153            IXmlType type = state.getXmlForDefinition(definition);
154            if (!entry.isInline()) {
155              QName qname = type.getQName();
156              String namespace = qname.getNamespaceURI();
157              if (!targetNS.equals(namespace)) {
158                // collect namespaces and prefixes for definitions with a different namespace
159                prefixToNamespaceMap.computeIfAbsent(qname.getPrefix(), x -> namespace);
160              }
161            }
162          });
163
164      // write some root elements
165      XMLStreamWriter2 writer = state.getXMLStreamWriter();
166      writer.writeStartDocument("UTF-8", "1.0");
167      writer.writeStartElement(PREFIX_XML_SCHEMA, "schema", NS_XML_SCHEMA);
168      writer.writeDefaultNamespace(targetNS);
169      writer.writeNamespace(PREFIX_XML_SCHEMA_VERSIONING, NS_XML_SCHEMA_VERSIONING);
170
171      // write namespaces for all indexed definitions
172      for (Map.Entry<String, String> entry : prefixToNamespaceMap.entrySet()) {
173        state.writeNamespace(entry.getKey(), entry.getValue());
174      }
175
176      IModule module = state.getModule();
177
178      // write remaining root attributes
179      writer.writeAttribute("targetNamespace", targetNS);
180      writer.writeAttribute("elementFormDefault", "qualified");
181      writer.writeAttribute(NS_XML_SCHEMA_VERSIONING, "minVersion", "1.0");
182      writer.writeAttribute(NS_XML_SCHEMA_VERSIONING, "maxVersion", "1.1");
183      writer.writeAttribute("version", module.getVersion());
184
185      generateSchemaMetadata(module, state);
186
187      for (IAssemblyDefinition definition : rootAssemblyDefinitions) {
188        IEnhancedQName xmlQName = definition.getRootQName();
189        if (xmlQName != null
190            && state.getDefaultNS().equals(xmlQName.getNamespace())) {
191          generateRootElement(definition, state);
192        }
193      }
194
195      state.generateXmlTypes();
196
197      writer.writeEndElement(); // xs:schema
198      writer.writeEndDocument();
199      writer.flush();
200    } catch (XMLStreamException ex) {
201      throw new SchemaGenerationException(ex);
202    }
203  }
204
205  /**
206   * Generates the schema metadata annotation containing module information.
207   * <p>
208   * This includes the schema name, version, short name, and optional remarks.
209   *
210   * @param module
211   *          the Metaschema module to extract metadata from
212   * @param state
213   *          the XML generation state for writing output
214   * @throws XMLStreamException
215   *           if an error occurs while writing XML content
216   */
217  protected static void generateSchemaMetadata(
218      @NonNull IModule module,
219      @NonNull XmlGenerationState state)
220      throws XMLStreamException {
221    String targetNS = ObjectUtils.notNull(module.getXmlNamespace().toASCIIString());
222    state.writeStartElement(PREFIX_XML_SCHEMA, "annotation", NS_XML_SCHEMA);
223    state.writeStartElement(PREFIX_XML_SCHEMA, "appinfo", NS_XML_SCHEMA);
224
225    state.writeStartElement(targetNS, "schema-name");
226
227    module.getName().writeXHtml(targetNS, state.getXMLStreamWriter());
228
229    state.writeEndElement();
230
231    state.writeStartElement(targetNS, "schema-version");
232    state.writeCharacters(module.getVersion());
233    state.writeEndElement();
234
235    state.writeStartElement(targetNS, "short-name");
236    state.writeCharacters(module.getShortName());
237    state.writeEndElement();
238
239    state.writeEndElement();
240
241    MarkupMultiline remarks = module.getRemarks();
242    if (remarks != null) {
243      state.writeStartElement(PREFIX_XML_SCHEMA, "documentation", NS_XML_SCHEMA);
244
245      remarks.writeXHtml(targetNS, state.getXMLStreamWriter());
246      state.writeEndElement();
247    }
248
249    state.writeEndElement();
250  }
251
252  private static void generateRootElement(@NonNull IAssemblyDefinition definition, @NonNull XmlGenerationState state)
253      throws XMLStreamException {
254    assert definition.isRoot();
255
256    XMLStreamWriter2 writer = state.getXMLStreamWriter();
257    IEnhancedQName xmlQName = definition.getRootQName();
258
259    writer.writeStartElement(PREFIX_XML_SCHEMA, "element", NS_XML_SCHEMA);
260    writer.writeAttribute("name", xmlQName.getLocalName());
261    writer.writeAttribute("type", state.getXmlForDefinition(definition).getTypeReference());
262
263    writer.writeEndElement();
264  }
265}