using System; using System.Collections.Generic; using System.Linq; using System.Xml; using System.Xml.Schema; using XSDVisualiser.Models; namespace XSDVisualiser.Parsing { public class XsdSchemaParser { private readonly XmlSchemaSet _set = new(); public SchemaModel Parse(string xsdPath) { _set.XmlResolver = new XmlUrlResolver(); using var reader = XmlReader.Create(xsdPath, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }); var schema = XmlSchema.Read(reader, ValidationCallback); _set.Add(schema); _set.CompilationSettings = new XmlSchemaCompilationSettings { EnableUpaCheck = true }; _set.Compile(); var model = new SchemaModel { TargetNamespace = schema.TargetNamespace }; foreach (XmlSchemaElement globalEl in _set.Schemas().Cast() .SelectMany(s => s.Elements.Values.Cast())) { var node = BuildNodeForElement(globalEl, parentContentModel: null); model.RootElements.Add(node); } return model; } private void ValidationCallback(object? sender, ValidationEventArgs e) { // For now, we do not throw; we capture compiled info best-effort. // Console.Error.WriteLine($"[XSD Validation {e.Severity}] {e.Message}"); } private SchemaNode BuildNodeForElement(XmlSchemaElement element, string? parentContentModel) { var node = new SchemaNode { Name = element.Name ?? element.RefName.Name, Namespace = (element.QualifiedName.IsEmpty ? element.RefName : element.QualifiedName).Namespace, IsNillable = element.IsNillable, Cardinality = new Occurs { Min = element.MinOccurs, Max = element.MaxOccurs, MaxIsUnbounded = element.MaxOccursString == "unbounded" }, ContentModel = parentContentModel }; var type = ResolveElementType(element); if (type != null) { node.TypeName = GetQualifiedTypeName(type); if (type.Datatype != null) { node.BuiltInType = type.Datatype.TypeCode.ToString(); } switch (type) { case XmlSchemaComplexType ct: HandleComplexType(node, ct); break; case XmlSchemaSimpleType st: node.ContentModel = "simple"; node.Constraints = ExtractConstraints(st); break; } } return node; } private static string? GetQualifiedTypeName(XmlSchemaType type) { if (!type.QualifiedName.IsEmpty) return type.QualifiedName.ToString(); return type.BaseXmlSchemaType != null && !type.BaseXmlSchemaType.QualifiedName.IsEmpty ? type.BaseXmlSchemaType.QualifiedName.ToString() : type.Name; } private XmlSchemaType? ResolveElementType(XmlSchemaElement el) { if (el.ElementSchemaType != null) return el.ElementSchemaType; if (!el.SchemaTypeName.IsEmpty) { return _set.GlobalTypes[el.SchemaTypeName] as XmlSchemaType; } if (el.SchemaType != null) return el.SchemaType; return null; } private void HandleComplexType(SchemaNode node, XmlSchemaComplexType ct) { // Attributes foreach (XmlSchemaAttribute attr in ct.AttributeUses.Values.OfType()) { node.Attributes.Add(ExtractAttribute(attr)); } // Content model if (ct.ContentTypeParticle is XmlSchemaGroupBase group) { var content = group switch { XmlSchemaSequence => "sequence", XmlSchemaChoice => "choice", XmlSchemaAll => "all", _ => "group" }; node.ContentModel = content; foreach (var item in group.Items) { switch (item) { case XmlSchemaElement childEl: node.Children.Add(BuildNodeForElement(childEl, content)); break; case XmlSchemaGroupBase nestedGroup: // Flatten nested groups by introducing synthetic nodes var synthetic = new SchemaNode { Name = "(group)", Namespace = node.Namespace, ContentModel = nestedGroup switch { XmlSchemaSequence => "sequence", XmlSchemaChoice => "choice", XmlSchemaAll => "all", _ => "group" }, Cardinality = new Occurs { Min = nestedGroup.MinOccurs, Max = nestedGroup.MaxOccurs, MaxIsUnbounded = nestedGroup.MaxOccursString == "unbounded" } }; foreach (var nestedItem in nestedGroup.Items) { if (nestedItem is XmlSchemaElement ngChild) { synthetic.Children.Add(BuildNodeForElement(ngChild, synthetic.ContentModel)); } } node.Children.Add(synthetic); break; // Skip other particles for now } } } else if (ct.ContentType == XmlSchemaContentType.TextOnly && ct.ContentModel is XmlSchemaSimpleContent simpleContent) { node.ContentModel = "simple"; if (simpleContent.Content is XmlSchemaSimpleContentExtension ext) { var baseType = ResolveType(ext.BaseTypeName); if (baseType is XmlSchemaSimpleType st) { node.Constraints = ExtractConstraints(st); node.TypeName ??= GetQualifiedTypeName(st); node.BuiltInType ??= st.Datatype?.TypeCode.ToString(); } foreach (XmlSchemaAttribute attr in ext.Attributes.OfType()) { node.Attributes.Add(ExtractAttribute(attr)); } } else if (simpleContent.Content is XmlSchemaSimpleContentRestriction res) { var baseType = ResolveType(res.BaseTypeName); if (baseType is XmlSchemaSimpleType st) { var cons = ExtractConstraints(st); MergeFacets(cons, res.Facets); node.Constraints = cons; node.TypeName ??= GetQualifiedTypeName(st); node.BuiltInType ??= st.Datatype?.TypeCode.ToString(); } } } } private XmlSchemaType? ResolveType(XmlQualifiedName qname) { if (qname.IsEmpty) return null; return _set.GlobalTypes[qname] as XmlSchemaType; } private AttributeInfo ExtractAttribute(XmlSchemaAttribute attr) { var info = new AttributeInfo { Name = attr.Name ?? attr.RefName.Name, Namespace = (attr.QualifiedName.IsEmpty ? attr.RefName : attr.QualifiedName).Namespace, Use = attr.Use.ToString() }; XmlSchemaSimpleType? st = null; if (attr.AttributeSchemaType != null) st = attr.AttributeSchemaType as XmlSchemaSimpleType; else if (!attr.SchemaTypeName.IsEmpty) st = ResolveType(attr.SchemaTypeName) as XmlSchemaSimpleType; else if (attr.SchemaType != null) st = attr.SchemaType; if (st != null) { info.TypeName = GetQualifiedTypeName(st); info.BuiltInType = st.Datatype?.TypeCode.ToString(); info.Constraints = ExtractConstraints(st); } return info; } private ConstraintSet? ExtractConstraints(XmlSchemaSimpleType st) { var cons = new ConstraintSet { BaseTypeName = GetQualifiedTypeName(st.BaseXmlSchemaType) }; if (st.Content is XmlSchemaSimpleTypeRestriction restr) { MergeFacets(cons, restr.Facets); } else if (st.Content is XmlSchemaSimpleTypeList list) { cons.Patterns.Add("(list)"); if (!list.ItemTypeName.IsEmpty) { var baseType = ResolveType(list.ItemTypeName); if (baseType is XmlSchemaSimpleType itemSt) { var sub = ExtractConstraints(itemSt); Merge(cons, sub); } } } else if (st.Content is XmlSchemaSimpleTypeUnion union) { cons.Patterns.Add("(union)"); foreach (var memberType in union.BaseMemberTypes) { if (memberType is XmlSchemaSimpleType mst) { var sub = ExtractConstraints(mst); Merge(cons, sub); } } } return cons; } private static void Merge(ConstraintSet target, ConstraintSet? source) { if (source == null) return; foreach (var e in source.Enumerations) if (!target.Enumerations.Contains(e)) target.Enumerations.Add(e); foreach (var p in source.Patterns) if (!target.Patterns.Contains(p)) target.Patterns.Add(p); if (source.Numeric != null) { target.Numeric ??= new NumericBounds(); target.Numeric.MinInclusive ??= source.Numeric.MinInclusive; target.Numeric.MaxInclusive ??= source.Numeric.MaxInclusive; target.Numeric.MinExclusive ??= source.Numeric.MinExclusive; target.Numeric.MaxExclusive ??= source.Numeric.MaxExclusive; } if (source.Length != null) { target.Length ??= new LengthBounds(); if (source.Length.LengthSpecified && !target.Length.LengthSpecified) { target.Length.Length = source.Length.Length; target.Length.LengthSpecified = true; } if (source.Length.MinLengthSpecified && !target.Length.MinLengthSpecified) { target.Length.MinLength = source.Length.MinLength; target.Length.MinLengthSpecified = true; } if (source.Length.MaxLengthSpecified && !target.Length.MaxLengthSpecified) { target.Length.MaxLength = source.Length.MaxLength; target.Length.MaxLengthSpecified = true; } } } private static void MergeFacets(ConstraintSet cons, XmlSchemaObjectCollection facets) { foreach (var f in facets) { switch (f) { case XmlSchemaEnumerationFacet enumFacet: cons.Enumerations.Add(enumFacet.Value); break; case XmlSchemaPatternFacet patternFacet: cons.Patterns.Add(patternFacet.Value); break; case XmlSchemaMinInclusiveFacet minInc: cons.Numeric ??= new NumericBounds(); cons.Numeric.MinInclusive = minInc.Value; break; case XmlSchemaMaxInclusiveFacet maxInc: cons.Numeric ??= new NumericBounds(); cons.Numeric.MaxInclusive = maxInc.Value; break; case XmlSchemaMinExclusiveFacet minEx: cons.Numeric ??= new NumericBounds(); cons.Numeric.MinExclusive = minEx.Value; break; case XmlSchemaMaxExclusiveFacet maxEx: cons.Numeric ??= new NumericBounds(); cons.Numeric.MaxExclusive = maxEx.Value; break; case XmlSchemaLengthFacet len: cons.Length ??= new LengthBounds(); if (int.TryParse(len.Value, out var l)) { cons.Length.Length = l; cons.Length.LengthSpecified = true; } break; case XmlSchemaMinLengthFacet minLen: cons.Length ??= new LengthBounds(); if (int.TryParse(minLen.Value, out var ml)) { cons.Length.MinLength = ml; cons.Length.MinLengthSpecified = true; } break; case XmlSchemaMaxLengthFacet maxLen: cons.Length ??= new LengthBounds(); if (int.TryParse(maxLen.Value, out var xl)) { cons.Length.MaxLength = xl; cons.Length.MaxLengthSpecified = true; } break; } } } } }