This topic contains 2 replies, has 2 voices, and was last updated by Garry Christensen 9 years, 6 months ago.
FHIR Connectathon experience
-
I took Iguana to the FHIR Connectathon at the International HL7 Interoperability Conference in Sydney Australia in October 2013. I’d done a little reading and been to some presentations on FHIR previously, and had a basic idea of what it was about. This was going to be a test to see if I could get it to work. This report is part one of my first FHIR experience.
The full project file for Iguana is at: https://drive.google…dit?usp=sharing
I really didn’t doubt that I could implement the communications in Iguana and I’d seen a number of articles where Iguana was used to convert V2 messages to FHIR. I wanted to see how FHIR performed in the real world. That meant putting it in front of a real application and working directly from the database.
In this instance I chose a Radiology Information System (RIS) and looked only at the patient resource and how it would be used. My goal was to put together a library of LUA functions that would support FHIR implementation, use them to set up an channel to send out patient updates from the RIS and to set up a server interface to receive query and updates from a third party.
Like any of these events, preparation is the key so I spent time in between usual jobs for a couple of week prior to the Connectathon setting up the interfaces. The first was some re-usable FHIR functionality and that meant creating templates for the resources. Like V2 and V3, FHIR is based on datatypes. It’s easy enough in Iguana to create a string in XML format that can be parsed into an XML node tree and the datatype definitions are available on the FHIR web site (http://www.healthint…r/datatypes.htm).
Here’s the datatype strings I generated:
_fhirStrings = {} _fhirStrings.datatypes = {} local _d = _fhirStrings.datatypes _d.resource = [[ <type value=""/><!-- 0..1 Resource Type --> <reference value=""/><!-- 0..1 Relative, internal or absolute URL reference --> <display value=""/><!-- 0..1 Text alternative for the resource --> ]] _d.extension = [[ <url value=""/><!-- 1..1 identifies the meaning of the extension --> <isModifier value=""/><!-- 0..1 If extension modifies other elements/extensions --> <!--<value[x]/>use fhirInsert[x](name,type) to add expension elements--> ]] _d.period = [[ <start value=""/><!-- 0..1 The start of the period --> <end value=""/><!-- 0..1 The end of the period, if not ongoing --> ]] _d.coding = [[ <system value=""/><!-- 0..1 Identity of the terminology system --> <version value=""/><!-- 0..1 Version of the system - if relevant --> <code value=""/><!-- 0..1 Symbol in syntax defined by the system --> <display value=""/><!-- 0..1 Representation defined by the system --> <primary value=""/><!-- 0..1 If this code was chosen directly by the user --> <valueSet><!-- 0..1 Resource(ValueSet) Set this coding was chosen from -->]] .. _d.resource .. [[</valueSet> ]] _d.quantity = [[ <value value=""/><!-- 0..1 Numerical value (with implicit precision) --> <comparator value=""/><!-- 0..1 < | <;= | >= | > - how to understand the value --> <units value=""/><!-- 0..1 Unit representation --> <system value=""/><!-- 0..1 System that defines coded unit form --> <code value=""/><!-- 0..1 Coded form of the unit --> ]] _d.age = _fhirStrings.datatypes.quantity _d.distance = _fhirStrings.datatypes.quantity _d.duration = _fhirStrings.datatypes.quantity _d.count = _fhirStrings.datatypes.quantity _d.money = _fhirStrings.datatypes.quantity _d.address = [[ <use value=""/><!-- 0..1 The use of this address --> <text value=""/><!-- 0..1 Text representation of the address --> <line value=""/><!-- 0..* Street name, number, direction & P.O. Box etc --> <city value=""/><!-- 0..1 Name of city, town etc. --> <state value=""/><!-- 0..1 Sub-unit of country (abreviations ok) --> <zip value=""/><!-- 0..1 Postal code for area --> <country value=""/><!-- 0..1 Country (can be ISO 3166 3 letter code) --> <period><!-- 0..1 Period Time period when address was/is in use -->]] .. _d.period .. [[</period> ]] _d.attachment = [[ <contentType value=""/><!-- 1..1 Mime type of the content, with charset etc. --> <language value=""/><!-- 0..1 Human language of the content (BCP-47) --> <data value=""/><!-- 0..1 Data inline, base64ed --> <url value=""/><!-- 0..1 Uri where the data can be found --> <size value=""/><!-- 0..1 Number of bytes of content (if url provided) --> <hash value=""/><!-- 0..1 Hash of the data (sha-1, base64ed ) --> <title value=""/><!-- 0..1 Label to display in place of the data --> ]] _d.codeableConcept = [[ <coding><!-- 0..* Coding Code defined by a terminology system -->]] .. _d.coding .. [[</coding> <text value=""/><!-- 0..1 Plain text representation of the concept --> ]] _d.contact = [[ <system value=""/><!-- 0..1 Telecommunications form for contact --> <value value=""/><!-- 0..1 The actual contact details --> <use value=""/><!-- 0..1 How to use this address --> <period><!-- 0..1 Period Time period when the contact was/is in use -->]] .. _d.period .. [[</period> ]] _d.range = [[ <low><!-- 0..1 Quantity Low limit -->]] .. _d.quantity .. [[</low> <high><!-- 0..1 Quantity High limit -->]] .. _d.quantity .. [[</high> ]] _d.ratio = [[ <numerator><!-- 0..1 Quantity The numerator -->]] .. _d.quantity .. [[</numerator> <denominator><!-- 0..1 Quantity The denominator -->]] .. _d.quantity .. [[</denominator> ]] _d.sampledData = [[ <origin><!-- 0..1 Quantity Zero value and units -->]] .. _d.quantity .. [[</origin> <period value=""/><!-- 0..1 Number of milliseconds between samples --> <factor value=""/><!-- 0..1 Multiply data by this before adding to origin --> <lowerLimit value=""/><!-- 0..1 Lower limit of detection --> <upperLimit value=""/><!-- 0..1 Upper limit of detection --> <dimensions value=""/><!-- 0..1 Number of sample points at each time point --> <data value=""/><!-- 0..1 Decimal values with spaces, or "E" | "U" | "L" --> ]] _d.identifier = [[ <use value=""/><!-- 0..1 The use of this identifier --> <label value=""/><!-- 0..1 Description of identifier --> <system value=""/><!-- 0..1 The namespace for the identifier --> <key value=""/><!-- 0..1 The value that is unique --> <period><!-- 0..1 Period Time period when id was valid for use -->]] .. _d.period .. [[</period> <assigner><!-- 0..1 Resource(Organization) Organization that issued id (may be just text) -->]] .. _d.resource .. [[</assigner> ]] _d.humanName = [[ <use value=""/><!-- 0..1 The use of this name --> <text value=""/><!-- 0..1 Text representation of the full name --> <family value=""/><!-- 0..* Family name (often called 'Surname') --> <given value=""/><!-- 0..* Given names (not always 'first'). Includes middle names --> <prefix value=""/><!-- 0..* Parts that come before the name --> <suffix value=""/><!-- 0..* Parts that come after the name --> <period><!-- 0..1 Period Time period when name was/is in use -->]] .. _d.period .. [[</period> ]] _fhirStrings.datatypes['repeat'] = [[ <frequency value=""/><!-- 0..1 Event occurs frequency times per duration --> <when value=""/><!-- 0..1 Event occurs duration from common life event --> <duration value=""/><!-- 1..1 Repeating or event-related duration --> <units value=""/><!-- 1..1 The units of time for the duration --> <count value=""/><!-- 0..1 Number of times to repeat --> <end value=""/><!-- 0..1 When to stop repeats --> ]] _d.schedule = [[ <event><!-- 0..* Period When the event occurs -->]] .. _d.period .. [[</event> <repeat><!-- 0..1 Only if there is none or one event -->]] .. _fhirStrings.datatypes['repeat'] .. [[</repeat> ]] _d.option = [[ <code value=""/><!-- 1..1 Possible code --> <display value=""/><!-- 0..1 Display for the code --> ]] _d.choice = [[ <!-- from Element: extension --> <code value=""/><!-- 0..1 Selected code --> <option> <!-- 1..* List of possible code values -->]] .. _d.option .. [[</option> <isOrdered value=""/><!-- 0..1 If order of the values has meaning --> ]] _d.narrative = [[ <status value=""/><!-- 1..1 generated | extensions | additional --> <div xmlns="http://www.w3.org/1999/xhtml"><!-- Limited xhtml content< --></div> ]] _d.contained = ''
Note that some of the datatypes include datatypes already defined. Creating the respources is now simple as the datatype definitions can be included within the resource. (http://www.healthint…esourcelist.htm).
Here’s the patient resource:
_fhirStrings.resources = {} _fhirStrings.components = {} local _r = _fhirStrings.resources local _c = _fhirStrings.components _r.resource = [[ <extension><!-- 0..* Extension See Extensions --></extension> <language value=""/><!-- 0..1 Human language of the content (BCP-47) --> <text><!-- 1..1 Narrative Text summary of resource content, for human interpretation -->]] .. _d.narrative .. [[</text> <contained><!-- 0..* Resource Contained Resources --></contained> ]] --+++++++++++++++++++++++++++++++++++++ -- Patient resource _c.patientContact = [[ <relationship><!-- 0..* CodeableConcept The kind of relationship -->]] .. _d.codeableConcept .. [[</relationship> <name><!-- 0..1 HumanName A name associated with the person -->]] .. _d.humanName .. [[</name> <telecom><!-- 0..* Contact A contact detail for the person -->]] .. _d.contact .. [[</telecom> <address><!-- 0..1 Address Address for the contact person -->]] .. _d.address .. [[</address> <gender><!-- 0..1 CodeableConcept Gender for administrative purposes -->]] .. _d.codeableConcept .. [[</gender> <organization><!-- 0..1 Resource(Organization) Organization that is associated with the contact -->]] .. _d.resource .. [[</organization> ]] _r.Patient = [[ <Patient xmlns="http://hl7.org/fhir"> <!-- from Resource: extension, language, text, and contained -->]] .. _r.resource .. [[ <identifier><!-- 0..* Identifier An identifier for the person as this patient -->]] .. _d.identifier .. [[</identifier> <name><!-- 0..* HumanName A name associated with the patient -->]] .. _d.humanName .. [[</name> <telecom><!-- 0..* Contact A contact detail for the individual -->]] .. _d.contact .. [[</telecom> <gender><!-- 0..1 CodeableConcept Gender for administrative purposes -->]] .. _d.codeableConcept .. [[</gender> <birthDate value=""/><!-- 0..1 The date and time of birth for the individual --> <deceasedBoolean value = ""/><!-- 0..1 boolean Indicates if the individual is deceased or not --> <deceasedDatetime value = ""/> <!-- 0..1 dateTime Indicates if the individual is deceased or not --> <address><!-- 0..* Address Addresses for the individual -->]] .. _d.address .. [[</address> <maritalStatus><!-- 0..1 CodeableConcept Marital (civil) status of a person -->]] .. _d.codeableConcept .. [[</maritalStatus> <multipleBirthBoolean value = ""/><!-- 0..1 boolean Whether patient is part of a multiple birth --> <multipleBirthInteger value = ""/><!-- 0..1 integer Whether patient is part of a multiple birth --> <photo><!-- 0..* Attachment Image of the person -->]] .. _d.attachment .. [[</photo> <contact> <!-- 0..* A contact party (e.g. guardian, partner, friend) for the patient -->]] .. _c.patientContact .. [[</contact> <animal> <!-- 0..1 If this patient is an animal (non-human) --> <species><!-- 1..1 CodeableConcept E.g. Dog, Cow -->]] .. _d.codeableConcept .. [[</species> <breed><!-- 0..1 CodeableConcept E.g. Poodle, Angus -->]] .. _d.codeableConcept .. [[</breed> <genderStatus><!-- 0..1 CodeableConcept E.g. Neutered, Intact -->]] .. _d.codeableConcept .. [[</genderStatus> </animal> <communication><!-- 0..* CodeableConcept Languages which may be used to communicate with the patient -->]] .. _d.codeableConcept .. [[</communication> <provider><!-- 0..1 Resource(Organization) Organization managing the patient -->]] .. _d.resource .. [[</provider> <link><!-- 0..* Resource(Patient) Other patient resources linked to this resource -->]] .. _d.resource .. [[</link> <active value=""/><!-- 0..1 Whether this patient's record is in active use --> </Patient>]]
Next, was to set up a library of some functions that I’d probably need. This library grew considerably as I discovered functions that I’d need. In the end, I had functions to
1. Create a resource
2. Insert a primitive and complex datatype and add an attribute
3. Insert a component (repeatable node in a resource)
4. Map a existing FHIR node into a second node
5. Find a named node in the FHIR node tree
6. Duplicate a child node
7. Check if a node has no data stored in it (is empty).
8. Remove all empty nodes
9. Some date format conversion functions to support the FHIR date format
10. Return the value of a node from a node path string
Here’s the functions so far:
function fhirCreate(sType) if not _fhirStrings.resources[sType] then error('Not a valid resource', 2) end return xml.parse(_fhirStrings.resources[sType])[1] end function node:fhirInsertComplex(sName, sType, nIndex) if not _fhirStrings.datatypes[sType] then error('Data type not recognised', 2) else if not nIndex or nIndex > self:childCount() + 1 then if not self:fhirFindChild(sName) then nIndex = self:childCount() + 1 else nIndex = self:fhirFindChild(sName) + 1 end end local _f = self:insert(nIndex, xml.ELEMENT, sName) _f:setInner(_fhirStrings.datatypes[sType]) return _f end end function node:fhirInsertPrimitive(sName, nIndex) if not nIndex or nIndex > self:childCount() + 1 then if not self:fhirFindChild(sName) then nIndex = self:childCount() + 1 else nIndex = self:fhirFindChild(sName) + 1 end end local _f = self:insert(nIndex, xml.ELEMENT, sName) _f:append(xml.ATTRIBUTE, 'value') return _f end function node:fhirFindChild(sName) local nFound, xFound = nil if type(sName) == 'string' then for nNode = self:childCount(), 1, -1 do if self[nNode]:nodeName() == sName then nFound = nNode xFound = self[nNode] break end end end return nFound, xFound end function node:fhirIsEmpty() local bIsEmpty = true if self:isLeaf() and self:nodeValue() ~= '' then bIsEmpty = false elseif self:nodeName() == 'div' and self:childCount() > 0 then bIsEmpty = false else for n=1, self:childCount() do if self:childCount() == 1 and self[1]:nodeName() == 'xmlns' then elseif self:childCount() == 1 and self[1]:nodeName() == 'value' and self[1]:nodeValue() ~= '' then bIsEmpty = false break elseif not self[n]:fhirIsEmpty() then bIsEmpty = false break end end end return bIsEmpty end function node:fhirDropEmpty() for nChild = self:childCount(), 1 , -1 do if self[nChild]:fhirIsEmpty() then self:remove(nChild) elseif self[nChild]:nodeName() == 'div' then else self[nChild]:fhirDropEmpty() if self[nChild]:fhirIsEmpty() then self:remove(nChild) end end end return self end function node:fhirInsertComponent(sName, sType, nIndex) if not _fhirStrings.components[sType] then error('Data type not recognised', 2) else if not nIndex or nIndex > self:childCount() + 1 then if not self:fhirFindChild(sName) then nIndex = self:childCount() + 1 else nIndex = self:fhirFindChild(sName) + 1 end end local _f = self:insert(nIndex, xml.ELEMENT, sName) _f:setInner(_fhirStrings.components[sType]) return _f end end function node:fhirAddNode(xNode) _s = '' for n = 1, self:childCount() do if self[n]:nodeType() ~= 'attribute' then _s = _s .. self[n]:S() end end _s = _s .. xNode:S() self:setInner(_s) end function node:fhirAddAttribute(sName, sValue) if not self:fhirFindChild(sName) then self:append(xml.ATTRIBUTE, sName) if sValue then self[sName] = sValue end end end function node:fhirDuplicateChild(sName) local nIndex = self:fhirFindChild(sName) local _s = self[nIndex]:S() local _d = self:insert(nIndex + 1, xml.ELEMENT, sName) _d:setInner(_s:sub(#sName + 4, #_s - #sName - 3)) return _d end function node:fhirDateTime(sZone) local sDefaultZone = '+10:00' if not sZone then sZone = sDefaultZone end local _sHl7Date = self:S() local _sFhirDate = '' for n = 1, #_sHl7Date do if n == 5 or n == 7 then _sFhirDate = _sFhirDate.. '-' .. _sHl7Date:sub(n, n) elseif n == 9 then _sFhirDate = _sFhirDate.. 'T' .. _sHl7Date:sub(n, n) elseif n == 11 or n == 13 then _sFhirDate = _sFhirDate.. ':' .. _sHl7Date:sub(n, n) else _sFhirDate = _sFhirDate.. _sHl7Date:sub(n, n) end if _sFhirDate:find('T') then _sFhirDate = _sFhirDate.. sZone end end return _sFhirDate end function fhirNow(sTZ) local sDefaultZone = '+10:00' if not sTZ then sTZ = sDefaultZone end return os.date('%Y-%m-%dT%H:%M:%S+' .. sTZ) end function node:fhirGetValue(sPath) tPath = sPath:split('%.') local tNode = self local sReturn = '' local nRepeat = 1 for n=1, #tPath do nRepeat = 1 if tPath[n]:find('%^') then nRepeat = tonumber(tPath[n]:sub(tPath[n]:find('%^') + 1)) tPath[n] = tPath[n]:sub(1, tPath[n]:find('%^') - 1) end local bOK, tChild = pcall(tNode.child,tNode, tPath[n], nRepeat) if n == #tPath then if bOK and tChild.value then sReturn = tChild.value:S() end elseif bOK then tNode = tChild else break end end return sReturn end function string:split(sDelimiter, bNoTrim) t = {} s = self .. sDelimiter:gsub('%%', '') nEsc = 0 if sDelimiter:find('%%') then nEsc = 1 end if not bNoTrim then s = s:gsub('%s$', '') end s:gsub('(.-' .. sDelimiter .. ')', function(ss) table.insert(t, ss:sub(1, -1-#sDelimiter+nEsc)) end) return t end function node:S() if self:isLeaf() then return self:nodeValue() else return tostring(self) end end
So, that gave me a start to begin implementation. The easy part was the outbound patient resource so that’s where I started. I already had the database triggers SQL queries set up for V2 messaging so the source of the outbound channel picked up data from the database. The output is triggered whenever a patient record is added or updated. This would normally be mapped to a V2 message but for this exercise I converted the result set table to XML and pushed it to the Iguana queue. This meant it was easy to have sample data in Translator to use when I was developing the FHIR resource.
This is where I hit the first reason the FHIR will not do us all out of a job. All of the FHIR servers that have been made available in the public domain are designed to store or update a FHIR resource and return results for queries. They can report on the validity of the resource but the do not need to understand the meaning of the content. I now have a result set from a database that is from an existing clinical system. That database was not structured with the way FHIR uses data in mind. (Many databases are not structured with the thought of any interoperability in mind in my experience.)
The data mapping caused me to spend many hours reading the FHIR documentation, interpreting how it was planned to be implemented then getting it to fit the actual data I needed to send to another clinical system. More about this when I talk about the FHIR server functionality in the next part.
With the mapping done, using Iguana with FHIR then came into its own. It’s a very simple job to use the RESTful interface to create a patient resource on the FHIR server.
Here’s an extract from the code:
local xPatient, sPatID = MapPatient(tPat) xPatient:fhirDropEmpty() sURL = sBaseURL .. 'patient?format=xml' iguana.logDebug('Send create:\r\n' .. xPatient:S()) -- create the patient resource sData, nCode, tHead = net.http.post{url = sURL,live = bLive, headers = {['Content-Type']='application/fhir+xml'}, body = xPatient:S()} iguana.logDebug('Patient create response: \r\n' .. sData) -- check the response if nCode == 201 then sResourceID = tHead['Content-Location']:gsub('/history/@.+', '') iguana.logInfo('Patient created: ' .. sPatID .. '. ' .. sResourceID) else iguana.logWarning('Error creating Patient: ' .. sPatID .. '\r\n' .. sData) bOK = false end
And that works just fine for the first time. In a FHIR server that is available for you to test against, it will continue to work, creating duplicates every time you POST the resource. In a real world scenario however, we only want 1 copy of the resource so we should be sending an update instead. Of course there’s no easy way to know if the resource already exists on the destination without doing a query first.
Here we start making some assumptions. In the V2 messaging environment, we send an ADT message with the patient information and it is up to the receiving system to decide what to do with it. In FHIR, the sending system has the power (or responsibility) to decide if this is a create or an update.
The FHIR update (PUT) action allows the resource to be updated if it exists or created if it doesn’t, but the sending system is able to define the resource ID it wants the receiving system to use. This may not always be suitable where multiple systems are feeding resources to a single destination. The most sociable way seems to be for the sending system to look if the resource exists first:
1. Query the destination,
2. If the resource exists, update it,
3. If it doesn’t exist, create it.
But one last issue. It’s still my job as the sender to find the correct resource on the destination. What parameters should I use? Is the Identifier enough? A patient resource can have multiple identifiers so am I sure I’m getting the right one, or the right patient resource for that matter? In this case, I had to assume that if I got a match on the identifier, it was the right patient. I hope I wasn’t updating some other patient’s resource.
In this configuration, the FHIR interface works quite well. I was able to easily create patients and update patient details in the RIS and have these details flow on to several other servers.
Of course, there is much more information in the V2 ADT message than the patient resource, such as insurance, allergies and alerts, visit details etc, and each of these have their own resource. Even the PID segment includes multiple addresses, residential, postal etc. In FHIR, only the postal address is intended to be in the patient resource. The residential address in a Location resource. That suggests that a real world implementation of FHIR will need some consideration.
A clinical system presents a large amount of information on the screen at the same time and this will invariable cover multiple resources. FHIR may end up being a very ‘chatty’ system to update all those resources in potentially several other systems. The only alternative is to bundle all the resources in a single ‘message’ – we will be back to the message-based paradigm and just replace V2 with a new syntax. Is that a step forward?
Of course, FHIR is really just a protocol for exchange of information, it does not define how that exchange will be implemented or even what the data content means to each system. As integration implimenters, we will still have a lot of work to do, perhaps more so as there seems to be lot of ways to achieve the same thing with FHIR. I think we will see that Implimentation Profiles (like IHE profiles) will be necessary.
More about this in part 2 where I look at putting a FHIR server functionality at the front of RIS.
The full project file for this interface is at: https://drive.google…dit?usp=sharing
Fascinating post Garry – thanks for sharing. So if I understand right you are generating FHIR resources off the FHIR definitions? That is pretty cool.
I think one of the challenging aspects of FHIR is that most applications won’t fit the data model exactly which may make FHIR more difficult to work with than a native RESTful API specific to the application. But let see see where things go.
We can repost this on our blog if you like?
G’day Eliot,
Thanks for your comments. I agree that the FHIR data model and the application data structure will rarely be a good fit. It will be interesting to hear comments from others when they start doing the same thing.I’m happy for you to repost this in the blog.
You must be logged in to reply to this topic.