If the embedded file is long enough, then that GetValueAt call will truncate it.  Instead you need to use GetFieldStreamRaw.  You do not need to save the file locally in order to attach it to an email.  If you want to do both, you should use a file operation for saving the file to disk and a separate email operation for sending out the email.

What I would do is extract the file from the HL7 with GetFieldStreamRaw and then store it in a message object.

To make Ensemble send an email you need a custom EMail operation:

http://docs.intersystems.com/ens20161/csp/docbook/DocBook.UI.Page.cls?KE...

Your message object that you send to the email operation needs to have a stream property to hold the file contents.

Your custom email operation will need to create a %Net.MailMessage object to send the mail.  This object is where you attach the file.  You can use the AttachStream method to attach the stream as a file.

http://docs.intersystems.com/ens20152/csp/documatic/%25CSP.Documatic.cls...

I think the way I would code this is create a new message object which contains properties for all the fields that might appear in the file.  Then create a new custom file service using the adapter EnsLib.File.InboundAdapter.  In the OnProcessInput for that new file service, use Dmitry and Carlos's suggestion to read the file with pRequest.ReadLine() and parse each line to extract the property name and value and store the value in the appropriate property of the new message object.

Then, have the service send that message object to a router with a DTL transform which will convert from your new message object class into an HL7 message and send that message to the target.

I don't think there is a way to repeatedly receive queue count alerts if the queue remains high.

What you could do is use Managed Alerts rather than simple alerts and you can configure the managed alert to repeatedly send notification emails until the person responsible closes the alert.

Doing it this way will require a person to actually look at the queue and either verify it's gone down and close the alert, or take action to reduce the queue.

I just thought of another method.  Getting the count should work for checking for existence as well.  If using DOM-style paths, then get the count with [*] and if using VDoc style paths, then get the count with (*).  Using the VDoc path example from above:

<if condition='source.{element1.element2.element3(*)}&gt;0'>

This will return True if there are any element3's inside the element2.  It will return False if there are not.

If you just want to know whether there is a value or not in element1.element2.element3, compare it to "".  The 'not equal' ('=) operator in Cache contains an apostrophe, so it must be escaped in the XML. (if a '= b write "a is not equal to b")

<if condition='source.{element1.element2.element3}&apos;=""'>

This will return True if element1.element2.element3 has a value, or it will return False if either element1.element2.element3 does not exist or if element1.element2.element3 exists but contains no value, like this:

<element3></element3>

If you need to differentiate between these two cases, things get more difficult.  There is no method in XML VDoc that can tell you whether an element exists or not.  In general with XML, the existence of an element shouldn't be used as a logical boolean value.  Instead you should use a boolean datatype in your XML schema.

If you must check whether element3 exists or not, you could do it like this:

say your XML document looks like this:

<element1><element2><element3>sometext</element3></element2></element1>

If you were to get the value of element2 with an assign action like this:

<assign value='element1.element2' property='e2' action='set' />

then after that line, the variable e2 will hold the value "<element3>sometext</element3>" so to check for the existence of element3, you could search e2 for the text "<element3>" with the ..Contains function:

<if condition='..Contains(e2,"&lt;element3&gt;")' >

If your element3 might be self-closed (like this: <element3 />) then you'll need to account for that possibility as well.

I am seeing the same thing.  OutputToString() internally uses OutputToIOStream() but it sets the CharEncoding on the stream to "binary" before passing it.  I think this is the source of the problem.

I was able to work around it using OutputToLibraryStream instead:

ENSEMBLE>set msg = ##class(EnsLib.EDI.XML.Document).ImportFromString("<Test>מִבְחָן</Test>")

ENSEMBLE>write msg.OutputToString()
<Test>???????</Test>
ENSEMBLE>set stream = ##class(%Stream.TmpCharacter).%New()

ENSEMBLE>write msg.OutputToLibraryStream(.stream)
1
ENSEMBLE>write stream.Read()
<Test>מִבְחָן</Test>

Just to add on to this, lookup tables are stored in ^Ens.LookupTable, subscripted by the table name and then the key.  The value of each node is the lookup value of the key in the subscript.

For example, if you have a table named Codes which contains a key named 123 that maps to value "ABC", it will look like this in the global:

^Ens.LookupTable("Codes",123)="ABC"

The reason reverse lookups are not supported is because we allow many keys to map to the same value.  So if you need to find a key, given a value, there may be many values that match.

Writing code to perform a reverse lookup will involve copying the ^Ens.LookupTable global into a variable subscripted by value rather than by key, like this:

ReverseLookupTable("Codes","ABC")=123

and then performing a lookup on that variable.

Hi Duncan,

The standard lookup tables are the closest thing to what you're looking for but as you noted, they don't support reverse lookups.  I think you will need something custom for this.  The easiest solution is probably to implement a reverse lookup for existing lookup tables.  If you implement a new classmethod to do this in a class that extends Ens.Rule.FunctionSet, then that classmethod will be available to DTLs.

-Brendan

