Search code examples
xmlxsltsequenceconfiguration-files

xsl prioritized configuration files


Ultimately, I need to create a convenient way to read multiple configuration files for controlling the processing of a complex xsl transformation (2.0, currently). Each configuration file may or may not have particular nodes. A relative priority exists between the configuration files, and the ultimate value for any particular value should come from the highest priority configuration file in which the value exists.

A simple configuration file (so.xml) with one variable is given below:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="urn:config.template.config" >
    <primary>Yes, this is primary</primary>
</config>

Old Method: I read one file by setting a parameter to the value "primary" in a single configuration file for a node "primary":

<xsl:param name="primary" select="$primaryConfig/myConfig:config/myConfig:primary/text()"/>

Now: I may have up to four configuration files that may have "primary" as a value. To do this, I chose to write two templates. pickConfigNode is to search in the template files (using a select to do the prioritizing of the read) to see if the requested node with the value contained in 'level1'.

<xsl:template name="pickConfigNode">
    <xsl:param name="level1"/>

    <xsl:choose>
        <xsl:when test="$primaryConfig/myConfig:config/*[local-name() = $level1]">
            <xsl:value-of select="$primaryConfig/myConfig:config/*[local-name() = $level1]"/>
        </xsl:when>
        <xsl:otherwise>
            <xsl:value-of select="$secondaryConfig/myConfig:config/*[local-name() = $level1]"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

This works sufficiently well enough for me as long as the value exists in some configuration file (shown with searching primary and secondary). However, it is possible that the value is not defined anywhere. I think I would like an empty sequence to be returned which is what happens if the node does not exist with the old method. However, I maybe misunderstanding something about how the '*' works when nothing is found.

pickConfigNode returns a partial document. So, this leads to problems with pickConfigText:

<xsl:template name="pickConfigText" as="xs:string">
    <xsl:param name="level1"/>

    <xsl:variable name="chosenNode">
        <xsl:call-template name="pickConfigNode">
            <xsl:with-param name="level1" select="$level1"/>
        </xsl:call-template>
    </xsl:variable>
    <xsl:value-of select="$chosenNode/text()"/>
</xsl:template>

Here is the secondary file:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="urn:config.template.config" >
    <onlySecondary>from secondary</onlySecondary>
    <primary>No, this is secondary</primary>
</config>

Here is the complete test case that uses the two configuration files:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="3.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                xmlns:myConfig="urn:config.template.config"
                xpath-default-namespace="http://www.w3.org/2001/XMLSchema"
                exclude-result-prefixes="myConfig xs"
