SOAP/XSLT transformation (Enterprise)

This feature requires a Gloo Gateway Enterprise license.

Introduction

SOAP remains prevalent today for enterprise web services across a number of industries, including financial services and healthcare. However, SOAP uses XML, a message format over 2 decades old. Modern services have adopted newer message formats, one of which is JSON. Modernizing a legacy SOAP service to use JSON can often mean rewriting the service entirely. This guide shows you a way of allowing for clients and services' message formats to differ by performing the translation within Gloo Gateway. We leverage powerful XSLT transformations to allow for an XML-based SOAP service to communicate with a JSON client.

Setup

This guide assumes that you have deployed Gloo to the gloo-system namespace and that the glooctl command line utility is installed on your machine. glooctl provides several convenient functions to view, manipulate, and debug Gloo resources; in particular, it is worth mentioning the following command, which we will use each time we need to retrieve the URL of the Gloo Gateway that is running inside your cluster:

glooctl proxy url
We will also be using the jq commmand line utility to pretty print JSON strings.

This guide uses a custom image for running a SOAP service. The source code for this image is available in the Gloo Gateway repository in docs/examples/xslt-guide. In this guide, we pull the pre-built image from a remote repository, but if you want to rebuild the image, simply run make docker-local.

Creating the SOAP service

Deployment

Let’s start by deploying our SOAP service. This service is a simple service that can be queried with a city name query, and the service will fuzzy find the city name amongst a list of world cities, responding with the data for each city that matches the query.

kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: world-cities-soap-service
spec:
  selector:
    matchLabels:
      app: world-cities-soap-service
  replicas: 1
  template:
    metadata:
      labels:
        app: world-cities-soap-service
    spec:
      containers:
        - name: world-cities-soap-service
          image: quay.io/solo-io/world-cities-soap-service:0.0.1
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
EOF

Service and Upstream

We will also create a Kubernetes service object to route traffic to.

kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: world-cities-soap-service
  labels:
    app: world-cities-soap-service
spec:
  ports:
  - port: 8080
    protocol: TCP
  selector:
    app: world-cities-soap-service
EOF

Once you create the service, Gloo Gateway discovery should have created an upstream, which you can confirm by running

glooctl get us --name default-world-cities-soap-service-8080

which should output

+----------------------------------------+------------+----------+--------------------------------+
|                UPSTREAM                |    TYPE    |  STATUS  |            DETAILS             |
+----------------------------------------+------------+----------+--------------------------------+
| default-world-cities-soap-service-8080 | Kubernetes | Accepted | svc name:                      |
|                                        |            |          | world-cities-soap-service      |
|                                        |            |          | svc namespace: default         |
|                                        |            |          | port:          8080            |
|                                        |            |          |                                |
+----------------------------------------+------------+----------+--------------------------------+

Virtual Service

We can create a simple virtual service for this upstream, telling the gateway-proxy to route all traffic to it.

kubectl apply -f - <<EOF
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: world-city-service-vs
  namespace: gloo-system
spec:
  virtualHost:
    domains:
      - '*'
    routes:
      - matchers:
        - prefix: /
        routeAction:
          single:
            upstream:
              # Upstream generated by gloo edge discovery
              name: default-world-cities-soap-service-8080
              namespace: gloo-system
        options:
          autoHostRewrite: true
EOF

Querying the SOAP service

SOAP services communicate to clients via XML. We can query this SOAP service with an XML curl:

curl $(glooctl proxy url) -H "SOAPAction:findCity" -H "content-type:application/xml" \
-d '<?xml version="1.0" encoding="UTF-8"?>
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap="http://schemas.xmlsoap.org/soap/">
   <Header />
   <Body>
      <Query>
         <CityQuery>south bo</CityQuery>
      </Query>
      \
   </Body>
</Envelope>' 

The service should return back the results as XML:

<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
	<Header xmlns="http://schemas.xmlsoap.org/soap/envelope/"></Header>
	<Body xmlns="http://schemas.xmlsoap.org/soap/envelope/">
		<Content>
			<Match>
				<City>south boston</City>
				<Country>United States</Country>
				<SubCountry>Massachusetts</SubCountry>
				<GeoNameId>4951305</GeoNameId>
			</Match>
			<Match>
				<City>south peabody</City>
				<Country>United States</Country>
				<SubCountry>Massachusetts</SubCountry>
				<GeoNameId>4951473</GeoNameId>
			</Match>
			<Match>
				<City>south bradenton</City>
				<Country>United States</Country>
				<SubCountry>Florida</SubCountry>
				<GeoNameId>4173392</GeoNameId>
			</Match>
			<Match>
				<City>south burlington</City>
				<Country>United States</Country>
				<SubCountry>Vermont</SubCountry>
				<GeoNameId>5241248</GeoNameId>
			</Match>
		</Content>
	</Body>
