Kenbo
Kenbo

Reputation: 23

Convert all XML elements without children to XML attributes of parent element

Have the following xml input

<?xml version="1.0" encoding="UTF-8"?>
<EnvelopeType1>
    <Header>
        <User>user1</User>
        <Password>password</Password>
    </Header>
    <CustomerNumber>1</CustomerNumber>
    <CustomerName>Me</CustomerName>
    <Address>
        <Number>5</Number>
        <StreetName>High St</StreetName>
        <State>MI</State>
    </Address>
</EnvelopeType1>

And I need the following output after running it through an xslt:

<EnvelopeType1 CustomerNumber="1" CustomerName="Me"/>
    <Header User="user1" Password="password"/>
    <Address Number="5" StreetName="High St" State="MI"/>
</EnvelopeType1>

The root element may change to another type, and the contents inside the envelope may all change to different elements, etc, and also go several levels deep so I need something that can work generically. There will never be existing attributes, its always going to be in elements, so there's no chance that a converted element will have the same name as an existing attribute.

I'm thinking this will use apply-templates and somehow go recursively through the nodes, but I'm not that cluey on XSLT, would appreciate any help with this.

There was another similar post here which I think would work, but requires XSLT2.0, I need something that would work with XSLT1.0

Upvotes: 1

Views: 294

Answers (2)

C. M. Sperberg-McQueen
C. M. Sperberg-McQueen

Reputation: 25034

Since you say you're not very clueful with respect to XSLT, I'm going to give you an answer that focuses on the conceptual issues, and not on the syntax. (If you want a cut/paste solution, you'll have to look for other answers.)

You want a template which applies to any element. So its match pattern can be *.

<xsl:template match="*">
  ...
</

Inside that template, you want:

  • to make an element with the same name as the context element
  • to give that element attributes with the same name as all the childless and attribute-less children of the context element
  • to give that element children corresponding to all the children of the context element which do have attributes or children

Since the second item in this list has to be handled before the first item, you will need two apply-templates instructions, one to produce attributes and one to produce children. Use a distinct mode for the first one; optionally use a select attribute on each apply-templates call to make the call apply only to the appropriate children; alternatively, write do-nothing templates for the elements which should produce no output in a particular mode.

<xsl:template match="*">
  <xsl:copy>
    <xsl:apply-templates select="@*"/>
    <xsl:apply-templates select="*[not(@*) and not(*)]"
                         mode="attribute-generation"/>
    <xsl:apply-templates select="*[@* or *]"/>
  </
</

To make the stylesheet a bit more general, we'll preserve attributes in the input instead of assuming there aren't any. (But for simplicity we will still assume there are never name collisions between attributes and children; checking for that would be a good exercise for an XSLT learner.)

<xsl:template match="@*"><xsl:copy/></xsl:template>

Finally, write a template to handle attribute-less, child-less elements in mode attribute-generation:

<xsl:template match="*" mode="attribute-generation">
  <xsl:attribute name="{name()}">
    <xsl:value-of select="."/>
  </
</

Upvotes: 2

Mathias M&#252;ller
Mathias M&#252;ller

Reputation: 22617

Below is a very generic solution to your problem. It makes only minimal assumptions about the nature of the input XML. What it does assume is only that elements that have child elements should include all of them as attributes.

Apart from that, any input XML will do. Tested with Saxon 6.5, try it online here. What it does, in plain English, is:

Match an element that has at least one child element. Copy this element to the output. For each of its child element that does not itself have child elements, add an attribute which is named after the "childless" child element, and add its text content as the attribute value.

Stylesheet

EDIT: As a response to your comment. Now, the stylesheet gives the correct output for an arbitrary number of "levels".

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

    <xsl:strip-space elements="*"/>

    <xsl:template match="*[*]">
      <xsl:copy>
          <xsl:for-each select="*[not(*)]">
              <xsl:attribute name="{local-name(.)}">
                  <xsl:value-of select="."/>
              </xsl:attribute>
          </xsl:for-each>
          <xsl:apply-templates/>
      </xsl:copy>
    </xsl:template>

    <xsl:template match="text()"/>
</xsl:transform>

XML Output

<?xml version="1.0" encoding="UTF-8"?>
<EnvelopeType1 CustomerNumber="1" CustomerName="Me">
   <Header User="user1" Password="password"/>
   <Address Number="5" StreetName="High St" State="MI"/>
</EnvelopeType1>

Upvotes: 0

Related Questions