Web services normally use an HTTP status code; for example, an ACK would be 200 OK for REST/HTTP and would be available through the %Net.HttpResponse Object in the StatusCode/StatusLine properties. SOAP usually provides some sort of payload along with the status code, and that would be found in the Data property. The type of response would likely be identified in the source/target system's WSDL for the SOAP interface.

This is something I wrote a long time ago; it extracts all business hosts and their settings. I've learned some things since I wrote it and would probably do a few things differently these days. It should be enough to give you some ideas, though ...

ClassMethod GetConfigs(pProduction As %String = {$G(^Ens.Runtime("Name"),$G(^Ens.Suspended,$G(^Ens.Configuration("csp","LastProduction"))))}, pFile As %String = {$System.Util.GetEnviron("HOME")_"/"_$NAMESPACE_"_hostconfigs.csv"}) As %Status
{
    Set tPrd = ##class(Ens.Config.Production).%OpenId(pProduction)
    Set tOut = ##class(%File).%New()
    Set tOut.Name = pFile
    Set tSC = tOut.Open("RWN")
    if '$$$ISERR(tSC)
    {
        Set tSC = vOut.WriteLine("""Type"",""Name"",""ClassName"",""Adapter"",""Enabled"",""ConfigName"",""ConfigValue""")
    }
    Quit:$$$ISERR(tSC) tSC
    If $ISOBJECT(tPrd)
    {
        For i=1:1:tPrd.Items.Count()
        {
            Set tHost = tPrd.Items.GetAt(i)
            Set tName = tHost.Name
            Set tClassName = tHost.ClassName
            Set tType = $CASE(tHost.BusinessType(),0:"Unknown",1:"Service",2:"Process",3:"Operation",4:"Actor",:"Huh?")
            Set tAdapter = $CLASSMETHOD(tClassName,"%GetParameter","ADAPTER")
            Set tEnabled = tHost.Enabled
            Set tCategory = tHost.Category
            Set tLine = """"_tType_""","""_tName_""","""_tClassName_""","""_tAdapter_""","""_tEnabled_""","""
            Do tOut.WriteLine(tLine_"Category"","""_tCategory_"""")
            For l=1:1:tHost.Settings.Count()
            {
                Set tCfg = tHost.Settings.GetAt(l)
                Set tCfgName = tCfg.Name
                Set tCfgVal = tCfg.Value
                Set tSC = vOut.WriteLine(tLine_tCfgName_""","""_tCfgVal_"""")
                Return:$$$ISERR(tSC) tSC
            }
        }
        Do tOut.Close()
    }
    Else
    {
        Return $$$ERROR(0,"Production Not Found in this namespace")
    }
    Return $$$OK
}

That should probably be a choice for completeness' sake.

But you can create an ObjectScript class by right-clicking the server or one of its packages in the Explorer pane and entering the package/class name with a .cls extension. Use slashes rather than periods as package/class delimiters (if you use periods, it may display differently in Explorer until you refresh the window).

So I forgot that there's a Pad() method in the DTL function list that would likely work better for your purposes than $EXTRACT() and $JUSTIFY(). You can use it to zero or space fill the fields to the required width. The first argument is the value to pad, the 2nd the width (positive numbers for pad right, negative for left), and the 3rd is the pad character to fill with.

Your update to the requirements is incomplete; it doesn't specify what, if anything, goes in the 2nd (and subsequent) row(s) of the output after the ItemCodeExternal.Identifier value, whether the fractional value is in the Quantity field is right or left justified zero-filled, or whether the UnitofMeasure and DateNeeded values are padded to make line length consistent across all records.

Here's an example of what it might look like and will need to be adjusted to accommodate your vendor's spec:

The code rules that write the records to the stream would need to be adjusted to eliminate the "|" delimiters and insert the renamed/added variables:

This should get you to where you need to be.

I went ahead and created a DTL that appears to do what you requested and does not require a custom File Operation to work; It assumes you're using EnsLib.File.PassthroughOperation as the outbound operation class.

