Example of Overriding SDA to FHIR Transform Process to Include "RequestMethod" Setting
When building a bundle from legacy data, I (and others) wanted to be able to control whether or not the resources were generated with a FHIR Request Method of PUT instead of the hard coded POST. I have extended the two classes responsible for transforming SDA to FHIR in an Interoperability Production to accomodate a setting that lets the user control the Request Method.
First is the Busines Process class. This includes a new parameter exposed to the "Settings" tab in Interoperability called FHIRRequestMethod. This also needs to pass the FHIRRequestMethod property to the transform class method as a parameter.
Class Demo.FHIR.DTL.Util.HC.SDA.FHIR.ProcessV2 Extends HS.FHIR.DTL.Util.HC.SDA3.FHIR.Process
{
/* ; **********************************************************
; * ** N O T I C E ** *
; * - TEST/DEMO SOFTWARE - *
; * This class is not supported by InterSystems as part *
; * of any released product. It is supplied by *
; * InterSystems as a demo/test tool for a specific *
; * product and version. The user or customer is fully *
; * responsible for the maintenance of this software *
; * after delivery, and InterSystems shall bear no *
; * responsibility nor liabilities for errors or misuse *
; * of this class. *
; **********************************************************/Parameter SETTINGS = "FHIRRequestMethod:Basic";/// This property can override the request method generated with each FHIR resource <br>/// This property will only apply to new resources that do not have an identifier from the source data.Property FHIRRequestMethod As%String(MAXLEN = 10) [ InitialExpression = "POST" ];/// This is an instance method because it needs to SendSync to a business host and get/// the response from the host.
Method ProcessSDARequest(pSDAStream, pSessionApplication As%String, pSessionId As%String, pPatientResourceId As%String = "") As%Status
{
New%HSIncludeTimeZoneOffsetsSet%HSIncludeTimeZoneOffsets = 1Set tSC = $$$OKTry {
// Check the base class for the Target business host. Determine if it is// a FHIRServer Interop business host or not.If '$Data(%healthshare($$$CurrentClass, "isInteropHost"))#10 {
$$$ThrowOnError(##class(HS.Director).OpenCurrentProduction(.tProdObj))
Set tClassName = ""For i = 1:1:tProdObj.Items.Count() {
If tProdObj.Items.GetAt(i).Name = ..TargetConfigName {
Set tClassName = tProdObj.Items.GetAt(i).ClassName
Quit
}
}
Kill tProdObj
Set tIsInteropHost = 0Set tRequiredHostBases("HS.FHIRServer.Interop.Operation") = ""Set tRequiredHostBases("HS.FHIRServer.Interop.HTTPOperation") = ""Set tHostBase = ""For {
Set tHostBase = $Order(tRequiredHostBases(tHostBase))
If tHostBase=""QuitIf$ClassMethod(tClassName, "%IsA", tHostBase) {
Set tIsInteropHost = 1Quit
}
}
Set%healthshare($$$CurrentClass, "isInteropHost") = tIsInteropHost
} Else {
Set tIsInteropHost = %healthshare($$$CurrentClass, "isInteropHost")
}
// Get the host and web server port of the current instance, to be used for populating// the FHIR request message HOST header. The HOST header is needed in the FHIR request// message when the message is being routed for processing in the local production, as// opposed to being passed to an external server.Do..GetHostAndPort(.tHost, .tPort)
Set tLocalHostAndPort = tHost_$Select(tPort'="":":",1:"")_tPort
If..FHIRFormat="JSON" {
Set tMessageContentType = "application/fhir+json"
} ElseIf..FHIRFormat="XML" {
Set tMessageContentType = "application/fhir+xml"
}
Set tFHIRMetadataSetKey = $ZStrip($Piece(..FHIRMetadataSet, "/", 1), "<>W")
Set tSchema = ##class(HS.FHIRServer.Schema).LoadSchema(tFHIRMetadataSetKey)
If '..FormatFHIROutput {
Set tIndentChars = ""Set tLineTerminator = ""Set tFormatter = ""
} Else {
Set tIndentChars = $Char(9)
Set tLineTerminator = $Char(13,10)
Set tFormatter = ##class(%JSON.Formatter).%New()
Set tFormatter.IndentChars = tIndentChars
Set tFormatter.LineTerminator = tLineTerminator
}
#dim tTransformObj As HS.FHIR.DTL.Util.API.Transform.SDA3ToFHIR
Set tTransformObj = $ClassMethod(..TransformClass, "TransformStream", pSDAStream, "HS.SDA3.Container", tFHIRMetadataSetKey, pPatientResourceId, "", ..FHIRRequestMethod)
// tTransformObj.bundle is a %DynamicObject.Set tBundleObj = tTransformObj.bundle
$$$HSTRACE("Bundle object", "tBundleObj", tBundleObj.%ToJSON())
// "individual" is not a transaction type or interaction.// This mode causes each entry in the Bundle to be sent// to TargetConfigName individually, not as a transaction.If..TransmissionMode="individual" {
For i = 0:1:tBundleObj.entry.%Size()-1 {
If tIsInteropHost {
Set tSC = ..CreateAndSendInteropMessage(tBundleObj.entry.%Get(i), tSchema, tMessageContentType, tFormatter, tIndentChars, tLineTerminator, pSessionApplication, pSessionId)
} Else {
Set tSC = ..CreateAndSendFHIRMessage(tBundleObj.entry.%Get(i), tSchema, tLocalHostAndPort, tMessageContentType, tFormatter, tIndentChars, tLineTerminator, pSessionApplication, pSessionId)
}
}
} Else {
If tIsInteropHost {
Set tSC = ..CreateAndSendInteropMessage(tBundleObj, tSchema, tMessageContentType, tFormatter, tIndentChars, tLineTerminator, pSessionApplication, pSessionId)
} Else {
Set tSC = ..CreateAndSendFHIRMessage(tBundleObj, tSchema, tLocalHostAndPort, tMessageContentType, tFormatter, tIndentChars, tLineTerminator, pSessionApplication, pSessionId)
}
}
} Catch eException {
Set tSC = eException.AsStatus()
}
Quit tSC
}
Storage Default
{
<Data name="ProcessV2DefaultData">
<Subscript>"ProcessV2"</Subscript>
<Value name="1">
<Value>FHIRRequestMethod</Value>
</Value>
</Data>
<DefaultData>ProcessV2DefaultData</DefaultData>
<Type>%Storage.Persistent</Type>
}
}
Second is the transformation class, for which we need to also add a new property to store the FHIRRequestMethod parameter. The value of FHIRRequestMethod comes from the Class Method call to ..TransformStream. Once this parameter from the Business Process is passed into ..TransformStream, I store it in the class property so that all methods of this transform class have access to the value.
Class Demo.FHIR.DTL.Util.API.Transform.SDA3ToFHIRV2 Extends HS.FHIR.DTL.Util.API.Transform.SDA3ToFHIR
{
/* ; **********************************************************
; * ** N O T I C E ** *
; * - TEST/DEMO SOFTWARE - *
; * This class is not supported by InterSystems as part *
; * of any released product. It is supplied by *
; * InterSystems as a demo/test tool for a specific *
; * product and version. The user or customer is fully *
; * responsible for the maintenance of this software *
; * after delivery, and InterSystems shall bear no *
; * responsibility nor liabilities for errors or misuse *
; * of this class. *
; **********************************************************//// Property to override the Request Method for unidentified resourcesProperty FHIRRequestMethod As%String(MAXLEN = 10);/// Transforms an SDA stream (Container or SDA class) to the specified FHIR version. Returns an instance of this class/// which contains a "bundle" property. That property will contain a FHIR Bundle with all the resources/// generated during the transformation, and with all references resolved. If <var>patientId</var> or/// <var>encounterId</var> are specified, those values will go into any applicable Patient and Encounter/// references./// @API.Method/// @Argument stream %Stream representation of an SDA object or Container/// @Argument SDAClassname Classname for the object contained in the stream (eg. HS.SDA3.Container)/// @Argument fhirVersion Version of FHIR used by the resource, eg. "STU3", "R4"/// @Argument patientId (optional) FHIR resource id to be assigned to the Patient resource/// @Argument encounterId (optional) FHIR resource id to be assigned to the Encounter resource, if not transforming a ContainerClassMethod TransformStream(stream As%Stream.Object, SDAClassname As%String, fhirVersion As%String, patientId As%String = "", encounterId As%String = "", FHIRRequestMethod As%String) As HS.FHIR.DTL.Util.API.Transform.SDA3ToFHIR
{
set source = $classmethod(SDAClassname, "%New")
if SDAClassname = "HS.SDA3.Container" {
$$$ThrowOnError(source.InitializeXMLParse(stream, "SDA3"))
}
else {
$$$ThrowOnError(source.XMLImportSDAString(stream.Read(3700000)))
}
return..TransformObject(source, fhirVersion, patientId, encounterId, FHIRRequestMethod)
}
/// Transforms an SDA object (Container or SDA class) to the specified FHIR version. Returns an instance of this class/// which contains a "bundle" property. That property will contain a FHIR Bundle with all the resources/// generated during the transformation, and with all references resolved. If <var>patientId</var> or/// <var>encounterId</var> are specified, those values will go into any applicable Patient and Encounter/// references./// @API.Method/// @Argument source SDA object or Container/// @Argument fhirVersion Version of FHIR used by the resource, eg. "STU3", "R4"/// @Argument patientId (optional) FHIR resource id to be assigned to the Patient resource/// @Argument encounterId (optional) FHIR resource id to be assigned to the Encounter resource, if not transforming a ContainerClassMethod TransformObject(source, fhirVersion As%String, patientId As%String = "", encounterId As%String = "", FHIRRequestMethod As%String) As HS.FHIR.DTL.Util.API.Transform.SDA3ToFHIR
{
set schema = ##class(HS.FHIRServer.Schema).LoadSchema(fhirVersion)
set transformer = ..%New(schema)
// Updated from parent class to set the FHIRRequestMethod for use of any class methodSet transformer.FHIRRequestMethod = FHIRRequestMethod
//SDA gets patient and encounter id and Container only gets patient id//because a Container can have multiple encounters and we can't assume which one they're referring toif source.%ClassName(1) = "HS.SDA3.Container" {
do transformer.TransformContainer(source, patientId)
}
else {
do transformer.TransformSDA(source, patientId, encounterId)
}
return transformer
}
/// Ensures the resource is valid FHIR, adds the resource to the output Bundle,/// and returns a reference to that resource. Will also output the resource as a/// %DynamicObject./// @Inputs/// source SDA object which created this resource/// resource Object model version of the resource/// resourceJson %DynamicObject version of the resource/// One of <var>resource</var> or <var>resourceJson</var> must be provided. If both are provided,/// the %DynamicObject representation will be given precedence
Method AddResource(source As HS.SDA3.SuperClass, resource As%RegisteredObject = "", ByRef resourceJson As%DynamicObject = "") As HS.FHIR.DTL.vR4.Model.Base.Reference [ Internal ]
{
if '$isobject(resourceJson) {
set resourceJson = ##class(%DynamicObject).%FromJSON(resource.ToJSON())
}
try {
do ..%resourceValidator.ValidateResource(resourceJson)
} catch ex {
do..HandleInvalidResource(resourceJson, ex)
return""
}
set entry = ##class(%DynamicObject).%New()
set entry.request = ##class(%DynamicObject).%New()
set id = ..GetId(source, resourceJson)
if id '= "" {
set resourceJson.id = id
}
//Check for an SDA identifier->id mapping to maintain references//Note: Provenance assigns a GUID to ExternalId for internal use, it is not an external id and shouldn't influence id assignment set sourceIdentifier = ""if resourceJson.resourceType = "Encounter" {
set sourceIdentifier = source.EncounterNumber
}
elseif ((source.%Extends("HS.SDA3.SuperClass")) && (resourceJson.resourceType '= "Provenance")) {
set sourceIdentifier = source.ExternalId
}
if id = "" {
if (resourceJson.resourceType = "Patient") && (..%patientId '= "") {
set id = ..%patientId
}
elseif$get(..%resourceIds(resourceJson.resourceType)) '= "" {
set id = ..%resourceIds(resourceJson.resourceType)
}
elseif (sourceIdentifier '= "") && $data(..%resourceIds(resourceJson.resourceType, sourceIdentifier)) {
set id = ..%resourceIds(resourceJson.resourceType, sourceIdentifier)
}
if id '= "" {
set resource.id = id
set resourceJson.id = id
}
}
if resourceJson.id '= "" {
set id = resourceJson.id
set entry.fullUrl = $select(..GetBaseURL()'="":..GetBaseURL() _ "/", 1:"") _ resourceJson.resourceType _ "/" _ resourceJson.id
set entry.request.method = "PUT"set entry.request.url = resourceJson.resourceType _ "/" _ resourceJson.id
}
else {
set id = $zconvert($system.Util.CreateGUID(), "L")
set entry.fullUrl = "urn:uuid:" _ id
// changed from parent class to accept parameter as input instead of hard coding "POST"set entry.request.method = ..FHIRRequestMethodset entry.request.url = resourceJson.resourceType
}
//Save id mappings for later accessif resourceJson.resourceType = "Patient" {
set ..%patientId = id
}
elseif sourceIdentifier '= "" {
set ..%resourceIds(resourceJson.resourceType, sourceIdentifier) = id
}
set duplicate = ..IsDuplicate(resourceJson, id)
if duplicate '= "" {
return duplicate
}
//Index for O(1) lookup if needed for post-processingset ..%resourceIndex(resourceJson.resourceType, id) = resourceJson
set entry.resource = resourceJson
do ..%bundle.entry.%Push(entry)
return..CreateReference(resourceJson.resourceType, id)
}
}
These classes are designed to be used in an Interoperability Production. The demonstration that highlights to base version of these classes can be found here:
Learning Services: Converting Legacy Data to HL7 FHIR R4 in InterSystems IRIS for Health & Github Repo for Legacy To FHIR Transformation Demo
Comments
Hi @Jeff Morgan,
you can't just replace POST by PUT ; with PUT requests you need to provide the id.
Yes, that is my mistake. POSTs can have a defined ID but is optional yes? PUTs and UPDATEs require the ID.