Templating workflow service

There is a subset of tasks that require substitution of several placeholders by provided values. Solving such tasks in XSLT is rather nonelegant.

<xsl:variable name="bodyText1" select="str:replace($bodyText, '$CustomerFirstName', @CustomerFirstName)" />
<xsl:variable name="bodyText2" select="str:replace($bodyText1, '$CustomerName', @CustomerName)" />
<xsl:variable name="bodyText3" select="str:replace($bodyText2, '$RentingStartDate', AS:FormatDate(@RentingStartDate, 'dd.MM.yyyy HH:mm'))" />
<xsl:variable name="bodyText4" select="str:replace($bodyText3, '$Object', AS:LookupValue('9b4c18af-12ad-4e05-8668-1cf448f899e7', @refRentalObjectId))" />
<xsl:variable name="bodyText5" select="str:replace($bodyText4, '$LocationAddress'
		, concat(AS:LookupValue('651cff99-00b2-461c-a792-1a766c865c54', @refBusinessUnitId)
		, ', ', AS:LookupValue('9d632a90-0470-4678-8516-977c69402143', @refBusinessUnitId)))" />
<xsl:variable name="bodyText6" select="str:replace($bodyText5, '$Location', AS:LookupValue('a392d1dc-519c-48b1-9f24-a3b8d9857337', @refBusinessUnitId))" />
<xsl:variable name="bodyText7" select="str:replace($bodyText6, '$TransactionNumber', @Number)" />
<xsl:variable name="bodyText8" select="str:replace($bodyText7, '$LesseeNote2', @LesseeNote2)" />
<xsl:variable name="bodyText9" select="str:replace($bodyText8, '$LesseeNote', @LesseeNote)" />
<xsl:variable name="bodyText10" select="str:replace($bodyText9, '$Logo', concat('&lt;img src=&quot;'
		,AS:GetConstant('Url'),'api/Organization/',@refOrganizationId,'/GetLogo&quot; alt=&quot;Logo&quot; width=&quot;200&quot;&gt;'))" />
<xsl:variable name="bodyText11" select="str:replace($bodyText10, '$Last6DigitsOfTransactionNumber', substring(@Number, string-length(@Number) -5))" />

Having an access to a templating engine in form of a workflow service could make solutions for such tasks easier/more readable/more elegant.

What about implementing it using XSLT? I think this could be implemented in ORIGAM.

Well, it actually expects the template to be tokenised already (using <span>).

But a recursive template would also do the job, I guess. It would do recursion as long as the source text would be different from the transformed text.

Designing the placeholders like this

Dear <name/>,

some text with other variables like <age/> or <name/> again

greetings <me/>

would make the XSLT much easier. You could then use standard matching template, e.g. <xslt:template match="name"> to handle those placeholders.

It is possible to implement pure XSLT function, but it is not pretty and it relies on the existence of placeholder-value mapping in the XML source. It would one extra workflow step to “inject” the mapping.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
    xmlns:AS="http://schema.advantages.cz/AsapFunctions"
    xmlns:date="http://exslt.org/dates-and-times" exclude-result-prefixes="AS date">

    <xsl:key name="kRep" match="item" use="@name"/>
    
    <xsl:template match="ROOT">
        <ROOT>
            <xsl:call-template name="replace-placeholders">
      <xsl:with-param name="in"           select="input"/>
      <xsl:with-param name="replacements" select="replacements"/>
    </xsl:call-template>
        </ROOT>
    </xsl:template>

  <xsl:template name="replace-placeholders">
    <xsl:param name="in" select="."/>
    <xsl:param name="replacements"/>
    <xsl:apply-templates select="$in/node()" mode="replace">
      <xsl:with-param name="replacements" select="$replacements"/>
    </xsl:apply-templates>
  </xsl:template>

  <xsl:template match="text()" mode="replace">
    <xsl:param name="replacements"/>
    <xsl:value-of select="."/>
  </xsl:template>

  <xsl:template match="*" mode="replace">
    <xsl:param name="replacements"/>
    <xsl:variable name="pname" select="name()"/>
    <xsl:for-each select="$replacements">
      <xsl:variable name="val"
        select="(key('kRep', $pname)/@value | key('kRep', $pname)/text())[1]"/>
      <xsl:choose>
        <xsl:when test="$val">
          <xsl:value-of select="$val"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:apply-templates select="current()/node()" mode="replace">
            <xsl:with-param name="replacements" select="$replacements"/>
          </xsl:apply-templates>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

