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}