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.

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.

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.

Hi Tuan,

1. I'm not sure if you are asking for timed execution or benchmarking execution, so here is an answer to both.

For timed execution use the task manager...

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

For benchmarking a loop you will want to use $zh, its the most granular time function in COS, the docs here show it being used for benchmarking the execution of code...

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

2. You will need to wrap your outer method call in a try catch block so that it will continue to repeat itself even if an error is thrown, something along the lines of...

ClassMethod Start()
{
    While 1 {
        try {
            do ..YourMainMethod()
        } catch err {
            //log error here
        }    
    }
}

More information can be found here...

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

Sean.

How about something along these lines...

Property AKI As %Boolean;

Property BIOID As %String;

Method AKIGet() As %Boolean [ ServerOnly = 1 ]
{
    If '$Data(^CODE("TNO")) Quit ""
    If '$Data(^CODE("TNO","BIO")) Quit ""
    If '$Data(^CODE("TNO","BIO",..BIOID)) Quit ""
    Quit $Get(^CODE("TNO","BIO",..BIOID,"AKI"))
}

Method AKISet(Arg As %Boolean) As %Status [ ServerOnly = 1 ]
{
    If Arg="" {
        Kill ^CODE("TNO","BIO",..BIOID,"AKI")    
    } Else {
        Set ^CODE("TNO","BIO",..BIOID,"AKI")=Arg
    }
    Quit $$$OK
}

I'm passing in your BIO number as another property, I guess you could use a public variable list.

I've tried to make the getter a little defensive. You could argue that you might also need to check if ..BIOID is empty, because this can cause subscript errors, but then again, maybe you want these to fail anyway so they can be handled upstream.

Sean.

Hi Rosti,

I suspect the problem is on both the desktop and on the iphone, if you squeeze down the size of the desktop browser you will probably also see the table expanding beyond the width of the visible viewport.

You might want to take a look at using table layout fixed, and allow text to overflow, take a look at the codepen examples listed on this page...

https://css-tricks.com/fixing-tables-long-strings/

You don't have many columns so this type of fix would be just about ok with users, any more information and I would suggest going for a card layout on the mobile device.

Sean.

Hi Hieu,

This is a very common problem that will trip you up the first few times you work with persistent strings.

The primary issue is that persistent strings have a default max length of 50, and in your example your are trying to save 164 characters.

When you call %Save(), the internal code will first check the object is valid, if it is not then it will return an error.

This is where you need a little more defensive code to catch these errors and bubble them up to your error handling code.

The %Save() method returns what we call a status code, it is either a 1 or a 0, if its a 0 then it was also include a status code error text.

So in your code you might do something like this

 Set sc=docObj.%Save()
If $$$ISERR(sc) Write !,##class(%SYSTEM.Status).GetOneErrorText(sc)

I've assigned the status code returned by %Save() to a variable called sc. It's fairly common to see developers use the naming convention for a status code variable of sc or tSC, but you can of course call it anything your wish within reason.

If you just Write sc it will look a little garbled and would probably make out the line it failed which you spotted in your debugger. In my example I have used a macro called $$$ISERR(sc) which checks to see if the status code passed or failed, if it fails then it will use the %SYSTEM.Status class which has several helpful method to write out a more friendly display of the error. This will have something like "length longer than MAXLEN allowed of 50", which will then point you to the validation failure.

OK, so lets fix the problem by increasing the %String size. You can do this two ways, the first would be to use the code inspector, if your in studio its a tab on the side panel, click on it and select Property from the top left drop down box. You will then see the property listed, click on this and you will see a list of all the settings you can apply to this property. You will see MAXLEN, you can click on its value and increase it to say 10000. You will notice this will change the code to this...

 Property jsonStorer As %String(MAXLEN = 10000);

The second way is to just manually type this setting to the property yourself.

When you now save the JSON it will pass validation and appear in your persistent store.

There are a couple more things to consider. What you probably want to do is bubble up these validation errors either via return values, or by throwing an error. So you might do...

 Quit docObj.%Save()

You can then call your saveJson() method like so..

Set sc=##class(DEMO.TestDocSource).saveJson()

and then deal with the sc status code in your outer calling code. For instance if you were writing an HTML form to submit the JSON, you might return your own error response that would then be handled with an HTML form error message about exceeding the length.

You should also consider that there is a limitation to how long strings can be, in older versions of Caché without long string support enabled the limitation is around 32K, modern version with long string support is around 3.6MB, take a look here...

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

