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