Search code examples
xmlgrailsgroovysoap

Building SOAP request in Grails


I have a requirment to send/receive data to a server from my grails (2.6) app using SOAP. I have managed to use SOAP UI to simulate what I want to do, and it works perfectly.

So, I'm looking at groovy-wslite, and also HTTPBuiler (I really dont mind what I use), but I'm a bit lost as I've never really done anything like this before.

The main problem is how I convert what I'm doing in SOAP UI to the grails environment. I've looked at loads of examples but nothing quite matches my scenario, as the WSDL file is downloaded from the server, and stored locally on my machine. So, I have a local WSDL file, and a remote server URL.

I also need to authenticate myself with the server using a username and password.

The actual xml is relatively striaghtforward (this is from SOAP UI) :-

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://www.cisco.com/AXL/API/14.0">
   <soapenv:Header/>
   <soapenv:Body>
      <ns:getCCMVersion>      
      </ns:getCCMVersion>
    </soapenv:Body>
 </soapenv:Envelope>

And this results in :-

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
   <soapenv:Body>
      <ns:getCCMVersionResponse xmlns:ns="http://www.cisco.com/AXL/API/14.0">
         <return>
              <componentVersion>
                   <version>14.0.1.12900(161)</version>
              </componentVersion>
         </return>
      </ns:getCCMVersionResponse>
   </soapenv:Body>
 </soapenv:Envelope>

SOAP UI allows me to create a project using the WSDL file on my local machine, and then allows me to set the binding for the project to my remote server, specifying the username and password.

But I'm at a loss as to how to translate all this into my Grails environment using either groovy-wslite or HTTP builder. They both imply the wsdl is on the server you are communicating with, rather than having a separate local WSDL file? And none of the examples I've looked at deal with authentication.

Can someone get me started please?!


