Introduction
This article was originally written for iguana 5 and may contain out of date references.
We’re not big fans of, but there are times you have to deal with it. One of those instances if you want to connect your EMR electronically to Quest Diagnostics care360 so that physicians can request and receive lab orders and results without paper.
Like many web service APIs in healthcare Quest’s web service API consists of a SOAP wrapper around an HL7 message. These types of interfaces are interesting because the plumbing always tends to show through. Looking at the error messages coming out of this interface it appears to be an older system which speaks HL7, which is then wrapped in a SOAP style web service API implemented using BEA Weblogic.
In this case SOAP is just a transport envelope you have to get through before you can get at the actual data, which is the HL7 message encoded in base64. This must be done before you can get to the meat of the problem which is simply processing HL7 messages.
We also used a similar approach with the Surescript E-Prescription interface.
How the plumbing works [top]
The Quest 360 interface uses HTTP basic authentication over HTTPS which is quite easy to implement. The error messages the web service gave good hints that this would be the case. It’s a very common way to implement authentication with web services. If you get the return HTTP code of 401 it indicates the user name and password were wrong.
Then it was a matter of taking example SOAP envelope from the Quest diagnostics documentation and slotting in the HL7 message payload encoding it in base64.
That’s what this code does:
T = [==[<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ord="http://medplus.com/orders"> <soapenv:Header/> <soapenv:Body> <ord:submitOrder soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <order xsi:type="java:Order" xs:type="type:Order" xmlns:java="java:com.medplus.serviceHub.orders.webservice" xmlns:xs="http://www.w3.org/2000/XMLSchema-instance"> <hl7Order xsi:type="xsd:base64Binary">${globalCleanHL7}</hl7Order> </order> </ord:submitOrder> </soapenv:Body> </soapenv:Envelope>]==] function MakePayload(HL7) local P = xml.parse{data=T} P["soapenv:Envelope"]["soapenv:Body"]["ord:submitOrder"].order.hl7Order[2] = base64.enc(HL7) return P end
I used a simple approach of defining an XML template and then slotting in the base64 encoded message. From there it’s a matter of calling the web service:
function PlaceOrder(HL7, User, Password) local E = MakePayload(HL7) trace(E) local D = net.http.post{ url=Url, auth={username=User, password=Password}, headers={['Content-Type']='text/xml', SOAPAction=Url..'/submitOrder'}, body=E:S(), live=true} return xml.parse{data=D} end
It wasn’t all that hard to get that working, the since Translator gives us immediate feedback with the error messages; which made it simple to figure out the required Content-Type and SOAPAction headers.
The next step is to decipher what we got back, there are three of possibilities:
- A SOAP fault response if you really screw things up like sending an empty string instead of an HL7 order message
- Or a normal response, containing a valid HL7 message
- A normal response, containing error information related to the HL7 message
When the Quest interface returns an error we need to identify whether it’s BEA logic stuff or the core system. It’s a rather leaky abstraction! So far the following code seems to be adequate to figure out what type of response we have and extract error messages in a useful manner:
-- Returns nil if we do not have a SOAP fault response function IsFault(X) return X["env:Envelope"]["env:Body"]["env:Fault"] end -- Extracts the response message from a non fault response function GetResponse(X) return X["env:Envelope"]["env:Body"] ["m:submitOrderResponse"].result.responseMsg[2]:S() end -- Extracts the error message from a fault response function GetFault(X) return X["env:Envelope"]["env:Body"]["env:Fault"]. detail["bea_fault:stacktrace"][2]:S() end
Code [top]
We packaged the code in a ‘quest’ shared module to make a nice clean API to work with.
This is how the Translator is meant to be used, by abstracting the complexity of the underlying plumbing into a shared into a shared modules. However when something goes wrong we can easily get under the hood to see what is going wrong. This is really important when dealing with a complex and ambiguous protocol like SOAP, because there will always be problems.
Here’s the code for the main:
require('node') require('quest') local User ='user' local Password = 'password' local function trace(a,b,c,d) return end function main(HL7) local X = quest.PlaceOrder(HL7, User, Password) trace(X) if quest.IsFault(X) then quest.GetFault(X) else quest.GetResponse(X) end end
Here’s the code for the ‘quest’ module:
quest ={} local function trace(a,b,c,d) return end local Url = 'https://cert.hub.care360.com/orders/service' T = [==[<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ord="http://medplus.com/orders"> <soapenv:Header/> <soapenv:Body> <ord:submitOrder soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <order xsi:type="java:Order" xs:type="type:Order" xmlns:java="java:com.medplus.serviceHub.orders.webservice" xmlns:xs="http://www.w3.org/2000/XMLSchema-instance"> <hl7Order xsi:type="xsd:base64Binary">${globalCleanHL7}</hl7Order> </order> </ord:submitOrder> </soapenv:Body> </soapenv:Envelope>]==] local function MakePayload(HL7) local P = xml.parse{data=T} P["soapenv:Envelope"]["soapenv:Body"]["ord:submitOrder"].order.hl7Order[2] = filter.base64.enc(HL7) return P end function quest.PlaceOrder(HL7, User, Password) local E = MakePayload(HL7) trace(E) local D,R = net.http.post{ url=Url, auth={username=User, password=Password}, headers={['Content-Type']='text/xml', SOAPAction=Url..'/submitOrder'}, body=E:S(), live=true} if (R == 401) then error('Looks like the user name and password were wrong.') end return xml.parse{data=D} end -- Returns nil if we do not have a SOAP fault response function quest.IsFault(X) return X["env:Envelope"]["env:Body"]["env:Fault"] end -- Extracts the response message from a non fault response function quest.GetResponse(X) return X["env:Envelope"]["env:Body"] ["m:submitOrderResponse"].result.responseMsg[2]:S() end -- Extracts the error message from a fault response function quest.GetFault(X) return X["env:Envelope"]["env:Body"]["env:Fault"]. detail["bea_fault:stacktrace"][2]:S() end
Next steps [top]
Looking at how this interface has been implemented I would expect that there will be a fairly long burn in time to find all the edge cases.
Fortunately this is the kind of problem that Iguana is designed for since it gives comprehensive logging, and the ability to take failed transactions and validate them against the web service APIs and fix the problems.