The testing form isn't smart enough to handle array or list properties.  The easiest way I've found to test these is to create a  file business service and add code to populate the array with some hard-coded values.  Then add that service to your production, set the file path, and copy any random file into the file path directory to trigger the service.  Here's an example file service to do this:

Class Community.TestArray Extends Ens.BusinessService
{

  Parameter ADAPTER = "EnsLib.File.InboundAdapter";

  Method OnProcessInput(pInput As %Stream.Object, Output pOutput As %RegisteredObject) As %Status
  {
    set tTestMsg = ##class(MyApp.MyRequest).%New()
    do tTestMsg.idToTokenArray.Insert("One")
    do tTestMsg.idToTokenArray.Insert("Two")
    do tTestMsg.idToTokenArray.Insert("Three")
    quit ..SendRequestAsync("OperationToTest",tTestMsg)
  }

}
 

Make sure the 'Enable Standard Requests' setting on the REST business service is checked, Port is blank, and Pool Size is set to 0.  This will ensure the business service only listens for requests through the CSP Gateway and not through its own port.  Be sure to construct the URL for accessing the REST service as described in the documentation here, under "Defining the URL Using UrlMap and EnsServicePrefix":

http://docs.intersystems.com/ens20161/csp/docbook/DocBook.UI.Page.cls?KE...

I see $$$TRACE statements in your code, which leads me to believe you are using Ensemble.

What you're trying to do is much easier with EnsLib.EDI.XML.Document, and there are adapters supporting the use of this message class.

Here's an example:

ENSEMBLE>set msg = ##class(EnsLib.EDI.XML.Document).ImportFromString(tData)

ENSEMBLE>w msg.GetValueAt("/")
<HHSOS><DIAGNOSES><DIAGNOSIS_DATA><DIAGNOSIS_DATA_GUID>3762875</DIAGNOSIS_DATA_GUID><DIAGNOSIS_DATA_GUID>37628752</DIAGNOSIS_DATA_GUID></DIAGNOSIS_DATA><DIAGNOSIS_DATA/><DIAGNOSIS_DATA/><DIAGNOSIS_DATA_GUID>37628753</DIAGNOSIS_DATA_GUID></DIAGNOSES></HHSOS>

ENSEMBLE>w msg.GetValueAt("/HHSOS/DIAGNOSES")
<DIAGNOSIS_DATA><DIAGNOSIS_DATA_GUID>3762875</DIAGNOSIS_DATA_GUID><DIAGNOSIS_DATA_GUID>37628752</DIAGNOSIS_DATA_GUID></DIAGNOSIS_DATA><DIAGNOSIS_DATA/><DIAGNOSIS_DATA/><DIAGNOSIS_DATA_GUID>37628753</DIAGNOSIS_DATA_GUID>

ENSEMBLE>w msg.GetValueAt("/HHSOS/DIAGNOSES/DIAGNOSIS_DATA[*]")
3

ENSEMBLE>w msg.GetValueAt("/HHSOS/DIAGNOSES/DIAGNOSIS_DATA[1]")
<DIAGNOSIS_DATA_GUID>3762875</DIAGNOSIS_DATA_GUID><DIAGNOSIS_DATA_GUID>37628752</DIAGNOSIS_DATA_GUID>

ENSEMBLE>w msg.GetValueAt("/HHSOS/DIAGNOSES/DIAGNOSIS_DATA[2]")

ENSEMBLE>w msg.GetValueAt("/HHSOS/DIAGNOSES/DIAGNOSIS_DATA[1]/DIAGNOSIS_DATA_GUID[*]")
2

ENSEMBLE>w msg.GetValueAt("/HHSOS/DIAGNOSES/DIAGNOSIS_DATA[1]/DIAGNOSIS_DATA_GUID[1]")
3762875

ENSEMBLE>w msg.GetValueAt("/HHSOS/DIAGNOSES/DIAGNOSIS_DATA[1]/DIAGNOSIS_DATA_GUID[2]")
37628752
 

These are called DOM-style paths as opposed to VDoc paths.  Here's the documentation:

http://docs.intersystems.com/ens20161/csp/docbook/DocBook.UI.Page.cls?KE... 

Basic Class Queries are similar to stored procedures.  You can hard-code some query parameters while allowing others to be passed as arguments.  The query is pre-compiled in the code.  With a dynamic query, when you call the .Prepare method, this causes code to be generated at runtime to run the query.

Embedded SQL is also pre-compiled into the code.  One benefit basic class queries have over embedded SQL is that basic class queries can be exposed to SQL, allowing them to be called like stored procedures.

If you use Managed Alerts, there is a custom FunctionSet function IsRecentManagedAlert() which you can use to suppress repeated alerts.  Here's an excerpt from the documentation:

The rule can check whether the alert is a repeat occurrence of a previous alert that is represented by a currently open managed alert. To do this, the rule uses the Ens.Alerting.Rule.FunctionSet.IsRecentManagedAlert() function. The IsRecentManagedAlert() function tests if there is a recent open managed alert that is from the same component and has the same text as the specified alert. You can optionally specify that the function adds a reoccurs action to the existing managed alert.

http://docs.intersystems.com/ens20161/csp/docbook/DocBook.UI.Page.cls?KE...