179 lines
8.9 KiB
C#
179 lines
8.9 KiB
C#
using System.Globalization;
|
|
using System.Xml.Linq;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect;
|
|
|
|
/// <summary>
|
|
/// The element/attribute reading rules shared by <see cref="MTConnectProbeParser"/> and
|
|
/// <see cref="MTConnectStreamsParser"/>. Both documents are the same dialect of XML and must be
|
|
/// read by the same rules; keeping one copy is what stops the two parsers drifting apart on the
|
|
/// two rules below, either of which fails silently rather than loudly.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Rule 1: elements are matched on <see cref="XName.LocalName"/>, ignoring the
|
|
/// namespace.</b> The namespace URI carries the MTConnect version
|
|
/// (<c>urn:mtconnect.org:MTConnectDevices:1.3</c> … <c>:2.0</c>), so a fully-qualified match
|
|
/// would hard-code a version. It also silently drops vendor-extension elements declared in
|
|
/// their own namespace, whose standard children still inherit the default MTConnect
|
|
/// namespace — the browse tree simply comes back short, with no error anywhere.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Rule 2: attributes are read <i>unqualified</i>.</b> A prefixed attribute
|
|
/// (<c>xsi:type</c> on a document root, a vendor's <c>x:dataItemId</c>) must never be
|
|
/// mistaken for the real <c>type</c> / <c>dataItemId</c>, which would invent a data item the
|
|
/// probe never declared.
|
|
/// </para>
|
|
/// </remarks>
|
|
internal static class MTConnectXml
|
|
{
|
|
/// <summary>Does <paramref name="element"/> have the given local name, whatever its namespace?</summary>
|
|
/// <param name="element">The element to test.</param>
|
|
/// <param name="localName">The unqualified name to match.</param>
|
|
public static bool IsNamed(XElement element, string localName) =>
|
|
string.Equals(element.Name.LocalName, localName, StringComparison.Ordinal);
|
|
|
|
/// <summary>Direct element children of <paramref name="parent"/> with the given local name.</summary>
|
|
/// <param name="parent">The element whose children to filter.</param>
|
|
/// <param name="localName">The unqualified name to match.</param>
|
|
public static IEnumerable<XElement> ChildrenNamed(XElement parent, string localName) =>
|
|
parent.Elements().Where(child => IsNamed(child, localName));
|
|
|
|
/// <summary>
|
|
/// Reads an unqualified attribute, normalizing a present-but-empty value to <c>null</c>.
|
|
/// See the type-level remarks for why only unqualified attributes are considered.
|
|
/// </summary>
|
|
/// <param name="element">The element carrying the attribute.</param>
|
|
/// <param name="name">The unqualified attribute name.</param>
|
|
public static string? OptionalAttribute(XElement element, string name)
|
|
{
|
|
var value = element.Attribute(name)?.Value;
|
|
|
|
return string.IsNullOrWhiteSpace(value) ? null : value;
|
|
}
|
|
|
|
/// <summary>Reads a required unqualified attribute, or throws naming the owner and the attribute.</summary>
|
|
/// <param name="element">The element carrying the attribute.</param>
|
|
/// <param name="name">The unqualified attribute name.</param>
|
|
/// <param name="owner">How to describe the element in the error message.</param>
|
|
/// <param name="subject">How to describe the document in the error message (e.g. "/probe response").</param>
|
|
/// <exception cref="InvalidDataException">The attribute is absent or blank.</exception>
|
|
public static string RequiredAttribute(XElement element, string name, string owner, string subject)
|
|
{
|
|
var value = OptionalAttribute(element, name);
|
|
|
|
return value ?? throw new InvalidDataException(
|
|
$"MTConnect {subject} is malformed: {owner} has no '{name}' attribute.");
|
|
}
|
|
|
|
/// <summary>Reads an optional unqualified attribute as an <see cref="int"/>.</summary>
|
|
/// <param name="element">The element carrying the attribute.</param>
|
|
/// <param name="name">The unqualified attribute name.</param>
|
|
/// <param name="owner">How to describe the element in the error message.</param>
|
|
/// <param name="subject">How to describe the document in the error message.</param>
|
|
/// <exception cref="InvalidDataException">The attribute is present but not an integer.</exception>
|
|
public static int? OptionalIntAttribute(XElement element, string name, string owner, string subject)
|
|
{
|
|
var raw = OptionalAttribute(element, name);
|
|
if (raw is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (!int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
|
{
|
|
throw new InvalidDataException(
|
|
$"MTConnect {subject} is malformed: {owner} has a non-numeric '{name}' attribute ('{raw}').");
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/// <summary>Reads a required unqualified attribute as a <see cref="long"/>.</summary>
|
|
/// <param name="element">The element carrying the attribute.</param>
|
|
/// <param name="name">The unqualified attribute name.</param>
|
|
/// <param name="owner">How to describe the element in the error message.</param>
|
|
/// <param name="subject">How to describe the document in the error message.</param>
|
|
/// <exception cref="InvalidDataException">The attribute is absent, blank, or not an integer.</exception>
|
|
public static long RequiredLongAttribute(XElement element, string name, string owner, string subject)
|
|
{
|
|
var raw = RequiredAttribute(element, name, owner, subject);
|
|
|
|
if (!long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
|
{
|
|
throw new InvalidDataException(
|
|
$"MTConnect {subject} is malformed: {owner} has a non-numeric '{name}' attribute ('{raw}').");
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the error message for a document whose root is not what was expected, lifting the
|
|
/// Agent's own error text when the response is an <c>MTConnectError</c> document — which
|
|
/// Agents return under <b>HTTP 200</b>, so <c>EnsureSuccessStatusCode</c> never fires and
|
|
/// this is the only place the operator can learn what the Agent objected to.
|
|
/// </summary>
|
|
/// <param name="root">The document's actual root element.</param>
|
|
/// <param name="expectedRootName">The root element name the caller required.</param>
|
|
/// <param name="subject">How to describe the document in the error message.</param>
|
|
public static string DescribeUnexpectedRoot(XElement root, string expectedRootName, string subject)
|
|
{
|
|
var message =
|
|
$"MTConnect {subject} root element is <{root.Name.LocalName}>, expected <{expectedRootName}>.";
|
|
|
|
if (!IsNamed(root, "MTConnectError"))
|
|
{
|
|
return message;
|
|
}
|
|
|
|
var errors = ExtractErrorText(root);
|
|
|
|
return errors is null ? message : $"{message} The Agent reported: {errors}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Agent's own words from an <c>MTConnectError</c> document: every <c><Error></c>
|
|
/// beneath <paramref name="root"/>, rendered <c>errorCode: text</c> where a code is present
|
|
/// and joined with <c>"; "</c>. <c>null</c> when the document carries no readable error
|
|
/// detail at all (no <c><Error></c> elements, or only blank ones).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Shared because both callers must read the same document the same way.</b> An
|
|
/// Agent's objection reaches this driver through two unrelated doors — a read path that
|
|
/// got an error document where it expected a model
|
|
/// (<see cref="DescribeUnexpectedRoot"/>), and the write path deciding whether a 200
|
|
/// actually accepted the observation. Two copies of this walk would drift on the same
|
|
/// two silent rules the type-level remarks describe, and on the write side a missed
|
|
/// <c><Error></c> does not merely degrade a message: it reads as a successful
|
|
/// write.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b><c>null</c> means "no readable detail", NOT "not an error document".</b> Only the
|
|
/// caller knows which document it is holding — check the root name first. A caller that
|
|
/// treats a <c>null</c> here as "no error occurred" turns a hollow
|
|
/// <c><MTConnectError/></c> into a success.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="root">The <c>MTConnectError</c> document's root element.</param>
|
|
public static string? ExtractErrorText(XElement root)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(root);
|
|
|
|
var errors = root.Descendants()
|
|
.Where(e => IsNamed(e, "Error"))
|
|
.Select(e =>
|
|
{
|
|
var code = OptionalAttribute(e, "errorCode");
|
|
var text = e.Value.Trim();
|
|
|
|
return code is null ? text : $"{code}: {text}";
|
|
})
|
|
.Where(text => text.Length > 0)
|
|
.ToList();
|
|
|
|
return errors.Count == 0 ? null : string.Join("; ", errors);
|
|
}
|
|
}
|