Hi Scott,

A "Web Service" is just a generic term for an electronic service that operates over HTTP.

SOAP and REST can both be considered as a web service. Whilst SOAP is a protocol, REST is just an architectural style that operates over HTTP.

The NPI registry API can also be considered a web service, but it's clearly not SOAP, and it doesn't really conform to a REST architecture style either.

In fact if you look at the demo page you realise that the API is actually just a simple HTML form submit handler where the form values are sent in the GET request, as you can see here it submits all of the form values even if we provide just one form value...

https://npiregistry.cms.hhs.gov/api/?number=&enumeration_type=&taxonomy_description=&first_name=&last_name=&organization_name=&address_purpose=&city=baltimore&state=&postal_code=&country_code=&limit=&skip=

You don't need to use a REST client library to work with this API, here is a simple command line demo just using the standard %Net library  - I had to create an empty SSL/TLS configuration in the management portal named npiregistry as this needs to work over SSL.

USER>set req=##class(%Net.HttpRequest).%New()                                    
USER>set req.SSLConfiguration="npiregistry"
USER>set sc=req.Get("https://npiregistry.cms.hhs.gov/api?city=baltimore&postal_code=212")
USER>set results={}.%FromJSON(req.HttpResponse.Data)
USER>write results.%Get("result_count")
10

For Ensemble you could just use the standard HTTP Outbound Adapter.

Hi Jack,

The important parts of the error message are

<UNDEFINED>
*k1

Which tells us that k1 is being used but has not been initialised.

Set k1=request.GetNextIndex("PIDgrpgrp("_(1)_").ORCgrp()",k1,.tSCTrans)
                                                          /\
                                                       UNDEFINED

Without the source code its a little hard to see how your are implementing this.

If this is your own code block then you could initialise k1 first

Set k1=""
Set k1=request.GetNextIndex("PIDgrpgrp("_(1)_").ORCgrp()",k1,.tSCTrans)

Alternatively, if you make k1 a context property then I believe it will initialise it for you.

Seems like a simple task to crack with a BPL sledgehammer. If there is no other BPL logic then you might want to consider just using a simple custom process, or via an HL7 routing rule with your own custom function

https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=EGDV_adv_custom_utilfunctions

Hi Fiona,

You shouldn't trust variables populated with embedded SQL.

You need to check the SQLCODE value to make sure you have actually found data, if SQLCODE=0 trust the data, if SQLCODE=100 the query didn't find any data, any other value should probably throw an error as there will be a problem with the query.

More here...

https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RERR_sql_noerr

I think Andew was looking for the Options to be escaped like so...

{"ID":1,"Name":"Smith, John","Options":"{\"Color\":\"Blue\",\"Count\":20}"}

Hence mentioning the CONCAT trick that does give the desired results...

select JSON_OBJECT('Name':Name, 'Raw': {fn CONCAT(Raw,' ')} )
from Foo.JSON

But given that he doesn't know if the field contains JSON or not he sounds a bit stuck with this approach.

The only thing I can think of with JSON_OBJECT is to append a space to known JSON before storing it, whitespace is valid and any consumer would ignore it.

Hi Eric,

Assuming you don't have escaped characters in the first or second field, you can just paste the following three lines into the terminal.

set file=##class(%Stream.FileCharacter).%New()
do file.LinkToFile("c:\Temp\file.csv")
for  quit:file.AtEnd  set ^People("Customers", "Acc.division",$zstrip($piece(file.ReadLine(),",",2),"<>W"))=""


You will end up with a global that looks like this...

^People("Customers","Acc.division",1234)=""
^People("Customers","Acc.division",1235)=""
^People("Customers","Acc.division",3214)=""


If you have more complex / escaped data then here is an example of a CSV reader that will safely traverse the data (look at the Test examples)

https://gist.github.com/SeanConnelly/cb4ff970c2df5266d24c8802d56f48da

Perhaps something along these lines...

 ClassMethod UpsertXML(xml, wrapper, class, id, Output object) As %Status
{
  set reader=##class(%XML.Reader).%New()
  $$$QuitOnError(reader.OpenString(xml))
  do reader.Correlate(wrapper,class)
  do reader.Next(.object,.sc)
  $$$QuitOnError(sc)
  if id'="" do object.%IdSet(id)
  quit object.%Save()
}

Usage...

w ##class(Foo.Person).UpsertXML("<Person><FirstName>Bob</FirstName><LastName>Smith</LastName><City>Paris</City></Person>","Person","Foo.Person",1)

Hi Bharath,

CallInterval is in seconds.

You can set this to -1 which will bypass waiting between calls. Theoretically there is no upper limit, but you should probably take a look at the Schedule setting if you want a high value.

As the description mentions, this setting is only for polling adapters such as the built in Email, FTP, File and SQL inbound adapters.

It would probably help to understand how and why the call interval is being used.

