Julius Kavay · Aug 4, 2021 go to post

You can change the definition, the data remains as is. But you have to take care about your applications, like SQL-queries. For example  a DATEPART() function could refuse to work because a property, defined as %String, may contain everything.

Julius Kavay · Jun 8, 2021 go to post

Somehow I don't get you right. To save obj.%Size() in a variable, just do a simple assign

set myVariable = obj.%Size()

but I'm pretty shure, this was not your intended question.

I suppose, you have JSON formatted data (a string or a stream) and you want to store those data in a table. Am I right?

If yes, then follow the next steps:

1) create a class which describes your JSON objects (strings)

Class DC.SehindeRaji Extends (%Persistent, %JSON.Adaptor)
{
Property byr As %String(%JSONFIELDNAME = "byr:");
Property iyr As %String(%JSONFIELDNAME = "iyr:");
Property eyr As %String(%JSONFIELDNAME = "eyr:");
// do the same for all other fields

ClassMethod Import(data)
{
    set obj=..%New()                    // create a new DC.SehindeRaji object
    set sts=obj.%JSONImport(data,"")    // import the (JSON) data
    
    if sts {
        set sts = obj.%Save()
        if sts {
            write "Saved, ID=",obj.%Id(),!
            quit 1
            
        } else {
            write "Not saved, Err=",$system.Status.GetOneErrorText(sts),!
            quit 0
        }
        
    } else {
        write "Can't import: ",$system.Status.GetOneErrorText(sts),!
        quit 0
    }
}
}

2) You can create some test data (interactively) in a terminal session

set dynObj = {"byr:":"1937", "iyr:":"2017", "eyr:":"2020"}
set data = dynObj.%ToJSON()

or get your data somehow from an input (possibly from a file),  the only important thing is, your data should look like this

write data  -->  {"byr:":"1937","iyr:":"2017","eyr:":"2020"}

3) import those data

write ##class(DC.SehindeRaji).Import(data) --> Saved, ID=1

4) Now open the saved data and check the result

set oref =  ##class(DC.SehindeRaji).%OpenId(1)

write oref.byr  --> 1937
write oref.iyr  --> 2017

write oref.%JSONExportToString(.exported,"") --> 1
write exported  --> {"byr:":"1937","iyr:":"2017","eyr:":"2020"}

zw ^DC.SehindeRajiD
^DC.SehindeRajiD=1
^DC.SehindeRajiD(1)=$lb("","1937","2017","2020")

I hope, this is what yoy want to do...

Julius Kavay · Jun 8, 2021 go to post

The facts:
1) According to the error message: "The system cannot find the file specified."
2) Futhermore, the error message shows slashes and backslashes, mixing is rarely good, Windows uses "\", Unix "/"

What to do is:
1) check the filename, you want to send (including the path)
2) check the existence of the file
3) Under which user accont is IRIS/Cache running?
4) May this user read the file?

Julius Kavay · Jun 2, 2021 go to post

If you can call a JavaScript function, then you could do something like this...

<html>
<head><title>Test</title>
<link id="fav" rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAF0lEQVQokWP8z0AaYCJR/aiGUQ1DSAMAQC4BH5CRCM8AAAAASUVORK5CYII=">

<script>
    function changeFavicon() {
        var lid=document.getElementById("fav");
        if (lid) {
            lid.setAttribute("href","data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAGElEQVQokWNk+M9AEmAiTfmohlENQ0kDAD8vAR+xLJsiAAAAAElFTkSuQmCC");
        }
    }
</script>
</head>
<body>
<button onclick="changeFavicon();")>Change my favicon</button><br>
</body>
</html>

The (red and green) icons are just a demo example.

Julius Kavay · May 25, 2021 go to post

If I got you correctly... for IRIS (and newer Cache Versions) you can use

select * from  INFORMATION_SCHEMA.ROUTINES where ROUTINE_NAME='...'

and for older Cache versions try

select * from %Dictionary.CompiledMethod where SqlProc=1 and Name='...'

(but be patient, this takes some time)

Julius Kavay · May 21, 2021 go to post

You have a string of digits... like

set result="12345678900987654321"

then you can easily extract groups of four digits as

