Search code examples
xmlxpath.net-6.0xpath-1.0

Select ancestor based on the attribute value of the selected node


input.xml file -

<?xml version="1.0" encoding="utf-8"?>
<sg:EmployeeDetails xmlns:sg="http://sg.iaea.org/ssac-qs/qsSchema.xsd">
    <sg:FirstName>Sagar</sg:FirstName>
    <sg:MiddleName>H</sg:MiddleName>
    <sg:LastName>Shinde</sg:LastName>
    <sg:EmailId>[email protected]</sg:EmailId>
    <sg:Mobile>9021063389</sg:Mobile>
    <sg:Years>
        <sg:Year>1954</sg:Year>
        <sg:Year>1980</sg:Year>
        <sg:Year>1954</sg:Year>
    </sg:Years>
    <sg:Address>135/214, Hindustan Chowk, Mulund Colony, Mulund (W) - 400082</sg:Address>
    <sg:Cars>
        <sg:Car id="car1" year="1980"  condition="count(./ancestor::sg:EmployeeDetails/sg:Years/sg:Year[text() = //sg:Car[@id='car1']/@year]) &gt;=0" message="Driver must be 100 yrs old.">BMW</sg:Car>
        <sg:Car id="car2" year="1954" condition="count(./ancestor::sg:EmployeeDetails/sg:Years/sg:Year[text() = //sg:Car[@id='car1']/@year]) &gt;=0" message="Driver must be 100 yrs old.">BMW</sg:Car>
    </sg:Cars>
</sg:EmployeeDetails>

I am going to parse this xml file using .net 6. I will select all nodes with attribute 'condition' with xpath as -

//Car[@condition]

I will iterate thru' each node found with the captioned expression, and then evaluate if the expression in the value of the condition attribute is true. (I can not use xs:assert as .net6 does not support xsd1.1)

I would like to set the value of the condition attribute such that I can find if there is at least one ancestor node of 'EmployeeDetails' with more than one 'Year' child-node, having value that matches value of the 'year' attribute of the car node whose 'condition' attribute's value is being evaluated.

                string xmlFilePath = "input.xml";
                XmlReaderSettings settings = new XmlReaderSettings();
                settings.Schemas.Add("http://sg.iaea.org/ssac-qs/qsSchema.xsd", "input.xsd"); 
                settings.ValidationType = ValidationType.Schema;
                ValidationEventHandler eventHandler = new ValidationEventHandler(ValidationEventHandler);

                using (XmlReader reader = XmlReader.Create(xmlFilePath, settings))
                {
                    XmlDocument document = new XmlDocument();
                    document.Load(reader);
                    
                    var nsmgr = new XmlNamespaceManager(document.NameTable);
                    nsmgr.AddNamespace("sg", "http://sg.iaea.org/ssac-qs/qsSchema.xsd");

                    document.Validate(eventHandler);
                    // Read the XML content and validate against the schema

                    var nav = document.CreateNavigator();
                    var list = nav.Select("//sg:Car[@condition]", nsmgr);
                    while(list.MoveNext())
                    {
                        var item = list.Current;
                        var condition = item.GetAttribute("condition", "");
                        var result = item.Evaluate(condition, nsmgr);
                        if (!(bool)result)
                        {
                            Console.WriteLine($"Validation error:Validation error");
                        }
                    }

                }

This code works fine.

I would to remove reference of '//sg:Car[@id='car1']' from

