I suspect this can be done in v3, but I will have to defer to my colleagues who are experts on it.
- Log in to post comments
I suspect this can be done in v3, but I will have to defer to my colleagues who are experts on it.
One of the very first things I learned about programming as a wee lad taking Computer Science in high school is that GOTOs should be avoided, as they lead to what the textbook called "spaghetti code".
XKCD agrees, but for a different reason:
In %SQL.StatementResult, do not use %Get to get columns by name, instead just reference the properties directly, e.g. 'Write resultOref.Name' instead of 'Write resultOref.%Get("Name")'
Just curious. What is the reason for this?
Hi Tom, HealthShare provides transforms between FHIR and SDA (the format HealthShare Information Exchange uses to store and transport patient data internally), as well as Ensemble Business Processes that call into these transforms. So if you wanted to feed FHIR data into Information Exchange, and your FHIR data were in patient-centric bundles, then one thing you could do is, implement a REST handler in a HealthShare Edge Gateway namespace that marshalls the FHIR request into an Ensemble request. You can look at HS.FHIR.REST.Handler for an example of how to do this, but I would recommend against trying to use this class as your REST handler. It does things that are specific to a FHIR namespace. Have the REST handler dispatch the request to a service in the Edge production, which should pass the request on to HS.FHIR.ToSDA.DTL.Transaction.Process. This will transform the FHIR bundle in an SDA container, which can be processed normally into the HealthShare Edge gateway. Note that if you decide to do this, you are essentially creating a FHIR endpoint that is write-only. The original FHIR resource cannot be read from that endpoint. Though technically, it isn't really even a FHIR endpoint unless it can serve up a conformance statement.
You can read more about transforming between FHIR and SDA here:
http://host:port/csp/docbook/DocBook.UI.Page.cls?KEY=FOVW_fhir#FOVW_fhi…
Hi Krystian,
I understand now, you mean you want to extend the FHIR data model, rather than use the "extension" property in the FHIR core spec. Extending the data model is not something that is supported in the 15.03 release of HealthShare Core, but currently we are planning to support this in the next release. However, even when this is supported, we will actually encourage users to use the "extension" property for simple extensions, rather than extending the data model. This is a sentiment shared by much of the FHIR community, based on their collective experience with extensions.
I don't have any specific suggestions for the Developer Community, but I really like what this post says about how the manual should be built in to the experience:
+1 for "Vogon poetry" ![]()
I tend to use "if" rather than postconditionals because it makes the code read more like spoken language, and I think it's easier to understand for those who are not seasoned Caché veterans.
However I make exceptions for "quit" and "continue":
quit:$$$ISERR(tSC)
continue:x'=y
I like to have these out in front, rather than buried in a line somewhere, so that when I'm scanning code I can easily see "this is where the execution will branch if a certain condition is met."
How is the client report being generated? Would it be possible to implement this as a Zen Report? That might allow you to embed the image in the report and specify its size within the report.
Agreed. The pitfalls of using GOTO, while not immediately obvious, can be catastrophic:

