Article
· Aug 6 10m read

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 %HSIncludeTimeZoneOffsets
	Set %HSIncludeTimeZoneOffsets = 1
	Set tSC = $$$OK
	Try {
		// 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 = 0
			Set tRequiredHostBases("HS.FHIRServer.Interop.Operation") = ""
			Set tRequiredHostBases("HS.FHIRServer.Interop.HTTPOperation") = ""
			Set tHostBase = ""
			For {
				Set tHostBase = $Order(tRequiredHostBases(tHostBase))
				If tHostBase="" Quit
				If $ClassMethod(tClassName, "%IsA", tHostBase) {
					Set tIsInteropHost = 1
					Quit
				}
			}
			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 resources
Property 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 Container
ClassMethod 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 Container
ClassMethod 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 method
    Set 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 to
	if 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 = ..FHIRRequestMethod
		set entry.request.url = resourceJson.resourceType
	}
	
	//Save id mappings for later access
	if 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-processing
	set ..%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
 

Discussion (2)2
Log in or sign up to continue