Abhishekh Gupta
Abhishekh Gupta

Reputation: 6236

Generate a XML from some defined XPATH

I am trying to generate a XML from another XML based on some defined XPATH.

XPATH:

country/name,
country/org_id,
country/lang,
country/currency,
generate_date,
schedule/category/id,
schedule/category/name,
schedule/category/classes/class/id,
schedule/category/classes/class/duration,
schedule/category/classes/class/price,
schedule/category/classes/class/instruction_language

Xpath is excluding the name of root node and it is a list.

XML:

<?xml version="1.0" encoding="utf-8" ?>
<ou_schedule>
  <country>
    <name>Country Name</name>
    <org_id>Org ID</org_id>
    <lang>language</lang>
    <currency>Currency</currency>
  </country>
  <generate_date>Date</generate_date>
  <schedule>
    <category>
      <id>cat id</id>
      <name>Cat name</name>
      <classes>
        <class>
          <id>class id</id>
          <duration>class duration</duration>
          <price>price</price>
          <instruction_language>Test Data</instruction_language>
        </class>
        <class>
          <id>class id</id>
          <duration>class duration</duration>
          <price>price</price>
          <instruction_language>Test Data</instruction_language>
        </class>
      </classes>
    </category>
  </schedule>
</ou_schedule>

Output:

<?xml version="1.0" encoding="utf-8"?>
<ou_schedule>
  <country.name>country name</country.name>
  <country.org_id>org id</country.org_id>
  <country.lang>language</country.lang>
  <country.currency>currency</country.currency>
  <generate_date>date</generate_date>
  <schedule.category.name>Cat Name</schedule.category.name>
  <schedule.category.id>Cat ID</schedule.category.id>
  <schedule.category.classes.class.id>class id</schedule.category.classes.class.id>
  <schedule.category.classes.class.duration>class duration</schedule.category.classes.class.duration>
  <schedule.category.classes.class.price>price</schedule.category.classes.class.price>
  <schedule.category.classes.class.instruction_language>Test Data</schedule.category.classes.class.instruction_language>

  <country.name>country name</country.name>
  <country.org_id>org id</country.org_id>
  <country.lang>language</country.lang>
  <country.currency>currency</country.currency>
  <generate_date>date</generate_date>
  <schedule.category.name>Cat Name</schedule.category.name>
  <schedule.category.id>Cat ID</schedule.category.id>
  <schedule.category.classes.class.id>class id</schedule.category.classes.class.id>
  <schedule.category.classes.class.duration>class duration</schedule.category.classes.class.duration>
  <schedule.category.classes.class.price>price</schedule.category.classes.class.price>
  <schedule.category.classes.class.instruction_language>Test Data</schedule.category.classes.class.instruction_language>
</ou_schedule>

Here, to remove ambiguity I am naming the nodes names with their ancestors except root node i.e., same as XPATH but replacing / with ..

Is it possible to achieve this using some generic XSLT?

Upvotes: 2

Views: 199

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243599

Is it possible to achieve this using some generic XSLT?