The sample input:

<ROOT>
    <replacements>
      <item name="placeholder1">ALPHA</item>
      <item name="placeholder2" value="BETA"/>
      <item name="placeholder3">GAMMA</item>
    </replacements>
    <input>Some text with <placeholder1/> and <placeholder2/>. Maybe <placeholder1/> again or <placeholder3/></input>
</ROOT>

I meant something like this:

Source

<source>Dear <name/>,
some text with other variables like <age/> or <name/> again
greetings <me/></source>

Xslt:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="source">
    <result>
      <xsl:apply-templates/>
    </result>
  </xsl:template>
  <xsl:template match="name">Arthur</xsl:template>
  <xsl:template match="age">42</xsl:template>
  <xsl:template match="me">Marvin</xsl:template>
</xsl:stylesheet>

Result:

<result>Dear Arthur,
some text with other variables like 42 or Arthur again
greetings Marvin
</result>

Obviously the source can be delivered in a form of a pure text in a variable, you would have to convert it to a node-set and apply templates as shown. Parameters could be easily delivered as XSLT parameters from outside of the template or as the transformation main data context.

If the data are delivered as a nodeset (within the source or in any other way), you can use this universal transformation:

Source:

<source>
  <string>Dear <name/>,
some text with other variables like <age/> or <name/> again
greetings <me/></string>
  <parameters>
    <name>Arthur</name>
    <age>42</age>
    <me>Marvin</me>
  </parameters>
</source>

XSLT:

<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <!-- we could have just referenced /source/parameters later all the time
       but this would affect performance, it is better to store it in a variable -->
  <xsl:variable name="params" select="/source/parameters"/>
  <xsl:template match="source">
    <!-- applies templates to both parameters and the text itself
         as both text() and element() are a node() -->
    <result>
      <xsl:apply-templates select="string/node()"/>
    </result>
  </xsl:template>
  <!-- here we resolve the parameter elements -->
  <xsl:template match="*">
    <xsl:value-of select="$params/*[name() = current()/name()]"/>
  </xsl:template>
  <!-- there is no template for text() as xslt has its default template
       that copies any text to the output -->
</xsl:stylesheet>

Result is the same.

If the template is delivered as a string parameter (in my example it is hardcoded in a variable) and the replacement values are loaded from the data context, it could look like this:

Source:

<ROOT>
  <Order FirstName="Arthur" Name="Dent" Object="Earth"/>
</ROOT>

XSLT:

<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:AS="http://schema.advantages.cz/AsapFunctions"
  exclude-result-prefixes="AS">
  <xsl:variable name="order" select="/ROOT/Order"/>
  <xsl:variable name="template">&lt;source&gt;Dear &lt;FirstName/&gt; &lt;Name/&gt;,
the &lt;Object/&gt; is ready for pick up.&lt;/source&gt;</xsl:variable>
  <xsl:template match="AS:ToXml($template)/source">
    <result>
      <xsl:apply-templates/>
    </result>
  </xsl:template>
  <xsl:template match="FirstName">
    <xsl:value-of select="$order/@FirstName"/>
  </xsl:template>
  <xsl:template match="Name">
    <xsl:value-of select="$order/@Name"/>
  </xsl:template>
  <xsl:template match="Object">
    <xsl:value-of select="$order/@Object"/>
  </xsl:template>
</xsl:stylesheet>```

@tvavrda From the offered solutions I like most the first option:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="source">
    <result>
      <xsl:apply-templates/>
    </result>
  </xsl:template>
  <xsl:template match="name">Arthur</xsl:template>
  <xsl:template match="age">42</xsl:template>
  <xsl:template match="me">Marvin</xsl:template>
</xsl:stylesheet>

It looks cleanest and it can incorporate the ideas from other options.

<xsl:template match="name">Arthur</xsl:template>

Is basically a mapping definition which is good enough.

So it looks like we don’t really need a templating service, we just need to use this practice. Of course if you can’t have placeholders in “element” form, it can’t be used in a such easy way.

The only other downside would be you have to escape xml characters & < > " '. That could complicate creating of the templates to ordinary users.