In general you will want to consider using streams instead of strings for properties that are larger than this.

The other consideration is that generating JSON by hand is going to trip you up in many ways. My guess is your are just testing out an idea here, but you will want to use a built in function to produce and correctly escape the JSON for you. If you search for JSON here on the DC site then you will see lots of different ways to achieve this safely.

On a final note you might want to go through the documentation a few times on Caché objects as this will explain some of these points in more detail, you will want to look at this section...

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

Good luck and welcome to the world of Caché persistence, it might seem a little quirky at first but you will learn to love it!!

Sean.

You can output the list to a file using this command...

ccontrol list nodisplay > mylist.txt

You could then create a class method to return the contents as a string, something along these lines using $ZF to run the command...

Class Foo.Ccontrol Extends %RegisteredObject
{

Parameter FOLDER = "C:\InterSystems\Cache2017\bin\";

Parameter FILE = "mylist.txt";

ClassMethod GetList()
{
  do $ZF(-1,..#FOLDER_"ccontrol list nodisplay > "_..#FILE)
  set stream=##class(%Stream.FileCharacter).%New()
  set sc=stream.LinkToFile(..#FOLDER_..#FILE)
  quit stream.Read()
}

}

Hi Julian,

Take a look at the documentation here...

http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=EGDV_prog#EGDV_prog_settings

Essentially, when you define a setting in the SETTINGS parameter you can assign a category name to the settings value, e.g.

Parameter SETTINGS = "Sheds:Foobar"

Which will result in

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

Foobar Settings

Sheds
[ 5 ]

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

Hi Laura,

I find it simpler to write an XSD (even if you don't have one) and then use the XML code generator wizard.

A good place to start is by using an online tool that will auto generate an XSD from XML for you, such at the one you can find here...

https://www.liquid-technologies.com/online-xml-to-xsd-converter

Sometimes you will need to massage the XML a little first, for instance add a second instance of an element when you only have one in your example and you know there will be many.

So you would end up with an XSD that looks like this...

<?xml version="1.0" encoding="utf-8"?>
<!-- Created with Liquid Technologies Online Tools 1.0 (https://www.liquid-technologies.com) -->
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="Envelope">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="Body">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="RESULT">
                <xs:complexType>
                  <xs:sequence>
                    <xs:element name="SUCCESS" type="xs:string" />
                    <xs:element maxOccurs="unbounded" name="LIST">
                      <xs:complexType>
                        <xs:sequence>
                          <xs:element name="ID" type="xs:unsignedShort" />
                          <xs:element name="NAME" type="xs:string" />
                        </xs:sequence>
                      </xs:complexType>
                    </xs:element>
                  </xs:sequence>
                </xs:complexType>
              </xs:element>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

Save this to a file and from studio select "Tools" from the main menu, then "add-ins" and then "XML Schema Wizard" which might be visible or you might need to click "add-ins" again to bring it into focus.

Select the file, click next, de-select the "Create Persistent Classes" option and type in a package name, click next and select "Serial" for each of the classes, click next and the classes will be generated. You might need to go and compile them before they can be used. The generated code looks like this...

Class Spuds.Envelope Extends (%SerialObject, %XML.Adaptor) [ ProcedureBlock ]
{

Parameter XMLNAME = "Envelope";

Parameter XMLSEQUENCE = 1;

Property Body As Spuds.RESULT(XMLNAME = "Body", XMLPROJECTION = "WRAPPED") [ Required ];

}
Class Spuds.RESULT Extends (%SerialObject, %XML.Adaptor) [ ProcedureBlock ]
{

Parameter XMLNAME = "RESULT";

Parameter XMLSEQUENCE = 1;

Property SUCCESS As %String(MAXLEN = "", XMLNAME = "SUCCESS") [ Required ];

Property LIST As list Of Spuds.LIST(XMLNAME = "LIST", XMLPROJECTION = "ELEMENT") [ Required ];

}
Class Spuds.LIST Extends (%SerialObject, %XML.Adaptor) [ ProcedureBlock ]
{

Parameter XMLNAME = "LIST";

Parameter XMLSEQUENCE = 1;

Property ID As %xsd.unsignedShort(XMLNAME = "ID") [ Required ];

Property NAME As %String(MAXLEN = "", XMLNAME = "NAME") [ Required ];

}

And here is the solution in action...

TEST>s xml="<Envelope><Body><RESULT><SUCCESS>TRUE</SUCCESS><LIST><ID>11111</ID><NAME>one</NAME></LIST><LIST><ID>22222</ID><NAME>two</NAME></LIST></RESULT></Body></Envelope>"
 
TEST>set reader=##class(%XML.Reader).%New()                                      

TEST>set sc=reader.OpenString(xml)                                               

TEST>do reader.Correlate("Envelope","Spuds.Envelope")                            

TEST>do reader.Next(.envelope,.sc)                                               
 
TEST>w envelope.Body.SUCCESS
TRUE

TEST>w envelope.Body.LIST.GetAt(1).ID
11111

TEST>w envelope.Body.LIST.GetAt(1).NAME
one

Hi Antonio,

Sounds like you are almost there.

The temp global name is case sensitive, so make sure it starts with ^CacheTemp and not ^CACHETEMP.

You will lose this temp global on a restart, so you will need to then make it permanent once you have dealt with all the duplicates. You can do this with the merge command...

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

You will need to kill the original global first otherwise you will merge the temp global into it.

Alternatively you could have keyed your original global in a way that duplicates are easily identified, e.g. ^foo(uniqueKeyValue)=data, you can then check if the entry exists first before deciding to write anything to the database.

Sean.

Hi Stephen,

I think the short (and potentially dangerous) answer that you are looking for is to use xecute to run COS provided as an argument, such that you have one generic method, e.g.

var h = foo.get_execute("$h");

where execute implements xecute on the passed argument(s).

It is better to try and understand JavaScripts built in functions then call out to Caché which could end up being expensive.

However, if you wanted to replicate functions to make context switching to JavaScript less of a cognitive load then you could write your own mini function library...

let $piece = (string,delimiter,from,to=from) => {
  return string.split(delimiter).slice(from-1,to).join("~");
}

let p1 = $piece("hello~world","~",1);
let $extract = (string,from,to=from) => {
    return string.substring(from-1,to);
}

let sub = $extract("Hello, World",2,5)
let $length = (string,delimiter) => {
  if (delimiter !== undefined) return string.split(delimiter).length;
  return string.length;
}

let len = $length("Hello, World")
>12

let count = $length("Hello~World","~")
>2

Probably the function of most use is $horolog, I've built a couple of solutions that uses horolog everywhere, but tbh I just use W3C dates and auto convert between the two these days, if your using JSON then the Cogs library will automatically do this for you.

I can't find my original horolog everywhere source code, but here is a quick bash...

let $h = function() {
  let now = new Date();
  return Math.floor(((now.getTime() + 4070908800000) / 86400000)) + "," + ((now.getHours() * 60 * 60) + (now.getMinutes() * 60) + now.getSeconds());
}

$h()
> "64776,60593"
let horologToJSDate = function(h) {
    let parts = h.split(",");
    return new Date(((parts[0] * 86400000) - 4070908800000) + (parts[1] === undefined ? 0 : seconds * 1000))
}

horologToJSDate("64776,61081")
> Tue May 08 2018 17:58:01 GMT+0100 (GMT Summer Time)

horologToJSDate("64776")
> Tue May 08 2018 01:00:00 GMT+0100 (GMT Summer Time)

Sean.

Hi David,

There are a couple of options.

The first is ZEN reports, you can read more about them here...

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

The solution is essentially built on top of Apache FOP.

Zen reports abstract some of the complexity of FOP, but you still need to learn ZEN's DSL syntax to get up and running.

Essentially you will create an XML data object in a format specified by the ZEN documentation, an XSLT stylesheet is applied to this, which produces Apache FOP XML. This is then passed into the FOP engine which outputs a PDF document.

It's not a straight forward solution, and if your not writing ZEN reports every day it might seem like a bit of a chore.

Personally I didn't want to learn and remember a new DSL when I am fluent in HTML and CSS, which is why I roll my own solutions on top of wkhtmltopdf...

https://wkhtmltopdf.org/

I have deployments built on top of this that have been generating millions of medical documents a year without a single problem.

Its easy enough to roll your own, just create an HTML string from within COS using data extracted from the HL7 message, include some CSS to format the text, tables etc, save this to a file, and then use the $ZF function to call out to the wkhtmltopdf exe.

You can pass in extra arguments on the call to add things like headers and footers, for instance this would add a page number

--footer-center [page]/[topage]

There is also another option that I am starting to lean towards and this is using the chrome browser in headless mode which enables support for more modern CSS features and allows for more creativity.

>"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --headless --disable-gpu --print-to-pdf=j:\TEMP\file1.pdf http://www.example.com/

I've got a half baked example of using Chrome, if it helps I could tidy it up a little and post it to GitHub.
 

Hi Jaoa,

You need to make sure you check the "Create Business Operation" in the wizard and type in a valid package name for the operation, request and response classes (the text inputs below the tick box).

You then add the generated operation to your production and set the credential settings.

Next, look through the generated request classes and find the two classes that have the name of the methods that you want to call on Exchange.

If you send an instance of these request classes to the operation, then the operation will implement the method call with the given request details, it will then return a response object that will be derived from one of the generated response classes.

Hi Keven,

The compilation is failing for the unit tests which I have included in the src folder, but thought I had removed from the main build file. I was planning on a new push so I will tidy that up. For now you can ignore those particular errors, the main Cogs.JsonClass will not be effected.

If you are on a new version of Caché then you should check to see if $ZCVT supports JSON, if you are currently rolling your own JSON and just want to escape it then just do...

$zcvt(string,"O","JSON")

Or if you are targeting old and new then you can call the base EscapeJSON method on Cogs which compiles by version to $ZCVT or its own escape method, you can call that with...

##class(Cogs.Lib.Json.Base).EscapeJSON(string)

I would imagin if you are working with QEWD then you might be shuffling lots of data around in globals / arrays.

In this instance you might not want to use the Cogs JSON Class as you will end up having to shim in a whole new set of classes.

The new Cogs push does have more options now, ability to work with globals and arrays and legacy classes (without extending them).

There are also the Zen utilities that might help, again these are only in the more recent versions of Caché.

Sean.

Hi Keven,

It can depend on what the data is and where (what technology) you are sending it from.

If you are using JavaScript for instance then calling its JSON.stringify() will auto escape the obvious characters for you.

These are mainly the JSON reserved characters and control characters which get prefixed with \u.

If you have binary data then you will want to Base64 encode the data which will make it JSON safe, or send the binary separately with the JSON, for instance in a multi form data request.

I have a backwards compatible (and battle tested library) for handling JSON inside Caché that demonstrates encoding and decoding these characters, you can find it here...

https://github.com/SeanConnelly/Cogs

I also have some updates to push to it which will auto convert binary data in and out of JSON without the 3.6GB large string limitation.

Sean.

Looks like a couple of issues, I can't see where you are saving the document.

Its a 5 minutes hack, but here is an example of what I might do...

Class Foo.QueryDataToCSV Extends Ens.BusinessOperation
{
Parameter ADAPTER = "EnsLib.SQL.OutboundAdapter";
Property Adapter As EnsLib.SQL.OutboundAdapter;
Parameter INVOCATION = "Queue";

Method OnMessage(pRequest As Ens.StringContainer, Output pResponse As %Library.Persistent) As %Status
{
    set sql="select * from Foo.Person"
    
    #dim rs as EnsLib.SQL.GatewayResultSet
    set sc=..Adapter.ExecuteQuery(.rs,sql)
    if $$$ISERR(sc) quit sc

    set file = ##class(%Stream.FileCharacter).%New()
    set file.Filename="C:\Temp\123.csv"
    
    while rs.Next() {
        set (comma,rec)=""
        for i=1:1:rs.GetColumnCount() {
            set rec=rec_comma_""""_rs.GetData(i)_""""
            set comma=","    
        }
        set sc=file.WriteLine(rec)    
    }
    
    quit file.%Save()
}

}

Staying with the old school CSP approach, I would bake the lists in with the page / page fragment and then swap them in and out using JavaScript. The lists will stay as fresh as the form and the user experience will be very snappy with zero lag, happy users! It will also remember the users selection if they can't make their checking minds up.

Class Foo.SamplePage Extends %CSP.Page
{

ClassMethod OnPage() As %Status [ ServerOnly = 1 ]
{

  set list1="<select id=""list-1""><option value=""A"">Alpha</option><option value=""B"">Bravo</option><option value=""C"">Charlie</option></select>"
  set list2="<select id=""list-2"" style=""display:none;""><option value=""X"">XRay</option><option value=""Y"">Yankie</option><option value=""Z"">Zulu</option></select>"

  &html<

    The Check Box: <input id="cbox" type="checkbox" /><span id="lists">#(list1)##(list2)#</span> 

    <script language="JavaScript">

      document.getElementById("cbox").addEventListener("click", function(el) {
        document.getElementById("list-1").style.display (el.srcElement.checked) 'none' 'inline';
        document.getElementById("list-2").style.display (el.srcElement.checked) 'inline' 'none'; 
      });

   </script>

  >

  Quit $$$OK
}

}