529 lines
21 KiB
C#
529 lines
21 KiB
C#
using System.Xml;
|
|
using System.Xml.Schema;
|
|
using System.Text;
|
|
using XSDVisualiser.Models;
|
|
|
|
namespace XSDVisualiser.Core
|
|
{
|
|
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 (var globalEl in _set.Schemas().Cast<XmlSchema>()
|
|
.SelectMany(s => s.Elements.Values.Cast<XmlSchemaElement>()))
|
|
{
|
|
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
|
|
};
|
|
|
|
// Prefer element-level documentation
|
|
node.Documentation = ExtractDocumentation(element);
|
|
|
|
var type = ResolveElementType(element);
|
|
if (type == null) return node;
|
|
node.TypeName = GetQualifiedTypeName(type);
|
|
if (type.Datatype != null)
|
|
{
|
|
node.BuiltInType = type.Datatype.TypeCode.ToString();
|
|
}
|
|
|
|
// Fallback to type-level documentation if none on element
|
|
if (string.IsNullOrWhiteSpace(node.Documentation))
|
|
{
|
|
switch (type)
|
|
{
|
|
case XmlSchemaComplexType ctDoc:
|
|
node.Documentation = ExtractDocumentation(ctDoc);
|
|
break;
|
|
case XmlSchemaSimpleType stDoc:
|
|
node.Documentation = ExtractDocumentation(stDoc);
|
|
break;
|
|
}
|
|
}
|
|
|
|
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 is { QualifiedName.IsEmpty: false }
|
|
? 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;
|
|
}
|
|
|
|
return el.SchemaType;
|
|
}
|
|
|
|
private void HandleComplexType(SchemaNode node, XmlSchemaComplexType ct)
|
|
{
|
|
// Collect attributes (ensure uniqueness)
|
|
var seenAttrKeys = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
// 1) Compiled attribute uses (includes inherited/group attributes after Compile)
|
|
foreach (var attr in ct.AttributeUses.Values.OfType<XmlSchemaAttribute>())
|
|
{
|
|
var qn = attr.QualifiedName.IsEmpty ? attr.RefName : attr.QualifiedName;
|
|
var key = qn.ToString();
|
|
if (seenAttrKeys.Add(key))
|
|
{
|
|
node.Attributes.Add(ExtractAttribute(attr));
|
|
}
|
|
}
|
|
|
|
// 2) Uncompiled attributes directly on the type (fallback)
|
|
foreach (var a in ct.Attributes.OfType<XmlSchemaAttribute>())
|
|
{
|
|
var qn = a.QualifiedName.IsEmpty ? a.RefName : a.QualifiedName;
|
|
var key = qn.ToString();
|
|
if (seenAttrKeys.Add(key))
|
|
{
|
|
node.Attributes.Add(ExtractAttribute(a));
|
|
}
|
|
}
|
|
|
|
// 3) Attributes from complexContent extension/restriction
|
|
if (ct.ContentModel is XmlSchemaComplexContent cc)
|
|
{
|
|
switch (cc.Content)
|
|
{
|
|
case XmlSchemaComplexContentExtension cext:
|
|
{
|
|
foreach (var a in cext.Attributes.OfType<XmlSchemaAttribute>())
|
|
{
|
|
var qn = a.QualifiedName.IsEmpty ? a.RefName : a.QualifiedName;
|
|
var key = qn.ToString();
|
|
if (seenAttrKeys.Add(key))
|
|
{
|
|
node.Attributes.Add(ExtractAttribute(a));
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case XmlSchemaComplexContentRestriction cres:
|
|
{
|
|
foreach (var a in cres.Attributes.OfType<XmlSchemaAttribute>())
|
|
{
|
|
var qn = a.QualifiedName.IsEmpty ? a.RefName : a.QualifiedName;
|
|
var key = qn.ToString();
|
|
if (seenAttrKeys.Add(key))
|
|
{
|
|
node.Attributes.Add(ExtractAttribute(a));
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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";
|
|
switch (simpleContent.Content)
|
|
{
|
|
case 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 (var attr in ext.Attributes.OfType<XmlSchemaAttribute>())
|
|
{
|
|
var qn = attr.QualifiedName.IsEmpty ? attr.RefName : attr.QualifiedName;
|
|
var key = qn.ToString();
|
|
if (seenAttrKeys.Add(key))
|
|
{
|
|
node.Attributes.Add(ExtractAttribute(attr));
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case 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();
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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) return info;
|
|
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)
|
|
};
|
|
|
|
switch (st.Content)
|
|
{
|
|
case XmlSchemaSimpleTypeRestriction restr:
|
|
MergeFacets(cons, restr.Facets);
|
|
break;
|
|
case 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);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case XmlSchemaSimpleTypeUnion union:
|
|
{
|
|
cons.Patterns.Add("(union)");
|
|
foreach (var memberType in union.BaseMemberTypes)
|
|
{
|
|
if (memberType is { } mst)
|
|
{
|
|
var sub = ExtractConstraints(mst);
|
|
Merge(cons, sub);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return cons;
|
|
}
|
|
|
|
private static void Merge(ConstraintSet target, ConstraintSet? source)
|
|
{
|
|
if (source == null) return;
|
|
foreach (var e in source.Enumerations.Where(e => !target.Enumerations.Contains(e))) target.Enumerations.Add(e);
|
|
foreach (var p in source.Patterns.Where(p => !target.Patterns.Contains(p))) target.Patterns.Add(p);
|
|
|
|
// Merge generic facets
|
|
if (source.AllFacets != null)
|
|
{
|
|
foreach (var sf in source.AllFacets)
|
|
{
|
|
var exists = false;
|
|
foreach (var tf in target.AllFacets)
|
|
{
|
|
if (string.Equals(tf.Name, sf.Name, StringComparison.Ordinal) && string.Equals(tf.Value, sf.Value, StringComparison.Ordinal))
|
|
{
|
|
exists = true; break;
|
|
}
|
|
}
|
|
if (!exists) target.AllFacets.Add(new FacetEntry { Name = sf.Name, Value = sf.Value });
|
|
}
|
|
}
|
|
|
|
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) return;
|
|
|
|
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) return;
|
|
|
|
target.Length.MaxLength = source.Length.MaxLength;
|
|
target.Length.MaxLengthSpecified = true;
|
|
}
|
|
|
|
private static void MergeFacets(ConstraintSet cons, XmlSchemaObjectCollection facets)
|
|
{
|
|
foreach (var f in facets)
|
|
{
|
|
// Map known facets to strongly-typed buckets for backward compatibility
|
|
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;
|
|
}
|
|
|
|
// Always capture all facets generically for dynamic display
|
|
if (f is XmlSchemaFacet baseFacet)
|
|
{
|
|
var name = GetFacetName(f);
|
|
var value = baseFacet.Value;
|
|
if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(value))
|
|
{
|
|
var exists = false;
|
|
foreach (var entry in cons.AllFacets)
|
|
{
|
|
if (string.Equals(entry.Name, name, StringComparison.Ordinal) && string.Equals(entry.Value, value, StringComparison.Ordinal))
|
|
{
|
|
exists = true; break;
|
|
}
|
|
}
|
|
if (!exists)
|
|
{
|
|
cons.AllFacets.Add(new FacetEntry { Name = name, Value = value });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string GetFacetName(XmlSchemaObject facet)
|
|
{
|
|
var typeName = facet.GetType().Name; // e.g., XmlSchemaMinInclusiveFacet
|
|
if (typeName.StartsWith("XmlSchema", StringComparison.Ordinal))
|
|
typeName = typeName.Substring("XmlSchema".Length);
|
|
if (typeName.EndsWith("Facet", StringComparison.Ordinal))
|
|
typeName = typeName.Substring(0, typeName.Length - "Facet".Length);
|
|
if (typeName.Length == 0) return typeName;
|
|
return char.ToLowerInvariant(typeName[0]) + typeName.Substring(1);
|
|
}
|
|
private static string? ExtractDocumentation(XmlSchemaAnnotated? annotated)
|
|
{
|
|
if (annotated?.Annotation == null) return null;
|
|
var sb = new StringBuilder();
|
|
foreach (var item in annotated.Annotation.Items)
|
|
{
|
|
if (item is not XmlSchemaDocumentation doc) continue;
|
|
|
|
if (doc.Markup is { Length: > 0 })
|
|
{
|
|
var pieceBuilder = new StringBuilder();
|
|
foreach (var node in doc.Markup)
|
|
{
|
|
try
|
|
{
|
|
var text = node?.InnerText;
|
|
if (!string.IsNullOrWhiteSpace(text))
|
|
{
|
|
pieceBuilder.Append(text);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore malformed nodes
|
|
}
|
|
}
|
|
|
|
var piece = pieceBuilder.ToString().Trim();
|
|
if (string.IsNullOrWhiteSpace(piece)) continue;
|
|
|
|
if (sb.Length > 0) sb.AppendLine().AppendLine();
|
|
sb.Append(piece);
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(doc.Source))
|
|
{
|
|
// If there is a source but no markup, skip; we only render text.
|
|
}
|
|
}
|
|
|
|
return sb.Length == 0 ? null : sb.ToString();
|
|
}
|
|
}
|
|
}
|