Solution

  • Here's an example of a SOAP API call we have in our Grails 5.1.9 application using the Rest Client Builder Grails Plugin to update a Learning Enrollment in Workday. The RestBuilder code works the same way in Grails 2; version 2.1.1 of the plugin was the last update to work with Grails 2 (compile ":rest-client-builder:2.1.1"). Note that for the sake of simplicity I removed some things from the SOAP body, so this API call would fail if you are actually trying to update a Learning Enrollment in Workday.

    import grails.plugins.rest.client.RestBuilder
    import grails.plugins.rest.client.RestResponse
    
    ...
    
        RestResponse callEndpoint(String url, String offeringID, String employeeID,
                String completionDate, String enrollmentId, String username, String password) {
            String workdayApiVersion = "v34.0"
    
            String soapBody = """
    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:bsvc="urn:com.workday/bsvc">
        <soapenv:Header>
            <bsvc:Workday_Common_Header>
                <bsvc:Include_Reference_Descriptors_In_Response>true</bsvc:Include_Reference_Descriptors_In_Response>
            </bsvc:Workday_Common_Header>
        </soapenv:Header>
        <soapenv:Body>
            <bsvc:Put_Learning_Enrollment_Request bsvc:version="${workdayApiVersion}">
                <bsvc:Learning_Enrollment_Data>
                    <bsvc:ID>${enrollmentId}</bsvc:ID>
                    <bsvc:Learning_Content_Reference>
                        <bsvc:ID bsvc:type="Learning_Course_Offering_ID">${offeringID}</bsvc:ID>
                    </bsvc:Learning_Content_Reference>
                    <bsvc:Learner_Reference>
                        <bsvc:ID bsvc:type="Employee_ID">${employeeID}</bsvc:ID>
                    </bsvc:Learner_Reference>
                    <bsvc:Learning_Enrollment_Completion_Date>${completionDate}</bsvc:Learning_Enrollment_Completion_Date>
                </bsvc:Learning_Enrollment_Data>
            </bsvc:Put_Learning_Enrollment_Request>
        </soapenv:Body>
    </soapenv:Envelope>
    """
    
            RestBuilder rest = new RestBuilder()
            // Example full URL: https://example-services1.workday.com/ccx/service/examplesystem4/Learning/v34.0
            RestResponse response = rest.post(url) {
                contentType("text/xml")
                body(soapBody)
                // The Workday SOAP API does not use basic auth, but I included it here since it sounds like
                // basic auth will work with the SOAP endpoint you want to call.
                auth(username, password)
            }
    
            return response
        }
    
        ...
    
            // Inside a different service method that calls the callEndpoint() method
            RestResponse response = callEndpoint(url, offeringID, employeeID, completionDate,
                    enrollmentId, username, password)
            GPathResult responseBody = response.xml
            String prettyPrintedBody = response.xml ? beautifyXmlString(response.text) : response.text
    
        ...
    
        /**
         * Returns a pretty printed version of the given XML string
         * with the namespaces preserved.
         */
        private static String beautifyXmlString(String xml) {
            Node root = new XmlParser().parseText(xml)
            StringWriter sw = new StringWriter()
            new XmlNodePrinter(new PrintWriter(sw)).print(root)
            return sw.toString()
        }
    

    And for anybody who is wanting to use the Micronaut Http Client, here is that same example but using Micronaut Http Client:

    import static io.micronaut.http.HttpHeaders.ACCEPT
    import static io.micronaut.http.HttpHeaders.CONTENT_TYPE
    
    import io.micronaut.http.HttpRequest
    import io.micronaut.http.HttpResponse
    import io.micronaut.http.client.HttpClient
    import io.micronaut.http.client.exceptions.HttpClientResponseException
    import io.micronaut.http.uri.UriBuilder
    
    ...
    
        HttpResponse<String> callEndpoint(String offeringID, String employeeID,
                String completionDate, String enrollmentId, String username, String password) {
            String workdayApiVersion = "v34.0"
    
            String soapBody = """
    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:bsvc="urn:com.workday/bsvc">
        <soapenv:Header>
            <bsvc:Workday_Common_Header>
                <bsvc:Include_Reference_Descriptors_In_Response>true</bsvc:Include_Reference_Descriptors_In_Response>
            </bsvc:Workday_Common_Header>
        </soapenv:Header>
        <soapenv:Body>
            <bsvc:Put_Learning_Enrollment_Request bsvc:version="${workdayApiVersion}">
                <bsvc:Learning_Enrollment_Data>
                    <bsvc:ID>${enrollmentId}</bsvc:ID>
                    <bsvc:Learning_Content_Reference>
                        <bsvc:ID bsvc:type="Learning_Course_Offering_ID">${offeringID}</bsvc:ID>
                    </bsvc:Learning_Content_Reference>
                    <bsvc:Learner_Reference>
                        <bsvc:ID bsvc:type="Employee_ID">${employeeID}</bsvc:ID>
                    </bsvc:Learner_Reference>
                    <bsvc:Learning_Enrollment_Completion_Date>${completionDate}</bsvc:Learning_Enrollment_Completion_Date>
                </bsvc:Learning_Enrollment_Data>
            </bsvc:Put_Learning_Enrollment_Request>
        </soapenv:Body>
    </soapenv:Envelope>
    """
    
            // Example full URL: https://example-services1.workday.com/ccx/service/examplesystem4/Learning/v34.0
            final String uri = "/ccx/service/examplesystem4/Learning/${workdayApiVersion}"
            try {
                HttpClient client = HttpClient.create("https://example-services1.workday.com".toURL())
                // The Workday SOAP API does not use basic auth, but I included it here since it sounds like
                // basic auth will work with the SOAP endpoint you want to call.
                HttpRequest request = HttpRequest.POST(UriBuilder.of(uri).build(), soapBody)
                        .header(CONTENT_TYPE, "text/xml")
                        .header(ACCEPT, "text/xml")
                        .basicAuth(username, password)
                HttpResponse<String> response = client.toBlocking().exchange(request, String)
                return response
            } catch (HttpClientResponseException exception) {
                return exception.response as HttpResponse<String>
            }
        }
    
        ...
    
            // Inside a different service method that calls the callEndpoint() method
            HttpResponse<String> response = callEndpoint(offeringID, employeeID, completionDate,
                    enrollmentId, username, password)
            final String responseBodyText = response.body()
            GPathResult responseBodyXml = new XmlSlurper().parseText(responseBodyText)
            final String prettyPrintedBody = beautifyXmlString(responseBodyText)
    
    

    The WSDL file is useful for showing the expected structure of the SOAP request body and SOAP response body, but you do not need to include the file in any way in your Grails project.