XSDVisualizer/XSDVisualiser/Utils/XmlValidation.cs

214 lines
8.4 KiB
C#

using System.Xml;
using System.Xml.Schema;
namespace XSDVisualiser.Core;
/// <summary>
/// Validates an XML document against a compiled XSD schema set and a specific global element (node).
/// </summary>
public static class XmlValidator
{
public static XmlValidationResult ValidateAgainstElement(string xsdPath, string elementName, string? elementNamespace, string xmlPath)
{
var set = BuildSchemaSet(xsdPath);
return ValidateAgainstElement(set, elementName, elementNamespace, xmlPath);
}
public static XmlValidationResult ValidateAgainstElement(XmlSchemaSet schemas, string elementName, string? elementNamespace, string xmlPath)
{
var result = new XmlValidationResult();
// Ensure the requested element exists in the schema set
var qname = new XmlQualifiedName(elementName, elementNamespace ?? string.Empty);
if (schemas.GlobalElements[qname] is not XmlSchemaElement)
{
result.AddError($"Element '{qname}' was not found in the compiled schema set.");
return result;
}
// Probe XML root element
(string localName, string nsUri)? rootInfo = TryReadRoot(xmlPath);
if (rootInfo is null)
{
result.AddError("Unable to read XML root element.");
return result;
}
var (rootLocal, rootNs) = rootInfo.Value;
var matchesRoot = string.Equals(rootLocal, elementName, StringComparison.Ordinal) && string.Equals(rootNs ?? string.Empty, elementNamespace ?? string.Empty, StringComparison.Ordinal);
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Ignore,
ValidationType = ValidationType.Schema,
Schemas = schemas,
CloseInput = true,
ConformanceLevel = ConformanceLevel.Auto
};
settings.ValidationFlags = XmlSchemaValidationFlags.ReportValidationWarnings | XmlSchemaValidationFlags.ProcessIdentityConstraints;
void Handler(object? sender, ValidationEventArgs e)
{
if (e.Severity == XmlSeverityType.Warning)
result.AddWarning(e.Message, e.Exception?.LineNumber, e.Exception?.LinePosition);
else
result.AddError(e.Message, e.Exception?.LineNumber, e.Exception?.LinePosition);
}
settings.ValidationEventHandler += Handler;
if (matchesRoot)
{
using var reader = XmlReader.Create(xmlPath, settings);
try
{
while (reader.Read())
{
// just advance to trigger validation callbacks
}
}
catch (XmlException xe)
{
result.AddError($"XML parsing error: {xe.Message}", xe.LineNumber, xe.LinePosition);
}
return result;
}
else
{
// Root does not match the selected schema element. Try to locate the first matching subtree and validate only that fragment.
// This enables validating an XML file towards a selected node from the XSD.
var fragmentSettings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Ignore,
ValidationType = ValidationType.Schema,
Schemas = schemas,
CloseInput = true,
ConformanceLevel = ConformanceLevel.Fragment
};
fragmentSettings.ValidationFlags = settings.ValidationFlags;
fragmentSettings.ValidationEventHandler += Handler;
try
{
var (elementNode, loadError) = FindFirstElementNode(xmlPath, elementName, elementNamespace);
if (loadError is not null)
{
result.AddError(loadError.Value.Message, loadError.Value.LineNumber, loadError.Value.LinePosition);
return result;
}
if (elementNode is null)
{
result.AddError($"Could not find any element '{{{elementNamespace}}}{elementName}' in the XML document to validate against.");
return result;
}
// Inform as a warning that we validate a subtree instead of the document root
result.AddWarning($"Validating against the first occurrence of '{{{elementNamespace}}}{elementName}' found in the document (root is '{{{rootNs}}}{rootLocal}').");
using var nodeReader = new XmlNodeReader(elementNode);
using var validatingReader = XmlReader.Create(nodeReader, fragmentSettings);
while (validatingReader.Read())
{
// advance to trigger validation callbacks for the subtree
}
}
catch (XmlException xe)
{
result.AddError($"XML parsing error: {xe.Message}", xe.LineNumber, xe.LinePosition);
}
return result;
}
}
private static XmlSchemaSet BuildSchemaSet(string xsdPath)
{
var set = new XmlSchemaSet
{
XmlResolver = new XmlUrlResolver(),
CompilationSettings = new XmlSchemaCompilationSettings { EnableUpaCheck = true }
};
using var reader = XmlReader.Create(xsdPath, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore });
var schema = XmlSchema.Read(reader, null);
if (schema != null)
set.Add(schema);
set.Compile();
return set;
}
private static (string localName, string nsUri)? TryReadRoot(string xmlPath)
{
using var reader = XmlReader.Create(xmlPath, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, IgnoreWhitespace = true, IgnoreComments = true });
try
{
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Depth == 0)
{
return (reader.LocalName, reader.NamespaceURI);
}
}
}
catch
{
// ignored; higher level will report XmlException separately
}
return null;
}
private static (XmlElement? Node, (string Message, int LineNumber, int LinePosition)? LoadError) FindFirstElementNode(string xmlPath, string elementName, string? elementNamespace)
{
try
{
var xr = XmlReader.Create(xmlPath, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore });
var doc = new XmlDocument();
doc.PreserveWhitespace = false;
doc.Load(xr);
static XmlElement? Traverse(XmlNode node, string name, string? ns)
{
if (node is XmlElement el)
{
if (string.Equals(el.LocalName, name, StringComparison.Ordinal) && string.Equals(el.NamespaceURI ?? string.Empty, ns ?? string.Empty, StringComparison.Ordinal))
return el;
}
foreach (XmlNode child in node.ChildNodes)
{
var found = Traverse(child, name, ns);
if (found != null) return found;
}
return null;
}
var match = Traverse(doc, elementName, elementNamespace);
return (match, null);
}
catch (XmlException xe)
{
return (null, ($"XML parsing error: {xe.Message}", xe.LineNumber, xe.LinePosition));
}
}
}
public sealed class XmlValidationResult
{
private readonly List<XmlValidationIssue> _issues = new();
public bool IsValid => _issues.TrueForAll(i => i.Severity != XmlSeverityType.Error);
public IReadOnlyList<XmlValidationIssue> Issues => _issues;
public IEnumerable<XmlValidationIssue> Errors => _issues.Where(i => i.Severity == XmlSeverityType.Error);
public IEnumerable<XmlValidationIssue> Warnings => _issues.Where(i => i.Severity == XmlSeverityType.Warning);
internal void AddError(string message, int? line = null, int? position = null) =>
_issues.Add(new XmlValidationIssue(XmlSeverityType.Error, message, line ?? 0, position ?? 0));
internal void AddWarning(string message, int? line = null, int? position = null) =>
_issues.Add(new XmlValidationIssue(XmlSeverityType.Warning, message, line ?? 0, position ?? 0));
}
public sealed record XmlValidationIssue(XmlSeverityType Severity, string Message, int LineNumber, int LinePosition);