Sean Connelly · Nov 19, 2018 go to post

"<>W" will strip whitespace so it doesn't end up in your global keys, more here on this...

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

Globals have a tree like structure, you can have many combinations of branches in the structure, but eventually your end branches (or leaves) will have to contain a value, even if that value is an empty string. It is not possible to set these end branches without a value, so you will always end up with a global looking like this...

^People("Customers", "Acc.divison", 1234.567)=""
^People("Customers", "Acc.divison", 1235.987)=""
^People("Customers", "Acc.divison", 3214.879)=""


Note also that the last key is a numeric value, so it will be automatically sequenced in numerical order, before any string values.

There is a post here that goes into more detail on globals...

https://community.intersystems.com/post/globals-are-magic-swords-managing-data-part-1

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 16, 2018 go to post

Shame I can't see the code behind. I will do some benchmarks against a direct ^oddCOM scan, if its as fast then it looks like it might be a viable alternative.

Sean Connelly · Nov 16, 2018 go to post

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

Hi Anthony,

Your custom header is being prefixed with "HTTP_" after it hits the gateway.

One simple thing you can do is dump all of the CGI variables in a test page...

classMethod OnPage() As %Status [ ServerOnly = 1 ]
{
  set var=$order(%request.CgiEnvs(""))
  while var'="" {
    write !,var," = ",$order(%request.CgiEnvs(var)),"</br>"
    set var=$order(%request.CgiEnvs(var))
  } 
  quit $$$OK
}
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 · Nov 3, 2018 go to post

I've created a Gist containing the source code for a CSV reader.

It will load CSV data from a file, stream or string, exposing each record via a Next() record method and Get() and GetAt() field methods.

It implements various options for changing the field and EOL delimiter, automatic encoding from UTF8 files, quote stripping and normalisation of EOL character(s) to a preferred internal EOL character(s).

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

I've been using some ugly data and looks to be working good enough, here is output from the Display() method...

Record No : 1
 address = 1 long street, somewhere, north pole
 poetry =
 
"Poetry"
========
 
From the "Anglo-Saxon" Rune Poem (Rune version):
"ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ
ᛋᚳᛖᚪᛚ᛫ᚦᛖᚪᚻ᛫ᛗᚪᚾᚾᚪ᛫ᚷᛖᚻᚹᛦᛚᚳ᛫ᛗᛁᚳᛚᚢᚾ᛫ᚻᛦᛏ᛫ᛞᚫᛚᚪᚾ
ᚷᛁᚠ᛫ᚻᛖ᛫ᚹᛁᛚᛖ᛫ᚠᚩᚱ᛫ᛞᚱᛁᚻᛏᚾᛖ᛫ᛞᚩᛗᛖᛋ᛫ᚻᛚᛇᛏᚪᚾ"᛬
 
From Laȝamon's Brut (The Chronicles of England, Middle English, West Midlands):
 
"An preost wes on leoden, Laȝamon was ihoten
He wes Leovenaðes sone -- liðe him be Drihten.
He wonede at Ernleȝe at æðelen are chirechen,
Uppen Sevarne staþe, sel þar him þuhte,
Onfest Radestone, þer he bock radde."
 
