XSL Transformations with Maven

In my last post I told about the XSLT processing that I have to do to get the XSD as I wanted it to be. Unfortunately there is not just one XSD that has to be processed but there are actually several of them. And for every change in our CDM (Common Data Model) I had to perform all these transformations by hand. Now that doesn’t feel good so I decided to automate that process. And since we are already heavily using Maven I also wanted to do this with Maven, which actuallly is rather easy to do. I even added a validation step so I can test the created XSD at the same time.
Here is my project setup:

As you can see I only have XML files in this project. I have grouped the following files together in directories in the resources folder:

  • xml
  • Contains xml file that respects the xsd that is generated. This file is used to validate it against the generated xsd to make sure it is valid. In this example ‘customer-msg.xml’ it will look like:

    <?xml version="1.0" encoding="UTF-8"?>
    <ns0:customer xmlns:ns0="htpp://www.redstream.nl/customer">
        <ns0:firstName>Pascal</ns0:firstName>
        <ns0:lastName>Alma</ns0:lastName>
        <ns0:address>
            <ns0:street>sesamestreet</ns0:street>
            <ns0:nr>13</ns0:nr>
            <ns0:postalCode>
                <ns0:firstPart>1234</ns0:firstPart>
                <ns0:secondPart>AB</ns0:secondPart>
            </ns0:postalCode>
        </ns0:address>
    </ns0:customer>
    
  • xsd
  • Contains the xsd’s, both the ‘concept’ one as well as the generated ‘final’ one.

    • concept
    • As said earlier this one contains the ‘concept’ xsd. In this example ‘customer_concept.xsd’ it looks like:

      <?xml version="1.0" encoding="UTF-8"?>
      <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
          <xs:complexType name="Address">
              <xs:sequence>
                  <xs:element name="street" type="xs:string"/>
                  <xs:element name="nr" type="xs:integer"/>
                  <xs:element name="postalCode" type="myPostalCode"/>
              </xs:sequence>
          </xs:complexType>
          <xs:complexType name="Customer">
              <xs:sequence>
                  <xs:element name="firstName" type="xs:string" minOccurs="0"/>
                  <xs:element name="lastName" type="xs:string"/>
                  <xs:element name="address" type="Address"/>
              </xs:sequence>
          </xs:complexType>
      </xs:schema>
      
    • final
    • Here is the output put. In this example ‘customer.xsd’ it looks like:

      <?xml version="1.0" encoding="UTF-8"?>
      <xs:schema xmlns:tns="htpp://www.redstream.nl/customer"
                 xmlns:xs="http://www.w3.org/2001/XMLSchema"
                 targetNamespace="htpp://www.redstream.nl/customer"
                 elementFormDefault="qualified">
         <xs:element name="customer" type="tns:CustomerType"/>
          <xs:complexType name="AddressType">
              <xs:sequence>
                  <xs:element name="street" type="xs:string"/>
                  <xs:element name="nr" type="xs:integer"/>
                  <xs:element name="postalCode" type="tns:myPostalCodeType"/>
              </xs:sequence>
          </xs:complexType>
          <xs:complexType name="CustomerType">
              <xs:sequence>
                  <xs:element name="firstName" type="xs:string" minOccurs="0"/>
                  <xs:element name="lastName" type="xs:string"/>
                  <xs:element name="address" type="tns:AddressType"/>
              </xs:sequence>
          </xs:complexType>
         <xs:complexType name="myPostalCodeType">
            <xs:sequence>
               <xs:element name="firstPart">
                  <xs:simpleType>
                     <xs:restriction base="xs:string">
                        <xs:length value="4"/>
                     </xs:restriction>
                  </xs:simpleType>
               </xs:element>
               <xs:element name="secondPart">
                  <xs:simpleType>
                     <xs:restriction base="xs:string">
                        <xs:length value="2"/>
                     </xs:restriction>
                  </xs:simpleType>
               </xs:element>
            </xs:sequence>
         </xs:complexType>
      </xs:schema>
      
  • xslt
  • And finally the one that does all the work. In this example ‘transform_customer.xslt’ it looks like:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema">
        <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
        <!-- first copy the root and apply templates-->
        <xsl:template match="/">
            <xsl:copy>
                <!-- copy the attributes (if any) of the root element -->
                <xsl:copy-of select="@*"/>
                <xsl:apply-templates/>
            </xsl:copy>
        </xsl:template>
        <!-- match any other element and copy it with the attributes -->
        <xsl:template match="*">
            <xsl:copy>
                <xsl:copy-of select="@*"/>
                <xsl:apply-templates/>
            </xsl:copy>
        </xsl:template>
        <!-- Add the necessary namespaces to the root of the schema -->
        <xsl:template match="xs:schema">
            <!-- Recreate the xs:schema element-->
            <xsl:element name="xs:schema">
                <!-- Copy the existing attributes in the original xs:schema element to this one -->
                <xsl:copy-of select="@*"/>
                <!-- add Namespace -->
                <xsl:attribute name="targetNamespace">htpp://www.redstream.nl/customer</xsl:attribute>
                <xsl:attribute name="elementFormDefault">qualified</xsl:attribute>
                <xsl:namespace name="tns">htpp://www.redstream.nl/customer</xsl:namespace>
    
                <!-- add toplevel element -->
                <xsl:element name="xs:element">
                    <xsl:attribute name="name">customer</xsl:attribute>
                    <xsl:attribute name="type">tns:CustomerType</xsl:attribute>
                </xsl:element>
                <!-- continue with matching al child elements of the xs:schema element-->
                <xsl:apply-templates/>
                <!-- call a custom template to add an extra complexType to the schema that couldn't be generated by the modelling tool-->
                <xsl:call-template name="addPostalCodeType"/>
            </xsl:element>
        </xsl:template>
        <!-- suffix all complexType names with 'Type' -->
        <xsl:template match="xs:complexType">
            <xsl:choose>
                <!-- Check if the type already has the suffix 'Type' -->
                <xsl:when test="substring(@name, (string-length(@name)-3)) = 'Type'">
                    <!-- if so, just copy the existing attributes -->
                    <xsl:copy>
                        <xsl:copy-of select="@*"/>
                        <xsl:apply-templates/>
                    </xsl:copy>
                </xsl:when>
                <xsl:otherwise>
                    <!-- if not, copy all attribute and overwrite the attribute 'name' so the suffix 'Type' is added-->
                    <xsl:copy>
                        <xsl:copy-of select="@*"/>
                        <xsl:attribute name="name"><xsl:value-of select="@name"/>Type</xsl:attribute>
                        <xsl:apply-templates/>
                    </xsl:copy>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:template>
        <!-- Prefix all types with 'tns:'. All default xs: types are untouched.    -->
        <xsl:template match="xs:element">
            <!-- Create a variable with the name of the type of the element in it. It appears this 'type' attribute
                can be of different types so take this into account -->
            <xsl:variable name="elementType">
                <xsl:choose>
                    <xsl:when test="@type instance of xs:string or @type instance of xs:untypedAtomic">
                        <xsl:choose>
                            <xsl:when test="contains(@type,':')">
                                <xsl:value-of select="substring-after(@type,':')"/>
                            </xsl:when>
                            <xsl:otherwise>
                                <xsl:value-of select="@type"/>
                            </xsl:otherwise>
                        </xsl:choose>
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:value-of select="@type"/>
                    </xsl:otherwise>
                </xsl:choose>-->
            </xsl:variable>
            <!-- Create a variable with the namespace prefix of the attribute if any. -->
            <xsl:variable name="elementTypePrefix">
                <xsl:choose>
                    <xsl:when test="@type instance of xs:QName">
                        <xsl:value-of select="prefix-from-QName(@type)"/>
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:value-of select="substring-before(@type,':')"/>
                    </xsl:otherwise>
    
                </xsl:choose>
            </xsl:variable>
            <!-- Now the variables are filled we can use it to determine if we need to add a prefix -->
            <xsl:choose>
    
                <!-- if type starts with xs: the prefix must not be changed -->
                <xsl:when test="$elementTypePrefix = 'xs'">
                    <xsl:copy>
                        <xsl:copy-of select="@*"/>
                        <xsl:apply-templates/>
                    </xsl:copy>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:copy>
                        <xsl:copy-of select="@*"/>
                        <!-- A few steps back we renamed complextypes so they ended with 'Type'. Now we have to suffix the places
                  where the types are reffered also with 'Type' -->
                        <xsl:if test="substring($elementType, (string-length($elementType)-3)) != 'Type'">
                            <xsl:attribute name="type">tns:<xsl:value-of select="@type"/>Type</xsl:attribute>
                        </xsl:if>
                        <xsl:if test="substring($elementType, (string-length($elementType)-3)) = 'Type'">
                            <xsl:attribute name="type">tns:<xsl:value-of select="@type"/></xsl:attribute>
                        </xsl:if>
                        <xsl:apply-templates/>
                    </xsl:copy>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:template>
        <!-- The next template generates the complexType 'myPostalCodeType' when the template is called -->
        <xsl:template name="addPostalCodeType">
            <xsl:element name="xs:complexType">
                <xsl:attribute name="name">myPostalCodeType</xsl:attribute>
                <xsl:element name="xs:sequence">
                    <xsl:element name="xs:element">
                        <xsl:attribute name="name">firstPart</xsl:attribute>
                        <xsl:element name="xs:simpleType">
                            <xsl:element name="xs:restriction">
                                <xsl:attribute name="base">xs:string</xsl:attribute>
                                <xsl:element name="xs:length">
                                    <xsl:attribute name="value">4</xsl:attribute>
                                </xsl:element>
                            </xsl:element>
                        </xsl:element>
                    </xsl:element>
                    <xsl:element name="xs:element">
                        <xsl:attribute name="name">secondPart</xsl:attribute>
                        <xsl:element name="xs:simpleType">
                            <xsl:element name="xs:restriction">
                                <xsl:attribute name="base">xs:string</xsl:attribute>
                                <xsl:element name="xs:length">
                                    <xsl:attribute name="value">2</xsl:attribute>
                                </xsl:element>
                            </xsl:element>
                        </xsl:element>
                    </xsl:element>
                </xsl:element>
            </xsl:element>
        </xsl:template>
    </xsl:stylesheet>
    