for i=1:4:$length(result) write $extract(result,i,i+3),!

this gives you

1234
5678
9009
8765
4321

assuming, there are no other characters between those numbers...

Julius Kavay · May 14, 2021 go to post

Long time ago I did some connections to external databases (MySql and PostGres).
The essential parts such a connection are:

1) First, you have to create in your OS the corresponding ODBC Data Source entries
   (System-DSN) after installing the required DB-Driver

2) The connection

    set gtwConn=##class(%SQLGatewayConnection).%New(), gtwHandle=0
    
    if gtwConn.Connect(OdbcName, OdbcUser, OdbcPass) {
        if gtwConn.AllocateStatement(.gtwHandle) {
            // check gtwConn.GatewayStatus
            // etc.
        } else { write "Can't Allocate: "_OdbcName }
    } else { write "Can't connect to "_OdbcName }

3) SQL-Commands

    do gtwConn.CloseCursor(gtwHandle)
    if gtwConn.PrepareW(gtwHandle, sqlStatement) {
        if gtwConn.Execute(gtwHandle) {
           ...
           ...
        } else { /* check gtwConn.GatewayStatus */ }
    } else { /* check.gtwConn.GatewayStatus */ }

   
4) Finish

    if gtwConn {
        do gtwConn.DropStatement(gtwHandle), gtwConn.Disconnect()
        set gtwConn="", gtwHandle=""
    }
Julius Kavay · May 6, 2021 go to post

Both possible structures are considered. Here, I use the  examples from my previous posting:

set obj=##class(DC.Rick.MemberData).%OpenId(1)
do obj.%JSONExport() --> {"members":[{"dob":"1990-07-18","firstName":"Bob","memberId":123956}]}
set obj=##class(DC.Rick.MemberData).%OpenId(2)
do obj.%JSONExport() --> {}

The second example outputs {} only and not {"members":null}, I don't know why. Maybe there is a parameter which control this behavior, please ask WRC. 

From the view of data value, you can consider {} and {"members":null} as equal.

write {"members":null}.%GetTypeOf("members") --> null
write {}.%GetTypeOf("members") ----------------> unassigned

Both representation mean, the members property has no value. But, yes, but you can philosophize about it ...

Julius Kavay · May 4, 2021 go to post

The simplest solution was already answered by Robert Cemper in https://community.intersystems.com/post/how-select-random-row-table. I just want to show a more "universal variant" of that solution.

First, create an SQL stored procedure

class SP.Utilis Extends %RegisteredObject
{
ClassMethod Random(number As %Integer, dummy As %String) As %Integer [SqlProc]
{
   quit $random(number) // we do not use dummy but we need it!!
}
}

then make your query as follows:

select top 10 * from whatever.table where SP.Utils_Random(100,ID)<50

This has following advantages:

1) works with all classes, i.e. the ID column has not to be an integer (greater 0), can be a compound column too (like part1||part2, etc)

2) by adjusting the comparison:

Random(1000,ID) < 50   // gives you more "greater" distances then

Random(1000,ID) <500  // between the returned rows

For testing of edge conditions you can use

Random(1000,ID)<0    // no rows will be returned or

Random(1000,ID)<1000 // all rows will be returnd

With the right side of the comparison you can fine tune the distances between the returned rows.

For the dummy argument in the above function you can use an arbitrary column name, the simplest is to use ID because the ID column always exists, it's purpose is to force the SQL-Compiler to call this function for each row (thinking, the result of the Random() function is row-dependet). A comparsion like Random(100)<50 is executed just once. Roberts solution works too because he uses Random(100)<ID but this works only for tables where ID is a Integer (>0). You can verify this by just issuing a simple query

select top 10 * fom your.table where SP.Utils_Random(100)<50

You will see (by repeatedly executing the above query) either 10 (subsequente) rows or nothing

Julius Kavay · May 4, 2021 go to post

According to your $ZV you have a unicode installation, so there should be no problem, neither with skandinavian- nor with other language characters.

Please check your filename string... start a terminal session and then:

zzdump FileNameFullPath

then check, in the same way, the file name in your file system

