Yone, what is the data type of the property "datos" in Mensajes.Response.HistoriaClinica.ConsultaCitasResponse. I assume it's a list of a custom class that has two properties named fecha and hora?

Property datos as List of my.class.dato;

If so, you would do something like this:

set dato=##class(my.class.dato).%New()
set dato.fecha=fecha
set dato.hora=hora
do target.datos.InsertAt(dato,indice)

I just did a test with .Find() and was able to compile with no error and the rule executes correctly depending on the values in the inbound record. Here's what my rule looks like:

Is your router receiving the Record object from the record map or the batch object?

Since Find() doesn't meet your use case, it sounds like you'll need to loop through all of the values in that field. Routing rules can't do loops, so the right way to do this would be to create a custom function and pass it either the main Document or Document.ObservationValue. In your custom function you can iterate through the entire list and return a boolean.

Docs on creating a custom function:
https://docs.intersystems.com/healthconnect20191/csp/docbook/Doc.View.cl...

Hi Neil,

Can you provide some more details?

Is this record map being used for pulling data into the system or for outputting data?
Which component is logging the error -- business service, router/business process, business operation?
What is the error message?
Can you give an example of a record that causes the error?
Can you show the record map definition?

-Marc

Yone,

I think there's a problem with your Write() method. It is outputting data from the "stream" object, but "stream" is always set to a new (empty) stream and populated with data from "writer", which is also a new (empty) %XML.Writer object. So I don't see how this method could ever output any data.

Regarding the CSP error when requesting the WSDL, have you checked the application log to see what the actual error is?

-Marc

You can't directly store an object in a global like this. In your code, you're actually just storing a text description of the object reference in the global. The way to store an object in a global is to either (1) define it as a persistent class and let the system handle storing it automatically, (2) make the list of objects a property of a parent class which is itself persistent, or (3) serialize the object into a string that can be stored directly in the global.

Can you explain why you need to store copyList in this manner? If you just want to persist the objects in copyList, you can consider making RSPK21QUERYRESPONSE a persistent class. 

https://docs.intersystems.com/healthconnect20191/csp/docbook/DocBook.UI....
https://docs.intersystems.com/healthconnect20191/csp/docbook/Doc.View.cl...
https://docs.intersystems.com/healthconnect20191/csp/docbook/Doc.View.cl...

Also, it's worth noting that when you define the property RSPK21QUERYRESPONSE  as a list of objects where the child object extends %SerialObject,  then that entire list will be stored in the same global as the parent class.

One other note:

At the top of your code example, you set RSPK21QUERYRESPONSE as a %ListOfDataTypes:
SET RSPK21QUERYRESPONSE = ##class(%ListOfDataTypes).%New()

While later you try to clone it into a %ListOfObjects, which is not equivalent:
set copyList = ##class(%ListOfObjects).%New()
set copyList = pResponse.RSPK21QUERYRESPONSE.%ConstructClone(1)

The source for this JSON seems to think that "data" holds a string rather than an object. Still, we can convert that back to a proper object using %DynamicObject's %FromJSON() method:

USER>set myJSONObj={"app_id":"5cf57b56-c3b4-4a0d-8938-4ac4466f93af","headings":{"en":"Cita Atención Primaria","es":"Cita Atención Primaria"},"subtitle":{"en":"C.P. ISORA","es":"C.P. ISORA"},"contents":{"en":"Aqui el contenido del mensaje si aplicase","es":"Aqui el contenido del mensaje si aplicase"},"data":"{\"centro\":\"C.P. ISORA\",\"fecha\":\"yyy/mm/dd\",\"hora\":\"hh:mm\",\"profesional\":\"nombre del profesional\",\"nomUsuario\":\"nombre de usuario\",\"codcita\":\"idCita\",\"sepuedeborrar\":\"1\"}","include_player_ids":["c2917a6f-6ecf-4f45-8b31-9b72538580fd"]}
 
USER>write myJSONObj.data
{"centro":"C.P. ISORA","fecha":"yyy/mm/dd","hora":"hh:mm","profesional":"nombre del profesional","nomUsuario":"nombre de usuario","codcita":"idCita","sepuedeborrar":"1"}

USER>set dataObj=##class(%DynamicObject).%FromJSON(myJSONObj.data)
 
USER>write dataObj
11@%Library.DynamicObject

USER>set myJSONObj.data = dataObj
 
USER>write myJSONObj.%ToJSON()
{"app_id":"5cf57b56-c3b4-4a0d-8938-4ac4466f93af","headings":{"en":"Cita Atención Primaria","es":"Cita Atención Primaria"},"subtitle":{"en":"C.P. ISORA","es":"C.P. ISORA"},"contents":{"en":"Aqui el contenido del mensaje si aplicase","es":"Aqui el contenido del mensaje si aplicase"},"data":{"centro":"C.P. ISORA","fecha":"yyy/mm/dd","hora":"hh:mm","profesional":"nombre del profesional","nomUsuario":"nombre de usuario","codcita":"idCita","sepuedeborrar":"1"},"include_player_ids":["c2917a6f-6ecf-4f45-8b31-9b72538580fd"]}

One possibility:

You can handle the two files with two separate interfaces:

The interface for the tracking file would just load it into a record map and then stop. This has the effect of saving the information as a row in a database table.

