SOAP and XML Processing

The following tutorial uses the example project codebase. Make sure you have the repository cloned so you can follow along and run the tests.

This example creates a SOAP endpoint on the test server that makes a follow-up SOAP request to an external endpoint. SOAP-based APIs use XML for their data format, so a large part of this tutorial focuses on how flows still process the data as JSON compliant objects.

Creating the Flow

Open the Kotlin file located at main/kotlin/flowexamples/e05/E05SoapFlow.kt. This is where the flow specification is written, along with the other information needed to deploy a SOAP endpoint.

SOAP Resources

In earlier tutorials, we created REST endpoints using OpenAPI specifications. With SOAP, we do something similar using WSDL documents. Note the following variable declarations in the E05SoapFlow object:

val soapFrontendDefinition = fromClasspath("/EchoService.wsdl")
val soapBackendDefinition = fromClasspath("/NumberConversionService.wsdl")

The NumberConversionService.wsdl file is copied from the Number Conversion Service, a third-party API we will use to convert a number to words. The other file, EchoService.wsdl, represents our own web service to receive and echo back a single string.

Open the file located at main/resources/EchoService.wsdl. This WSDL document includes an XML schema with the following elements:

<xsd:element name="SayHi">
    <xsd:complexType>
        <xsd:sequence>
            <xsd:element name="Hi" type="xsd:string"/>
        </xsd:sequence>
    </xsd:complexType>
</xsd:element>
<xsd:element name="SayHiResponse">
    <xsd:complexType>
        <xsd:sequence>
            <xsd:element name="HiResponse" type="xsd:string"/>
        </xsd:sequence>
    </xsd:complexType>
</xsd:element>

These elements outline the expected request and response data for the web service.

If you have a schema that uses imports or includes, you will need to use an additional merge tool to combine the definitions into a single resource. See the SOAP Web Service Tools section for more details.

The web service for the SOAP API is defined farther down in the WSDL document with the following element:

<service name="EchoService">
    <port name="EchoService" binding="tns:EchoServiceBinding">
        <soap:address location="http://api.bioinfo.no/services/EchoService"/>
    </port>

    ...
</service>

The <service> element lists several network endpoints as child <port> elements, but the only one we’re concerned with is the EchoService endpoint/port. We will reference the names of this port and service when creating the flow configuration.

Revisit the E05SoapFlow.kt file. The following lines in the E05SoapFlow object establish two XML namespace variables:

const val echoNamespace = "http://www.bccs.uib.no/EchoService.wsdl"
const val numberConversionNamespace = "http://www.dataaccess.com/webservicesserver/"

These namespaces match the namespaces in the WSDL files. We assign them to variables, because we’ll need to reference them frequently as we move forward.

Lastly, we create resource keys for each WSDL file, as the following code shows:

val soapFrontendResourceKey = newResourceRevisionKey {
    ownerId = OWNER_ID
    type = "WSDLv1"
    id = "soap-echo-num-conversion"
    revision = "latest"
}

Similar to REST resources, the flow spec references the WSDL with an identifier and not the content directly. For example, the above resource key will become WSDLv1:soap-echo-num-conversion:latest after being deployed.

Flow Specification

Farther into the E05SoapFlow object, we create a flow spec configuration with the following code:

val soapFrontendFlowSpec = flowConfig {
    id = "soap"
    description = "SOAP Flow"
    ownerId = OWNER_ID
    exchangePattern = RequestResponse

    soapApi {
        id = "soap-echo-num-conversion-api"
        wsdlSpecId = soapFrontendResourceKey.toResourceIdentifier()
        serviceName = "EchoService"
        portName = "EchoService"
        namespacePrefixMapping = """
            _default = $echoNamespace
        """.trimIndent()
    }

    ...
}

The exchangePattern is set to RequestResponse, but because the first processor is a soapApi processor, the expected request and response will be XML. However, flow processors still rely on JSON compliant data to perform their operations. As such, XML data is dynamically converted to JSON (and vice versa) when needed. See the documentation on payload types for more details.