Julius Kavay · Apr 28, 2021 go to post
ClassMethod Size(file)
{
   if '##class(%File).Exists(file) quit -1
   quit ##class(%File).GetFileSize(file)
}

Returnvalues:

-1=File does not exists, 0...n=Filesize

Julius Kavay · Apr 8, 2021 go to post

If you get data as ISO-8859-1 (aka Latin1) and have a Unicode (IRIS/Cache) installation then usually you have nothing to do (except, to process the data). What do you mean with "convert the text to UTF-8"? In IRIS/Cache you have  (and work with) Unicode codepoints, UTF-8 comes into play only when you export your data but in your case, it will rather be ISO-8859-1 or do I something misunderstand?

By the way, if you return your data back to your Latin1 source (as Latin1) then you have to take some precautions because you have an unicode installation, so during the data processing you could mix your Latin1 data with true unicode data from other sources!

See: https://unicode.org/charts/

Also, you may download and read:

https://www.unicode.org/versions/Unicode13.0.0/UnicodeStandard-13.0.pdf

Julius Kavay · Mar 17, 2021 go to post

Checking status codes is a good starting point...

set str=##class(%Stream.FileCharacter).%New()
write str  --> 4@%Stream.FileCharacter
write $system.OBJ.DisplayError(str.LinkToFile("/root/my_file.txt")) --> 1
write $system.OBJ.DisplayError(str.WriteLine("line-1"))  --> ERROR #5005: Cannot open file '/root/my_file.txt'1

Your %Save() returns with 1 (OK), because there is nothing to save...

Note: on linux,  for a standard user (like me now) is forbidden to write to '/root' directory

Julius Kavay · Mar 4, 2021 go to post

In addition to Ben's answer, 

on Windows: netstat -ano | find "LISTENING"

will in the far right column show the process ID of the listening process

on Linux: sudo netstat -pln | grep tcp

the last column shows the process name which is opened the port

Julius Kavay · Jan 14, 2021 go to post

I'm not sure, what you want to do, but if you want to read a tiff file by byte-by-byte and the interpret it in some kind, there is a very simple example for the start.
 The method below returns the file type (gif,jpg,png or tif) based on the magic number.

/// Identify an image by its magic number
/// (only for gif,jpg,png and tif)
ClassMethod ImageType(file, ByRef err)
{
   o file:"ru":0
   i $t {
      s io=$i, err=""
      u file r x#8
      
      i x?1"GIF8"1(1"7",1"9")1"a" { s typ="gif" }
      elseif $e(x,1,2)=$c(255,216), $$end()=$c(255,217) { s typ="jpg" }
      elseif x=$c(137,80,78,71,13,10,26,10) { s typ="png" }
      elseif $case($e(x,1,4), $c(73,73,42,0):1, $c(77,77,0,42):1, :0) { s typ="tif" }
      else { s typ="", err="File type unknown" }
      
      c file
      u:io]"" io
   
   } else { s typ="", err="Can't open "_file }
   
   q typ

end() s t="" r:$zseek(-2,2) t#2 q t
}

I have also a method to retrive the image size (pixelsWidth and pixelsHeight) for the same (gif,jpg,png and tif) files. If you are working on similar problem, I could post this method too.

Julius Kavay · Dec 9, 2020 go to post

To make things clear, the mentioned popup message should be seen by someone, who is (most of the time) working on the servers console. Is it so? Or you want to popup this message on an arbitrary desktop, where a user works?

If I need to send a message to a user, either I send an email or I activate a popup message in my client (I have a very special client-UI).

By the way, you wrote, I quote "an email is NOT a realistic expectation and far from reliable". Today, (almost) everybody has a smartphone and I think, if somebody does not read an email, he/she won't read thos popup messages either. Of course, you should send short plain text messages and not those fancy bloated colorful emails.

So, a way to a solution... you could write a  small program (C++, VB, Delphi, etc.) which is started after user login.  The program should listen on a TCP port for messages from an arbitrary Cache/Iris (possibly background) application. If such a message arrives, this helper program changes to foreground, displays the message with or without an OK button. That's it.

Julius Kavay · Dec 9, 2020 go to post

Store only hashed passwords... that's all.

