8.0 KiB
title | date | draft |
---|---|---|
Soaping With Rest, or Resting With Soap | 2021-04-11T13:49:55+01:00 | false |
This week, I have had the opportunity, as part of my new job, to reacquaint myself with the SOAP protocol. I was tasked with standing up a facade service, that would act as a live integration mock, for a new client interface being built which will be accessing a real (old) SOAP backend service, written in Java. Problem is, due to the nature of the situation, there is no way to see the innards of the service I'm mocking.
My first impulse was to panic. Who writes SOAP services anymore? Who has any idea what it would take to build a proper SOAP mock, and would that involve having to marshall/unmarshall xml into domain objects, and how in the world would I be able to find out what the data model is, and what these objects should look like if I can't look at the code? Would I have to build my own class generator?
Turns out, it's actually not that hard. While SOAP is more rigid and intensive in terms of its xml messaging protocol specifications, and what is to be done with its payloads at the nodes, it is actually utterly platform and application layer independent. Which means you could, theoretically use SOAP as a messaging protocol over SMTP, FTP, DNS, or HTTP. The same is not the case for REST, which is designed to use the features of the HTTP protocol to facilitate uniform lightweight communications between network attached devices. So, there's nothing stopping me from using SOAP as a payload on an HTTP request. In fact, when you look at network traffic between two applications talking to each other in SOAP, looks an awful lot like two applications talking to each other in REST, but with two important differences:
- All of the request calls are POST calls (there are no GET, PUT, or DELETE calls, because the functions represented by those, are built into the xml document object itself).
- All of the payload
Content-Type
's are eitherapplication/xml
ortext/xml
(depending on the version of SOAP in use, though I was unable to see any difference using either, in experimentation).
This being the case, I decided to try something sneaky. I happen to be somewhat handy at Java Springboot (who isn't these days). Could I just stand up a Springboot application, and have it serve xml responses that would be appropriate to what the original service would have produced, had it received the request and fully processed it? I wouldn't be doing any processing. All I'd have to do, is convert the incoming request body into an xml document, look for what's being requested, and then respond with an appropriate xml body that looked as if it were an actual processed response.
That's exactly what I did. And it worked like a champ. To set this up in Springboot turns out to be really simple, as well. It's not strictly necessary to do this, but the first thing I did was to set up the framework to always default to a Content-Type
of text/xml
:
@Configuration
public class ApiConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(final ContentNegotiationConfigurer configurer {
configurer.defaultContentType(MediaType.valueOf(MediaType.TEXT_XML_VALUE));
}
}
This will save you the trouble of having to specify the media type on all of your controller's endpoint specifications.
The next step was to set up the controller. Since this is a flat facade mock, and since (thankfully), there was no need to unmarshall request objects, or subject them to any sort of logic, there was no need for a service or data layer in the mock app. I could just do everything I needed to, right in the controller.
Since Springboot is Java, you could skip the configuration, and just put the MediaType specification at the class level so that all your methods will inherit the setting:
@RestController
@RequestMapping(
consumes = MediaType.TEXT_XML_VALUE,
produces = MediaType.TEXT_XML_VALUE)
public class MockController {
No path is specified here, because we're going to do that at the method level. But every method will now default to both Accept: text/xml
and Content-Type: text/xml
, when they interact over HTTP. Note also, that I'm using @RestController
, and not @Controller
. This was counter-intuitive for me, at first. But paradoxically, it is indeed the @RestController
you want. This is for a few reasons. First, from the standpoint of the HTTP layer, there's literally no difference between an xml response body produced by the RestController, and one produced by the Controller. Since the former is easier to work with, just go with it. Second, it is in fact, easier to work with, when all you're doing is mocking. If I were to employ the Controller instead, I'd have to build a service layer unmarshalling the xml and constructing a response body from objects. Otherwise, you'll get Unsupported Protocol
(415) errors.
Finally, we get to the actual controller method. This is just a generic example of what I actually ended up building for my job. So, some of the decision logic is not presented here. But, there's enough present to make it clear. Technically, you could just search the body string for whatever you wanted, but I thought I'd be a bit more sophisticated than that. I convert the response body into an actual xml document, and then look for the nodes I'm expecting, to tell me what canned-response to ship back. This is what it looks like:
@PostMapping(path ="/call1")
public String postCall1(@RequestBody String xmlRequestBody) throws Exception {
Document xmlRequest = parseRequestBody(xmlRequestBody);
Element root = xmlRequest.getDocumentElement();
NodeList nodeList = root.getChildNodes();
System.out.println(root.getNodeName());
// use the document nodes to tell me what to send back...
return getStaticResponse("soap-example.xml");
}
parseRequestBody()
is just a straightforward conversion of a string into a Document object:
private Document parseRequestBody(String xmlRequestBody) throws Exception {
DocumentBuilderFactory dBfactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dBfactory.newDocumentBuilder();
Document document =
builder.parse(new InputSource(new StringReader(xmlRequestBody)));
document.getDocumentElement().normalize();
return document;
}
And getStaticResponse()
is just a straightforward retrieval to string, from a static resources
directory:
private String getStaticResponse(String fileName) throws Exception {
File file = null;
String content = null;
String filePath = "static/responses/" + fileName;
ClassLoader classLoader = getClass().getClassLoader();
try {
file = new File(classLoader.getResource(filePath).getFile());
} catch (NullPointerException e) {
e.printStackTrace();
}
try {
assert file != null;
content = new String(Files.readAllBytes(file.toPath()));
} catch (IOException e) {
e.printStackTrace();
}
return content;
}
Springboot's black magic will convert the string into a @RequestBody
object, when it sends the response. So we don't have to worry about anything else. That's it! That is literally all there is to this. From here, it's just a matter of adding controllers and methods, and populating your exemplars static directory with mock responses. This was made much easier by the fact that the application in question included a whole collection of WSDL documents, specifying the endpoints and interfaces for every call.
To test this generic version of the implementation, I used a variety of examples I found still scattered around the internet, of SOAP request bodies, and responses. I plugged the request bodies into Insomnia (you can also do this with Postman), and this is what it looked like:
Request/Response bodies:
Request/Response headers:
That's it. I hope this has been helpful. Incidentally, if you want to experiment with the code yourself, the repo is available {{< newtab title="right here." url="https://gitea.gmgauthier.com/gmgauthier/soapmock" >}} Enjoy!