Alex Woodhead · Oct 10, 2022 go to post

Hi Fedor,

The type DataObj.Services is a ListOfDataTypes not a primitive List.

// create delimited string
set str="A_B_C"
// conver to list
set tempList = $ListFromString(str, "_")
// debug output list primitive
zw tempList
tempList=$lb("A","B","C")

// create stand alone List Of DataTypes
set o=##class(%Library.ListOfDataTypes).%New()
// add primitive list to object ListOfDataTypes
set tSC=o.InsertList(tempList)
// Itterate on ListOfDataTypes collection
set k=""
for {set item=o.GetNext(.k)  quit:k=""  write !,item}

A
B
C

... So anticipate using "GetNext" method on the collection is the way to go.

Alex Woodhead · Oct 9, 2022 go to post

Hi Evgeny,

You could consider using the Python "traceback" module.

For example:

 Class TEST.TraceBack [ Abstract ]
{ ClassMethod GetErrorTrace() As %String [ Language = python ]
{
import traceback
errStr=""
try:
  7/0
except:
  errStr=traceback.format_exc()
return errStr
} }

Demonstration:

USER>w ##class(TEST.TraceBack).GetErrorTrace()
Traceback (most recent call last):
                                    File "TraceBack", line 6, in GetErrorTrace
                                                                              ZeroDivisionError: division by zero

Kind regards,

Alex

Alex Woodhead · Oct 9, 2022 go to post

Hi Prashanth,

It is possible to view how this is stored.

Consider the following class definition.

After compiling its default Data Storage location is global: ^TEST.PropertyLenD

 Class TEST.PropertyLen Extends %Persistent
{
Property Abc As %String(MAXLEN = "");
Property Def As %String(MAXLEN = "");

Storage Default
{
<Data name="PropertyLenDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Abc</Value>
</Value>
<Value name="3">
<Value>Def</Value>
</Value>
</Data>
<DataLocation>^TEST.PropertyLenD</DataLocation>
<DefaultData>PropertyLenDefaultData</DefaultData>
<IdLocation>^TEST.PropertyLenD</IdLocation>
<IndexLocation>^TEST.PropertyLenI</IndexLocation>
<StreamLocation>^TEST.PropertyLenS</StreamLocation>
<Type>%Storage.Persistent</Type>
} }

Creating a record:

USER>s o=##class(TEST.PropertyLen).%New()
 
USER>s o.Abc="Hello"
 
USER>s o.Def="World!"
 
USER>d o.%Save()
 
USER>w !,"RowId is ",o.%Id()
 
RowId is 1

Then viewing the record

zw ^TEST.PropertyLenD(1)
^TEST.PropertyLenD(1)=$lb("","Hello","World!")
 
USER>

So by default there is a single global data node made from a list of all property values.

This can also can be viewed in System Management Portal -> System Explorer -> Globals:

Kind regards,

Alex

Alex Woodhead · Sep 26, 2022 go to post

Could "docker commit" help out as an image snapshot that you can simply resume later. 

Would an image saved to tar or pushed to a repo make itteration portable also?

More simpler for a stopped container locally is to just start it again? Assuming the container is allowed to cache and not too much time has passed to allow the space to have been automatically reclaimed.

Alex Woodhead · Sep 21, 2022 go to post

Hi Phillip,

This can be done locally and depending on access, remotely also.

From a windows host running commands against a Linux Database can use a file for example: SC_ImportBase.txt

Could use os authentication but including credentials as an example for what is possible:

cd /tmp; sudo -u irisowner iris session Instance -U "USER" << EOF
superuser
SYS