count(./ancestor::sg:EmployeeDetails/sg:Years/sg:Year[text() = //sg:Car[@id='car1']/@year]) &gt;=0

and make it something like

count(./ancestor::sg:EmployeeDetails/sg:Years/sg:Year[text() = @year]) &gt;=0

This exact expression fails as '@year' is searched in the sg:Year node instead of sg:Car node.

Does any one has any idea how to do that?

For completeness the input.xsd file is as below -

<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
        targetNamespace="http://sg.iaea.org/ssac-qs/qsSchema.xsd"
        xmlns:sg="http://sg.iaea.org/ssac-qs/qsSchema.xsd"
        xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="EmployeeDetails">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="FirstName" type="xs:string"/>
                <xs:element name="MiddleName">
                    <xs:simpleType>
                        <xs:restriction base="xs:string">
                            <xs:minLength value="1"/>
                        </xs:restriction>
                    </xs:simpleType>
                </xs:element>
                <xs:element name="LastName" type="xs:string" />
                <xs:element name="EmailId" type="xs:string" />
                <xs:element name="Mobile" type="xs:integer" minOccurs="1" maxOccurs="1"/>
                <xs:element name="Years">
                    <xs:complexType>
                        <xs:sequence>
                            <xs:element name="Year" type="sg:Year" minOccurs="1" maxOccurs="unbounded"/>
                        </xs:sequence>
                    </xs:complexType>
                </xs:element>
                <xs:element name="Address">
                    <xs:simpleType>
                        <xs:restriction base="xs:string">
                            <xs:minLength value="5"/>
                            <xs:maxLength value="100"/>
                        </xs:restriction>
                    </xs:simpleType>
                </xs:element>
                <xs:element name="Cars" type="sg:Cars"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:complexType name="Cars">
        <xs:sequence>
            <xs:element name="Car" type="sg:Car" maxOccurs="unbounded">
            </xs:element>
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="Car">
        <xs:simpleContent>
            <xs:extension base="sg:CarModel">
                <xs:attribute name="id" type="xs:string" use="required"/>
                <xs:attribute name="year" type="sg:Year" use="required"/>
                <xs:attribute name="condition" type="xs:string" use="optional"  />
                <xs:attribute name="message" type="xs:string" use="optional" fixed="Driver must be 100 yrs old."/>
            </xs:extension>
        </xs:simpleContent>
    </xs:complexType>
    <xs:simpleType name="CarModel">
        <xs:restriction base="xs:string">
            <xs:enumeration value="Audi"/>
            <xs:enumeration value="Golf"/>
            <xs:enumeration value="BMW"/>
        </xs:restriction>
    </xs:simpleType>
    <xs:simpleType name="Year">
        <xs:restriction base="xs:int">
            <xs:minInclusive value="1900" />
            <xs:maxInclusive value="2200" />
        </xs:restriction>
    </xs:simpleType>

</xs:schema>

Solution

  • In terms of XPath 1.0, I think it should work to just check

    @year = ancestor::sg:EmployeeDetails/sg:Years/sg:Year
    

    in the context of the sg:Car element. That condition is only true if there is a year match.

    As for doing it with open-source software and XPath 3.1, you can use Saxon HE 10 cross-compiled with IKVM to .NET (Core) as follows:

    using Saxon.Api;
    using System.Xml;
    
    string xmlFilePath = "input2.xml";
    XmlReaderSettings settings = new XmlReaderSettings();
    settings.Schemas.Add("http://sg.iaea.org/ssac-qs/qsSchema.xsd", "input.xsd");
    settings.ValidationType = ValidationType.Schema;
    settings.ValidationEventHandler += (sender, e) =>
    {
        Console.WriteLine($"{e.Severity} {e.Message}");
    };
    
    using (XmlReader reader = XmlReader.Create(xmlFilePath, settings))
    {
        XmlDocument document = new XmlDocument();
        document.Load(reader);
    
        Processor processor = new Processor(false);
    
        DocumentBuilder docBuilder = processor.NewDocumentBuilder();
    
        XdmNode wrappedDoc = docBuilder.Wrap(document);
    
        XPathCompiler xpathCompiler = processor.NewXPathCompiler();
    
        xpathCompiler.DeclareNamespace("", "http://sg.iaea.org/ssac-qs/qsSchema.xsd");
        xpathCompiler.DeclareNamespace("sg", "http://sg.iaea.org/ssac-qs/qsSchema.xsd");
    
        foreach (XdmNode car in xpathCompiler.Evaluate("//Car[@condition]", wrappedDoc))
        {
            var conditionEvaluationResult = xpathCompiler.EvaluateSingle(car.GetAttributeValue("condition"), car) as XdmAtomicValue;
            if (!conditionEvaluationResult.GetBooleanValue())
            {
                Console.WriteLine($"Validation failed for Car {car.GetAttributeValue("id")} with year {car.GetAttributeValue("year")}.");
            }
        }
    
    }
    

    Your .NET project would have e.g.

      <ItemGroup>
        <PackageReference Include="SaxonHE10Net31Api" Version="10.9.0.1" />
      </ItemGroup>
    

    your conditions would be (I think you want > not >=) e.g.

    <sg:Car id="car3" year="1984" condition="let $car := . return count(ancestor::sg:EmployeeDetails/sg:Years/sg:Year[. = $car/@year]) &gt;0" message="Driver must be 100 yrs old.">BMW</sg:Car>
    

    Example .NET 6 console project is at https://github.com/martin-honnen/SaxonHE10Net6XPath31Example1.

    Disclaimer: Saxon 10 HE is an open-source product from Saxonica that exists on NuGet from Saxonica itself, but only for .NET framework; the project SaxonHE10Net31Api used and referenced above is my recompilation/rebuild of that open source project for .NET (Core) aka .NET 3.1 and later.