If I'm reading your question and subsequent replies correctly, you're trying to take the value of PV1:7.1, and then use that in a SQL query. The answer has been given by Ashok when you put their replies together, but hopefully putting it all into a single response will make things easier to follow.
If this is the case, then you will want to do the following:
Step 1: Set a variable to the value of PV1:7.1:
Step 2: Add a code block, and use this to run your sql query:
Step 3: Do what you need to with the value of ID - for the sake of this response, I'm just setting the value of PV1:7.2 to the ID returned from the query that inserted into the variable "Output":
It's worth knowing that, when working with Embedded SQL, prefixing a variable with a colon is how you can pass variables in and out of the Embedded SQL section of code. However it's a bit clearer when working directly with ObjectScript vs a DTL.
For example, if we had the following table:
ID
COL_A
COL_B
1
ABC
123
2
DEF
234
We could have the following in ObjectScript:
SetX = ""// X is nullSet Y = "ABC"
&SQL(SELECT COL_B
into :X
From TestTable
WHERE COL_A = :Y
)
WRITEX//X is 123
I don't believe there is a way of increasing the system limit on string lengths. Even if there is, it's best to approach this by working with the data as a stream.
Otherwise you could end up in a cat and mouse game of needing to increase the length the next time you get a larger document
The input is a string, so the max length will be your system max (which should be 3,641,144).
Assuming you're trying to retrieve the stream from a HL7 message, you will probably want to use the built in method GetFieldStreamBase64
So you could try something like:
Set tStream = ##class(%Stream.TmpBinary).%New()
Set tSC = pHL7.GetFieldStreamBase64(.tStream,"OBX:5")
ObjectScript
ObjectScript
And then your decoded file would be in the temp stream.
(You may need to tweak this slightly depending on how you intend to then use the stream, and the correct property path of the Base64 within the HL7 message)
As the default was set to "TEST" in my function, it worked fine throughout testing. Once the upgrade to Prod occurred, the issue was spotted, and the simple solution was to just reset the values. As I was moving from an adhoc to a official release, I chalked it up to that.
This is a rather subjective based on the skill level of the intended audience.
You could add a comment to the ClassMethod to provide context to what is being done and why. For example:
/// This ClassMethod takes a delimited String from System-X that consists of sets of Questions and Answers. /// The Sets are delimited by a pipe "|" and then the questions and answers are delimeted by a colon ":"/// The response from this ClassMethod is a %Library.DynamicArray object containing the questions and answersClassMethod createResponse(data As%String(MAXLEN="")) As%Library.DynamicArray
{
;1.- Questions splitted by "|"Set listQuestions = $LISTFROMSTRING(data, "|")
Set items = []
Set questionNumber = 0;2.- IterateFor i=1:1:$LISTLENGTH(listQuestions) {
Set questionAnswer = $LISTGET(listQuestions, i)
;3.- Update variablesSet questionNumber = questionNumber + 1Set question = $PIECE(questionAnswer, ":", 1)
Set answer = $ZSTRIP($PIECE(questionAnswer, ":", 2), "<W") //Get rid of initial whitespace;4.- Generate itemSet item =
{
"definition": ("question "_(questionNumber)),
"text": (question),
"answer":
[
{
"valueString": (answer)
}
]
}
Do items.%Push(item)
}
Quit items
}
ObjectScript
ObjectScript
Or you could go one step further and be more descriptive with your comment at each action within your code. So, instead of:
;2.- Iterate
You could write something like:
;2.- Iterate through the list of Questions and Answers
If your intended audience is not familiar with ObjectScript, then you may want to introduce them to features in stages. For example, you could use $ZSTRIP on both the question and answer in your For loop, but only nest it for the answer and use comments to describe it all. Something like:
// Retrieve the question from the delimited entrySet tQuestion = $PIECE(questionAnswer, ":", 1)
// Strip any whitespace from the start of the questionSet question = $ZSTRIP(tQuestion, "<W")
// It is also possible to nest functions, so below we will retrieve the answer and remove the whitespace in a single line.Set answer = $ZSTRIP($PIECE(questionAnswer, ":", 2), "<W")
If you were to use the Parenthesis () Syntax in your routing rule, you could simply make your rule something like:
The reason this works is that using the parenthesis syntax to access repeating values from a message will return all of the entries in the repeating segment as a *delimited string, and you can then check the string contains the - character using the contains function.
*Do make sure that the delimiter isn't the character you're looking for. Maybe throw in a trace when you first test this and check how it's returned.
Basically, I'm stuck trying to work out if I should scrap the wakeup calls etc, and just call the external data when I get a C-FIND-RQ message and then call "CreateIntermediateFindResponse" for each result set entry, or if it's necessary to use the wakeup calls and somehow hold the result set in context and move to the next result set entry on each Ens.AlarmResponse received.
ETA: The approach taken was to use the initial message as a trigger to call off to an external db, and write the results into a local table, and then use the Ens.AlarmResponse as the trigger to grab the top entry from the local table and return this to the calling system. This then allows for a cancel to come in and interrupt the process (the cancel will trigger a deletion of the appropriate rows in the local table)
A good use of a lookup table would be when working with an integration between two systems that use differing codes for the same values.
For example, you could have System A that records Sex as 1 for Male, 2 for Female, 0 for Not Known, and 9 for Not Specified, whereas System B uses M for Male, F for Female, and O for Other.
You could have a winding If/Else in a transform, or you could simply reference a lookup table in your DTL using the ..Lookup() function:
and then build up your lookup table to look like this:
As you can see, System A has more values than System B so the values for Not Known and Not Specified are being added as Other in my example.
Another example could be you needed to filter messages in a router based on a code within the HL7 message. You could add the codes to a lookup table as the key and a description as the value, and then use the Exists() function within your routing rule:
One option could be to change the permissions for the source directory to be read only for the account running Ensemble/Healthshare? This way, the adapter will copy the file from the directory but will then be unable to delete it from the directory, while also keeping track of what files it has copied.
The log for the passthrough service will be a bit messy at first. but you'll end up with something like:
However, if the directory you're going to be checking for documents will be an ever growing list of a large number of documents, then your own suggestion of copying the files into a secondary working directory before being picked up by ensemble might actually be the best option as there's a bit of an overhead for the adapter when it's scanning a directory that contains a large number of files.
Depending on how soon after the files creation you need it for onward processing, you could create a scheduled task that copies all files from the previous day into your working directory and then sends an email if that fails for any reason?
For #2, your transform would then be something like:
And then for #3, your file operation would be something along the lines of:
Class Demo.Operations.SystemX.FileWriter Extends Ens.BusinessOperation
{
Parameter ADAPTER = "EnsLib.File.OutboundAdapter";Property Adapter As EnsLib.File.OutboundAdapter;Parameter INVOCATION = "Queue";
Method OnMessage(pRequest As Demo.Messages.SystemX.CustomBody, Output pResponse As Ens.Response) As%Status
{
Set Line1 = pRequest.Param1_"|"pRequest.Param2
Set Line2 = pRequest.Param3_"|"pRequest.Param4
Set Line3 = pRequest.Param5_"|"pRequest.Param6
Set outString = Line1_$C(13)_$C(10)_Line2_$C(13)_$C(10)_Line3_$C(13)_$C(10)
Set fileName = "Filename"Set sc = ..Adapter.PutString(fileName_".dat",outString)
Quit sc
}
}
ObjectScript
ObjectScript
Please note that the above is a super rough draft - there's no error handling, and you'd need to consider how you'd make the filename unique per message, but I'm hoping this gets you on the right path.
I don't have anything immediately to hand as I still feel that the reuse of code in Step 3 from the original class method is not best practice, although I do have this running in a live production for 2-3 operations where this was needed.
I will try and see if I can get something put together that can be exported and put onto the Open Exchange. Just a warning, I'm not one for Docker, so it'll be a Production deployment export
The users are held within the SQL table "Security.Users" in the %SYS namespace, so you could use embedded sql to return the information, however as you're unlikely to be executing your code directly from the %SYS namespace, I'd suggest creating a function that you pass the email address, and it returns the username.
Something like:
Class Demo.Utils.General.Users
{
ClassMethod UserFromEmail(Email As%String, Output Username As%String) As%Status
{
//Initially set this to null, as we want to return it empty when we get no resultsSet Username = ""//Hold the Namespace within a variable so we can use the variable to set the namespace back once the SQL has been run.Set CurrNamespace = $NAMESPACE//Change NameSpace to %SYSSet$NAMESPACE = "%SYS"//Run query to get the Username based on the email address - note the use of the UPPER function to remove issues with case sensitivity
&SQL(SelectIDinto :Username
FROM Security.Users
WHEREUPPER(EmailAddress) = UPPER(:Email)
)
//Set namespace back to the namespace the function was run fromSet$NAMESPACE = CurrNamespace
//Evaluate SQLCODE for result//Less than 0 is an error.If SQLCODE <0{
WRITE"SQLCODE="_$SYSTEM.SQL.Functions.SQLCODE(SQLCODE)
QUIT0
}
//Greater than 0 can really only mean Code 100, which is no results found.If SQLCODE > 0 {
QUIT1//No Result Found
}
Else {
QUIT1//Result Found
}
}
}
There's a few "gotchas" when it comes to Character Encoding. But the key thing in you case is understanding the character encoding being used by the receiving system. This should be something specified in the specification of the receiving system, but many times it's not.
If I had to guess, it's most likely that the receiving system is using UTF-8 simply because latin1/ISO-8859-1 encodes the pound symbol as hex "A3" whereas UTF-8 encodes to "C2 A3". As there's no solitary "A3" in UTF-8, there's nothing to print, which is why you get the ? instead. I'm sure there's other character sets where this can happen, but I would start there.
To quickly answer your second question: you will want to take a look at the table Ens.MessageHeader which contains the Session ID, and then the Ens.MessageBody which is linked to the Ens.MessageHeader on the field "MessageBodyId".
go to post
I haven't seen anything "official" but there was this article from 2021 that provided a way to recreate it:
VSCode Tips & Tricks - SOAP Wizard | InterSystems Developer Community |
go to post
Hey Christine.
If I'm reading your question and subsequent replies correctly, you're trying to take the value of PV1:7.1, and then use that in a SQL query. The answer has been given by Ashok when you put their replies together, but hopefully putting it all into a single response will make things easier to follow.
If this is the case, then you will want to do the following:
Step 1: Set a variable to the value of PV1:7.1:
Step 2: Add a code block, and use this to run your sql query:
Step 3: Do what you need to with the value of ID - for the sake of this response, I'm just setting the value of PV1:7.2 to the ID returned from the query that inserted into the variable "Output":
It's worth knowing that, when working with Embedded SQL, prefixing a variable with a colon is how you can pass variables in and out of the Embedded SQL section of code. However it's a bit clearer when working directly with ObjectScript vs a DTL.
For example, if we had the following table:
We could have the following in ObjectScript:
Set X = "" // X is null Set Y = "ABC" &SQL( SELECT COL_B into :X From TestTable WHERE COL_A = :Y ) WRITE X //X is 123
go to post
I don't believe there is a way of increasing the system limit on string lengths. Even if there is, it's best to approach this by working with the data as a stream.
Otherwise you could end up in a cat and mouse game of needing to increase the length the next time you get a larger document
go to post
The input is a string, so the max length will be your system max (which should be 3,641,144).
Assuming you're trying to retrieve the stream from a HL7 message, you will probably want to use the built in method GetFieldStreamBase64
So you could try something like:
Set tStream = ##class(%Stream.TmpBinary).%New() Set tSC = pHL7.GetFieldStreamBase64(.tStream,"OBX:5")
And then your decoded file would be in the temp stream.
(You may need to tweak this slightly depending on how you intend to then use the stream, and the correct property path of the Base64 within the HL7 message)
go to post
Stupidly, no.
As the default was set to "TEST" in my function, it worked fine throughout testing. Once the upgrade to Prod occurred, the issue was spotted, and the simple solution was to just reset the values. As I was moving from an adhoc to a official release, I chalked it up to that.
Next time, WRC will be getting a call 🙂
go to post
Word of warning for using this - I have had this value be reset during an upgrade on Healthconnect.
I was using this value in a function to control the output of a Transform, and I hadn't accounted for the chance of this returning null.
My code looked something like this:
ClassMethod WhatEnvAmI() { Set Env = $SYSTEM.Version.SystemMode() If (Env = "LIVE")||(Env = "FAILOVER"){ Quit "LIVE" } Quit "TEST" }
So, post upgrade, the transform suddenly begun outputting values specific to the test system from the live environment.
go to post
This is a rather subjective based on the skill level of the intended audience.
You could add a comment to the ClassMethod to provide context to what is being done and why. For example:
/// This ClassMethod takes a delimited String from System-X that consists of sets of Questions and Answers. /// The Sets are delimited by a pipe "|" and then the questions and answers are delimeted by a colon ":" /// The response from this ClassMethod is a %Library.DynamicArray object containing the questions and answers ClassMethod createResponse(data As %String(MAXLEN="")) As %Library.DynamicArray { ;1.- Questions splitted by "|" Set listQuestions = $LISTFROMSTRING(data, "|") Set items = [] Set questionNumber = 0 ;2.- Iterate For i=1:1:$LISTLENGTH(listQuestions) { Set questionAnswer = $LISTGET(listQuestions, i) ;3.- Update variables Set questionNumber = questionNumber + 1 Set question = $PIECE(questionAnswer, ":", 1) Set answer = $ZSTRIP($PIECE(questionAnswer, ":", 2), "<W") //Get rid of initial whitespace ;4.- Generate item Set item = { "definition": ("question "_(questionNumber)), "text": (question), "answer": [ { "valueString": (answer) } ] } Do items.%Push(item) } Quit items }
Or you could go one step further and be more descriptive with your comment at each action within your code. So, instead of:
;2.- Iterate
You could write something like:
;
2.- Iterate through the list of Questions and Answers
If your intended audience is not familiar with ObjectScript, then you may want to introduce them to features in stages. For example, you could use $ZSTRIP on both the question and answer in your For loop, but only nest it for the answer and use comments to describe it all. Something like:
// Retrieve the question from the delimited entry Set tQuestion = $PIECE(questionAnswer, ":", 1) // Strip any whitespace from the start of the question Set question = $ZSTRIP(tQuestion, "<W") // It is also possible to nest functions, so below we will retrieve the answer and remove the whitespace in a single line. Set answer = $ZSTRIP($PIECE(questionAnswer, ":", 2), "<W")
go to post
If you were to use the Parenthesis () Syntax in your routing rule, you could simply make your rule something like:
The reason this works is that using the parenthesis syntax to access repeating values from a message will return all of the entries in the repeating segment as a *delimited string, and you can then check the string contains the - character using the contains function.
*Do make sure that the delimiter isn't the character you're looking for. Maybe throw in a trace when you first test this and check how it's returned.
go to post
Hey Guillaume.
Funnily enough - it's one of your github repos where I located the demo I'm trying use as a jumping off point (but from https://github.com/grongierisc/InstallEnsDemoHealth/blob/master/src/CLS/Demo/DICOM/Process/WorkList.cls)
Basically, I'm stuck trying to work out if I should scrap the wakeup calls etc, and just call the external data when I get a C-FIND-RQ message and then call "CreateIntermediateFindResponse" for each result set entry, or if it's necessary to use the wakeup calls and somehow hold the result set in context and move to the next result set entry on each Ens.AlarmResponse received.
ETA: The approach taken was to use the initial message as a trigger to call off to an external db, and write the results into a local table, and then use the Ens.AlarmResponse as the trigger to grab the top entry from the local table and return this to the calling system. This then allows for a cancel to come in and interrupt the process (the cancel will trigger a deletion of the appropriate rows in the local table)
go to post
Hey Michael.
A good use of a lookup table would be when working with an integration between two systems that use differing codes for the same values.
For example, you could have System A that records Sex as 1 for Male, 2 for Female, 0 for Not Known, and 9 for Not Specified, whereas System B uses M for Male, F for Female, and O for Other.
You could have a winding If/Else in a transform, or you could simply reference a lookup table in your DTL using the ..Lookup() function:
and then build up your lookup table to look like this:
As you can see, System A has more values than System B so the values for Not Known and Not Specified are being added as Other in my example.
Another example could be you needed to filter messages in a router based on a code within the HL7 message. You could add the codes to a lookup table as the key and a description as the value, and then use the Exists() function within your routing rule:
Which becomes:
go to post
Hey Jon.
One option could be to change the permissions for the source directory to be read only for the account running Ensemble/Healthshare? This way, the adapter will copy the file from the directory but will then be unable to delete it from the directory, while also keeping track of what files it has copied.
The log for the passthrough service will be a bit messy at first. but you'll end up with something like:
However, if the directory you're going to be checking for documents will be an ever growing list of a large number of documents, then your own suggestion of copying the files into a secondary working directory before being picked up by ensemble might actually be the best option as there's a bit of an overhead for the adapter when it's scanning a directory that contains a large number of files.
Depending on how soon after the files creation you need it for onward processing, you could create a scheduled task that copies all files from the previous day into your working directory and then sends an email if that fails for any reason?
go to post
It's great to see that there's a built in macro for this!
go to post
Hey Kurro.
I'm not sure of a built in function for this, but if you wanted to have your own:
Class Demo.FunctionSets.Example { ClassMethod Format(InputString As %String, Params... As %String) As %String { Set OutputString = InputString For i = 1 : 1 : $GET(Params, 0){ Set OutputString = $Replace(OutputString,"{"_i_"}",Params(i)) } Quit OutputString } }
And then:
go to post
StackOverflow suggests using svglib and reportlab to achieve this with python:
from svglib.svglib import svg2rlg from reportlab.graphics import renderPM drawing = svg2rlg("my.svg") renderPM.drawToFile(drawing, "my.png", fmt="PNG")
go to post
Oooh, I like this way more than my approach!
go to post
I would recommend approaching this in three parts:
For #1, this could be as basic as:
Class Demo.Messages.SystemX.CustomBody Extends Ens.Request { Property Param1 As %String; Property Param2 As %String; Property Param3 As %String; Property Etc As %String; }
For #2, your transform would then be something like:
And then for #3, your file operation would be something along the lines of:
Class Demo.Operations.SystemX.FileWriter Extends Ens.BusinessOperation { Parameter ADAPTER = "EnsLib.File.OutboundAdapter"; Property Adapter As EnsLib.File.OutboundAdapter; Parameter INVOCATION = "Queue"; Method OnMessage(pRequest As Demo.Messages.SystemX.CustomBody, Output pResponse As Ens.Response) As %Status { Set Line1 = pRequest.Param1_"|"pRequest.Param2 Set Line2 = pRequest.Param3_"|"pRequest.Param4 Set Line3 = pRequest.Param5_"|"pRequest.Param6 Set outString = Line1_$C(13)_$C(10)_Line2_$C(13)_$C(10)_Line3_$C(13)_$C(10) Set fileName = "Filename" Set sc = ..Adapter.PutString(fileName_".dat",outString) Quit sc } }
Please note that the above is a super rough draft - there's no error handling, and you'd need to consider how you'd make the filename unique per message, but I'm hoping this gets you on the right path.
go to post
Hi @Evgeny Shvarov
I don't have anything immediately to hand as I still feel that the reuse of code in Step 3 from the original class method is not best practice, although I do have this running in a live production for 2-3 operations where this was needed.
I will try and see if I can get something put together that can be exported and put onto the Open Exchange. Just a warning, I'm not one for Docker, so it'll be a Production deployment export
go to post
Hey Yuri.
The users are held within the SQL table "Security.Users" in the %SYS namespace, so you could use embedded sql to return the information, however as you're unlikely to be executing your code directly from the %SYS namespace, I'd suggest creating a function that you pass the email address, and it returns the username.
Something like:
Class Demo.Utils.General.Users { ClassMethod UserFromEmail(Email As %String, Output Username As %String) As %Status { //Initially set this to null, as we want to return it empty when we get no results Set Username = "" //Hold the Namespace within a variable so we can use the variable to set the namespace back once the SQL has been run. Set CurrNamespace = $NAMESPACE //Change NameSpace to %SYS Set $NAMESPACE = "%SYS" //Run query to get the Username based on the email address - note the use of the UPPER function to remove issues with case sensitivity &SQL( Select ID into :Username FROM Security.Users WHERE UPPER(EmailAddress) = UPPER(:Email) ) //Set namespace back to the namespace the function was run from Set $NAMESPACE = CurrNamespace //Evaluate SQLCODE for result //Less than 0 is an error. If SQLCODE <0{ WRITE "SQLCODE="_$SYSTEM.SQL.Functions.SQLCODE(SQLCODE) QUIT 0 } //Greater than 0 can really only mean Code 100, which is no results found. If SQLCODE > 0 { QUIT 1 //No Result Found } Else { QUIT 1 //Result Found } } }
This is by no means perfect as I have thrown it together for the example - please forgive the messy if/else's!
go to post
There's a few "gotchas" when it comes to Character Encoding. But the key thing in you case is understanding the character encoding being used by the receiving system. This should be something specified in the specification of the receiving system, but many times it's not.
If I had to guess, it's most likely that the receiving system is using UTF-8 simply because latin1/ISO-8859-1 encodes the pound symbol as hex "A3" whereas UTF-8 encodes to "C2 A3". As there's no solitary "A3" in UTF-8, there's nothing to print, which is why you get the ? instead. I'm sure there's other character sets where this can happen, but I would start there.
go to post
Hey Joe.
To quickly answer your second question: you will want to take a look at the table Ens.MessageHeader which contains the Session ID, and then the Ens.MessageBody which is linked to the Ens.MessageHeader on the field "MessageBodyId".