Modifying an XML Schema with XSLT

Recently I ran into an issue with a XSD that I had generated by using our modelling tool. It appeared the XSD did not match with my wishes so I had to modify the XSD. Now I could do this by hand quite easily but that would mean I had to redo it every time I regenerated the XSD after a model change. So I decided to use XSLT for the modifications. I have used XSLT quite extensively at previous projects but these where about 8 years ago and it just made me realize how fast one looses his knowledge if you don’t use it anymore.
Anyway, to avoid this situation for the next few years I decided to post about the basics of XSLT that I used in this situation so I always have a reference to get back at. The situation was that an XSD was generated but it was lacking several things:

  • no use of namespaces (both import and prefixes)
  • names of custom defined types didn’t end with the suffix ‘Type’
  • some complex types were missing

And several more things but the idea stays the same.
Here is an example XSD that could be generated by the tool:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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>

Here is how I would like the XSD to be look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?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">
	<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>

To get to this last XSD I used the following XSLT (see inline documentation for explanation):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<?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:namespace name="tns">htpp://www.redstream.nl/customer</xsl:namespace>
			<xsl:attribute name="targetNamespace">htpp://www.redstream.nl/customer</xsl:attribute>
			<!-- 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:when test="@type instance of xs:string">
					<xsl:value-of select="substring-before(@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>

Although this example might be very specific and there are better ways to obtain similar results, to me this might be a handy reminder how to solve an issue with XSLT and perhaps it might even help you in some way. In another post I will show how I do this translation in Maven and also validate the outcome.

tags:

About Pascal Alma

Pascal started as an Oracle Developer in 1997 and developed numerous applications with Oracle Designer/Developer and PL/SQL. Since 2001 Pascal becomes more and more active with the development of software at the Java/J2EE platform. Nowadays Pascal is a senior JEE Developer/ Architect and has a lot of experience with several open source initiatives/ frameworks especially within the Enterprise Integration area. Besides these technical skills Pascal is a big Scrum enthusiastic.