Steve Ostroff
Steve Ostroff

Reputation: 1

Problem using a structure (for xPath translation) in XSLT 1.0

Our company has a system that regularly gets xml files from customer systems. We use xslt to transform the data from the customer structure to an xPath structure supported by our database. 95% of this activity is simply coping the data from one element in the customer file to an element in our internal structure. Sometimes customers migrate from one format of data to another (usually if changing entry forms or providers). So, ultimately I want to be able to support either path to support a smooth switchover.

To improve maintenance, I would like the solution to use a table which would generally not require code to be added, but where additional mapping elements can be inserted.

When I run this code, I am not seeing the data from the input file detected.

XSLT:<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes"/>
    <!-- Define the mapping within the XSLT -->
    <xsl:variable name="mapping">
        <mapping>
            <entry key="DocumentIdentifier" value1="/FORM/DocNumber" value2=""/>
            <!-- Add more mapping entries as needed -->
        </mapping>
    </xsl:variable>
    <!-- Identity template to copy the rest of the XML as is -->
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    <!-- Template to match the root element and start the transformation -->
    <xsl:template match="/">
        <output>
            <xsl:apply-templates select="$mapping/mapping/entry"/>
        </output>
    </xsl:template>
    <!-- Template to process each mapping entry -->
    <xsl:template match="entry">
        <xsl:variable name="key" select="@key"/>
        <xsl:variable name="value1" select="@value1"/>
        <xsl:message>
            <xsl:text>Processing entry: </xsl:text>
            <xsl:value-of select="$key"/>
            <xsl:text> - Value1: </xsl:text>
            <xsl:value-of select="$value1"/>
        </xsl:message>
        <xsl:element name="{$key}">
            <xsl:value-of select="*[name() = $value1]"/>
        </xsl:element>
    </xsl:template>
</xsl:stylesheet>

Sample source XML:

<?xml version='1.0' encoding='utf-8'?>
<FORM>
    <DocNumber>INV100123</DocNumber>
</FORM>

Output:

<?xml version="1.0" encoding="UTF-8"?>
<output>
   <DocumentIdentifier/>
</output>

Upvotes: 0

Views: 62

Answers (2)

y.arazim
y.arazim

Reputation: 3162

The basic problem with your idea of using "mappings" to store alternative paths is that the stored value is a string, not a path. In order to turn a string into a path you need some sort of an "evaluate" function which is not available in XSLT 1.0 without using an extension function.

If your processor does not support this type of extension, you could still implement your idea albeit in two steps. Using an XML document to store the mappings as:

<mappings>
    <entry key="DocumentIdentifier">
        <path>/FORM/DocNumber</path>
        <path>/root/data/@docref</path>
        <!-- Add more paths as needed -->
        
    </entry>
    <!-- Add more mapping entries as needed -->

</mappings>

you would then apply the following transform to the mappings XML:

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:axsl="http://www.w3.org/1999/XSL/TransformAlias">
<xsl:output method="xml" indent="yes"/>

<xsl:namespace-alias stylesheet-prefix="axsl" result-prefix="xsl"/>

<xsl:template match="/mappings">
    <axsl:stylesheet version="1.0">
        <axsl:output method="xml" indent="yes"/>
        <!-- create a template -->
        <axsl:template match="/">
            <output>
                <xsl:for-each select="entry">
                    <!-- create a literal element -->
                    <xsl:element name="{@key}">
                        <!-- create an instruction -->
                        <axsl:value-of>
                            <xsl:attribute name="select">
                                <xsl:for-each select="path">
                                    <xsl:value-of select="."/>
                                    <xsl:if test="position()!=last()">|</xsl:if>
                                </xsl:for-each>
                            </xsl:attribute>
                        </axsl:value-of>
                    </xsl:element>  
                </xsl:for-each>
            </output>
        </axsl:template>
    </axsl:stylesheet>
</xsl:template>

</xsl:stylesheet>

to generate this stylesheet:

<?xml version="1.0"?>
<axsl:stylesheet xmlns:axsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <axsl:output method="xml" indent="yes"/>
  <axsl:template match="/">
    <output>
      <DocumentIdentifier>
        <axsl:value-of select="/FORM/DocNumber|/root/data/@docref"/>
      </DocumentIdentifier>
    </output>
  </axsl:template>
</axsl:stylesheet>

Applying the resulting stylesheet to any one of the example inputs in the above answer will produce:

<?xml version="1.0"?>
<output>
  <DocumentIdentifier>INV100123</DocumentIdentifier>
</output>

This is of course based on the assumption that every mapping should produce an element that is a child of output and has no children of its own except a text node with the value retrieved by one of its paths.


Whether all of this is worth the trouble is a separate question.

Upvotes: 0

michael.hor257k
michael.hor257k

Reputation: 116959

I don't really understand the method you are trying to implement.

If you have a rigid output structure that you need to populate from different (but known in advance) sources, you could just use alternative paths to the value that you need.

For example, this stylesheet:

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
 
<xsl:template match="/">
    <output>
        <DocumentIdentifier>
            <xsl:value-of select="/FORM/DocNumber | /root/data/@docref"/>
        </DocumentIdentifier>
    </output>
</xsl:template>

</xsl:stylesheet>

will return:

Result

<?xml version="1.0"?>
<output>
  <DocumentIdentifier>INV100123</DocumentIdentifier>
</output>

from both of these XML inputs:

XML A

<FORM>
    <DocNumber>INV100123</DocNumber>
</FORM>

XML B

<root>
    <data docref="INV100123"/>
</root>

To improve maintenance, I would like the solution to use a table which would generally not require code to be added, but where additional mapping elements can be inserted.

I am not sure you can do this in pure XSLT 1.0. But if your processor supports the EXSLT dyn:evaluate() extension function, you could do something like this:

XSLT 1.0 + EXSLT

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
xmlns:dyn="http://exslt.org/dynamic"
extension-element-prefixes="exsl dyn">
<xsl:output method="xml" indent="yes"/>

<xsl:variable name="xml" select="/" />

<xsl:variable name="mappings">
    <entry key="DocumentIdentifier" path="$xml/FORM/DocNumber | $xml/root/data/@docref"/>
    <!-- Add more mapping entries as needed -->
</xsl:variable>
 
<xsl:template match="/">
    <output>
        <xsl:for-each select="exsl:node-set($mappings)/entry">
            <xsl:element name="{@key}">
                <xsl:value-of select="dyn:evaluate(@path)"/>
            </xsl:element>
        </xsl:for-each>
    </output>
</xsl:template>

</xsl:stylesheet>

Upvotes: 1

Related Questions