(The third letter in the author's name is Yogh, missing from many fonts; CLICK HERE for another Middle English sample with some explanation of letters and encoding).
 
From the Tagelied of Wolfram von Eschenbach (Middle High German):
 
"Sîne klâwen durh die wolken sint geslagen,
er stîget ûf mit grôzer kraft,
ich sih in grâwen tägelîch als er wil tagen,
den tac, der im geselleschaft
erwenden wil, dem werden man,
den ich mit sorgen în verliez.
ich bringe in hinnen, ob ich kan.
sîn vil manegiu tugent michz leisten hiez."
 
Some lines of Odysseus Elytis (Greek):
 
Monotonic:
 
"Τη γλώσσα μου έδωσαν ελληνική
το σπίτι φτωχικό στις αμμουδιές του Ομήρου.
Μονάχη έγνοια η γλώσσα μου στις αμμουδιές του Ομήρου.
από το Άξιον Εστί
του Οδυσσέα Ελύτη"
 
Polytonic:
 
"Τὴ γλῶσσα μοῦ ἔδωσαν ἑλληνικὴ
τὸ σπίτι φτωχικὸ στὶς ἀμμουδιὲς τοῦ Ὁμήρου.
Μονάχη ἔγνοια ἡ γλῶσσα μου στὶς ἀμμουδιὲς τοῦ Ὁμήρου.
ἀπὸ τὸ Ἄξιον ἐστί
τοῦ Ὀδυσσέα Ἐλύτη
The first stanza of Pushkin's Bronze Horseman (Russian):
На берегу пустынных волн
Стоял он, дум великих полн,
И вдаль глядел. Пред ним широко
Река неслася; бедный чёлн
По ней стремился одиноко.
По мшистым, топким берегам
Чернели избы здесь и там,
Приют убогого чухонца;
И лес, неведомый лучам
В тумане спрятанного солнца,
Кругом шумел."
 
Šota Rustaveli's Veṗxis Ṭq̇aosani, ̣︡Th, The Knight in the Tiger's Skin (Georgian):
 
"ვეპხის ტყაოსანი შოთა რუსთაველი
ღმერთსი შემვედრე, ნუთუ კვლა დამხსნას სოფლისა შრომასა, ცეცხლს, წყალსა და მიწასა, ჰაერთა თანა მრომასა; მომცნეს ფრთენი და აღვფრინდე, მივჰხვდე მას ჩემსა ნდომასა, დღისით და ღამით ვჰხედვიდე მზისა ელვათა კრთომაასა."
 
Tamil poetry of Subramaniya Bharathiyar: சுப்ரமணிய பாரதியார் (1882-1921):
 
"யாமறிந்த மொழிகளிலே தமிழ்மொழி போல் இனிதாவது எங்கும் காணோம்,
பாமரராய் விலங்குகளாய், உலகனைத்தும் இகழ்ச்சிசொலப் பான்மை கெட்டு,
நாமமது தமிழரெனக் கொண்டு இங்கு வாழ்ந்திடுதல் நன்றோ? சொல்லீர்!
தேமதுரத் தமிழோசை உலகமெலாம் பரவும்வகை செய்தல் வேண்டும்."
 
Kannada poetry by Kuvempu — ಬಾ ಇಲ್ಲಿ ಸಂಭವಿಸು
 
"ಬಾ ಇಲ್ಲಿ ಸಂಭವಿಸು ಇಂದೆನ್ನ ಹೃದಯದಲಿ
ನಿತ್ಯವೂ ಅವತರಿಪ ಸತ್ಯಾವತಾರ
ಮಣ್ಣಾಗಿ ಮರವಾಗಿ ಮಿಗವಾಗಿ ಕಗವಾಗೀ...
ಮಣ್ಣಾಗಿ ಮರವಾಗಿ ಮಿಗವಾಗಿ ಕಗವಾಗಿ
ಭವ ಭವದಿ ಭತಿಸಿಹೇ ಭವತಿ ದೂರ
ನಿತ್ಯವೂ ಅವತರಿಪ ಸತ್ಯಾವತಾರ || ಬಾ ಇಲ್ಲಿ ||"
 
 spaceship =
 
 
       _________
      (=========)
      |=========|
      |====_====|
      |== / \ ==|
      |= / _ \ =|
   _  |=| ( ) |=|
  /=\ |=|,,,,,|=| /=\
  |=| |=| USA |=| |=|
  |=| |=|,,,,,|=| |=|
  |=| |=| | | |=| |=|
  |=| |=| | | |=| |=|
  |=| |=| | | |=| |=|
  |=| |/  | |  \| |=|
  |=|/    | |    \|=|
  |=/NASA |_| NASA\=|
  |(_______________)|
  |=| |_|__|__|_| |=|
  |=|   ( ) ( )   |=|
 /===\           /===\
|||||||         |||||||
-------         -------
 (""")           (""")
 
 size = Big
 qty = 5.7
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 30, 2018 go to post

I just re-read and see you have tried from a different client, maybe Ensemble is losing the connection, I will have a look around the code.

Sean Connelly · Oct 30, 2018 go to post

Hi Francisco,

At a guess, I would say your FTP user account doesn't have permission to write into the archive path.

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

Sean Connelly · Oct 29, 2018 go to post

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=filenamequit 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.

Sean Connelly · Oct 29, 2018 go to post

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.

Sean Connelly · Oct 18, 2018 go to post

I was just thinking, if you are on a schema before 2.4 then you might not have sub fields to do this.

In which case here is a quick hack, set the value to...

$Piece(target.{PID:PhoneNumberHome(1)},"^",1)

Long term its better to customise your schema's.

Sean Connelly · Oct 18, 2018 go to post

These are paths from a 2.4 specification. You will need to figure out the paths that are specific to your schema. Try these steps...

1. Create a set on PID:13.1

2. Cut the path from the property box

3. Delete that set

4. Create a set on PID:13 (not PID:13.1)

5. Paste in the path you cut into the value box

Sean Connelly · Oct 18, 2018 go to post

If you can't / don't want to fix the source schema then you can always copy values from the target, assuming it does have a schema.

So for instance your would do a set on...

target.{PID:PhoneNumberHome(1)}

And give it the value...

target.{PID:PhoneNumberHome(1).phonenumber1}

This will replace the entire target field with just the target phone number.

Sean Connelly · Oct 18, 2018 go to post

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.

Sean Connelly · Oct 17, 2018 go to post

Thanks Robert, I didn't realise indirection itself would work on an object property, far too obvious now, lol.

Sean Connelly · Oct 17, 2018 go to post

OK, so I guess I could use xecute for this...

xecute ("(obj,key) set key=$order(obj."_prop_"(key))",obj,.key)
Sean Connelly · Oct 15, 2018 go to post

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.

Sean Connelly · Oct 10, 2018 go to post

Hi Richard,

You can create and delete accounts via the %SYS namespace using the following class methods...

https://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=Security.Users

It sounds like you might want to take a look at delegated authentication as well...

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

If this is just for a CSP application then you can  detect the user logging out or a session timeout using the %CSP.SessionEvents class.

Sean.

Sean Connelly · Oct 10, 2018 go to post

Hi Mauri,

The short answer is going to be not via the reply code actions.

You can customise an operation to send a message back around (..SendRequestAsync for instance) which would put it to the back of the queue.

You don't mention the type of message or error that would require this type of behaviour. It does seem a little odd since a message that fails either fails because of a network / end point down problem, or that the message data has failed validation. In the first instance you would most likely want to just keep retrying the same message with a failure timeout of -1, and in the second instance, a message that fails validation would surely keep failing validation until you manually fixed and resent the problem by hand.

Sean.

Sean Connelly · Oct 10, 2018 go to post

Hi Sansa,

If you are getting back an empty string (I suspect not null) then you should be able to use the GetError method to investigate the problem. This is from the class method comments...

If the function fails, it returns "". 
The actual session error status can be returned by a call to the GetError method.

Examples:

s Status=##Class(%SYS.LDAP).SearchExts(LD,BaseDN,$$$LDAPSCOPESUBTREE,Filter,Attributes,0,"","",2,ServerTimeout,.SearchResult)
i Status'=$$$LDAPSUCCESS q Status
s Entry=##Class(%SYS.LDAP).FirstEntry(LD,SearchResult)
s Status=##Class(%SYS.LDAP).GetError(LD)
i Status'=$$$LDAPSUCCESS q Status
s ValueList=##Class(%SYS.LDAP).GetValues(LD,Entry,"sAMAccountname") i ValueList="" q ##Class(%SYS.LDAP).GetError(LD)
w !,"sAMAccountname="_$li(ValueList,1)

It's certainly worth looking at the class documentation for this...

https://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls

From experience LDAP can be tricky to get working, and even harder to help diagnose without having all the information at hand. It's analogous to posting the question, my car won't start, whats the problem. It can be 101 things, so you will need to narrow down the error and provide as much information as you can if you are still stuck.

Sean.