Try cutting and pasting this fullwidth comma into your code, looks similar...
>>> ,<<<
- Log in to post comments
Try cutting and pasting this fullwidth comma into your code, looks similar...
>>> ,<<<
Taking a quick look at the Zen code, I can see that it does an HTML escape on the comma separated values with
$ZCVT(tHeader,"O","HTML")
The problem with that is...
House number, apartment
will turn into...
House number&#44; apartment
which defeats the idea of using an escape for the comma, so no, this suggestion won't work for you
You could try...
,
Out of context of the original code, I agree.
The actual implementation is there to stop an infinite loop on objects that reference each other as part of a JSON serialiser, see line 9...
https://github.com/SeanConnelly/Cogs/blob/master/src/Cogs/Lib/Json/Cogs…
I'm not sure the construction of seen($THIS) is so much incorrect, just problematic. Both seen($THIS) and seen(""_$THIS) will produce an array item with a stringy representation, and works perfectly fine with exception of the unwanted side effect.
My assumption was that $THIS used inside the $get was to be avoided for persistent classes, whilst I could continue to use the OREF for non persistent classes, hence finding that the persistent objects OID was a perfectly good workaround.
However, as it turns out seen(""_$THIS) prevents the unwanted behaviour and makes the code much simpler to read, so thanks to Timothy for testing a different idea out.
Out of interest, I have since discovered that the pattern of seen(+$THIS) is used extensively in Caché / Ensemble library code, where the + symbol will coerce the string to the ordinal integer value from the OREF. I was tempted to use this approach, but one thing I am not sure of is if ordinal values are unique across a mixed collection of objects...
yes, much simpler!
and passes all my unit tests
2017.1 and 2014.1
The short (incorrect) answer that you are looking for is
set ..Count=..Count+1
$$$TRACE(..Count)
BUT, if you do this, you will notice that whilst Count does indeed increase with each message, it will not persist back to your settings.
Setting properties on an operation are just for convenience, they provide read access to the values held back in the production XDATA / the table Ens_Config.Item
If you want to dynamically update the setting values then you will need to look at the classes, Ens.Config.Production and Ens.Config.Item
But, as stated, it's best not to hijack static settings for a dynamic purpose. Updating the settings with every message will cause a production update event each time, and possibly other side effects.
If you want to track dynamic values at the operation level then have a look at pre-built services and operations that implement $$$EnsStaticAppData and $$$EnsRuntimeAppData, these macro helpers will save things like counts back to a persistent global.
Seems like a lot of hard work when you could just query the count and add a base value to it.
Probably not a good idea to change the production configuration settings with every operation call, each change will trigger a production update request on the production view.
If the customer wants to see a counter since a defined point then it sounds like there might be a point in time that can be used. In which case you get a count using SQL...
have you tried...
Halt
Set arr=##class(%ArrayOfDataTypes).%New()
; place items into the array
Do arr.SetAt("red","color")
Do arr.SetAt("large","size")
Do arr.SetAt("expensive","price")
; iterate over contents of array
Set key=""
For Set value=arr.GetNext(.key) Quit:key="" Write key,":",value,!From the documentation...
The %Collection.ArrayOfDataTypes class represents an array of literal (i.e., data type) elements, each of which is associated with a unique key value. Keys can have any value, string or numeric. These %Collection classes can only be used when you have a collection property of another object as they rely on storing the data inside the parent object, they can not be used as 'stand alone' collections, for this use the %ArrayOfDataTypes.
Hi Sebastian,
> The rest service won't see a whole lot of usage still I wonder whether it is a good idea. Or let me rephrase this, it certainly isn't a good idea but is it a viable one due to lack of alternatives?
You don't have to use REST, you can use a standard CSP page (particularly for anyone using a pre REST version of Caché).
There are three things to point out.
1. You need to set the ContentType on the %response object
2. If the user is going to want to use the URL directly and download the file to a local disk, then set the content disposition, otherwise, the file name will end in .CLS
3. Simply use the OutputToDevice() method on the %File class to stream the file contents to the client.
This can be easily applied to REST, but there is a caveat that you need to look out for.
The initial REST path might look like this...
However, if you allow a full path name in the file name (including folders) then you will hit two problems
1. Security, file paths will need validating, otherwise, any file could be accessed
2. Caché REST just does not work well with \ characters or %5C characters in the URL
If you want to get around the second problem, use a different folder delimiter.
Personally, I would limit the solution to a few nick-named folders, such that the URL match would be
So
/file/Test/Hello.txt
would map to say C:\REST\Test\Hello.txt, or the alternative file that you would provide. Putting that together the solution would look something like...
Note, if you look at Dmitry's solution the file name is added to the CGI variables which would work around some of the issues I have mentioned, but of course, the REST path would no longer keep its state if bookmarked etc.
In general, REST is fine for this type of use case.
Sean
Hi Kishan,
Can you provide the source code at zFile+15^User.zKQRest.1
If you are not sure how to get this, open User.zKQRest.1 and then press Ctrl+Shift+V , this will open up the compiled code, now press Ctrl+G and paste in zFile+15, the cursor will now be on that line.
Could you also provide the source code for the property...
Sean
There are a few approaches.
The schedule setting on a service can be hijacked to trigger some kind of start job message to an operation. It's not a real scheduler and IMHO a bit of a fudge.
A slightly non Ensemble solution is to use the Caché Task manager to trigger an Ensemble service at specific times. The service would be adapterless and would only need send a simple start message (Ens.StringContainer) to its job target. A custom task class (extends %SYS.Task.Definition) would use the CreateBusinessService() method on the Ens.Director to create an instance of this service and call its ProcessInput() method.
The only downside to this is those scheduled configuration settings are now living outside of the production settings. If you can live with that then this would be an ok approach.
Alternatively, you could write your own custom schedule adapter that uses custom settings for target names and start times. The adapters OnTask would get called every n seconds via its call interval setting and would check to see if it's time to trigger a process input message for one of the targets. The service would then send a simple start message to that target.
I prefer this last approach because it's more transparent to an Ensemble developer new to the production, also the settings stay with the production and are automatically mirrored to fail over members.
Hi Scott,
It does sound like you have duplicate or previously processed records. The SQL inbound adapter will skip these.
One of the things to note about the inbound adapter is that the underlying adapter will call the on process input of your service for every row in the resultset. The potential problem with this is that the service could be killed before it has finished off all of the rows in the resultset (forced shut down etc). For this reason, the adapter has to keep track of where it last processed a record, such that it can continue from where it left off.
In your instance, it sounds like this behavior does not fit your data.
If you want to implement your own ground up service solution then you would have to build your own adapter and make the query execution from that adapters OnTask() method. Your adapter should probably extend EnsLib.SQL.Common and implement ExectureQuery* as a member method of the adapter.
If you are going to go down this route then be mindful of building resilience to handle forced shut downs so that it can continue from where it left off.
Also, it's good behavior for adapters to not block the production for long periods. Any adapter such as this will be looping around a set of data, calling out to the ProcessInput method of its business host. If there are many rows then this loop could be going for minutes. It's only when an adapter drops out of its OnTask method can the Ensemble Director cleanly shut down a production. This is why you sometimes see a production struggling to shut down. To not block the production, the adapter will need to chunk its work down, for instance limiting a query size and continuing on from that point on the next OnTask().
Alternatively, you could look to use the SQL outbound which avoids all of the high water mark functionality. Your query will always return everything you expect. I often do it this way and have never had any problems with skipped rows (I run separate audit jobs that also double check this).
Sean.
Jeffrey has the right answer.
Murillo, here are the comments for the %SYS.Task.PurgeErrorsAndLogs task that you are currently using, the ERRORS global is for Caché wide errors and not Ensemble errors...
/// This Task will purge errors (in the ^ERRORS global) that are older than the configured value.<br>
/// It also renames the cconsole.log file if it is larger than the configured maximum size.<br>
/// On a MultiValue system it also renames the mv.log file if it grows too large.<br>
/// This Task is normally run nightly.<br>
Hi Simcha,
Your production class has a parent method called OnConfigChange() which you can override.
The method receives two objects, the updated production config object (Ens.Config.Production) and the production item config object that changed (Ens.Config.Item).
You will need to write your own diff solution around this.
Note, this method only gets called for changes made via the management portal, it will not record changes made to the production class directly.
Alternatively, you could implement an abstract projection class to trigger a diff method on compilation. This will work for both cases. Take a look at this post... https://community.intersystems.com/post/class-projections-and-projectio… on how to implement this alternative.
If you want to know who made the change via the management portal then you can get the users login name using this value...
%session.Username
Sean.
Can you provide your system $ZV version?
>Is there a way to transform this $lb (without the need of openig the object itself) to a JSON object with the proper table fields as properties?
Is there a good reason for not wanting to open the object?
If not, there are several ways to spin the object into JSON.
One option could be to generate a sibling class that implements the generated code.
You could use the abstract create projection event to create the sibling implementation class and then tie the two together using the base class methods.
Alternatively, if you wanted to trigger code on a save event then create a class that extends %Studio.Extension.Base and override its OnAfterSave method. You will then need to enable that class in management portal > system admin > config > additional settings > source control.
Hi Russel,
I would solve the problem along the following lines...
set k2=$s($d(^G("ABC","A")):"A",1:$o(^G("ABC","A")))while $e(k2,1)="A" {
set k3=$o(^G("ABC",k2,""))
while k3'="" {
write !,k2," ",k3
set k3=$o(^G("ABC",k2,k3))
}
set k2=$o(^G("ABC",k2))}Explanation...
//set k2 to either "A" if that key exists in the data, or the next key following "A".set k2=$s($d(^G("ABC","A")):"A",1:$o(^G("ABC","A")))//only process k2 when it starts with an "A", this is the wildcard functionality you are looking for//when k2 does not start with an "A" the logic will drop throughwhile $e(k2,1)="A" {
//get first child key of k2
set k3=$o(^G("ABC",k2,""))
//loop on all child keys found
while k3'="" {
write !,k2," ",k3
//get next child key of k2
set k3=$o(^G("ABC",k2,k3))
}
//get the next k2 key
set k2=$o(^G("ABC",k2))}I don't think there is a function that will do it in one step for you.
There is the $zhex function which will convert a decimal to a hex value.
If you combine it with $ascii you can do one character at a time...
Use a for loop and you can get the result you want...
There is also a utility which may of be of use to you when working on the command line...
Sean
I've knocked up a quick example using dual ack's.
I've put the source code in a gist here...
https://gist.github.com/SeanConnelly/19b79c790daad530a754461923f9f2f1
Save the code to a file and import into a test namespace.
Either create the "in" and "archive" folders are per the inbound test file feeder, or change to suit your environment.
Drop an HL7 message into the "in" folder and this is what you will see in the trace...

