Hi Evgeny,
Community and Express licensing for InterSystems IRIS sounds very interesting. Do you have any more information on this?
Sean.
- Log in to post comments
Hi Evgeny,
Community and Express licensing for InterSystems IRIS sounds very interesting. Do you have any more information on this?
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.
No, the internal code will call AKISet when you assign a value to the AKI property. I bashed this out as a registered object, probably had a singleton pattern going on in my mind. On reflection it would be a really bad idea to mix this with a persistent object.
If you really want to wrap legacy data with persistent objects then do it the right way...
https://community.intersystems.com/post/art-mapping-globals-classes-1-3
You can set up ^CODE("TNO","BIO",{BIOID},"AKI") as the storage structure for the AKI property value. For any new objects, it doesn't matter if you assign "1", "0" or "" to that property, it will always create the storage location with one of those three values. The state will never be broken.
There is no point moving to persistent objects if there is legacy code that is bypassing it. It would be an all or nothing solution, everything uses the persistent object or not at all. In which case, when you come to deploy the object and legacy updates, you just need to run a small utility that will add in missing "AKI" nodes with an empty string value.
This would be a much more maintainable approach in the long run, and you will get all the benefits of SQL as well.
>s x=##class(Foo.Bananas).%New()
>s x.BIOID=291
>zw ^CODE
>s x.AKI=1
>zw ^CODE
^CODE("TNO","BIO",291,"AKI")=1
>s x.AKI=""
>zw ^CODE
>s x.AKI=0
>zw ^CODE
^CODE("TNO","BIO",291,"AKI")=0How 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,
Studio will run under Wine on Linux. I've not tried it myself, but others have reported it working ok. Just make sure you are using the latest version of Wine. I seem to remember there being one issue reported here, not sure if it was ever resolved...
https://community.intersystems.com/post/cache-studio-20172
Alternatively you could use Atelier, Serenji or Visual Studio Code.
I've also listed some other editors on this page...
https://github.com/SeanConnelly/Public-Cache-Projects
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()}}Thanks Evgeny, probably sounds about right.
I would hazard a guess then that upwards of 0.1% of developers worldwide use Caché in one shape or form.
It's interesting to compare that to the last stackoverflow survey...
https://insights.stackoverflow.com/survey/2018/#technology-databases
especially since some of those at the bottom of the list are developed on top of Hadoop, opens up some ideas for what could be possible on top of the IRIS Hadoop platform.
On a side note, how about an annual DC survey? Would give some fascinating insights.
Hi Wouter,
I'm not looking at any HL7 documentation at the moment, but I think \X0D\ is the valid escape sequence. The X denotes that this is one or more hex characters and the 0D is hex for carriage return. The value 00D would not be a valid hex value.
how about...
write $zdate($h,12)
Right, probably because the settings are not in the reg to read.
In which case, the OP question asked for the registry path, which would be...
Computer\HKEY_USERS\(your HKEY user value here)\Software\InterSystems\Cache Studio\Settings\Editor
Set the value of "Hover" to Hex 0.
Hi Hendri,
Have you tried disabling hover tips from within Studio itself, you can find it under the top menu...
Tools > Options > Editor > Syntax Check and Assist > Hover Tips
Well done getting Studio to run under Wine, never tried it, would be interesting to read a "how to" post on it if things turn out stable / usable for you.
Sean.
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 ]
---------------------------
Ahh, just re-read your question, I think your asking how to change the settings label?
You will need to change the language lookup value, take a look at...
^CacheMsg("EnsColumns","en")
Hi Larry,
Have you added the property into the SETTINGS parameter, e.g.
If its not in the SETTINGS then it won't appear on the config UI settings.
Sean.
It's not a bad question, but the short answer is not in a single line of code.
Take a look at this post, it might give you a few ideas if you just wanted a Dynamic Object...
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
oneYou won't need a package name for the global name, this is just a naming convention for globals that are auto named by the %Persistent class.
If you provide your own global name then it can be anything you want, so for the transitory data just call it something like...
^CacheTempDedup
I think there is probably an easier way to solve the dedup problem by passing the log file twice and recording the line numbers of interest into a temporary global, you can then skip all the early duplication and minimise the transaction writes.
This is a 2 min bash but it should give you a general idea of how to do that...
kill ^CacheTempDedupSeen
kill ^CacheTempDedupLine
set lineNo=1
set file=##class(%File).%New("file.txt")
do file.Open("R")
set data=file.ReadLine
while file.AtEnd=0 {
set code=$piece(data,",",1) //e.g
if $data(^CacheTempDedupSeen(code)) kill ^CacheTempDedupLine(^CacheTempDedupSeen(code))
set ^CacheTempDedupSeen(code)=lineNo
set ^CacheTempDedupLine(lineNo)=code
set data=file.ReadLine
set lineNo=lineNo+1
}
do file.Rewind()
set lineNo=1
set data=file.ReadLine
while file.AtEnd=0 {
if $data(^CacheTempDedupLine(lineNo)) {
//this data is the last in the log for its code type, update the persistent class directly
}
set data=file.ReadLine
set lineNo=lineNo+1
}
It will be the name of the global... ^CacheTempGlobal
Global export import seems like a good strategy.
I was just looking at your SQL again, do you have all of the lookup values in one single global node? If this is the case then that would explain why the transaction log was getting full up.
What you should really do is split the data out into many global entries...
E.g.
^Lookup("UK")="United Kingdom"
^Lookup("USA")="United States"
^Lookup("ESP")="Spain"
^Lookup("ITA")="Italy"
Making the key (or keys depending on your data) unique and easy to check for duplicates on those key(s).
Then, when you make updates to the global, only the node that changes will be committed to the transaction log, which will have a tiny overall impact.
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.
How about using ServerSideRedirect ?
Looks to be there in 2017.1 as well...
Hi Rich,
I've not used it, but perhaps take a look at the export tasks and import tasks on the %SYS.Task class...
Sean
This is a bit of an experimental (not released) error dump utility that I developed not so long back to explore the idea of dumping all variables into a JSON string.
It uses undocumented $zu functions which are listed on this site...
http://www.cachewiki.org/index.php/Undocumented_Syntax
Essentially you use $zu(42 to $order over the variables at a specific stack level.
In this instance I build an exception object with all of the primitives and objects at the stack level in error.
I then use the Cogs JSON library to covert the entire error object into a serialised JSON string that is embedded into a Status string. The good thing here is that it will do a deep serialisation of objects which often get clipped in other solutions.
From here I can pass the JSON part of the Status string to an error log viewing tool that formats the JSON into an HTML view.
It doesn't look like I fully got the stack levels into the object, so the code would need some further tweaking.
Class Cogs.Lib.Status Extends Cogs.JsonClass
{
Property Error As %String(MAXLEN = 50000);
Property Stack As array Of %String(MAXLEN = 50000);
Property Objects As array Of Cogs.Lib.Status.Object;
Property Primatives As array Of %String(MAXLEN = 50000);
ClassMethod AsStatus(pException As %Exception.AbstractException) As %Status
{
set sc=pException.AsStatus()
try {
do pException.Log()
set status=##class(Cogs.Lib.Status).%New()
set status.Error=$zerror
for i=1:1:$stack-1 do status.Stack.SetAt($stack(i,"PLACE")_" : "_$zstrip($stack(i, "MCODE"),"<W"),i)
set level=$ZU(41)-2
set var=$zu(42,level,"~")
while var'="" {
set name=$p(var,"~",2)
set item=$zu(43,level,var)
if $data(item),item'["%Exception.SystemException" {
if $IsObject(item) {
set object=##class(Cogs.Lib.Status.Object).%New()
set object.Reference=item
set object.Properties=##class(Cogs.JsonObject).toJSON(item)
do status.Objects.SetAt(object,name)
} else {
do status.Primatives.SetAt(item,name)
}
}
set var=$zu(42,level,var)
}
set sc=$$$ERROR($p(##class(%SYSTEM.Status).GetErrorCodes(sc),","),status.toJSON())
} catch err {}
quit sc
}
}Example in use...
Class Foo.Exception Extends %RegisteredObject
{
ClassMethod Foo()
{
try {
set customer=##class(Northwind.Customers).%OpenId("ALFKI")
set foo="Hello, World"
set sc=$$$ERROR($$$GeneralError,"A general error type")
$$$ThrowOnError(sc)
} catch exception {
set errorString=##class(Cogs.Lib.Status).AsStatus(exception)
write !,errorString
}
}
}The error object...
{
"Error":"",
"Objects":{
"customer":{
"Properties":{
"Address":"Obere Str. 57",
"City":"Berlin",
"CompanyName":"Alfreds Futterkiste",
"ContactName":"Maria Anders",
"ContactTitle":"Sales Representative",
"Country":"Germany",
"CustomerID":"ALFKI",
"Fax":"030-0076545",
"Phone":"030-0074321",
"PostalCode":"12209",
"Region":""
},
"Reference":"1@Northwind.Customers"
},
"exception":{
"Properties":{
"NextException":""
},
"Reference":"2@%Exception.StatusException"
}
},
"Primatives":{
"foo":"Hello, World",
"sc":"0 $\u0001\u0004\u0004\u0013\u0016\u0001A general error type\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001"
},
"Stack":{
"1":"zFoo+7^Foo.Exception.1 +1 : set errorString=##class(Cogs.Lib.Status).AsStatus(exception)"
}
}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","~")
>2Probably 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...
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.