With this JSON conversion in mind, an XML-based request to our SOAP endpoint will internally become the following:

{
  "_declaration": {
    "version": "1.0",
    "standalone": "no"
  },
  "SayHi": {
    "Hi": {
      "_text": "hello world"
    }
  },
  "_xmlns": {
    "_default": "http://www.bccs.uib.no/EchoService.wsdl"
  }
}

Note how the data includes an _xmlns property with applicable namespaces. In the soapApi processor, we set the _default namespace to the EchoService namespace to remove prefixes from the data. If we were to omit the namespacePrefixMapping, the data would look like the following, where data that falls under the namespace is now prefixed with an auto-generated ns1_:

"ns1_SayHi": {
  "ns1_Hi": {
    "_text": "hello world"
  }
},
"_xmlns": {
  "ns1": "http://www.bccs.uib.no/EchoService.wsdl"
}

These prefixes are important for transforming the data. The following map processor in the flow does exactly that:

map {
    id = "echo-req-to-num-conversion"
    mapSpec = echoRequestToNumberConversionRequestMapping
}

Recall that data mappings can format and output JSON compliant objects, and these mappings can be stored in separate variables instead of being hardcoded inline. The purpose of this map processor is to transform the data from the soapApi processor into an XML compliant JSON object that the following soapRequest processor can understand:

soapRequest {
    id = "soap-num-conversion-request"
    address = URL("https://www.dataaccess.com/webservicesserver/NumberConversion.wso")
    wsdlSpecId = soapBackendResourceKey.toResourceIdentifier()
    serviceName = "NumberConversion"
    portName = "NumberConversionSoap"
    responseNamespacePrefixMapping = """
        n = $numberConversionNamespace
    """.trimIndent()
}

The soapRequest processor makes a request to an external SOAP endpoint, referencing the information from the NumberConversionService WSDL. As a SOAP endpoint, its request and response are still formatted as XML, but the processor dynamically performs the XML/JSON conversion for us.

The soapRequest processor then passes a JSON compliant object to the following map processor:

map {
    id = "no-to-words-resp-to-echo-resp"
    mapSpec = numberConversionResponseToEchoResponseMapping
}

This processor uses another mapping to convert the data from the number service into a format that our own EchoService endpoint can understand, thus fulfilling the flow response.

Transformation Logic

Let’s look at the data transformations more closely. Open the file located at main/kotlin/flowexamples/e05/E05RequestTransformationMapping.kt. This file includes the following mapping logic:

val echoRequestToNumberConversionRequestMapping = """
    {
        "_xmlns" : {
            "n" : "$numberConversionNamespace"
        },
        "n_NumberToWords" : {
            "n_ubiNum" : {
                "_text": #input.payload.SayHi.Hi._text
            }
        }
    }
""".trimIndent()

This mapping converts the payload from the soapApi processor into the following JSON format:

{
  "_xmlns": {
    "n": "http://www.dataaccess.com/webservicesserver/"
  },
  "n_NumberToWords": {
    "n_ubiNum": {
      "_text": "10428"
    }
  }
}

By following this format, the soapRequest processor will make an actual SOAP/XML request for us. Note how n_NumberToWords and n_ubiNum match the XML schema from the NumberConversionService WDSL, per the following:

<xs:element name="NumberToWords">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="ubiNum" type="xs:unsignedLong"/>
        </xs:sequence>
    </xs:complexType>
</xs:element>

To avoid namespace conflicts, a namespace must be appended to the _xmlns object. The name of the property (e.g., _xmlns.n) can be anything as long as the mapped properties (e.g., n_NumberToWords.n_ubiNum) use it as their prefix. We don’t include a prefix when accessing SayHi.Hi, because the EchoService namespace was already set as the default.

Open the E05ResponseTransformationMapping.kt file next. This file uses the following mapping specification to transform the data:

val numberConversionResponseToEchoResponseMapping = """
    {
        "_xmlns" : {
            "_default" : "$echoNamespace"
        },
        "SayHiResponse" : {
            "HiResponse" : {
                "_text" : #input.payload.n_NumberToWordsResponse.n_NumberToWordsResult._text
            }
        }
    }
""".trimIndent()