Source: https://xkcd.com/292/
I tried to use one of the "GetStored" methods of %Dictionary.MethodDefinition (specifically, "DescriptionGetStored") and got a "<METHOD DOES NOT EXIST>" error. I suspect this is because this class has "StorageStrategy=custom". So if a class specifies a custom storage definition, does that mean these methods won't be auto-generated?
Hi Cyriak, the HS_SDA3_Streamlet.Abstract table is used on *both* the Edge *and* the Access Gateway. When a patient's data is fetched on the Access Gateway, it is stored in this table temporarily, until the CSP session that initiated the fetch has ended, plus however long it takes for the session cleanup process to fire (usually a few minutes). So once you've loaded a patient's record into the Viewer and you have their Aggregation Key, you can use that to query for their data on the Access Gateway.
Assuming that tSC is a %Library.Status, I don't think this will work the way you intend it to. The argument to a THROW has to be an oref to a class that extends %Exception.AbstractException:
https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_cthrow
If you have a %Status that you need to throw as an exception, you can use $$$ThrowOnError or $$$ThrowStatus, both defined in %occStatus.inc. They both call ##class(%Exception.StatusException).ThrowIfInterrupt(), but $$$ThrowOnError only does so if the status is an error, so it's slightly more efficient if you haven't checked the status yet.
I know of no such built in tool, however here's a routine I wrote to pretty print an XML string:
PrettyPrintXML(pXML) for tI = 1:1:10 { write ! } set tTabCount = 0 // if starting with an XML prolog, do not increment tab on next line if ($E(pXML,1,5)="<?xml") { set tI = $FIND(pXML,">") set $EXTRACT(pXML,tI-1) = ">"_$C(13,10) } else { set tI = 1 } for { set tI = $FIND(pXML,"<",tI) quit:tI=0 //cdata if ($EXTRACT(pXML,tI,tI+7)="![CDATA[") { //set tTabCount = tTabCount + 1 do Tabs set $EXTRACT(pXML,tI-1) = tTabs_"<" set tI = $FIND(pXML,">",tI) quit:tI=0 set $EXTRACT(pXML,tI-1) = ">"_$C(13,10) //set tTabCount = tTabCount - 1 } //open tag elseif ($EXTRACT(pXML,tI) '= "/") { do Tabs set $EXTRACT(pXML,tI-1) = tTabs_"<" set tI = $FIND(pXML,">",tI) quit:tI=0 // only increment tab count if this is not an empty tag (<tag/>) if ($E(pXML,tI-2)'="/") { set tTabCount = tTabCount + 1 } //if followed by another open tag, insert a newline before it if ($EXTRACT(pXML,tI) = "<") { set $EXTRACT(pXML,tI) = $C(13,10)_"<" } } //close tag else { set tTabCount = tTabCount - 1 //if following another close tag, put tabs before this one ($C(62) = ">") if ($EXTRACT(pXML,tI-4,tI-2) = $C(62,13,10)) { do Tabs set $EXTRACT(pXML,tI-1) = tTabs_"<" } set tI = $FIND(pXML,">",tI) quit:tI=0 set $EXTRACT(pXML,tI-1) = ">"_$C(13,10) } } write pXML quitTabs set tTabs = "" for i=1:1:tTabCount { set tTabs = tTabs_$C(9) } quitHi Scott,
One way you could do this is via an MDM^T02 HL7 message. There is actually an example message containing a PDF document distributed with HealthShare: <install-directory>\Data\Scenario_4.hl7
The document data is encapsulated in a series of OBX segments:
OBX||ED|||^^PDF^Base64^JVBERi0xLjINCiXi48/TDQolICAgICAgICAgIA0KJTEwMDIzMFszMl0NCjEgMCBvYmogDTw8DQov
The critical pieces of this are:
OBX-2: Must be "ED" for "Encapsulated Data"
OBX-5.3: Must be the file type. In this case it's "PDF". If you want to view this document in the HealthShare Clinical Viewer, then you can find a list of supported doc types at websys.Document:ValidTypes in the Access Gateway namespace.
OBX-5.4: Must be "Base64" if the data is base64-encoded, otherwise it can be left blank.
OBX-5.5: The document data
Best,
Jorge
Hi Surya,
HealthShare has no single transform that can convert from HL7 to FHIR. Rather, what you would do is, first convert the HL7 to an SDA3 container via the Ensemble operation HS.Gateway.HL7.InboundProcess, then convert the SDA3 container into a FHIR bundle via the process HS.FHIR.FromSDA.DTL.Transaction.Process.
"The only reason I can think of, why we do this, is to ..."
I can think of one other reason for this pattern, that is perhaps better illustrated if we think of "Film" as a code table, and change the name of "CinemaFilm" to something like "Showing". In the entire world there is only one movie called "Wonder Woman", released in 2017, and starring Gal Godot. So rather than reproducing this object thousands of times for every cinema that happens to be showing this movie, the object exists once in the "Film" code table,
Hi Joao,
Probably what has happened is you've corrupted the resource storage by doing a physical delete against the storage table. If you need to delete a resource in the resource repository, the correct way to do this is via the delete interaction:
http://hl7.org/fhir/DSTU2/http.html#delete
http://hl7.org/fhir/STU3/http.html#delete
STU3 is the current version of FHIR. The version currently supported by HealthShare is DSTU2.
Hi Tom, can you describe how you created the FHIR namespace? Also, I'm pretty sure the version of HSCore you're using is 15.03, but could you also include the full version string, for future reference?
My guess is that you are trying to interact with a FHIR Gateway namespace, which does not support the create interaction from clients.
Basically, HealthShare currently supports two types of FHIR namespaces. The first is a regular FHIR reference server, that supports most of the FHIR DSTU2 standard, including create, read, update, delete, search, history, and vread interactions. However, this server is not a part of the HealthShare federation in any way. It neither sends data to nor receives data from the rest of HealthShare.
The other type of FHIR namespace is what is known as a FHIR Gateway. Clients interact with it by first starting a session (there are different ways of doing this), then, within that session, loading a patient's data from the HealthShare Information Exchange into the FHIR Gateway by doing a read on that patient (where ID=the patient's MPIID from Information Exchange). Then queries that are explicitly scoped to that patient via search parameter may be submitted to the FHIR endpoint. Clients may not create resources on the FHIR Gateway. Data can only be loaded into the FHIR Gateway from Information Exchange. Clients can only see data in their session, and after a session ends, any data in that session is physically deleted.
You can find more information about the FHIR Gateway in the HealthShare documentation:
http://<host>:<port>/csp/docbook/DocBook.UI.Page.cls?KEY=HESUP_fhir
Or, if you have any specific questions, I'd be happy to answer them here.
Hi Conor,
The issue is that HealthShare does not support CORS requests against FHIR endpoints that are secured with standard Caché authentication. So if you look at the Network tab in your browser's developer tools, you'll see that before your browser sends the GET request to that URL, it sends an OPTIONS request to that same URL. HealthShare responds with a 404, and what the browser actually complains about is the response missing some headers that are expected in a CORS response.
For an endpoint to support CORS, the authentication in the CSP application settings for the endpoint has to be set to "Unauthenticated". If you're just developing or trying out FHIR, this is fine. In production, the expectation is that this endpoint would be secured via OAuth.
Hi Krystian, are you referring to the "extension" property in the data model? If so, then that is currently supported by FHIR repository in HealthShare. Resources can be created in the repository with extensions and these will be saved. I'm attaching an example of a patient resource that contains multiple extensions. Note that this is a DSTU2 resource, which is the FHIR version that HealthShare currently supports. The current version of FHIR is STU3. HealthShare will support this in a future release.
EDIT: I *thought* I could attach a file, but that seems not to be the case. In any event, you can download the FHIR DSTU2 examples directly from the hl7.org site as XML or JSON:
http://hl7.org/fhir/DSTU2/examples.zip
http://hl7.org/fhir/DSTU2/examples-json.zip
And you can look through those for examples of extensions.
Here's a routine I use to restart a particular host in the production:
RestartEnsHost(pName)
set tSC = ##class(Ens.Director).EnableConfigItem(pName,0) if 'tSC write !, $system.Status.GetOneErrorText(tSC), !! quit
set tSC = ##class(Ens.Director).EnableConfigItem(pName,1) if 'tSC write !, $system.Status.GetOneErrorText(tSC), !! quit
quit
How about saving the values of a, b, and c in whatever way makes sense given the context - global, PPG, %-var, %session, or have the method return them, and then also make them arguments to the method. When calling the method again, check for them in whatever way they were saved, and if not found initialize them to "".
If you do find them, you might have to "rewind" each subscript by one to make sure you don't miss anything, which is easy to do with $ORDER:
set a = $ORDER(^data(a), -1)
Hi, with regard to your second question, if your property "CODE" is unique and indexed, ie, your class definition includes something like:
Index CodeIndex on CODE [Unique];
Then there is a generated method you can use to open the object that has CODE=Xparameter:
set myObject = ##class(User.MyPersistentClass).CodeIndexOpen(Xparameter, concurrency, .sc)
For any unique index, the method name is always "[index-name]Open". You can read more about auto-generated methods here:
https://community.intersystems.com/post/useful-auto-generated-methods
With regard to your first question, I'm not aware of any system method that returns a list of all saved objects for a class, but you could implement that by creating a class method that creates and executes the appropriate query, then iterates over the results and copies them to a list. I would be cautious about doing something like this with large tables though, since you would essentially be loading the entire table into memory. This could lead to <STORE> errors:
https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RERR_system
A better solution might be to implement some kind of iterator method that only loads one object at a time.
Hi Cyriak, you do not need to generate the Aggregation Key, as that is done automatically when the Access Gateway fetches a patient's data.
I believe the simplest way for custom code running in the Viewer to get the Aggregation Key value would be to first get the ID of the patient object in the Viewer Cache from the CSP session:
Set tViewerPatientID = $listget(%session.Data("lastPatientId"))
(For historical reasons the value of %session.Data("lastPatientId") is a $list, though it should only ever have a single value.)
Then you would call web.SDA3.Loader:GetAgKey(), passing it the ID of the patient object to get the Aggregation Key:
Set tAgKey = ##class(web.SDA3.Loader).GetAgKey(tViewerPatientId)
GetAgKey() is documented as an API method, so it should be safe to use.
You are correct that you can then use the Aggregation Key to query for the current patient's data in the Aggregation Cache.
Does that answer your question?
I'm not sure in what version %JSON.Adaptor was introduced, but if it's in your version, you can have the class modeling your object extend %JSON.Adaptor, which will give you access to the %JSONImport() method, which can de-serialize a JSON payload into an object.
One caveat to this is that I believe any serial objects referenced by the top-level object will also need to extend %JSON.Adaptor for the import to work properly.
Hi Bill, I was facing a similar (but not identical) issue not too long ago where I was trying set %response.Status in my REST handler class, only to have a different status code reflected in the response to the client.
The issue ended up being that the request to the REST handler class that I was working on was being forwarded from another REST handler class that extends EnsLib.REST.Service rather than %CSP.REST. The EnsLib REST handler works a little differently. The user code is supposed to write the output to a response stream, with the response headers being set as attributes of the stream, rather than setting headers of %response directly.
So my question is, is your REST handler downstream from an EnsLib.REST.Service REST handler? I also see that your REST handler extends Ens.BusinessService. I wonder if something in that class is overriding your %response headers. Is there any way you can test your class with that superclass removed?
Hi Clayton, I don't know how you would map from one set of units to another in the data in UCR (that would probably need to be done prior to ingestion into HealthShare), but there is a mechanism in the HS Clinical Viewer that enables you to define a "calculated" observation, the value of which is calculated from other observations. The value of this observation is calculated as the patient's data is loaded into the Viewer Cache and only exists in the Viewer Cache, not in the patient's data in UCR. If you look at <install-dir>\distlib\trak\misc\HS-Default-ObservationItem.txt, you'll see an example of how this can be done:
[...] 8302-2^Height^^^^N [...] 3141-9^Weight Measured^^^^N // Body mass index calulations // BMI = Weight (lb) / (Height (in) x Height (in)) x 703 39156-5^Body mass index^^^^C^([3141-9]/([8302-2]*[8302-2]))*703^2^ // Metric Calculation // BMI = Weight (kg) / (Height (cm) / 100 x Height (cm) / 100) // 39156-5^Body mass index^^^^C^([3141-9]/([8302-2]/100*[8302-2]/100))
This file is used to pre-populate the "observation item" code table in the Viewer Cache when the Viewer namespace is reset. What the file is doing here is creating a couple of standard observation items for weight and height. Then it defines a "calculated" item for BMI that is defined as "(weight / height2)*703", where the "height" and "weight" inputs are references to items the appear elsewhere in this file.
So you should be able to use this to convert from one set of units to another, however - it sounds like your use case is to be able to normalize multiple input formats into a single output format, which I'm not sure if you can do with this mechanism. That is, while it should be possible to define an item that converts inches to feet/inches and another that converts centimeters into feet/inches, I don't know if there is a way to define a single item that can convert both inches and centimeters into feet/inches.
I haven't used this feature extensively, though, so maybe it can do some things (like conditional logic) that I'm not aware of. Someone from Trak might now. The HS-Default-ObservationItem.txt file is a uniquely HealthShare concept, but the code table that it populates (User.MRCObservationItem) belongs to Trak.
Hi Carl, To expand on Tim's answer:
Keys in the HS Configuration Registry can either be set by a user in the Management Portal, or they can be set programmatically. I am only seeing one place in the HS codebase where these keys get set, in HS.Util.Installer.Kit.IHE.XDM.Direct:AddHub():
Do ##class(HS.Registry.Config).AddNewKey("\ZipUtility\ZipCommand",$S($zv["Win":"""c:\program files\7-zip\7z"" a %1 . -r",1:"zip -rm %1 ."))Do ##class(HS.Registry.Config).AddNewKey("\ZipUtility\UnZipCommand",$S($zv["Win":"""c:\program files\7-zip\7z"" x %1 -o. -r",1:"unzip %1 -d ."))However this class appears to be related to setting up support for the XDM IHE profile, so unless that is something that's important to you, you shouldn't worry about getting that code to run. Rather, this is just an example of the values for these keys that is expected by the code that uses them.
Without being able to see your environment, it's difficult to say where the disconnect is or what would need to be tweaked to decode those characters correctly. However, if you have an opportunity to manually process the HL7 data at any point as it flows through the system, then you may be able to call $ZConvert/$ZCVT on the encoded data to decode it:
USER>s str = $C(90,111,108,195,173,118,97,114,101,115)
USER>w str
ZolÃvares
USER>w $ZCVT(str, "I", "UTF8")
Zolívares
USER>https://docs.intersystems.com/iris20212/csp/docbook/Doc.View.cls?KEY=RC…
However, there should be a way to specify to your business service the encoding of input data so that it can decode the data for you. I would have thought that this would be done with either the "Charset" or "Default Char Encoding" settings, but it sounds like you've already tried that. I'm not sure why this wouldn't be working, but I'm fairly confident that this is how encoded data is supposed to be decoded, so it may be worth another look.