A polling adapter should try and do two things, yield to its host as often as possible and sleep when there is nothing to do. If it does not yield very often then a production shutdown will not be able to bring down the adapter cleanly. If the adapter never goes to sleep then its going to hammer the CPU and IO 24/7 even when there is nothing to do.

To get a balance between going to sleep and working, the adapter can set a property called %WaitForNextCallInterval. This can immediately re-invoke the adapters on task method. The adapter would normally set this to 0 when its finding items, and to 1 when there are no items left. By setting it to 1 it gives other service a better share of the CPU and IO.

If your using one of the above adapters then this behaviour is built in. Even if you have thousands of items to collect, they will all be collected without any call delay, again, only when there are none left will it sleep.

You can find some more information here...

https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=EGDV_busservice

Sean.

Hi Keshav,

%ID is a psuedo field that will always reference the RowID, by default this is named as ID, but could also be ID1, ID2 .. IDn

More about it here...

https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQL_tables#GSQL_tables_idfield

Not sure about your error, would need to see your full code.

If you are not naming properties as ID, then just use ID.

Sean.

A couple of thoughts, as you have a backslash in the path, the new file path is going to be a concat of ..Adapter.%serverPath and NewFilePath, I would log this and not just the fullPath so you can be sure what directory you are trying to write to...

$$$TRACE("Path for archive: "_..Adapter.%serverPath_NewFilePath)

Also, might just be worth checking that you are connected when you do the rename

$$$TRACE("Connected: ",..Adapter.Connected)

Hi Stephen,

Interesting question.

The routing engine gets passed a context object that contains a Document object. This is the request object, so in your example you are directly accessing your Ens.AlertRequest with Document.AlertText.

I did a full dump of the context object so you can see what properties are available...

{
   "ActionTargets":"Foo.Productions.TEST2RoutingRule",
   "Adapter":"",
   "AlertGroups":"",
   "AlertOnBadMessage":null,
   "AlertOnError":false,
   "AlertRetryGracePeriod":0,
   "BadMessageHandler":"",
   "BusinessPartner":"",
   "BusinessRuleName":"Foo.Productions.TEST2RoutingRule",
   "Document":{
      "AlertDestination":"The alert destination",
      "AlertText":"The alert text",
      "AlertTime":"2018-10-29 15:29:18.517",
      "SessionId":null,
      "SourceConfigName":"souce"
   },
   "FailureTimeout":15,
   "ForceSyncSend":null,
   "InactivityTimeout":0,
   "MsgClass":"Ens.AlertRequest",
   "QueueCountAlert":0,
   "QueueWaitAlert":0,
   "ReplyCodeActions":"",
   "ResponseFrom":"",
   "ResponseTargetConfigNames":"",
   "ResponseTimeout":-1,
   "Retry":false,
   "RetryInterval":5,
   "Source":"EnsLib.Testing.Process",
   "SuspendMessage":false,
   "ThrottleDelay":0,
   "Validation":"",
   "aRespFrom":{

   }
}

There doesn't look to be any access to the Header object on the context object.

So I did a dump of the stack to see if its visible outside of the context object, but it looks like the routing rules are scoped off to just the arguments it takes...

{
   "Objects":{
      "pContext":{
         "Properties":{
            "ActionTargets":"Foo.Productions.TEST2RoutingRule",
            "Adapter":"",
            "AlertGroups":"",
            "AlertOnBadMessage":null,
            "AlertOnError":false,
            "AlertRetryGracePeriod":0,
            "BadMessageHandler":"",
            "BusinessPartner":"",
            "BusinessRuleName":"Foo.Productions.TEST2RoutingRule",
            "Document":{
               "AlertDestination":"3",
               "AlertText":"2",
               "AlertTime":"2018-10-29 15:36:24.273",
               "SessionId":null,
               "SourceConfigName":"1"
            },
            "FailureTimeout":15,
            "ForceSyncSend":null,
            "InactivityTimeout":0,
            "MsgClass":"Ens.AlertRequest",
            "QueueCountAlert":0,
            "QueueWaitAlert":0,
            "ReplyCodeActions":"",
            "ResponseFrom":"",
            "ResponseTargetConfigNames":"",
            "ResponseTimeout":-1,
            "Retry":false,
            "RetryInterval":5,
            "Source":"EnsLib.Testing.Process",
            "SuspendMessage":false,
            "ThrottleDelay":0,
            "Validation":"",
            "aRespFrom":{

            }
         },
         "Reference":"3@EnsLib.MsgRouter.RoutingEngine"
      }
   },
   "Primatives":{
      "pEffectiveBegin":"",
      "pEffectiveEnd":"",
      "pPassed":"1",
      "pReason":"",
      "pReturnValue":"",
      "pRuleSet":"",
      "tSC":"1"
   },
   "Stack":{
      "1":"zStart+62^Ens.Job.1 +2 : Set tInstance.%QuitTask=0, tSC=tInstance.OnTask()  Quit:('tSC)||tInstance.%QuitTask",
      "2":"zOnTask+42^Ens.Host.1 +1 : Set tSC=..MessageHeaderHandler(tRequestHeader,.tResponseHeader)",
      "3":"zMessageHeaderHandler+42^Ens.Actor.1 +1 : Set tSC=tBP.MessageHeaderHandler(pRequestHeader,.pResponseHeader,.tHandledError) ; Sets SessionId, we clear it",
      "4":"zMessageHeaderHandler+18^Ens.BusinessProcess.1 +1 : Set tSC=..OnRequest(..%request,.tResponse)",
      "5":"zOnRequest+21^EnsLib.MsgRouter.RoutingEngine.1 +1 : Quit ..doOneAction(request,\"rule:\"_tRuleName,,,.response) }",
      "6":"zdoOneAction+8^EnsLib.MsgRouter.RoutingEngine.1 +1 : Set tSC=##class(Ens.Rule.RuleDefinition).EvaluateRulesEx(tOneTarg, ..%SessionId, $this, $classname(), .tActionList) Quit:('tSC)",
      "7":"zEvaluateRulesEx+3^Ens.Rule.RuleDefinition.1 +1 : Quit ##class(Ens.Rule.Definition).EvaluateRules(pRuleName,pSessionId,pContext,pActivityName,.pReturnValue,.pReason)",
      "8":"zEvaluateRules+12^Ens.Rule.Definition.1 +1 : set tSC=$classmethod(tClassName,\"evaluateRuleDefinition\",pContext,.tRuleSet,.tEffectiveBegin,.tEffectiveEnd,.pReturnValue,.pReason)",
   }
}