zn "USER"
set schedule=##class(ompare.Schedule).%New()
set schedule.Environment="BASELABDB"
set schedule.Namespaces="APPONE,INTEG-*"
set schedule.RunSourceHandlers=0
set schedule.ExportToFile=0
set schedule.ExportDirectory="/home/irisowner/SourceCompare"
set schedule.ImportDirectory="/home/irisowner/SourceCompare"
set schedule.ImportFromFile=1
set schedule.ReImportUpdatedFiles=0
set schedule.DeleteImportedFiles=0
set schedule.RetainExportDays=1
set schedule.EnableLogging=0
set schedule.IncludeSourceCode=1
set schedule.RetainSigSrcHistoricVersions=-1
set schedule.BackupSourceCode=0
set schedule.Debug=1
set schedule.OverwriteSourceOnReLoad=1
Do:\$NAMESPACE="USER" schedule.OnTask()
W !,"Completed"
HALT
exit
EOF

And can launch this script against a remote Linux instance using plink from a local batch file:

"%cmdPLINK%" -v account@10.123.456.789 -pw %LinuxPass% -m "%cmdBase%scr\SC_ImportBase.txt" > "%cmdBase%out\SC_ImportBasePlink.log"

Where:

cmdPLINK = C:\Program Files\PuTTY\plink.exe
LinuxPass = LinuxPass
cmdBase = Local Directory with source commands and for screen output log

Hope this gives some ideas.

Alex Woodhead · Sep 21, 2022 go to post

Great spot @Timothy Leavitt 
Extra syntax sugar, can project as method expression to make more transparently embedded.

 ClassMethod makeComplement(As %String) As %String [CodeMode = expression]
{
 $tr(d,"ATGC","TACG")
}

Also I believe Java lacks convenience of compile time ObjectScript macros:

#define DNAComplement(%dna) $tr(%dna,"ATGC","TACG")

Write $$$DNAComplement("ATC")
Alex Woodhead · Sep 19, 2022 go to post

Revisiting challenge in Erlang.

Tail recursive (no increase in stack height), single pass of string, using language primitives only.

-module(shortest_word).
-export([v/1]).

v([])->0;
v(T)->v(T,0,999).
v([],C,M) when C>0,M>C->C;
v([],_,M)->M;
v([32|T],C,M) when C>0,C<M->v(T,0,C);
v([32|T],_,M)->v(T,0,M);
v([_|T],C,M)->v(T,C+1,M).

--------------

>shortest_word:v("abc deff").

3

Alex Woodhead · Sep 14, 2022 go to post

Sharing for readers, an alternative code approach, to generate a pad-string without Translate and Justify, uses another application of the Piece function.

set $Piece(replace,",",81)=""

This is sets a comma delimited string, at piece 81, to an empty string.

Which effectively results in a string of 80 commas.

So:

 Parameter WhiteSpace = {$C(9,10,13,32,160)}; 

