Back to the Future: SOAP and XSLT with Gloo Edge 1.8

No, you don’t need a time machine to encounter SOAP (Simple Object Access Protocol), an XML messaging protocol from the turn of the century. SOAP remains prevalent today for enterprise web services across a number of industries, including financial services and healthcare. Unfortunately, SOAP (and associated legacy middleware applications) hold back large scale modernization efforts because there isn’t a clear migration that enables incremental deprecation of SOAP web services over time while gradually adopting newer APIs such as REST, gRPC, and GraphQL.

What if there was a clear and present migration path that was congruent with moving to the cloud and Kubernetes? Something that is fast, lightweight, modern, but still supports XML and XSLT to transform SOAP messages into modern JSON? Even better, what if this migration path enabled termination of expensive, multi-year contracts for legacy middleware? And what if it might eliminate dozens of servers in your data center as well? Such a migration path finally exists!

With Gloo Edge Enterprise 1.8 (in beta now), we have added XSLT 3.0 (Extensible Stylesheet Language Transformations) support, and it can be used to modernize SOAP/XML clients and endpoints without fully eliminating SOAP from your web service portfolio. Should it take years to fully decommission your SOAP web services, you can retain backward compatibility while adopting modern protocols and run them in parallel, all on Kubernetes.

Our benchmarking shows that a simple SOAP XML to JSON XSLT transformation performs with the same scalability as the rest of Gloo Edge, serving around 9.5K requests per second.

XML to XSLT transforms

This article will dive in further into our implementation and offer a guide for trying out XSLT in Gloo Ege 1.8.

Embedded XSLT Engine for Envoy Proxy

Gloo Edge Enterprise 1.8 injects an XSLT engine into its Envoy-based architecture, and this differs from legacy, monolithic API Gateway products that run XSLT either as an internalized, monolithic implementation, or an external service to be called by the API Gateway. Figure 1 shows the three stereotypical API gateway architectures that support XSLT.

 

Spectrum of applications

Figure 1. Common XSLT Implementations for API Gateways

The implementation of XSLT for Gloo Edge is revolutionary because it runs in-process with Envoy directly on the request path without an additional network hop to an external XSLT service. The XSLT engine uses the industry-standard Saxon HE XSLT library that is compiled natively from Java to a C library using Oracle’s open-source GraalVM native image compiler. As such, a Java virtual machine process is not required, and the XSLT engine can be embedded into the Envoy process itself. As such, we get all the performance benefits of Envoy’s highly-parallel threading model, even when performing XSLT transformations.

A major benefit of using Saxon is the adherence to the XSLT 3.0 specification that offers xml-to-json and json-to-xml functions built in. With other products, based on XSLT 1.0/2.0, equivalent APIs are proprietary, more complex, and slower to execute.

Getting Started with XSLT in Gloo Edge 1.8

First, you can find the full guide for SOAP with Gloo Edge 1.8, as well as example code, in our documentation here. 

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 command 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 Edge 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.

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 Edge 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.

Next Steps

We’ve talked about the clear advantage that Gloo Edge brings to the table, enabling a migration path from legacy SOAP services without having to rewrite them. To tie it all together, this feature is built in to the latest release of Gloo Edge Enterprise, available to all enterprise customers at no additional cost. Getting started with XSLT transformations is as simple as installing Gloo Edge Enterprise (version 1.8 or later), and following the guide above. 

To learn more or getting started, please see our SOAP/XSLT transformation guide or visit our Gloo Edge install page.

You can also watch our webinar on this topic, sign up here.