Sean Connelly · Aug 12, 2022 go to post
ClassMethod OnPage() As %Status [ ServerOnly = 1 ]
{
    
    //just the query string...
    set qs=%request.CgiEnvs("QUERY_STRING")
        
    //SOLUTION 1: $piece only
    set externalCenterCode=$p(qs,":")	
    set startDateRange=$p($p(qs,":authoredOn=le",2),":")
    set endDataRange=$p($p(qs,":authoredOn=ge",2),":")

    
    //SOLUTION 2: generic solution if params grow	
    for i=1:1:$l(qs,":") {
        set nvp=$p(qs,":",i),name=$p(nvp,"=",1),value=$p(nvp,"=",2)
        //fix the quirks
        if value="" set value="name",name="ecc"
        if name="authoredOn" set name=$e(value,1,2),value=$e(value,3,*)
        set params(name)=value
    }

    //SOLUTION 3: regex(ish) solution
    set code=$p(qs,":")
    set loc=$locate(qs,"le\d{4}-\d{2}-\d{2}")
    set start=$e(qs,loc+2,loc+11)
    set loc=$locate(qs,"ge\d{4}-\d{2}-\d{2}")
    set end=$e(qs,loc+2,loc+11)


    //some helper code to dump the variables into the CSP page
    write !,"<pre>"
    zwrite
    //use this to take a good look at the request object...
    zwrite %request
    write !,"</pre>"
    quit $$$OK
}

Here are three solutions and a couple of inline tips, including your request for regex example

I wouldn't worry too much about using $piece, its very common to use it in this way

Eduards comment above also has a fourth suggestion to use $lfs (list from string) which is also commonly used as a way of piecing out data

Sean Connelly · Jun 8, 2022 go to post

I think you might have missed my point about the adapter being non standard.

E.g. what if the adapter has code like this...

Method OnTask() As %Status
{
    //Set tSC = ..BusinessHost.ProcessInput($$$NULLOREF)
    Set tSC = ..BusinessHost.General($$$NULLOREF)
    Set ..BusinessHost.%WaitForNextCallInterval=1
    Quit tSC
}

The normal sequence of

OnTask() → ProcessInput() → OnProcessInput()

becomes

OnTask() → General()

Of course this then raises the question, how does the interval then work as it would most likely only run once.

Main point is to check if the adapter has hard wired it in directly. It's the only logical explanation outside of this being an observer error.

Sean Connelly · May 23, 2022 go to post

The solution is to add the parameter STUDIO=1

http://localhost:52773/csp/healthshare/user/EnsPortal.DTLEditor.zen?DT=Foo.NewDTL1.dtl&STUDIO=1
Sean Connelly · Apr 23, 2022 go to post

Hi Scott,

My understanding is that search table classes are maintained in Ens.DocClassMap. When you do a purge the Ens.MessageHeader appears to have the responsibility for calling RemoveSearchTableEntries() on the Ens.SearchTableBase which uses indirection to call RemoveIndex() on the class name maintained in the DocClassMap.

The docs briefly mention DocClassMap...

The Ensemble classes use this class to remove search table entries when message bodies are deleted.

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

Short answer, purges will automatically include new search tables even if you scheduled the purge before creating the search table.

Sean Connelly · Sep 24, 2021 go to post

Hi Kurro,

Some random suggestions without being able to see the implementation details...

1. Enable the SOAP log and compare the request and response headers to SOAP UI to see what is different

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

2. Does changing the SOAP version make any difference

https://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?LIBRARY=%25SYS&CLASSNAME=%25SOAP.WebClient#PROPERTY_SoapVersion