ClassMethod Short(t) As %Integer
{
  $P(replace,",",$L(..#WhiteSpace)+1)=""
  t=$zstrip($TR(t,..#WhiteSpace,replace),"<=>",",")
  q:'$L($TR(t,",")) 0
  l=$L(t,","),m=$L($P(t,","))
  i=1:1:{n=$L($P(t,",",i)) s:((n>0)&&(n<m)) m=n}
  m
}

Regarding input. I don't trust calling code to never provide an empty string :)

Example updated as feedback.

Alex Woodhead · Sep 13, 2022 go to post

I have an example implemetation in the Ompare tool.

See: https://github.com/alexatwoodhead/ompare/blob/main/src/cls/ompare/Sched…

Extends: %SYS.Task.Definition

Implements: OnTask method

By the way, you can run Schedules programatically from IRIS Session outside of the Task Scheduler if you are investigating / debugging the behaviour. For example:

set tBASE=##class(ompare.Schedule).%New()
set tBASE.Environment="BASE"
set tBASE.Namespaces="OMPARE-BASE,INST-*,-INST-TEMP"
set tBASE.RunSourceHandlers=1
set tBASE.ExportToFile=1
set tBASE.ExportDirectory="C:\TMP\ompare\"
set tBASE.ExportCompression=0
set tBASE.DiscardProfileData=1
set tBASE.RetainExportDays=0
set tBASE.IncludeSourceCode=1
set tSC1=tBASE.OnTask()

The reason I would subclass %SYS.Task.Definition is that the Management Portal knows how to display input configuration fields for your class properties.

This is more visably useful / supportable. Easier to see the task paramaters.

They can have validation also (Required, Pattern, MINLEN, MAXLEN, Setter methods, %OnSave methods).

Hope that helps :)

Alex Woodhead · Sep 13, 2022 go to post

Thanks for recommendations. Taking these changes onboard think I would then make the whitespace characters configurable.

Then others can just override this behaviour in a subclass or maintain the original a bit easier.

Parameters are a great ObjectScript syntactic sugar :)

Parameter WhiteSpace = {$C(9,10,13,32,160)};

ClassMethod Short(t) As %Integer
{
  t=$zstrip($TR(t,..#WhiteSpace,",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"),"<=>",",")
  q:'$L($TR(t,",")) 0
  l=$L(t,","),m=$L($P(t,","))
  i=1:1:{n=$L($P(t,",",i)) s:((n>0)&&(n<m)) m=n}
  m
}
Alex Woodhead · Sep 13, 2022 go to post

ClassMethod Short(t) As %Integer
{
  t=$TR(t,$C(9,10,13,32,160),",,,,,")
  q:'$L($TR(t,",")) 0
  l=$L(t,","),m=200
  i=1:1:{n=$L($P(t,",",i)) s:((n>0)&&(n<m)) m=n}
  m
}

Alex Woodhead · Sep 6, 2022 go to post

Hi Jimmy,

Yes this is using the router context to test if a message has some invalid characters.

As an example I simply used the content "BAD" in the message to demonstrate.

If you want to identify specific characters by numeric code-point an additional FunctionSet method could be employed. For example:

 /// RuleSet function to evaluate whether a given raw input string contains unwanted characters
/// Unwanted characters are supplied as a comma delimited string.
/// For example detect "9,32" would match any ASCII Tab and ASCII space characters
/// The $ASCII function can be used to identify the numeric code point of a character, to be supplied in the detect parameter.
ClassMethod ContainsChars(rawData As %String = "", detect As %String = "") As %Boolean
{
    quit:$Length(rawData)<1 0
    quit:$Length(detect)<1 0
    set found=0
    set len=$Length(detect,",")
    for item=1:1:len {
        set charNum=$Piece(detect,",",item)
        continue:charNum'?1.N
        set char=$Char(charNum)
        if rawData[char {
            $$$TRACE("Found Character "_charNum_" in rawData")
            set found=1
            quit
        }
    }
    quit found
}

Usage example:

Cheers,

Alex

Alex Woodhead · Aug 30, 2022 go to post

Thanks @Sean Connelly 

Reminded within a "csp page" context can use the following to output context information when not in error.

set %response.TraceDump=1
Alex Woodhead · Aug 26, 2022 go to post

This is sucessful if no longer showing class not found error.

It found the class and ran the method sucessfully.

Set rowId=1

If ##class(EnsLib.HL7.Message).%ExistsId(rowId)=0 or 1 then

    The class EnsLib.HL7.Message has been mapped to the current namespace.

It is giving 0 because there has been no HL7 messages created yet (with rowId 1) or message rowId 1 has been purged from namespace.

A better non-error prone way to programatically test whether the class is available to the namespace could use:

write ##class(%Dictionary.CompiledClass).%ExistsId("EnsLib.HL7.Message")

Alex Woodhead · Aug 26, 2022 go to post

Some ideas for general configuration.

In a target namespace can test integration is enabled with:
write ##class(%EnsembleMgr).IsEnsembleNamespace()

Can enable integration for an existing namespace with:
do ##class(%EnsembleMgr).EnableNamespace($NAMESPACE)

When enabled, as a test can Run a integration method from mapped class:
write ##class(EnsLib.HL7.Message).%ExistsId(1)

Alex Woodhead · Aug 24, 2022 go to post

Suggest try Edit in settings.json

"pathPrefix": "/[Instance Name]",

For example:
"intersystems.servers": {
 "test1": {
   "webServer": {
     "scheme": "http",
     "pathPrefix": "/IRIS",
     "host": "localhost",
     "port": ...
   }
  }
}

Alex Woodhead · Aug 18, 2022 go to post

Hi Scott,

Many thanks for reporting this. Yes this is unintuitive.

I have updated the implementation so:

1. Method GenerateTestCases now has additional parameter autocompile which is default yes.

2. Method AddTestFromMessageBody now has additonal parameter recompile default no.

Now when you generate new TestCases by default they will be auto-compiled and ready to accept attaching new HL7 messages for testing.

For clarity the message id "1218515" will be an EnsLib.HL7.Message RowId. This is the same ID seen in System Management Portal Integration Messages view as MessageBodyId.

To have added messages immediately ready (compiled) for next test run can use:

set recompile=1

set tSC=##class(UnitTest.Test.DTL.TestTrans.TransformSource2).AddTestFromMessageBody("EnsLib.HL7.Message",1218515,1,.sourceXdataName,.targetXdataName,recompile)

The class "UnitTest.Test.DTL.TestTrans.TransformSource2" was generated from an an HL7 DTL class "UnitTest.DTL.TestTrans.TransformSource2" in my test environment.

I have also added reference test class: UnitTest.DTL.TestTrans.TransformSource2.xml to github,

# Example Generate from reference class:
Do ##class(UnitTest.DTL.HL7TestCase).GenerateTestCases("UnitTest.DTL.TestTrans.TransformSource2", "UnitTest.DTL.Test2.TestTrans.", 0 , , 0,1, 1, .pStatus)

Skipping class UnitTest.DTL.TestTrans.seg.MSH not matched
Skipping class UnitTest.DTL.TestTrans.seg.PID not matched
Created TestCase: UnitTest.DTL.Test2.TestTrans.TransformSource2
Compilation started on 08/18/2022 19:56:23 with qualifiers 'ckf'
Compiling class UnitTest.DTL.Test2.TestTrans.TransformSource2
Compiling routine UnitTest.DTL.Test2.TestTrans.TransformSource2.1
Compilation finished successfully in 0.031s.

# Add a new test message pair from an existing HL7 message with rowid 189
set tSC=##class(UnitTest.DTL.Test2.TestTrans.TransformSource2).AddTestFromMessageBody("EnsLib.HL7.Message",189,1,.sourceXdataName,.targetXdataName,1)

# Run UnitTest
do ##class(UnitTest.DTL.Test2.TestTrans.TransformSource2).Debug()

# There will be one intentional error as the original template for manually adding messages is still present. But it does demonstrate attempting to process both pairs of message blocks.
# Perhaps this placeholder template should be suppressable when generating new TestCases if always programatically adding them.

 XData TESTMessageSource
{
<test><![CDATA[
<!-- Your Source HL7 Message or Segment content goes here -->
]]></test>
} XData TESTMessageTarget
{
<test><![CDATA[
<!-- Your Expected output for HL7 Message or Segment content goes here -->
]]></test>
}

Thank you for trying out the utility.

Alex Woodhead · Aug 18, 2022 go to post

Hi Rochdi,

If you have a look at class ompare.SourceHandler.Class, in Open Exchange app https://openexchange.intersystems.com/package/ompare.

It has a range of functionality for decomposing classes into their different components.

Extracting functional lines of source code but discarding comments and empty lines.

It could be enhanced to add a counter to method lines extracted in the profile class.

The schedule also happily jumps namespaces to build report information for multiple namespaces in an environment.

Kind regards,

Alex

Alex Woodhead · Aug 17, 2022 go to post

Hi Jimmy,

One approach could be to define a FunctionSet method:

 Class MyRule.FunctionSet Extends Ens.Rule.FunctionSet
{ ClassMethod StopService(serviceName As %String = "") As %Boolean
{
job ##class(Ens.Director).EnableConfigItem(serviceName,0,1)
quit 1
} }

Then in your routing rule that is recieving bad messages from your service you could use the new function:

You might also decide to simply match and suspend bad messages or route to a bad message handler if sequence is not an issue.

Assumes the characters are within the first 10,000 characters of HL7 message.

Hope this helped inspire some ideas.

Cheers,

Alex

Alex Woodhead · Aug 9, 2022 go to post

Not sure of impact but for R&D just to save a global node, total properties needs to add up to a max of 32K.

Property json As %String(MAXLEN = 32000, TRUNCATE=1);

Alex Woodhead · Jul 20, 2022 go to post

Is the timeout occuring waiting for System Managemt Portal web page to return html content to the web browser?

In the "Web Gateway" configuration, "Default Parameters" page the setting "Server Response Timeout" is default 60 Seconds.

Also if using internal Apache webserver for SMP (install dir/httpd/conf/httpd.conf)
the default "Timeout" is 300 seconds.

The lower of these values is applied. So could start by increasing the Web Gateway timeout setting first to see if gives more time for page to return content.

Alex Woodhead · Jul 13, 2022 go to post

Great questions to consider.

1. The single message.
  The princplie with the example is a single base message for maximum reuse.
  However you define many Methods with the prefix "Test" and these automatically get run by the UnitTest runner.
  Within each method you can send one or more variations of the base test message.
  For example: Consider RoutingRules with different behaviour for Age and Gender.
  Within the same TestMethod you can implemented small updates to the base message to ensure full coverage of your routing rule logic.
  ie: TestAgeGender Method, could send adjusted messages for various age ranges and genders.
2a. Yes the automatic correlation of one base message in BeforeAllTests was done as a convenience.
  It is trivial to add a helper method to pull messages for different XData sources as needed. I will add this. Good suggestion.
 b. Yes. I could also implement a much simpler utility that simply reads HL7 files from a directory and outputs a CSV report of:
   * Source filename
   * Routing Result: Which rule number was applied
   * Routing Return Value: Where it was sent, What transform was employed
   This doesn't need the UnitTest runner and the corresponding web report to be generated.
   Expect you could run the "Test Reporting" class on both the pre-migration and the post migration platform and compare the output report.
3. Yes I have experience with UnitTests for DTLs.
   The particular challenge for these I find are with generated fields like some Dates, and Episode Number.
   ie: You want to compare most things you transform about a message, but wish to exclude certain fields for UnitTest validation as they always have a different / unique value.
   A non-intellegent comparison tool will just say the output files are always different.
   For this I have some other code that analyses all DTLs in a namespace and generates corresponding UnitTest classes with targeted "Compare" methods, generated by parsing the DTL XML.
   This allows you to easily go in after generation and comment out a few specific HL7 fields you wish to exclude from comparison between the expected and the actual HL7 transform result.
   The reuse here is in adding new XData message pairs, for source and expected transformed message, to Tests to the class definition.
   The unit Test automatically picks up the new messages to transforma and validate. So you could actually employ an analyst role instead of developer, to enhance DTL UnitTests for edge cases.  

The main value with UnitTests is for continious integration and / or avoiding future changes breaking code behaviour in unexpected ways.
It also allows you to validate parts of a production in isolation.
It automates regression tests of code.
It can save time with rework. For example I had a routing rule for routing and filtering messages based on their Snomed classifications. The rules were quite complex and would take hours to manually test.
However with one Routing Rule UnitTest I could add an enhancement and retest in minutes with the confidence that previous behaviour was preserved.
There is a judgement on which approach brings the most immediate value, but in the longer term incrementally adding UnitTests can enforce more robust and controlled transformation and routing behaviors.

Alex Woodhead · Jul 11, 2022 go to post

I liked Craig's suggestion which is a single object script function

"("_$P(input,"(",2,999)

Alex Woodhead · Jul 11, 2022 go to post

Taking a step back and please don't be offended but I just wanted to confirm something.
When accessing the Web Gateway does the URL in the browser contain the superserver port number.
ie: Is this: http://servername:52773/csp/bin/Systems/Module.cxw&nbsp; (Internal WebServer)
or http://servername/csp/bin/Systems/Module.cxw&nbsp; (External WebServer)

Also...
In the Web Gateway under Configuration -> Default Parameters.
Do you have a non-default value set for "Event Log File"