The mapping formats the incoming payload (#input.payload) into an XML compliant object for the EchoService response. The soapRequest processor used a responseNamespacePrefixMapping property, so the number-specific data is prefixed with n_. We don’t want our response data prefixed, though, so we add a _default that points to the EchoService namespace.

The flow will then convert this JSON to XML and respond with a message similar to the following:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <SayHiResponse xmlns="http://www.bccs.uib.no/EchoService.wsdl">
      <HiResponse>ten thousand four hundred and twenty eight </HiResponse>
    </SayHiResponse>
  </soap:Body>
</soap:Envelope>

Testing the Flow

Open the Kotlin class located at test/kotlin/flowexamples/e05/E05SoapFlowTest.kt. The first thing the test does is establish the following baseline variables:

val number = "10428"
val numberAsWord = "ten thousand four hundred and twenty eight"

When the flow receives the number 10428, we expect the number conversion service to convert and return "ten thousand four hundred and twenty eight".

Next, we create the SOAP resources for the echo and number conversion services and add them to the test context along with the flow itself. The following code accomplishes these steps:

val frontendSoapApiResource = Resource(key = soapFrontendResourceKey, content = soapFrontendDefinition)
val backendSoapApiResource = Resource(key = soapBackendResourceKey, content = soapBackendDefinition)

ctx.addFlowTestConfig {
    resource(frontendSoapApiResource)
    resource(backendSoapApiResource)
    flow(soapFrontendFlowSpec)
}

When testing REST APIs, we used the SDK’s restApiEndpoint() function to make an HTTP request. SOAP APIs require a little more setup. First, we create the following WebServiceClientConfig object:

val clientConfig = WebServiceClientConfig.newWebServiceClientConfig {
    flowSpec = soapFrontendFlowSpec
    serviceName = "EchoService"
    portName = "EchoService"
    wsdlDoc = soapFrontendDefinition
}

The clientConfig must use the same WSDL information of the service we want to connect to. Next, we use the following code to set up the flow test and client with nested callback functions:

flowTest(ctx) {
    val nsMap = mapOf("_default" to echoNamespace)
    withWebServiceFlowClient(clientConfig) {
        ...
    }
}

The remaining test logic is written in the withWebServiceFlowClient() callback. Fortunately, we do not need to format our own XML for the SOAP request and can let the test components dynamically convert JSON to XML for us. We simply need to provide an XML compliant object. The following code uses a mapOf() function to create such an object:

val xmlJson = mapOf(
    "_xmlns" to nsMap,
    "SayHi" to mapOf("Hi" to mapOf("_text" to number))
)
The client also supports alternative payload modes like XML strings and Document or JAXB objects.

The XML compliant object is passed to the request() function to make the actual SOAP request, per the following line:

val response = request(xmlJson, nsMap)

The request() function returns the response as a JsonCompliantMap that will look similar to the following:

{
  "_xmlns": {
    "_default": "http://www.bccs.uib.no/EchoService.wsdl"
  },
  "SayHiResponse": {
    "HiResponse": {
      "_text": "ten thousand four hundred and twenty eight "
    }
  }
}

We then use assertThat() to compare the returned object with a mapOf() object of our own, as the following code demonstrates:

assertThat(response).isEqualTo(
    mapOf(
        "_declaration" to mapOf("standalone" to "no", "version" to "1.0"),
        "_xmlns" to nsMap,
        "SayHiResponse" to mapOf("HiResponse" to mapOf("_text" to "$numberAsWord "))
    )
)

If the response includes a nested property called _text with a value of "ten thousand four hundred and twenty eight ", then our test has been successful! Note, however, that we needed to account for the extra space that the number conversion service appends to the end of the string.

To further practice with SOAP and XML, try changing the namespace prefixes or the service/port names and verifying that the test still passes. Also read up on the other SOAP web service tools that are available in the SDK.