>

    <xsl:output method="xml" omit-xml-declaration="no" indent="yes" encoding="us-ascii" cdata-section-elements="p i b u li"/>

    <xsl:variable name="configFile" select="'so.xml'"/>
    <xsl:variable name="primaryConfig" select="document($configFile)"/>
    <xsl:variable name="secondaryConfig" select="document('second.xml')"/>

    <xsl:template name="pickConfigNode">
        <xsl:param name="level1"/>

        <xsl:choose>
            <xsl:when test="$primaryConfig/myConfig:config/*[local-name() = $level1]">
                <xsl:value-of select="$primaryConfig/myConfig:config/*[local-name() = $level1]"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$secondaryConfig/myConfig:config/*[local-name() = $level1]"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <xsl:template name="pickConfigText" as="xs:string">
        <xsl:param name="level1"/>

        <xsl:variable name="chosenNode">
            <xsl:call-template name="pickConfigNode">
                <xsl:with-param name="level1" select="$level1"/>
            </xsl:call-template>
        </xsl:variable>
        <xsl:value-of select="$chosenNode/text()"/>
    </xsl:template>

    <xsl:param name="primary">
        <xsl:call-template name="pickConfigText">
            <xsl:with-param name="level1" select="'primary'"/>
        </xsl:call-template>
    </xsl:param>

    <xsl:param name="onlySecondary">
        <xsl:call-template name="pickConfigText">
            <xsl:with-param name="level1" select="'onlySecondary'"/>
        </xsl:call-template>
    </xsl:param>

    <xsl:param name="neither">
        <xsl:call-template name="pickConfigText">
            <xsl:with-param name="level1" select="'neither'"/>
        </xsl:call-template>
    </xsl:param>

    <xsl:template match="/">
        <PRIMARY>
            <xsl:choose>
                <xsl:when test="$primary"><SUCCESS><xsl:value-of select="$primary"/></SUCCESS></xsl:when>
                <xsl:otherwise>
                    <FAILURE>
                        <xsl:value-of select="'No value for primary'"/>
                    </FAILURE>
                </xsl:otherwise>
            </xsl:choose>
        </PRIMARY>
        <SECONDARY>
            <xsl:choose>
                <xsl:when test="$onlySecondary"><SUCCESS><xsl:value-of select="$onlySecondary"/></SUCCESS></xsl:when>
                <xsl:otherwise>
                    <FAILURE>
                        <xsl:value-of select="'No value for onlySecondary'"/>
                    </FAILURE>
                </xsl:otherwise>
            </xsl:choose>
        </SECONDARY>
        <NEITHER>
            <xsl:choose>
                <xsl:when test="not($neither)"><SUCCESS>NOT in either file</SUCCESS></xsl:when>
                <xsl:otherwise>
                    <FAILURE>
                        <xsl:value-of select="'Got value of for neither='"/>
                        <xsl:value-of select="$neither"/>
                    </FAILURE>
                </xsl:otherwise>
            </xsl:choose>
        </NEITHER>       
    </xsl:template>

</xsl:stylesheet>

The output is:

<?xml version="1.0" encoding="us-ascii"?>
<PRIMARY>
   <SUCCESS>Yes, this is primary</SUCCESS>
</PRIMARY>
<SECONDARY>
   <SUCCESS>from secondary</SUCCESS>
</SECONDARY>
<NEITHER>
   <FAILURE>Got value of for neither=</FAILURE>
</NEITHER>

So, I want the NEITHER result to be "SUCCESS".

Your help in helping me understand my misconception of xslt processing is appreciated. Also, if you have an alternative approach to handling prioritized configuration files, I would be eager to hear that also.


Solution

  • Ultimately, I need to create a convenient way to read multiple configuration files for controlling the processing of a complex xsl transformation (2.0, currently). Each configuration file may or may not have particular nodes. A relative priority exists between the configuration files, and the ultimate value for any particular value should come from the highest priority configuration file in which the value exists.

    Here is a general technique to extract values from multiple files according to their predefined priority:

    Let's have these four config files in a C:\temp\DeleteMe directory -- and no other .xml files there:

    Config1.xml

    <config xmlns="urn:config.template.config" >
        <exists>Yes</exists>
    </config>
    

    Config2.xml

    <config xmlns="urn:config.template.config" >
        <SomethingElse>Yes</SomethingElse>
    </config>
    

    Config3.xml

    <config xmlns="urn:config.template.config" >
        <exists>YesConfig3</exists>
    </config>
    

    Config4.xml

    <config xmlns="urn:config.template.config" >
        <SomethingElseEvenMore>Yes</SomethingElseEvenMore>
    </config>
    

    Do note that only Config1.xml and Config3.xml have <exists> element.

    This transformation:

    <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
     xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:my="my:my"
     xmlns:x="urn:config.template.config" >
     <xsl:output omit-xml-declaration="yes" indent="yes"/>
    
     <xsl:param name="pDirectory" as="xs:string" select="'file:///c:/temp/DeleteMe'"/>
     <xsl:variable name="vConfigs" select="collection(concat($pDirectory, '?select=*.xml'))"/>
    
    <xsl:template match="/">
        <xsl:value-of select="my:GetConfigValue('exists', $vConfigs)"/>
      </xsl:template>
    
      <xsl:function name="my:GetConfigValue">
        <xsl:param name="pConfigName" as="xs:string"/>
        <xsl:param name="pConfigs" as="document-node()*"/>
    
        <xsl:variable name="vConfigsMatching" as="document-node()*">
           <xsl:perform-sort select="$pConfigs[*/*[name() eq $pConfigName and text()]]">
             <xsl:sort 
               select="number(substring-before(substring-after(base-uri(.), 'Config'), '.xml'))" 
               order="descending"/>
           </xsl:perform-sort>
        </xsl:variable>
    
        <xsl:sequence select="$vConfigsMatching[1]/*/*[name() eq $pConfigName]/text()"/>
      </xsl:function>
    </xsl:stylesheet>
    

    when applied on any xml file (not used), uses as priority the number in the name of the config file -- thus the priorities from highest to lowest are:

    • Config4.xml
    • Config3.xml
    • Config2.xml
    • Config1.xml

    The transformation correctly produced the value of the config item "exists" from the highest-priority congfig file (Config3.xml):

    YesConfig3
    

    If we ask for the value of a non-existing element:

    <xsl:value-of select="my:GetConfigValue('not-found', $vConfigs)"/>
    

    The function correctly returns the empty sequence -- the count() of the above is 0.

    If we remove the text-node child of <exists> in Config3.xml, then correctly the function returns the string-value of <exists> in Config1.xml"

    Yes

    If we also remove the text-node child of <exists> in Config1.xml, then correctly the function returns no text node -- the empty sequence -- which is confirmed by the count of the resulting sequence returned as zero.