Thanks DC 😊
- Log in to post comments
Thanks DC 😊
Hi Evgeny,
In ISOS, the "quit" command is inherited from MUMPS ("Q[UIT]") and has two forms (see the documentation for details) :
The "return" command was introduced by InterSystems, I think in Caché 2016.2 (@all please, correct me if I'm wrong). It was added to avoid confusion, making ISOS clearer and also more aligned with 'modern' programming conventions.
"return" is equivalent to "quit" with argument, thus is has only one form and one, clear meaning : returning a value.
When the code intent is to return a value, nowadays I favor using "return".
p.s. IMHO, generative AIs, while useful in some contexts, are nor artificial, because they are trained with human sourced corpus, nor intelligent, because they are not able of any real creativity and are not doing any reasoning (even if they can mimic both rather well) 😇
Hi Evgeny,
Thank you for sharing the details, now I see why you have so much lines in a single class 😊
As an example of what Scott and Robert are explaining in their excellent answers, when dealing with a REST API spec defining (mostly) CRUD operation on many resources, I think one way to divide and, hopefully, conquer is to delegate the implementation of operations related to each resource to a separate class.
Taking as example petstore, for which, of course, this is an overkill, this would result in five classes :
dc.petstore.impl class methods simply delegate the call to resource class, e.g.
ClassMethod findPetsByStatus(status As%String) As%DynamicArray
{
return##class(PetOperations).FindByStatus(status)
}Hope this helps
Why is there so many lines in this class ?
From code quality point of view, a class exceeding 500 lines is usually considered a candidate for refactoring.
Is the code generated ?
Following these two insightful days, I wanted to thank you for the incredibly warm welcome and kind feedback.
Here is my Bingo card 😁
🖼 Musée du Quai Branly

Le Jardin d’O outside the Museum
Much common ancestry!
The Diola mask from Senegal with horns, woven leafs canvas and seeds, share similarities with Celt masks of Cernunos made of plants, fruits and deer horns
I'm particularly interested in the musical instrument collection, we did not have the time to visit
« Take Me to the Moon » 🌙
Shoot for the moon, even if you miss, you’ll land among the stars — Oscar Wilde
🎤 Sessions & Ideas
CHU Toulouse FHIR + interop, a very inspiring design. I wonder what is the strategy for change/release management, such as FHIR release migration.
Zweder Bergman talking about how difficult it is to pick a bakery in Paris and comparing this to the many choices facing us with FHIR interoperabiliy
From « Care Coordination: From Patient Journey to Sustainable Population Health » : public health agencies often do not measure the value of prevention and how quality data exchange networks contribute to cost reduction.
Retailer SPAR putting InterSytems technologies to effective and global use on such a large scale is quite surprising to me, I wonder how it initially sparked.
☕️ Informal Moments
Chatting about where we are now, in a world obsessed by AI, with MUMPS developers about my age 😁
Meeting Egveny Shvarov, Guillaume Rongier, Irène Mykhailova and Anastasia Dyubaylo in real life : we are now more than avatars to each other 😊
The vast collection of python libraries is now available to IRIS, and it’s especially useful when dealing with AI. Thanks @Guillaume Rongier
for pointing this out !
During the evening, chatting about using a FHIR repository a central storage for an hospital, and how it empowers the institution vs data being captive in supplier software. Also outlining how it is difficult to convince hospital boards and software suppliers to invest in the technology, the ROI being on the long term.
🤝 Reflection & Connections
.png)
Learning more about EHDS initiative 😎
I offered some cards during informal moments, but cards seems to be somewhat deprecated, and attendees did not have a card to exchange.
It was a pleasure to meet @Lorenzo Scalese in real life, and sharing about developping with ObjectScript.
My rules, motivated by readability :
To get file name platform independently, use %Library.File GetFilename method : https://docs.intersystems.com/irisforhealthlatest/csp/documatic/%25CSP…
By the look of the stack trace, I think the adapter is trying to write request but it fails. Check the SSLConfiguration setting of the operation, it should be the name of an existing, valid, TLS configuration.
It is also possible to specify an ODBC connection string instead of system DSN name :
Driver={ODBC Driver 13 for SQL Server};server=localhost;database=WideWorldImporters;trusted_connection=Yes;
I would check out EnsLib.REST.DynamicObjVDoc, looks like it can parse something like JSONPath.
In interop Ens.MessageHeader, we have TimeCreated and TimeProcessed. I would along the same lines and have TimeCreated and TimeUpdated. I find Time… more appropriate as those are time stamps, not dates.
Use the concatenation operator, _ (underscore) in the value of the assign action. Something like : source.{EVN:5.2}_source.{EVN:5.3}
You can achieve this by querying Ens.MessageHeader (see https://docs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic…).
something along the lines of
select top 1 TimeProcessed From Ens.MessageHeader where TargetConfigName='MyConfigItem'orderby TimeProcessed descAbsolutely. The new nomenclature does not reflect the conceptual differences between services and operations : the origin of the initial event that triggers processing. This could cause confusion for beginners.
I like ‘Services’ and ‘Operations’ better. The words also keep the link with the actual class names and wording in the documentation.
Error message
ERROR #5001: element 'sql' is not allowed for content model '(annotation?,((true,false)|(false,true)|(true)|(false)))'
Indicates the <sql> element in the DTL XData block is in an unexpected location in the xml document, and that expected elements in that location are <annotation>, <true> or <false>. Maybe it is directly under <if> ?
Congratulations everyone, and thank you so much for making such an excellent community of developers possible !💐
You can query the message body class table with a left join to Ens.MessageHeader to get the orphaned message body identifiers :
select
%NOLOCK bod.Id
from
MySample.BodyTable bod
leftjoin Ens.MessageHeader hdr on bod.Id=hdr.MessageBodyId and hdr.MessageBodyClassName='MySample.BodyTable'where
hdr.MessageBodyId isNULLAn approach using an operation that, on message :
If PoolSize = 1, it will process 1 message at a time.
If PoolSize > 1, multiple jobs will process incoming messages from other operations in sequence for each patient
Class dc.interop.PatientMessageOperation Extends Ens.BusinessOperation
{
Parameter SETTINGS = "DeQueueTimeout,MinProcessTime,MaxProcessTime";Property DeQueueTimeout As%Integer [ InitialExpression = 1 ];Property MinProcessTime As%Integer [ InitialExpression = 1 ];Property MaxProcessTime As%Integer [ InitialExpression = 5 ];
Method HandlePatientMessage(request As test.src.ut.ks.lib.interop.PatientMessage, Output response As%Persistent) As%Status
{
#Dim sc as%Status#Dim ex as%Exception.AbstractExceptions sc = $$$OKtry {
$$$TOE(sc,..EnqueueRequest(request))
$$$TOE(sc,..ProcessPatientMessages())
} catch (ex) {
s sc = ex.AsStatus()
}
return sc
}
Method EnqueueRequest(request As PatientMessage) As%Status
{
#Dim sc as%Status#Dim ex as%Exception.AbstractException#Dim queue as Ens.Queue
#Dim queueName as%String#Dimsas EnsLib.File.InboundAdapter
s sc = $$$OKtry {
s queueName = ..%ConfigName_".patient."_request.PatientId
$$$TOE(sc,##class(Ens.Queue).Create(queueName))
s ..%RequestHeader.TargetQueueName = queueName
$$$TOE(sc,##class(Ens.Queue).EnQueue(..%RequestHeader))
s$$$EnsRuntimeAppData(..%ConfigName,"queues",queueName) = 1
} catch (ex) {
s sc = ex.AsStatus()
}
return sc
}
Method ProcessPatientMessages() As%Status
{
#Dim sc as%Status#Dim ex as%Exception.AbstractExceptions sc = $$$OKtry {
s queueName = ""s queueName = $order($$$EnsRuntimeAppData(..%ConfigName,"queues",queueName))
while queueName '= "" {
$$$TOE(sc,..ProcessQueuedMessages(queueName))
s queueName = $order($$$EnsRuntimeAppData(..%ConfigName,"queues",queueName))
}
} catch (ex) {
s sc = ex.AsStatus()
}
return sc
}
Method ProcessQueuedMessages(queueName As%String) As%Status
{
#Dim sc as%Status#Dim ex as%Exception.AbstractException#Dim header as Ens.MessageHeader
#Dim timedOut as%Booleans sc = $$$OKtry {
$$$TOE(sc,##class(Ens.Queue).DeQueue(queueName,.header,..DeQueueTimeout,.timedOut,1))
while 'timedOut && $isobject(header) {
$$$TOE(sc,..ProcessHeader(header))
$$$TOE(sc,##class(Ens.Queue).DeQueue(queueName,.header,..DeQueueTimeout,.timedOut,1))
}
} catch (ex) {
s sc = ex.AsStatus()
}
return sc
}
Method ProcessHeader(header As Ens.MessageHeader) As%Status
{
#Dim sc as%Status#Dim ex as%Exception.AbstractException#Dim msg as PatientMessage
s sc = $$$OKtry {
s msg = $classmethod(header.MessageBodyClassName,"%OpenId",header.MessageBodyId)
$$$TOE(sc,..ProcessMessage(msg))
} catch (ex) {
s sc = ex.AsStatus()
}
return sc
}
Method ProcessMessage(msg As PatientMessage) As%Status
{
#Dim sc as%Status#Dim ex as%Exception.AbstractException#Dim processTime as%Integers sc = $$$OKtry {
$$$TRACE("job "_$job_" is processing message for patient "_msg.PatientId_" seq "_msg.Seq)
s processTime = $random(..MaxProcessTime-..MinProcessTime)+..MinProcessTimehang processTime
$$$TRACE("job "_$job_" processed message for patient "_msg.PatientId_" seq "_msg.Seq_" in "_processTime_" seconds")
} catch (ex) {
s sc = ex.AsStatus()
}
return sc
}
Method OnTearDown() As%Status
{
#Dim sc as%Status#Dim ex as%Exception.AbstractExceptions sc = $$$OKtry {
s queueName = ""s queueName = $order($$$EnsRuntimeAppData(..%ConfigName,"queues",queueName))
while queueName '= "" {
$$$TOE(sc,##class(Ens.Queue).Delete(queueName,"*"))
s queueName = $order($$$EnsRuntimeAppData(..%ConfigName,"queues",queueName))
}
} catch (ex) {
s sc = ex.AsStatus()
}
return sc
}
XData MessageMap
{
<MessageMap>
<MapItem MessageType="ut.ks.lib.interop.PatientMessage">
<Method>HandlePatientMessage</Method>
</MapItem>
</MessageMap>
}
}
The message class
Class ut.ks.lib.interop.PatientMessage Extends Ens.MessageBody
{
Property PatientId As%Integer;Property Seq As%Integer;Property Payload As%String;
Storage Default
{
<Data name="PatientMessageDefaultData">
<Subscript>"PatientMessage"</Subscript>
<Value name="1">
<Value>PatientId</Value>
</Value>
<Value name="2">
<Value>Seq</Value>
</Value>
<Value name="3">
<Value>Payload</Value>
</Value>
</Data>
<DefaultData>PatientMessageDefaultData</DefaultData>
<Type>%Storage.Persistent</Type>
}
}Sample test method
ClassMethod SendMessages(patientCount As%Integer = 100, messageCount = 1000) As%Status
{
#Dim sc as%Status#Dim ex as%Exception.AbstractExceptions sc = $$$OKtry {
for i = 1:1:messageCount {
s msg = ##class(PatientMessage).%New()
s msg.PatientId = $random(patientCount)+1s msg.Seq = $increment(seq(msg.PatientId))
s msg.Payload = "message "_msg.Seq_" for "_msg.PatientId
$$$TOE(sc,##class(EnsLib.Testing.Service).SendTestRequest("PatientOperation",msg))
}
} catch (ex) {
s sc = ex.AsStatus()
}
return sc
}Another approach would be to write a custom router class extending EnsLib.MsgRouter.RoutingEngine, overriding OnRequest() method.
In OnRequest(), before calling ##super(), assign the JSON %DynamicAbstractObject build from the input request stream to a property.
Use the property in the router rules.
You can override the Page() method of %CSP.Page and redirect output to a stream, process the resulting stream and write it to the original device the page is using to send data back to client.
Here is a quick and dirty example, using IO-Redirect package available on OpenExchange.
The redirecting page :
Class test.src.RedirectedPage Extends%CSP.Page
{
ClassMethod Page(skipHeader As%Boolean = 1) As%Status [ ServerOnly = 1 ]
{
#dim sc as%Status#dim ex as%Exception.AbstractException#dim pageStream,processedPageStream As%Stream.Object#dim len as%Integer#dim buffer as%Strings sc = $$$OKtry {
Set pageStream = ##class(%Stream.GlobalCharacter).%New()
Do##class(IORedirect.Redirect).ToStream(pageStream)
$$$TOE(sc,##super(skipHeader))
Do##class(IORedirect.Redirect).RestoreIO()
Set pageStream = ##class(IORedirect.Redirect).Get()
$$$TOE(sc,..ProcessPageStream(pageStream,.processedPageStream))
while 'processedPageStream.AtEnd {
s len = 32768s buffer = processedPageStream.Read(.len,.sc)
$$$TOE(sc,sc)
write buffer
}
} catch (ex) {
s sc = ex.AsStatus()
}
return sc
}
ClassMethod ProcessPageStream(pageStream As%Stream.Object, Output processedPageStream As%Stream.Object) As%Status
{
#dim sc as%Status#dim ex as%Exception.AbstractExceptions sc = $$$OKtry {
s processedPageStream = ##class(%Stream.TmpCharacter).%New()
$$$TOE(sc,processedPageStream.CopyFrom(pageStream))
d processedPageStream.Write("<div><span>original page had "_pageStream.Size_" bytes </span></div>")
} catch (ex) {
s sc = ex.AsStatus()
}
return sc
}
}
The original page :
Class test.src.OriginalPage Extends test.src.RedirectedPage
{
ClassMethod OnPage() As%Status [ ServerOnly = 1 ]
{
&html<
<div><span>Hello, world, again</span></div>
>return$$$OK
}To see the content, the CSP Gateway has an HTTP trace facility, see documentation.
To access the response just before it is sent back to client, override methods of %CSP.Page
You can use the auxiliary property (aux attribute) of the <subtransform> DTL element to pass the index of the group to process to the subtransform, passing the entire source message as source object.
EnsLib.EDI.XML.Document implements Ens.VDoc.Interface, and exposes GetValueAt(propertyPath) method.
This method accepts XPath-like property paths such as "/foo/bar" or "/foo/bar/@attr".
This also means you can use the curly brace syntax in business rules (classes extending Ens.Rule.Definition).
For example, if the context class has a "xmlDocument" property, this expression
xmlDocument.{/foo/bar}
will be compiled into
xmlDocument.GetValueAt("/foo/bar")
For example (in open exchange package ks-iris-lib) :
/// list of data types exposing %GetSerial and %SetSerialClass ks.lib.collections.ListOfDataTypes Extends%Library.ListOfDataTypes
{
/// serialize object
Method %GetSerial(force As%Integer = 0) As%String
{
return##super(force)
}
/// deserialize object
Method %SetSerial(serialized As%String) As%Status
{
return##super(.serialized)
}
Storage Custom
{
<Type>%Library.CompleteCustomStorage</Type>
}
}There is a limit to string length, as explained in the documentation.
To convert a binary stream to a base64 encoded character stream, you have to call the function in a loop using a buffer. Due the way the function works, the buffer length must be a multiple of 57.
Here a sample implementation, from ks.lib.stream.Utils class in the ks-iris-lib package available on Open Exchange.
/// encode stream data into encoded using bufferSize (due the implementation of $system.Encryption.Base64Encode, this must be a multiple of 57)ClassMethod Base64Encode(stream As%Stream.Object, Output encoded As%Stream.Object, bufferSize As%Integer = 5700) As%Status
{
#Dim sc as%Status#Dim ex as%Exception.AbstractException#Dim len As%Integers sc = $$$OKtry {
throw:((bufferSize#57)'=0) ##class(%Exception.General).%New("buffer size must be a multiple of 57")
$$$TOE(sc,stream.Rewind())
s:'$d(encoded) encoded = ##class(%Stream.TmpCharacter).%New()
s len=bufferSize
while 'stream.AtEnd {
$$$TOE(sc,encoded.Write($system.Encryption.Base64Encode(stream.Read(.len),1)))
s len = bufferSize
}
}
catch (ex) {
s sc = ex.AsStatus()
}
return sc
}Using the second approach : edited down to 39 characters
ClassMethodascii()
{
f i=32:1:126w:$t(ascii+1)'[$c(i) *i
}ah, yes, thanks, comments are indeed part of the source code 😁
Yes Enrico, it is my understanding exactly : if your program code (i.e. the lines in the routine generated by the compiler) contains all printable ASCII characters, it should output an empty string. Of course it sounds silly to devise a very clever way to output an empty string, but after all, golf - from a practical point of view - is a silly sport isn't it ? 🤣
Alas, I'm well behind : 54 characters (according to %Dictionary.MethodDefinition Implementation.Size).
Please, enlighten me 😅
ClassMethodascii() [ CodeMode = objectgenerator ]
{
f i=0,83,0,33,29,2,3:1:94,2d%code.Write($c(i+32))
}