In case there are two solutions: one for XSLT 1.0 and one for XSLT 2.0, it may be possible to (rather artificially) combine them into one, using techniques as the XSLT 2.0 conditional compilation, that will exclude at "pre-compile time" the templates and declarations of the XSLT 1.0 solution. The XSLT 1.0 solution, on the other hand, will operate in forwards-compatibility mode and will also have higher priority specified for its templates (higher than the priority of the XSLT 2.0 solution's template), thus no XSLT 2.0 solution's template will be selected for execution, when the transformation is run with an XSLT 1.0 processor.

One can regard this as an interesting exercise, and follow the example in the book of Michael Kay "XSLT 2.0 and XPath 2.0", Chapter 3: "Stylesheet Structure", Section "Writing Portable stylesheets", Subsection: "Conditional Compilation". The example (in the edition I have) is on page 128.


Here is a short XSLT 2.0 solution (18 lines if the parameters values are omitted), pure (no extension functions) , that doesn't use explicit XSLT conditional instructions or any xsl:variable. Even the tokenize() function isn't used:

<xsl:stylesheet version="2.0"  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>
 <xsl:param name="pPaths" as="xs:string+" select=
  "'country/name',
   'country/org_id',
   'country/lang',
   'country/currency',
   'generate_date',
   'schedule/category/id',
   'schedule/category/name',
   'schedule/category/classes/class/id',
   'schedule/category/classes/class/duration',
   'schedule/category/classes/class/price',
   'schedule/category/classes/class/instruction_language'"/>

  <xsl:template match="/*">
    <xsl:copy><xsl:apply-templates/></xsl:copy>
  </xsl:template>

  <xsl:template match=
   "*/*[string-join((ancestor::*[position() ne last()]| .)/name(), '/') = $pPaths]">
    <xsl:element 
       name="{string-join((ancestor::*[position() ne last()]|.)/name(), '.')}">
      <xsl:value-of select="."/>
    </xsl:element>
  </xsl:template>
  <xsl:template match="text()"/>
</xsl:stylesheet>

Solution 2:

Here the URI (filepath) of a resource (file) is passed as a parameter. This file contains all wanted XPath expressions -- each one on a separate line.

<xsl:stylesheet version="2.0"  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>
 <xsl:param name="pFilePath" select="'file:///C:/temp/expressions.txt'"/>

 <xsl:variable name="vExprs" select="tokenize(unparsed-text($pFilePath), '\r?\n')"/>

  <xsl:template match="/*">
    <xsl:copy><xsl:apply-templates/></xsl:copy>
  </xsl:template>

  <xsl:template match=
   "*/*[string-join((ancestor::*[position() ne last()]| .)/name(), '/') = $vExprs]">
    <xsl:element name=
       "{string-join((ancestor::*[position() ne last()]|.)/name(), '.')}">
      <xsl:value-of select="."/>
    </xsl:element>
  </xsl:template>
  <xsl:template match="text()"/>
</xsl:stylesheet>

Solution 3:

Both previous solutions can be further optimized and simplified, if for the input XPath expressions it is known that they select elements that have a single text-node child (and this is the case with the originally-provided input XPath expressions and provided source XML document):

<xsl:stylesheet version="2.0"  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>
 <xsl:param name="pFilePath" select="'file:///C:/temp/expressions.txt'"/>

 <xsl:variable name="vExprs" select="tokenize(unparsed-text($pFilePath), '\r?\n')"/>

  <xsl:template match="/*">
    <xsl:copy><xsl:apply-templates/></xsl:copy>
  </xsl:template>

  <xsl:template match=
   "text()[string-join(ancestor::*[position() ne last()]/name(), '/') = $vExprs]">
    <xsl:element 
      name="{string-join(ancestor::*[position() ne last()]/name(), '.')}">
      <xsl:value-of select="."/>
    </xsl:element>
  </xsl:template>
  <xsl:template match="text()"/>
</xsl:stylesheet>

Upvotes: 2

leu
leu

Reputation: 2081

My first thought was: Interesting, here we'll get a dynamically built XSL transformation. But this does not seem achievable as dynamic xpath in xslt explains.

So, a second idea is needed: You can think an XSL transformation as a list of XPATH expressions. In this sense you just need an XSLT file like the following

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0">
    <xsl:output method="xml" indent="yes"/>

    <!-- the following select-attributes are the set of XPATH expressions 
         (relative to /ou_schedule/schedule/category/classes/class) -->
    <xsl:template name="XPathList">
        <category_name>
            <xsl:apply-templates select="ancestor::category/name"/>
        </category_name>

        <category_id>
            <xsl:apply-templates select="ancestor::category/id"/>
        </category_id>

        <id>
            <xsl:apply-templates select="id"/>
        </id>

        <duration>
            <xsl:apply-templates select="duration"/>
        </duration>

        <price>
            <xsl:apply-templates select="price"/>
        </price>

        <instruction_language>
            <xsl:apply-templates select="instruction_language"/>
        </instruction_language>
    </xsl:template>

    <!-- Basis -->
    <xsl:template match="/">
        <ou_schedule>
            <xsl:apply-templates select="//class"/>
        </ou_schedule>
    </xsl:template>

    <xsl:template match="class">
        <xsl:copy>
            <xsl:call-template name="XPathList"/>
        </xsl:copy>    
    </xsl:template>
</xsl:stylesheet>

Well, one could have written this transformation in a more compact way. But the aim was to translate the idea of "having a list of XPATHs to transform an XML" into code.

Upvotes: 0

Related Questions