0

I have an XML structure and I'd like to transform this with XSLT. However, it's important to make this as dynamic as possible. I believe it's possible to make a copy of the feed, and then just select a specific section and transform this. When a new node is added to the original XML, no changed should be necessary in the XSLT to have this new node included in the output of the XSLT.

Example of original XML:

<?xml version="1.0" encoding="UTF-8"?>
<catalog week_id="629" generated_at="15.11.2017 23:53" version="2">
  <item>
    <lot>
      <id>2982641</id>
      <title_local><![CDATA[Title]]></title_local>
      <sub_title_local>Subtitle</sub_title_local>
      <promo>false</promo>
    </lot>
    <lot_specifics>
      <case_material>
        <name_local>Materiaal kast</name_local>
        <name_en>Case material</name_en>
        <slug>s-10-materiaal-kast</slug>
        <option>
          <value_local>Verguld</value_local>
          <value_en>Gold-plated</value_en>
          <slug_value>1750-verguld</slug_value>
        </option>
        <option>
          <value_local>Zilver</value_local>
          <value_en>Silver</value_en>
          <slug_value>1751-silver</slug_value>
        </option>
      </case_material>
    </lot_specifics>
    <associations>
      <category_id>1</category_id>
      <auction_id>2</auction_id>
    </associations>
  </item>
</catalog>

Desired output:

<?xml version="1.0" encoding="UTF-8"?>
<catalog week_id="629" generated_at="15.11.2017 23:53" version="2">
  <item>
    <lot>
      <id>2982641</id>
      <title_local><![CDATA[Title]]></title_local>
      <sub_title_local>Subtitle</sub_title_local>
      <promo>false</promo>
    </lot>
    <lot_specifics>
      <case_material>
        <name_local>Materiaal kast</name_local>
        <name_en>Case material</name_en>
        <slug>s-10-materiaal-kast</slug>
        <value_local>Verguld,Silver</value_local>
        <value_en>Gold-plated,Silver</value_en>
      </case_material>
    </lot_specifics>
    <associations>
      <category_id>1</category_id>
      <auction_id>2</auction_id>
    </associations>
  </item>
</catalog>

I came across a SO question that pretty much does what I'm aiming for, but I need to define the complete XML structure in this XSLT. As we might add other XML nodes in a later stage and this XSLT will be used in various places, I want to make this as low maintenance as possible.

I think this SO question shows a low maintenance version but frankly I'm not able to understand how the second xsl:template works.

All help is much appreciated. If XSLT is provided, I'd appreciate it if you could include some comments of what you're doing where.

yas
  • 3,520
  • 4
  • 25
  • 38
Casper
  • 1,435
  • 10
  • 22

2 Answers2

2

As you didn't specify the XSLT version, I used 2.0, which is more powerful and better suited for such a task.

Your script should contain:

  • An identity template.
  • A template for case_material elements.

This template should:

  • Copy all elements other than option.
  • Provide special handling of option elements.

This special handling should include grouping of all child elements of option by element name. For each such group you should:

  • Create an element with the name of the source element (grouping key).
  • Print out the content of all group members, separated with a comma.

I noticed that you want to "exclude" certain elements (actually only slug_value), so the loop contains a condition to exclude them.

So the whole script looks like below:

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

  <xsl:template match="case_material">
    <xsl:copy>
      <xsl:apply-templates select="*[name() != 'option']|@*"/>
      <xsl:for-each-group select="option/*" group-by="name()">
        <xsl:if test="current-grouping-key() != 'slug_value'">
         <xsl:element name="{current-grouping-key()}">
           <xsl:value-of select="current-group()" separator=","/>
         </xsl:element>
        </xsl:if>
      </xsl:for-each-group>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="node()|@*">
    <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>
  </xsl:template>
</xsl:stylesheet>

It is possible to write such a script in XSLT 1.0, but then you would have to write much more code, as version 1.0 does not support e.g. grouping.

Valdi_Bo
  • 30,023
  • 4
  • 23
  • 41
  • Using the `except` operator can shorten `*[name() != 'option']` to `* except option` and also the grouping and nested `xsl:if` to `xsl:for-each-group select="option/(* except slug_value)" group-by="name()"`. – Martin Honnen Nov 16 '17 at 16:31
1

I would try copying everything except for the option element. Iterate through option elements and group on the items you need to merge. For example:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
xmlns:xs="http://www.w3.org/2001/XMLSchema">

    <xsl:param name="pGroupElems" as="xs:string *" select="'value_local', 'value_en'" />

    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()" />
        </xsl:copy>
    </xsl:template>

    <xsl:template match="case_material">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()" />
            <xsl:for-each-group select="option/*" group-by="local-name()[ . = $pGroupElems]">
                <xsl:copy>
                    <xsl:sequence select="string-join(current-group(), ',')" />
                </xsl:copy>
            </xsl:for-each-group>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="option" />

</xsl:stylesheet>
yas
  • 3,520
  • 4
  • 25
  • 38