3. The error is probably raised in this block of code, might be worth working backwards from there, its at the end of DoSOAPRequest in %SOAP.WebClient

    Set responseContentType=$zcvt($piece(response.ContentType,";",1),"L")
    If ..SoapBinary {
        If (responseContentType'="application/octet-stream") Quit $$$ERROR($$$SOAPUnexpectedType,response.ContentType)
    } Else {
        If (responseContentType'="text/xml") && 
           (responseContentType'="application/soap+xml") &&
           (responseContentType'="multipart/related") {
            Quit $$$ERROR($$$SOAPUnexpectedType,response.ContentType)
        }
    }

4. If your running out of ideas then maybe reinstall / restart the server code (is it .NET by any chance)?

Sean Connelly · Sep 17, 2021 go to post

>  this is a very serious that I can't wait to resolve... 
Hi Thomas, if this is serious then you should raise a WRC with InterSystems.

Sean Connelly · Sep 17, 2021 go to post

It depends what you really need.

SYSTEM MODE

There are existing global flags that are available through various classes. For instance SystemMode() will provide system wide constants for Live, Test and Dev...

https://docs.intersystems.com/healthconnectlatest/csp/documatic/%25CSP.Documatic.cls?&LIBRARY=%25SYS&CLASSNAME=%25SYSTEM.Version

The SystemMode() is admin configured via the Memory and Startup screen on the management portal.

LOOKUP TABLES

For some global settings, particularly business related, it would be good practice to use lookup tables and not a global...

https://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?&LIBRARY=ENSLIB&PRIVATE=1&CLASSNAME=Ens.Util.LookupTable

UTIL FUNCTIONS
Also, if you are not aware of them yet, you should take a look at creating your own Ens util functions which are automatically inherited into routers and DTL's and provide a good place to stash various levels of global and business value / logic...

https://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?&LIBRARY=ENSLIB&PRIVATE=1&CLASSNAME=Ens.Util.FunctionSet

https://docs.intersystems.com/latest/csp/docbook/Doc.View.cls?KEY=EBUS_utility_functions

Sean Connelly · Jun 17, 2019 go to post

Hi Michael,

1. Copy the entire first source OBX to the first target OBX
2. Loop over the source OBX group
3. In the loop, if the loop key is greater than 1, append the source OBX 5 value to the first target OBX 5 with a preceding delimiter

It should look something like this...

If the source is...

OBX|1||||Alpha|
OBX|2||||Bravo|
OBX|3||||Charlie|


Then your target will output as...

OBX|1||||Alpha Bravo Charlie|
Sean Connelly · Jun 15, 2019 go to post

Hi David,

The ruleset including assign actions are evaluated before the send actions.

In this instance, if both rules are true then the first assign will be overwritten by the second assign, before both send actions are processed.

I guess you could work around this by having two different property names and testing for the existence of a value in the DTL.

It feels like there is a better solution altogether to suggest, but its not clear what the real world use case is here.

Sean.

Sean Connelly · May 22, 2019 go to post

Hi Francisco,

Have you tried using the RemoveItem() method on the Ens.Config.Production class?

Sean Connelly · Apr 23, 2019 go to post

There is nothing wrong with escaping a Solidus character. The JSON specification states that a Solidus "may" be escaped but does not have to be. You should find that most decoders will unescape the Solidus correctly, so you could just leave it as is.

Sean Connelly · Jan 3, 2019 go to post

Hi Chip.

In general, there is a danger of the tail (OOP) wagging the dog (ORM).

Polymorphism itself is a funny old thing. When it comes to true abstract things, its more easy to understand where its benefits come from. I always like to use IDataReader as a strong example of explaining Polymorphism. Where it becomes a bit murky is with the daft examples that try and teach Polymorphism, such as Cat and Horse implement Animal. It never really makes any real world sense, and as soon as you mix database storage it can go from murky to bizarre.

If we simplify it down to just shared data attributes then we can see how Person might make sense, but this type of design will have a trade off somewhere else in the architecture. You could have a Person table, and many concrete role looking implementations of Person, but then other areas such as SQL reporting can become really complex.

From the perspective of the example provided there are well established design patterns, we would have a Patient class and a separate Staff and Role class. Staff might share similarities to Patient, but we would try and solve this through composition over inheritance (and polymorphism), so for instance Address could be a shared thing. In this sense there is no problem that a Doctor can also be a Patient. There is duplication of storage here, but its a small trade off from other aspects of the architecture. There is also the aspect that something like Address has a different context anyway, a Patient Address would be home, whilst a Doctors Address would be work, and here the two database pools don't mix that well, hence why they tend to be two separate data entities.

That all said, its still an interesting question, and perhaps it just needs a better real world use case to explore it further...

Sean Connelly · Dec 31, 2018 go to post

There are a few ways to test the existence of a sub-string in a string...

$find(string,tag)

string[tag

$match(string,tagPatternMatcher)

$lf(list,tag)

Given...

set string="a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z"

and testing for "x"

within a tight loop with a million tests

$find  = .004609

[      = .004813

$match = .008951

$lf    = .023201

$find is marginally quicker than the contains operator with a million operations in .004609 seconds, whilst the other two methods are relatively slower. I say relative because they are still quick in their own rights.

A wider question might not be which is the fastest, but how it's implemented with multiple tag tests. For instance a single variable creation is almost as expensive as the string test itself, and if you wrapped it all in a re-usable method, the method call would be 10x as expensive.

If you are dealing with two strings in the first instance, then a tight loop on $find is going to be your most optimum option. The other functions are probably just as performent in the right context, but if you have to dissect the strings to implement them, then the cost is not the test, but in the other operations that you have to do first. 

Sean Connelly · Dec 20, 2018 go to post

The IDE (or enhanced code editor if you like) is actually dog food for my own standards based UI library.

In terms of building web based editors I've been building them from scratch for years so I am fairly comfortable with the level of difficulty. There is a screen shot below of one.

And yes, TTD will be one of the last features, but, I do want to prove the concept up front and inject any design decisions into the foundations so that I don't end up with lots of code re-factoring.

I've not used Monaco, mainly ACE. Perhaps there is some synergy between the two projects to use Monaco.

Any help with the original question would be much appreciated.

Sean Connelly · Dec 1, 2018 go to post

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.

Sean Connelly · Nov 26, 2018 go to post

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

Sean Connelly · Nov 19, 2018 go to post

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

Sean Connelly · Nov 9, 2018 go to post

One approach would be to search the raw content.

Criterion Type = Body Property
Class = EnsLib.HL7.Message
Condition, IF [RawContent] [Contains] [OBX|]

Or you could search on a field that is always present in that segment

Criterion Type = VDoc Segment Field
Class = EnsLib.HL7.Message
Condition, IF [2.3:OBX] [SetIDOBX)] [!=] []

Sean Connelly · Nov 7, 2018 go to post

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.

Sean Connelly · Oct 31, 2018 go to post

Hi Thiago,

Try

D:\>csession CACHE20172 -U %SYS "##class(test.MyClass).MyMethod(""""""test"""""")"

Sean Connelly · Oct 30, 2018 go to post

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)
Sean Connelly · Oct 29, 2018 go to post

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