The ACK message is sent into the service and is automatically forwarded to the sending operation where it is returned to the calling process as if it was original messages ACK.
Make sure to follow the instructions here when configuring the service and operation, they specifically need to be implemented using EnsLib.HL7.Operation.TCPAckOutOperation and EnsLib.HL7.Service.TCPAckInService
http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY…
Also note, that you need to set the reply code actions so that the AE ACK is returned to the process, otherwise it will stop at the operation, I have set the actions to...
:?R=RF,:?E=W,:~=S,:?A=C,:*=S,:T?=C
Where a match on ?E will just warn and continue as if the message was ok.
Take a look at the custom class Examples.DeferredHL7.CustomProcess
Which contains the following OnResponse method...
This is where you will have both the original request messages and the ACK in the same scope. From here you can construct a new message from the data in both messages.
This is just one approach but should fit your needs.
Sean.
Just remembered that you can also use dual ACK's using built in HL7 adapters...
http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY…
I don't have any cut and paste examples in front of me at the moment. Will see what I can post tomorrow morning...
Hi Joao,
I'm assuming you are sending an HL7 v2.x message from an operation, and its ACK is coming back via your service.
If this is the case then you might want to look at the deferred response functionality...
http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY…
This allows you to automagically send the ACK back as a response to the sending process.
Depending on how you implement the process, you will have the original message in scope, or it will be passed as an on response argument with the ACK response.
Sean.
You might be looking for this...
Set tSC = ##class(Ens.Director).CreateBusinessService("MyService",.tService)Docs...
http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY…
FOO>set msg=##class(EnsLib.HL7.Message).%OpenId(15)
FOO>w msg.RawContent
PID|2|2161348462|20809880170|1614614|20809880170^TESTPAT||19760924|M|||^^^^00000
OBR|1|8642753100012^LIS|20809880170^LCS|008342^UPPER RESPIRATORYCULTURE^L|||19980727175800||||||SS#634748641 CH14885 SRC:THROASRC:PENI|19980727000000||||||20809
OBX|1|ST|008342^UPPER RESPIRATORY||POSITIVE~~~~~~~|
FOO>w !,msg.SetValueAt("Positive","PIDgrpgrp(1).ORCgrp(1).OBXgrp(1).OBX:5")
0 0<Ens>ErrGeneralObject is immutable
FOO>set msg2=msg.%ConstructClone()
FOO>w !,msg2.SetValueAt(msg.GetValueAt("PIDgrpgrp(1).ORCgrp(1).OBXgrp(1).OBX:5.1"),"PIDgrpgrp(1).ORCgrp(1).OBXgrp(1).OBX:5")
1
FOO>w msg2.RawContent
PID|2|2161348462|20809880170|1614614|20809880170^TESTPAT||19760924|M|||^^^^00000
OBR|1|8642753100012^LIS|20809880170^LCS|008342^UPPER RESPIRATORYCULTURE^L|||19980727175800||||||SS#634748641 CH14885 SRC:THROASRC:PENI|19980727000000||||||20809
OBX|1|ST|008342^UPPER RESPIRATORY||POSITIVE|
Hi Gigi,
As you've not supplied any code it's hard to know where you are going wrong.
Firstly, just in case you are trying to change the original message object, you can't (it's immutable).
If you are doing this from code then you will need to make a clone of the original object and then set the OBX:5 from the OBX:5.1 value.
If you are using a DTL then map from the OBX:5.1 value to the OBX:5.
Sean.
try changing
var st=#(..cmEmpTable(EmployeeName,EmployeeNumber,EmployeeDOB,EmployeeDOJ))#;
to
var st=#server(..cmEmpTable(EmployeeName,EmployeeNumber,EmployeeDOB,EmployeeDOJ))#;
Hi Kishen,
I think you are missing
#(..HyperEventHead())#
from the head of your page.
Take a look at the section "Using #server in CSP Classes"...
https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KE…
Sean.