</Envelope>

Modernizing the SOAP service to JSON using transformation

We can convert our XML communication to JSON using XSLT transformations on the request/response path. To do so, we must modify our virtual service to include the transformations.

kubectl apply -f - <<EOF
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
  name: world-city-service-vs
  namespace: gloo-system
spec:
  virtualHost:
    domains:
      - '*'
    routes:
      - matchers:
        - prefix: /
        routeAction:
          single:
            upstream:
              # Upstream generated by gloo edge discovery
              name: default-world-cities-soap-service-8080
              namespace: gloo-system
        options:
          autoHostRewrite: true
          stagedTransformations:
            regular: # any transformation stage is fine here
              requestTransforms:
                - requestTransformation:
                    xsltTransformation:
                      xslt: |
                        <?xml version="1.0" encoding="UTF-8"?>
                          <xsl:stylesheet
                          xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                          xmlns:math="http://www.w3.org/2005/xpath-functions/math"
                          xmlns:xs="http://www.w3.org/2001/XMLSchema"
                          exclude-result-prefixes="xs math" version="3.0">
                            <xsl:output indent="yes" omit-xml-declaration="yes" />
                            <xsl:strip-space elements="*"/>
                            <xsl:template match="/" xmlns="http://schemas.xmlsoap.org/soap/envelope/">
                              <Envelope >
                                <Header/>
                                <Body>
                                  <Query>
                                    <xsl:apply-templates select="json-to-xml(.)/*"/>
                                  </Query>
                                </Body>
                              </Envelope>
                            </xsl:template>
                            <xsl:template match="map" xpath-default-namespace="http://www.w3.org/2005/xpath-functions"
                            xmlns:web="http://www.qas.com/OnDemand-2011-03">
                              <CityQuery><xsl:value-of select="string[@key='cityQuery']" /></CityQuery>
                            </xsl:template>
                          </xsl:stylesheet>                        
                      nonXmlTransform: true
                      setContentType: text/xml
                  responseTransformation:
                    xsltTransformation:
                      xslt: |
                        <?xml version="1.0" encoding="UTF-8"?>
                        <xsl:stylesheet
                        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                        xmlns:xs="http://www.w3.org/2001/XMLSchema"
                        xpath-default-namespace="http://schemas.xmlsoap.org/soap/envelope/"
                        version="3.0">
                          <xsl:output method="text" omit-xml-declaration="yes" />
                          <xsl:variable name="myMap">
                            <map xmlns="http://www.w3.org/2005/xpath-functions">
                              <array key="matches" >
                                <xsl:for-each select="/Envelope/Body/Content/Match">
                                  <map>
                                    <string key="city"><xsl:value-of select="City"/></string>
                                    <string key="country"><xsl:value-of select="Country" /></string>
                                    <string key="subCountry"><xsl:value-of select="SubCountry" /></string>
                                    <string key="geoNameId"><xsl:value-of select="GeoNameId" /></string>
                                  </map>
                                </xsl:for-each>
                              </array>
                            </map>
                          </xsl:variable>
                          <xsl:template match="/">
                            <xsl:apply-templates select="xml-to-json($myMap, map{'indent': true()})" />
                          </xsl:template>
                        </xsl:stylesheet>                        
                      setContentType: application/json
EOF

Running glooctl check after modifying the Virtual Service should show that the Virtual Service and Proxy have been accepted.

Querying the service with JSON

Now that the XSLT transformation is in place, we can query the service:

curl $(glooctl proxy url) -d '{"cityQuery": "south bo"}' -H "SOAPAction:findCity" -H "content-type:application/json" | jq

and should get back the following JSON:

{
  "matches": [
    {
      "city": "south boston",
      "country": "United States",
      "subCountry": "Massachusetts",
      "geoNameId": "4951305"
    },
    {
      "city": "south peabody",
      "country": "United States",
      "subCountry": "Massachusetts",
      "geoNameId": "4951473"
    },
    {
      "city": "south bradenton",
      "country": "United States",
      "subCountry": "Florida",
      "geoNameId": "4173392"
    },
    {
      "city": "south burlington",
      "country": "United States",
      "subCountry": "Vermont",
      "geoNameId": "5241248"
    }
  ]
}

What happened to our SOAP/xml service?

We previously queried the world cities service with XML, and got back the response as an XML, so how are we now seeing JSON from our client, even though we haven’t changed our service at all?

The XSLT transformations we’ve specified in our Virtual Service do all the work for us of translating our JSON -> XML for the service request, and XML -> JSON from the service response to the client. The requestTransformation specified on the VirtualService uses an XSLT 3.0 function, json-to-xml() to convert our JSON to an XML. The responseTransformation uses another function, xml-to-json() to convert the XML from the service back to a JSON, which we see above.

The XSLT Transformations

Request Transformation

Let’s break down what is happening in these transformations. On the request path, we have the following transformation config:

requestTransformation:
  xsltTransformation:
    xslt: |
      <?xml version="1.0" encoding="UTF-8"?>
        <xsl:stylesheet
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:math="http://www.w3.org/2005/xpath-functions/math"
        xmlns:xs="http://www.w3.org/2001/XMLSchema"
        exclude-result-prefixes="xs math" version="3.0">
          <xsl:output indent="yes" omit-xml-declaration="yes" />
          <xsl:strip-space elements="*"/>
          <xsl:template match="/" xmlns="http://schemas.xmlsoap.org/soap/envelope/">
            <Envelope >
              <Header/>
              <Body>
                <Query>
                  <xsl:apply-templates select="json-to-xml(.)/*"/>
                </Query>
              </Body>
            </Envelope>
          </xsl:template>
          <xsl:template match="map" xpath-default-namespace="http://www.w3.org/2005/xpath-functions"
          xmlns:web="http://www.qas.com/OnDemand-2011-03">
            <CityQuery><xsl:value-of select="string[@key='cityQuery']" /></CityQuery>
          </xsl:template>
        </xsl:stylesheet>      
    nonXmlTransform: true
    setContentType: text/xml

xslt: This is the main XSLT transformation. The highlighted line uses json-to-xml, an XSLT 3.0 function which transforms our JSON to the XML which the world cities service understands.

nonXmlTransform: This is set to true since we are transforming JSON to XML. Natively, XSLT can only transform XML data. However, our input to the transformation is JSON, so by specifying this flag, we signal to our XSLT transformation filter that we are supplying non-xml (JSON) data as the input.

setContentType: Since we are transforming the content type of the data from application/json to text/xml, we can set the new content-type header here.

Response transformation

responseTransformation:
  xsltTransformation:
    xslt: |
      <?xml version="1.0" encoding="UTF-8"?>
      <xsl:stylesheet
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      xmlns:xs="http://www.w3.org/2001/XMLSchema"
      xpath-default-namespace="http://schemas.xmlsoap.org/soap/envelope/"
      version="3.0">
      <xsl:output method="text" omit-xml-declaration="yes" />
        <xsl:variable name="myMap">
          <map xmlns="http://www.w3.org/2005/xpath-functions">
            <array key="matches" >
              <xsl:for-each select="/Envelope/Body/Content/Match">
                <map>
                  <string key="city"><xsl:value-of select="City"/></string>
                  <string key="country"><xsl:value-of select="Country" /></string>
                  <string key="subCountry"><xsl:value-of select="SubCountry" /></string>
                  <string key="geoNameId"><xsl:value-of select="GeoNameId" /></string>
                </map>
              </xsl:for-each>
            </array>
          </map>
        </xsl:variable>
        <xsl:template match="/">
          <xsl:apply-templates select="xml-to-json($myMap, map{'indent': true()})" />
        </xsl:template>
      </xsl:stylesheet>      
    setContentType: application/json

Once again, we can see the important line highlighted. The xml-to-json function translates the XML response from the server to the JSON that we see on the client side. We transform the content-type header from the server to application/json using the setContentType field.

Summary

In this guide, we installed Gloo Gateway Enterprise and created a SOAP service which uses XML as it’s message format. We were then able to modernize the service using XSLT transformations to convert JSON -> XML and XML -> JSON. This allowed us to query our SOAP service with a JSON query, and to receive a JSON response in return, without ever changing the service.