The BPL for the PO file interface would just query the tracking file record map table to find the relevant entry, open it, and add the necessary info to the PO. To avoid timing issues where a PO file is processed before the corresponding tracking file you could add a check in the BPL: if a row doesn't exist in the tracking table for this PO, delay for 5 seconds using the BPL "Delay" action and try again before failing with an error.

Have a look at the StayConnected setting under Connection Settings. By default, it is set to -1 which means the adapter expects to always have an active connection and will throw an error if it doesn't.

Setting StayConnected to 0 would mean the remote system can connect and disconnect as needed without triggering an error.

Stay Connected

Applies to all TCP adapters.

If StayConnected is a positive value, the adapter stays connected to the remote system for this number of seconds between input events. A zero value means to disconnect immediately after every input event. The default of –1 means to stay permanently connected, even during idle times. Adapters are assumed idle at startup and therefore only auto-connect if they are configured with a StayConnected value of –1.

The value of StayConnected controls how the TCP adapter treats disconnections. If StayConnected
has a value of –1, the TCP adapter treats a disconnection as an error. If it has a value of 0 or a positive integer, the TCP adapter does not consider a disconnection an error.

$TRANSLATE might be a possibility. It accepts a list of characters and replaces them either with other characters or just removes them. You could compare the length of the original column with the length of the column after using $TRANSLATE to remove illegal characters. For rows without illegal characters the length will match.


This would identify rows that have tilde (~), pipe (|), or backtick (`) in MyField:

SELECT * FROM MyTable WHERE CHAR_LENGTH($TRANSLATE(MyField,'~`|'))  <  CHAR_LENGTH(MyField)

It's worth noting that a statement like this can't make use of indices, so it will have to scan every row in the table.

I would be tempted to, as a next troubleshooting step, take the BPL and JDBCGateway out of the equation and test the stored procedure calls using the same JDBC driver from a Java-based SQL query tool such as Squirrel.

It's not SQL Server specific, but this step-by-step walkthrough for using Squirrel to connect to Caché might save some time.

[Edit: should have said "queries of the views" rather than "stored procedure calls"]

Business rule classes store the rules as XML in an XData block with the name "RuleDefinition":

Class Demo.Rule.GenericRouter Extends Ens.Rule.Definition
{

Parameter RuleAssistClass = "EnsLib.MsgRouter.RuleAssist";

XData RuleDefinition [ XMLNamespace = "http://www.intersystems.com/rule" ]
{
<ruleDefinition alias="" context="EnsLib.MsgRouter.RoutingEngine" production="TESTINGPKG.FoundationProduction">
<ruleSet name="" effectiveBegin="" effectiveEnd="">
<rule name="">
<when condition="1">
<send transform="Demo.DTL.Generic" target="Test.DummyOperation"></send>
<return></return>
</when>
</rule>
</ruleSet>
</ruleDefinition>
}

}

One approach would be to find all of your business rule classes, retrieve the "RuleDefinition" XData block for each one, and then parse the XML to see which DTLs are called.

To find the business rule classes, have a look at %Dictionary.ClassDefinition. You could do a query like this to find your business rule classes:

select ID from %Dictionary.ClassDefinition where super='Ens.Rule.Definition'

Then, for each business rule class you can find the %Dictionary.XDataDefinition for the RuleDefinition XData block with a query like this:

select ID from %Dictionary.XDataDefinition where parent='Demo.Rule.GenericRouter' and name='RuleDefinition'

The raw XML from the XData block can be accessed via the stream object stored in the "Data" property of the %Dictionary.XDataDefinition row.

Have you considered using web services to exchange messages between the productions? This has the advantage of allowing the two productions to later be placed on separate instances.

If you want to do it through the mapped DB, you could write a custom business operation to store the records in a custom table which is mapped to from both namespaces and then write a custom business service that polls that table for new entries.

I have 2 productions A and B on the same IRIS instance sharing one operational database.

To be sure the two productions aren't conflicting with each other, I'm assuming that only custom tables/globals are mapped to the shared database and not any of the Ens* globals/tables/routines/packages.

Are these standard HL7 batch headers and footers (BHS/BTS, FHS/FTS, etc) or something custom to your organization?

If they are one of the HL7 standards, have a look at these docs on HL7 batches:
https://docs.intersystems.com/healthconnect20191/csp/docbook/Doc.View.cl...

One tricky part will be triggering when an old batch ends/new batch begins -- this will depend on your local requirements.

If they are custom, there are a few approaches you can consider:

  1. You could use the Record Mapper to define a record map class with header/footer and a single field record for the HL7 content. You would then use one of the  EnsLib.RecordMap.Operation.* classes instead of EnsLib.HL7.Operation.FileOperation: https://docs.intersystems.com/healthconnect20191/csp/docbook/DocBook.UI.... In particular, have a look at RolloverSchedule and/or RolloverLimit to control when a new batch file is created.
  2. If you're comfortable creating a custom class that extends EnsLib.HL7.Operation.FileOperation, you could override the outputDocument method. In your custom version of outputDocument you could check if the file already exists using ##class(%File).Exists(pathToFile) and if it doesn't you would write out the footer to the previous file and the header to the new file before calling the standard version of outputDocument using ##super. https://docs.intersystems.com/healthconnect20191/csp/docbook/Doc.View.cl...