And of course the pom file. This one looks like this:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>net.pascalalma.xsd</groupId>
    <artifactId>xslt-project</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>${artifactId}</name>

    <!-- Use relative paths otherwise stylesheet includes will fail miserably -->
    <properties>
		<xsd.path>src/main/xsd</xsd.path>
        <xsd.path.concept>${xsd.path}/concept</xsd.path.concept>
		<xsd.path.final>${xsd.path}/final</xsd.path.final>
		<xslt.path>src/main/xslt</xslt.path>
		<xml.path>src/main/xml</xml.path>
	</properties>
    <build>
        <plugins>
            <!-- Copy the basic-types.xsd to the target location. This one doesn't
            need any translation -->
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-resources</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${xsd.path.final}</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>${xsd.path}</directory>
                                    <includes>
                                        <include>basic-types.xsd</include>
                                    </includes>
                                    <filtering>false</filtering>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!-- Transform all 'concept' xsd to the correct 'final' xsd's -->
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>xml-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>transform</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <transformationSets>
                        <transformationSet>
                            <dir>${xsd.path.concept}</dir>
                            <includes>
                                <include>customer_concept.xsd</include>
                            </includes>
                            <stylesheet>${xslt.path}/transform_customer.xslt</stylesheet>
                            <outputDir>${xsd.path.final}</outputDir>
                            <fileMappers>
                                <fileMapper implementation="org.codehaus.plexus.components.io.filemappers.MergeFileMapper">
                                    <targetName>customer.xsd</targetName>
                                </fileMapper>
                            </fileMappers>
                        </transformationSet>

                    </transformationSets>
                </configuration>
                <!-- make the translation working with xslt2.0 -->
                <dependencies>
                    <dependency>
                        <groupId>net.sf.saxon</groupId>
                        <artifactId>saxon-B</artifactId>
                        <version>9.1</version>
                    </dependency>
                </dependencies>
            </plugin>
            <!-- Validate the generated XSD against valid XML files to make
            sure nothing broke-->
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>xml-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>validate</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <validationSets>
                        <validationSet>
                            <dir>${xml.path}/</dir>
                            <includes>
                                <include>customer-msg.xml</include>
                            </includes>
                            <validating>true</validating>
                            <systemId>${xsd.path.final}/customer.xsd</systemId>
                        </validationSet>
                    </validationSets>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>net.sf.saxon</groupId>
                        <artifactId>saxon-B</artifactId>
                        <version>9.1</version>
                    </dependency>
                </dependencies>
            </plugin>
            <!-- Plugin to clean the generated XSD's when rebuilding the project -->
            <plugin>
                <artifactId>maven-clean-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <filesets>
                        <fileset>
                            <directory>${xsd.path.final}</directory>
                            <includes>
                                <include>**/*.xsd</include>
                            </includes>
                        </fileset>
                    </filesets>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

This completes the example of how you can perform xslt transformations within the Maven build cycle.

About Pascal Alma

Pascal is a senior IT consultant and has been working in IT since 1997. He is monitoring the latest development in new technologies (Mobile, Cloud, Big Data) closely and particularly interested in Java open source tool stacks, cloud related technologies like AWS and mobile development like building iOS apps with Swift. Specialties: Java/JEE/Spring Amazon AWS API/REST Big Data Continuous Delivery Swift/iOS
This entry was posted in Maven, XML/ XSD/ XSLT and tagged , . Bookmark the permalink.

2 Responses to XSL Transformations with Maven

  1. Mads says:

    What Maven repo do you have the Saxon-B jars hosted? I haven’t been able to find anywhere that has them.

  2. Pascal Alma says:

    Hmm, I can’t recall where I got them. Possibly I downloaded it here:http://saxon.sourceforge.net/ and then installed it manually in our Artifactory (http://sourceforge.net/projects/artifactory/).

Comments are closed.