The filename is created using the value set for target.OriginalFilename in Ens.StreamContainer in the DTL, so you could base it on something from the HL7 message itself or just set it to a static value (as I've done). You can use date/time tokens in the outbound operation's File Name field to aggregate multiple messages per file, or just let it create uniquely named files for each message with the default pattern.

Here's the DTL Configuration:

And the rules:

To test, I created a HL7 file with repeating ORC groups based on the sample provided in your post, but the DTL will work whether it's repeating or not:

The Filename pattern I used in the outbound operation:

This file was created:

And contained this output:

Hope this helps.

Well I guess there IS a setting (thanks, @Eduard Lebedyuk!) laugh

The parameter Undefined specifies the behavior when ObjectScript attempts to fetch the value of a variable that has not been defined. The value of Undefined may be 0, 1, or 2:

  • 0 - Always throw an <UNDEFINED> error. (default)
  • 1 - If the undefined variable has subscripts, return a null string, but if the undefined variable is single-valued, throw an <UNDEFINED> error.
  • 2 - Always return a null string.

You can change that setting in System Administration | System Configuration | Additional Settings | Compatibility.

Not sure what version of Caché or IRIS you're on; for future reference it's helpful to include that information. In IRIS 2021.2, you can do this from the IRIS SQL Shell:

JEFF>do $system.SQL.Shell()
SQL Command Line Shell
----------------------------------------------------
The command prefix is currently set to: <<nothing>>.
Enter <command>, 'q' to quit, '?' for help.

[SQL]JEFF>>set displaypath /home/jeff/tmp/
displaypath = /home/jeff/tmp/

[SQL]JEFF>>set displayfile sqlout
displayfile = sqlout

[SQL]JEFF>>set displaymode csv
displaymode = csv

[SQL]JEFF>>set selectmode display
selectmode = display

[SQL]JEFF>>select top 100 * from Ens_Util.Log
13.     select top 100 * from Ens_Util.Log

/home/jeff/tmp/sqlout.csv
/home/jeff/tmp/sqloutMessages.txt

statement prepare time(s)/globals/cmds/disk: 0.0002s/6/831/0ms
          execute time(s)/globals/cmds/disk: 0.0035s/467/20822/0ms
                          cached query class: %sqlcq.JEFF.cls115
---------------------------------------------------------------------------

The default delimiter is comma, but you can change that. For example, the tab character:

[SQL]JEFF>>set displaydelimiter = $C(9)

A Business Process Component is the BPL analogue of a subroutine or function and is called exclusively from a BPL. The idea is that they can be reusable components applicable to potentially multiple, different business processes. I don't think that's really what you're looking for.

If you don't have a FIFO concern with this database processing and are thinking that increasing the number of parallel processes performing these database activities might improve performance, you could try increasing the pool size for the BP.

There are methods for dealing with what are essentially embedded streams in HL7 Objects. See the methods GetFieldStreamRaw() and StoreFieldStreamRaw() in class EnsLib.HL7.Message; these are useful for copying streams from one message to another. If the need is to extract the Base64 stream as a binary stream for writing to a file, there's also GetFieldStreamBase64() in the same class; the stream obtained from it can be used with file-based streams to write to a disk file.

I'm not sure whether this will work in Ensemble 2018.1, but it does seem to work fine in IRIS for Health Interoperability 2022.2.

I'm testing with a simple JSON file that looks like this:

{
    "mrn": 12345678,
    "name": "Johann Smythe",
    "firstname": "Johann",
    "lastname": "Smythe",
    "dob": "1989-03-21 14:20:00",
    "phone": "(555) 555-4917",
    "mobile": "(555) 555-6401",
    "email": "johann@smythe.com",
    "address": "123 Anystreet St",
    "city": "Anytown",
    "state": "ME",
    "zip": "04121"
}

I've used the File Passthrough Service (EnsLib.File.PassthroughService) to read the JSON document into a stream, message class Ens.StreamContainer. Because this isn't an HL7 object, my router is based on the "General Message Routing Rule" rule type, and my constraint consists of the source service name with a message class of Ens.StreamContainer.

In the DTL called by the send action, I use Ens.StreamContainer as the source message type. The target message type is EnsLib.HL7.Message with whatever Document Category and Type is needed.

The first rule in the DTL is this:

After setting a number of default values for the target HL7 message (Event Type/Trigger, Date/Time of message, etc.) I populate the PID fields as follows:

And I now have an HL7 message created from JSON.

This isn't going to work with a batch of patient records in a JSON array; you'd need to create a BPL to process that. But for input that consists of a simple structure like the example I used, you can accomplish what you need without building a custom service or creating a BPL.

Are you using LDAP for authentication? I seem to remember running into this when the web applications created as part of enabling Ensemble/Interoperability weren't set to support LDAP.

Compare the settings for the web applications created for your new namespace in Security | Applications | Web Applications with those from other (working) Ensemble-enabled namespaces.

For those that use Interoperability/HealthConnect, nc/netcat is also an excellent tool for verifying that remote ports are accessible for HL7 MLLP, HTTP or other protocols that require a TCP socket client connection.

And while this thread is specifically for Unix/Linux, there's a Windows PowerShell analogue named Test-NetConnection (alias tnc) that provides a subset of nc's features.

Something like this, perhaps?

Class User.Util.StringFunctions Extends Ens.Util.FunctionSet
{

ClassMethod ReReplace(pStr As %String, pPat As %String, pRepl As %String = "") As %String
{
    Set tStrt = $LOCATE(pStr,pPat,,tEnd) - 1
    // in case the pattern isn't found, return source string
    Return:(tStrt < 0) pStr
    Set tPrefix = $EXTRACT(pStr,1,tStrt)
    Set tSuffix = $EXTRACT(pStr,tEnd,*)
    Return tPrefix_pRepl_tSuffix
}

}
USER> set mystr = "REASON->Blood(1.23)"
USER> set newstr = ##class(User.Util.StringFunctions).ReReplace(mystr,"->\w+")
USER> write newstr
REASON(1.23)
USER> set altstr =  ##class(User.Util.StringFunctions).ReReplace(mystr,"->\w+","-CODE")
USER> write altstr
REASON-CODE(1.23)

Hi Blake,

This might get you started in the right direction:

Set tRuleName = "<rulename>"
Set tTarget = $ORDER(^Ens.Rule.Targets(tRuleName,""))
Set tArr = 0
Set tCnt = 1
While tTarget '= ""
{
    Set tArr(tCnt) = tTarget
    Set tTarget = $ORDER(^Ens.Rule.Targets(tRuleName,tTarget))
    Set tArr = tCnt
    Set tCnt = tCnt + 1
}

Replace <rulename> with the name of the rule as it appears in the router configuration pane.

With some help from a fellow DC member, I wrote the method below. Its intent is to support auto-resolution of managed alerts:

/// Returns the connection status ("AdapterState") of the Business Service or Operation
/// named in <var>pItemName</var>
ClassMethod GetConnectionStatus(pItemName As %String) As %String [ Language = objectscript ]
{
    Set tStatement = ##class(%SQL.Statement).%New()
    Set tStatus = tStatement.%PrepareClassQuery("Ens.Util.Statistics","EnumerateJobStatus")
    If $$$ISERR(tStatus)
    {
        Return "Error in Status Query: "_$system.Status.GetErrorText(tStatus)
    }
    Set tRS = tStatement.%Execute(pItemName)
    If tRS.%SQLCODE = 0
    {
        Do tRS.%Next()
        Return tRS.%Get("AdapterState")
    }
    Return "Status not Found"
}

Here's a little code snippet that the Management Portal uses to get the Arbiter state:

	Set state = $SYSTEM.Mirror.ArbiterState()
	Set thisConnected = $SELECT($ZB(+state,+$$$ArbiterConnected,1)'=0:1,1:0)
	Set otherConnected = $SELECT($ZB(+state,+$$$ArbiterPeerConnected,1)'=0:1,1:0)
	
	If 'thisConnected {
		Set stateString = $$$Text("This member is not connected to the arbiter")
	} ElseIf 'otherConnected {
		Set stateString = $$$Text("Only this member is connected to the arbiter")
	} Else {
		Set stateString = $$$Text("Both failover members are connected to the arbiter")
	}

You'll need to add an Include statement for %syMirror to use the $$$Arbiter* macros.

Note that the ArbiterState() method is undocumented, and its behavior may change in future releases.