Class DC.MyUsers Extends %Persistent
{ 
Property Name As %String;
Property Password As %String;
Property passHash As %String [ Internal, Private, ReadOnly ];
Property passSalt As %String [ Internal, Private, ReadOnly ];
Parameter ITER = 1024;
Parameter LENGTH = 20;
Method PassCheck(psw) As %Boolean
{
  set salt = $system.Encryption.Base64Decode(..passSalt) 
  set hash = $system.Encryption.Base64Decode(..passHash)
  quit $system.Encryption.PBKDF2(psw, ..#ITER, salt, ..#LENGTH)=hash
}
Method PasswordSet(psw) As %Status
{
  // optionally, quality/requirement-check
  if '..pswQuality(psw) quit $$Error^%apiOBJ(5001,"Poor password quality")
  
  set salt=$system.Encryption.GenCryptRand(8)
  set hash=$system.Encryption.PBKDF2(psw, ..#ITER, salt, ..#LENGTH)
  set i%passHash=$system.Encryption.Base64Encode(hash)
  set i%passSalt=$system.Encryption.Base64Encode(salt)
  quit $$$OK
}

Method pswQuality(psw) As %Boolean
{
  quit 1
}
}

Julius Kavay · Dec 4, 2020 go to post

Instead of inserting debug_macros, try Intersystems TRACE utility.

write $$DIR^TRACE("c:\Temp\") ; to set an output directory
write $$ON^TRACE(jobnr) ; the pid of the process you want to trace
; zn "appNamespace"
; do ^yourProgram
; zn "%SYS"
write $$OFF^TRACE(jobnr) ; to stopp the trace
do ^TRACE ; to display the trace result

TRACE displays the function-/method-calls with arguments.

Julius Kavay · Nov 11, 2020 go to post

A quick and dirty way:

set ^|"%SYS"|%SYS("SystemMode")="TEST" // or "LIVE" or "DEVELOPMENT" or "FAILOVER"
Julius Kavay · Oct 19, 2020 go to post

As Robert said, XSLT... but as he indicated too, in this case XSLT would be an overkill.

But there is also a "forgotten" function, $locate() too for such cases! This function is almost never used in DC examples. Two or three lines of code, and you have the perfect solution... but for "simple" cases only. Beware of nested tags!

SetFixedValue(str,tag,value) Public
{
   set i=0, t="(?i)<"_tag_">[^<]+", s=$l(tag)+2
   while $locate(str,t,i,j,v) { set $e(str,j+s-$l(v),j-1)=value, i=j+s-$l(v)+$l(value) }
   quit str
}

It works like a charm...

set a="<Name>..."
write $$SetFixedValue^test(a,"rollno","***")  -->  <Name>ABC</Name><RollNo>***</RollNo><Name>XYZ</Name><RollNo>***</RollNo><Name>xyz</Name><RollNo>***</RollNo>
Julius Kavay · Oct 12, 2020 go to post

Thanks for the adjustment, I looked at the very first line (odbc format, which is 3).

Anyway, the $tr() construct is called the $tr()-backwards format, I found this on the internet more then ten years ago... so the credit goes to a unknown inventor.

$tr(targetPattern, sourcePattern, sourceValue) --> targetValue
Julius Kavay · Oct 7, 2020 go to post

It's simple and easy... just follow the picture (I meant, the JSON string):

[
  {
    "patient":
      {
        "guid":"12345",
        "id":12345,
        "forename":"Joe",
        "surname":"Bloggs",
        "dateOfBirth":"2002-12-10T00:00:00Z",
        "NOK":[
                {
                   "NOKname":"Alison Bloggs",
                   "NOKrelationship":"Wife",
                   "telephone":"02081234567",
                   "email":"alison@bloggs.com"
                }
              ]
      }
  }
]

Now we create a dynamic Object:

set obj = {}.%FromJSON(pData)

Your pData is a JSON-Array, where the (array)elements/items are objects

 (we have just one elemment), so we can say:

set item = obj.%Get(0)

This  item object has a patient property, which is an object, so we go one step deeper

set item = obj.%Get(0)
set patient = item.patient 
set patient = obj.%Get(0).patient  // same as above

Our patient object has properties like guid,  idforename, surname, dateOfBirth and NOK

The property NOK itself is, again, an array where the array elements/items are objects.

set nok = patient.NOK
set nok = obj.%Get(0).patient.NOK // same as above

Now, we know, our property NOK is an array, so we have to take of those elements. This nok elements are objects, so we can take the properties (NOKname, ...email):

set nokItem = nok.%Get(0)
write nokItem.NOKname," ",nokItem.email,!

or, all in one line

write obj.%Get(0).patient.NOK.%Get(0).NOKname,!

Now all together, a small routine to print out all NOKxxxx properties:

set obj = {}.%FromJSON(pData)
for i=0:1:obj.%Size()-1 { // loop over pData items grabbing patient props
    set patient = obj.%Get(i).patient
    for j=0:1:patient.NOK.%Size()-1 { // now we loop over all NOK items
        set nok = patient.NOK.%Get(j)
        write nok.NOKname,!
        write nok.NOKrelationship,!
    }
}

So I hope, you see the light at the end of the tunnel...

Julius Kavay · Sep 13, 2020 go to post

For such cases use the macro: #def1arg

See this example:

Test ;my Macro test
#def1arg ADD(%a) $$add(%a)
 write $$$ADD(1),!
 write $$$ADD(1,2),!
 write $$$ADD(1,2,3),!
 write $$$ADD(1,2,3,4),!
 quit

add(a,b=0,c=0,d=0) { quit a+b+c+d }
do ^Test

gives you the output

1

3

6

10
Julius Kavay · Aug 27, 2020 go to post

All ISC products (Ensemble, Cache, IRIS...) have an callin and callout interface via dll/so.  As a starting point, read this

https://docs.intersystems.com/healthconnectlatest/csp/docbook/DocBook.U…

and this

https://cedocs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?…

You will also need some experience in C/C++ or Delphi or in other language, where you can compile *.dll (Win) or *.so (Linux) files.

See the functions $zf(-3), $zf(-4), $zf(-5) and $zf(-6)

Julius Kavay · Aug 25, 2020 go to post

Create a FindLab() method in your ListLabCenter class, something like:

Method FindLab(start = 0, LabId = "", Center = "", Code = "")

{    set nc1=LabId="", nc2=Center="", nc3=Code=""

     for i=start+1:1:..Labs.Count() {

         set tmp=..Labs.GetAt(i)

         if tmp.LabId=LabId!nc1, tmp.Center=Center!nc2, tmp.Code=Code!nc3 return i

}

   quit 0

}

Then use it as follows:

write obj.FindLab(0,"A08829848","A088298480003","") to find a specific Lab

or 

set  center=0
for  { set center = obj.FindLab(center,,"A088298480003")
       quit:'center
       /* do something with center *./
}

to find all LabCenter objects where Center = "A088298480003"

Julius Kavay · Jul 29, 2020 go to post

I assume,  your JSON is generated by converting a dynamic object to string, i.e.: 

do obj.%ToJSON()

In elsecase, it's created manually an there you could put as many quotes as you like around the numbers.

For the first case, just make a function which converts the numbers into string, see below:

test ;test for stringify
   set a={},a.Name="John",a.Age=47,a.xx="ab:1234",a.City="Boston",a.Year=2020
   set a."Arr"=[11,22,"aa","bb"]
   set a."Obj"={"aa":"bb"}

   write a.%ToJSON(),!
   write $$stringify(a).%ToJSON(),!
   quit

stringify(x)
{
   set y=x.%GetIterator()
   while y.%GetNext(.i,.v) {
     set t=x.%GetTypeOf(i)
     if t="number" { d x.%Set(i,v,"string") } elseif t="array"!(t="object") { do stringify(v) }
   }
   quit x
}

The output  is then:

do ^test
{"Name":"John","Age":47,"xx":"ab:1234","City":"Boston","Year":2020,"Arr":[11,22,"aa","bb"],"Obj":{"aa":"bb"}}
{"Name":"John","Age":"47","xx":"ab:1234","City":"Boston","Year":"2020","Arr":["11","22","aa","bb"],"Obj":{"aa":"bb"}}