So I think the short answer is going to be not directly.

I can access the request ID in the code behind, using the %Id() method, but the rule validator doesn't seem to like referencing it.

I had a quick hack and managed to find one way by using a custom function, its a hack but something like this, but maybe using dynamic SQL to access the value...

 Class Foo.FSET Extends Ens.Rule.FunctionSet
{

ClassMethod HeaderPropertyEquals(obj, name, value)
{
set bid=obj.%Id()
&sql(select id into :id from Ens.MessageHeader where MessageBodyId=:bid)
set mh=##class(Ens.MessageHeader).%OpenId(id)
quit $property(mh,name)=value
}

}

You can then call this function in the rule set as a condition...

HeaderPropertyEquals(Document,"SourceConfigName","Foo Service")

Can't say I have ever wanted to access the Header object, but interesting to hack around the code behind to see whats going on.

Sean

Hi Sabit,

This code looks familiar, a 5 minute hack from another answer I made blush

The problem is probably with %File, try replacing the file output with the following code...


set file=##class(%FileBinaryStream).%New()
set file.Filename=filename
quit file.OutputToDevice()

If you are still getting a file not found then the file name is probably wrong / corrupt, try logging the file name and opening the file from the command line to be sure.

Sean.

Hi Arun,

Looks like the test form doesn't support the payload property type.

You will have more luck creating the request object from the command line and then calling the test service, something along these lines...

set sc=##class(EnsLib.Testing.Service).SendTestRequest("Put Target Name Here",request,.response,.sessionId,1)

You will need to create an instance of a stream object and assign it to the Stream property of the request object.

If you don't have a WSDL then you could try contacting the TRUD for ITK documentation...

https://isd.digital.nhs.uk/trud3/user/guest/group/0/pack/30

There is one tip that I use when I don't have a WSDL. I take an example XML message and create an XSD for it, there are online generators that can help you do this, such as this one here...

https://www.freeformatter.com/xsd-generator.html

You can then paste the XSD into the XML Schema Wizard found in Studio > Tools > Add-Ins > Add-Ins, this will generate a class that you can use as your request object.

Sean.

Hi Tiago,

The ? pattern match is native to COS and has been in the product since day 1. You will also find it in GT.m and a few other fringe M systems, but probably nowhere else.

The $match feature was introduced back in 2012. It uses an external Regex library called ICU.

Both are highly optimised for general use cases (millions of operations per second).

The ? pattern matching syntax is less expressive than Regex, but this also makes it easy to learn and remember.

Regex would be the better choice for more complex pattern matching requirements.

Sean.

Hi Oliver,

The Transform is a generated method, it doesn't make sense to override it and call its base implementation, hence the ##super() error.

If you want the Transform status code then just call the generated Transform on your transform class from the command line, e.g.

write ##class(My.Transform).Transform(in,.out)

But you are not going to get anything meaningful from this unless you actual capture and return the error, so you might want some code inside your transform like this...

Set tSCTrans=target.%Save() if ('tSCTrans) goto Exit

But this doesn't feel right.

I would have a custom process and do it this way...

set sc=##class(My.Transform).Transform(in,.out)
if $$$ISERR(sc) quit sc
set sc=out.%Save()
quit sc

You will quit the status code up to the ensemble director that will then handle the error logging for you.

And I bet you a pint that one of your X12 fields is longer than 50 characters and you are getting a MAXLEN error returned from the save method.

Sean.