449 lines
18 KiB
C#
449 lines
18 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:
|
|
{
|
|
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:
|
|
{
|
|
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;
|
|
|
|
// Merge generic constraints (name + value de-duplication)
|
|
if (source.Constraints != null)
|
|
{
|
|
foreach (var sc in source.Constraints)
|
|
{
|
|
var exists = false;
|
|
foreach (var tc in target.Constraints)
|
|
{
|
|
if (string.Equals(tc.Name, sc.Name, StringComparison.Ordinal) && string.Equals(tc.Value, sc.Value, StringComparison.Ordinal))
|
|
{
|
|
exists = true; break;
|
|
}
|
|
}
|
|
if (!exists)
|
|
{
|
|
target.Constraints.Add(new ConstraintEntry { Name = sc.Name, Value = sc.Value });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void MergeFacets(ConstraintSet cons, XmlSchemaObjectCollection facets)
|
|
{
|
|
foreach (var f in facets)
|
|
{
|
|
// Capture all constraints 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.Constraints)
|
|
{
|
|
if (string.Equals(entry.Name, name, StringComparison.Ordinal) && string.Equals(entry.Value, value, StringComparison.Ordinal))
|
|
{
|
|
exists = true; break;
|
|
}
|
|
}
|
|
if (!exists)
|
|
{
|
|
cons.Constraints.Add(new ConstraintEntry { 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();
|
|
}
|
|
}
|
|
}
|