1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.schemagen.xml;
7   
8   import com.ctc.wstx.stax.WstxOutputFactory;
9   
10  import org.codehaus.stax2.XMLOutputFactory2;
11  import org.codehaus.stax2.XMLStreamWriter2;
12  import org.eclipse.jdt.annotation.Owning;
13  
14  import java.io.Writer;
15  import java.util.HashMap;
16  import java.util.List;
17  import java.util.Map;
18  
19  import javax.xml.namespace.QName;
20  import javax.xml.stream.XMLOutputFactory;
21  import javax.xml.stream.XMLStreamException;
22  
23  import dev.metaschema.core.configuration.IConfiguration;
24  import dev.metaschema.core.datatype.markup.MarkupMultiline;
25  import dev.metaschema.core.model.IAssemblyDefinition;
26  import dev.metaschema.core.model.IModule;
27  import dev.metaschema.core.qname.IEnhancedQName;
28  import dev.metaschema.core.util.AutoCloser;
29  import dev.metaschema.core.util.ObjectUtils;
30  import dev.metaschema.schemagen.AbstractSchemaGenerator;
31  import dev.metaschema.schemagen.SchemaGenerationException;
32  import dev.metaschema.schemagen.SchemaGenerationFeature;
33  import dev.metaschema.schemagen.xml.impl.IndentingXMLStreamWriter2;
34  import dev.metaschema.schemagen.xml.impl.XmlDatatypeManager;
35  import dev.metaschema.schemagen.xml.impl.XmlGenerationState;
36  import dev.metaschema.schemagen.xml.impl.schematype.IXmlType;
37  import edu.umd.cs.findbugs.annotations.NonNull;
38  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
39  
40  /**
41   * Generates XML Schema (XSD) documents from Metaschema modules.
42   * <p>
43   * This generator produces W3C XML Schema documents that validate XML instances
44   * conforming to the Metaschema module definitions.
45   */
46  public class XmlSchemaGenerator
47      extends AbstractSchemaGenerator<
48          AutoCloser<XMLStreamWriter2, SchemaGenerationException>,
49          XmlDatatypeManager,
50          XmlGenerationState> {
51    // private static final Logger LOGGER =
52    // LogManager.getLogger(XmlSchemaGenerator.class);
53  
54    /** The namespace prefix for XML Schema elements. */
55    @NonNull
56    public static final String PREFIX_XML_SCHEMA = XmlDatatypeManager.PREFIX_XML_SCHEMA;
57    /** The XML Schema namespace URI. */
58    @NonNull
59    public static final String NS_XML_SCHEMA = XmlDatatypeManager.NS_XML_SCHEMA;
60    @NonNull
61    private static final String PREFIX_XML_SCHEMA_VERSIONING = "vs";
62    @NonNull
63    private static final String NS_XML_SCHEMA_VERSIONING = "http://www.w3.org/2007/XMLSchema-versioning";
64    /** The XHTML namespace URI used for documentation content. */
65    @NonNull
66    public static final String NS_XHTML = XmlDatatypeManager.NS_XHTML;
67  
68    @NonNull
69    private final XMLOutputFactory2 xmlOutputFactory;
70  
71    /**
72     * Creates and configures a default XML output factory for schema generation.
73     *
74     * @return a configured XML output factory
75     */
76    @NonNull
77    private static XMLOutputFactory2 defaultXMLOutputFactory() {
78      WstxOutputFactory xmlOutputFactory = new WstxOutputFactory();
79      xmlOutputFactory.configureForSpeed();
80      xmlOutputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
81      return xmlOutputFactory;
82    }
83  
84    /**
85     * Constructs a new XML schema generator using the default XML output factory.
86     */
87    public XmlSchemaGenerator() {
88      this(defaultXMLOutputFactory());
89    }
90  
91    /**
92     * Constructs a new XML schema generator using the specified XML output factory.
93     *
94     * @param xmlOutputFactory
95     *          the XML output factory to use for creating XML writers
96     */
97    @SuppressFBWarnings("EI_EXPOSE_REP2")
98    public XmlSchemaGenerator(@NonNull XMLOutputFactory2 xmlOutputFactory) {
99      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 }