📜 ⬆️ ⬇️

XData Studio Assist - autocompletion in XData blocks of InterSystems Caché classes

This article is a translation of my article published on the new InterSystems Developer Community portal. It tells about another possibility in Studio - support for autocompletion when creating XML documents in XData. This article develops the idea raised by Albert Fuentes about the use of XData and code generators to simplify the creation of certain rules. You may have already encountered autocompletion in XData when developing a ZEN application, % Installer- manifest or REST broker. This is called Studio Assist . I will tell you how to configure and use this feature.


XData XML XML Completion


There are several ways to implement autocompletion for XML. But they all come down to one degree or another to using the class % Studio.SASchemaClass . Some schemes are described not through classes but in the form of a single file, examples of these files can be seen in the folder with installed Caché / dev / studio / saschema. For example, here is the location of the routing description scheme file used in % CSP.REST , this class defines the XML schema, but it is used only for parsing UrlMap. The format is quite simple, it describes the xml namespace and the prefix. The following describes the hierarchy of tags, with attributes and their values.
# This file defines the Rest UrlMap studio assist database # Define the prefix mapping !prefix-mapping:urlmap:http://www.intersystems.com/urlmap # Set the default namespace to urlmap !default-namespace:http://www.intersystems.com/urlmap # Set the default prefix for element definitions that follow !default-prefix:urlmap /#Routes Routes/#Map Routes/#Route Map|Prefix Map|Forward Route|Url Route|Method@enum:!,GET,HEAD,POST,PUT,DELETE,TRACE,CONNECT Route|Call Route|Cors@enum:!,true,false 

But in this case it is only suitable as an assistant in the studio, we still need to add XML-based code generation. Classes from the % XGEN package will help us in this. Unfortunately, these classes are marked as not recommended for use, as they may or may not be removed from future versions, and it is recommended to contact InterSystems if you need them. Thus, now, to describe the schema, we need to create a series of classes: for each tag in our XML, we need to create a separate class, another class that will compile all our rules will be the superclass for the new rules. I slightly modified the XML format for the rules from Albert’s article, and as a result we have the Definition root tag, which may contain Rule tags, and those in turn, any number of Action tags. Below is an example of XML that we should get.

 XData XMLData [ XMLNamespace = RuleEngine ] { <Definition Identifier="PatientAlerts"> <Rule Title="Not young anymore!" Condition="context.Patient.DOB > $horolog-30"> <Action Type="call" Class="IAT.RuleEngine.Test.Utils" Method="SendEmail" Args=""test@server.com","Patient is so old!""/> <Action Type="call" Class="IAT.RuleEngine.Test.Utils" Method="ShowObject" Args="context.Patient"/> <Action Type="return"/> </Rule> </Definition> } 

Next, we need to generate code based on the XML that will check the condition in the rule and perform the actions described in this rule.

Thanks to% XGEN, we not only get auto-completion in XData, but also the ability to generate code based on it. Our tag classes get several methods that allow you to generate code for a specific tag. These are % OnGenerateCode , % OnBeforeGenerateCode, and % OnAfterGenerateCode methods .
')
Classes for the Definition root tag:

 Class IAT.RuleEngine.Definition Extends %XGEN.AbstractDocument [ System = 3 ] { Parameter NAMESPACE = "RuleEngine"; Parameter XMLNAMESPACE = "RuleEngine"; Parameter ROOTCLASSES As STRING = "IAT.RuleEngine.Definition:Definition"; Property Identifier As %String(MAXLEN = 200, XMLPROJECTION = "ATTRIBUTE"); Property Rules As list Of Rule(XMLPROJECTION = "ELEMENT"); /// This method is called when a class containing an XGEN /// document is compiled. It is called <em>before</em> the <method>%GenerateCode</method> method /// processes its children.<br> /// <var>pTargetClass</var> is the class that contains the XGEN document.<br/> /// <var>pCode</var> is a stream containing the generated code.<br/> /// <var>pDocument</var> is the top-level XGEN document object that contains this node.<br/> /// A subclass can provide an implementation of this method that will /// generate specific lines of code.<br/> Method %OnBeforeGenerateCode(pTargetClass As %Dictionary.CompiledClass, pCode As %Stream.TmpCharacter, pDocument As %XGEN.AbstractDocument) As %Status { do pCode.WriteLine("#define AddLog(%line) set log($i(log))=""[""_$zdatetime($ztimestamp,3)_""] ""_%line") do pCode.WriteLine(..%Indent(1)_"Set tSC = $$$OK ") do pCode.WriteLine(..%Indent(1)_"try { ") quit $$$OK } /// This method is called when a class containing an XGEN /// document is compiled. It is called <em>after</em> the <method>%GenerateCode</method> method /// processes its children.<br> /// <var>pTargetClass</var> is the class that contains the XGEN document.<br/> /// <var>pCode</var> is a stream containing the generated code.<br/> /// <var>pDocument</var> is the top-level XGEN document object that contains this node.<br/> /// A subclass can provide an implementation of this method that will /// generate specific lines of code.<br/> Method %OnAfterGenerateCode(pTargetClass As %Dictionary.CompiledClass, pCode As %Stream.TmpCharacter, pDocument As %XGEN.AbstractDocument) As %Status { do pCode.WriteLine(..%Indent(1)_"} catch ex { set tSC = ex.AsStatus() }") do pCode.WriteLine(..%Indent(1)_"quit tSC") quit $$$OK } } 

Next, the Rule tag:

 Class IAT.RuleEngine.Rule Extends IAT.RuleEngine.Sequence [ System = 3 ] { Property Title As %String(XMLPROJECTION = "ATTRIBUTE"); Property Condition As %String(XMLPROJECTION = "ATTRIBUTE"); Property Actions As list Of Action(XMLPROJECTION = "ELEMENT"); /// This method is called when a class containing an XGEN /// document is compiled. It is called <em>before</em> the <method>%GenerateCode</method> method /// processes its children.<br> /// <var>pTargetClass</var> is the class that contains the XGEN document.<br/> /// <var>pCode</var> is a stream containing the generated code.<br/> /// <var>pDocument</var> is the top-level XGEN document object that contains this node.<br/> /// A subclass can provide an implementation of this method that will /// generate specific lines of code.<br/> Method %OnBeforeGenerateCode(pTargetClass As %Dictionary.CompiledClass, pCode As %Stream.TmpCharacter, pDocument As %XGEN.AbstractDocument) As %Status { do pCode.WriteLine(..%Indent()_"If ("_..Condition_") { set actionCounter=0 ") do pCode.WriteLine(..%Indent(1)_"$$$AddLog(""Rule: "_..Title_" "")") quit $$$OK } /// This method is called when a class containing an XGEN /// document is compiled. It is called <em>after</em> the <method>%GenerateCode</method> method /// processes its children.<br> /// <var>pTargetClass</var> is the class that contains the XGEN document.<br/> /// <var>pCode</var> is a stream containing the generated code.<br/> /// <var>pDocument</var> is the top-level XGEN document object that contains this node.<br/> /// A subclass can provide an implementation of this method that will /// generate specific lines of code.<br/> Method %OnAfterGenerateCode(pTargetClass As %Dictionary.CompiledClass, pCode As %Stream.TmpCharacter, pDocument As %XGEN.AbstractDocument) As %Status { do pCode.WriteLine(..%Indent()_"}") quit $$$OK } } 

And the last Action tag:

 Class IAT.RuleEngine.Action Extends IAT.RuleEngine.RuleEngineNode [ System = 3 ] { Parameter NAMESPACE = "RuleEngine"; Property Type As %String(VALUELIST = ",call,return", XMLPROJECTION = "ATTRIBUTE"); Property Class As %String(XMLPROJECTION = "ATTRIBUTE"); Property Method As %String(XMLPROJECTION = "ATTRIBUTE"); Property Args As %String(XMLPROJECTION = "ATTRIBUTE"); /// Generate code for this node.<br/> /// This method is called when a class containing an XGEN /// document is compiled.<br/> /// <var>pTargetClass</var> is the class that contains the XGEN document.<br/> /// <var>pCode</var> is a stream containing the generated code.<br/> /// <var>pDocument</var> is the top-level XGEN document object that contains this node.<br/> /// A subclass will provide an implementation of this method that will /// generate specific lines of code.<br/> /// For example: /// <example> /// Do pCode.WriteLine(..%Indent()_"Set " _ ..target _ "=" _ $$$quote(..value)) /// </example> Method %OnGenerateCode(pTargetClass As %Dictionary.CompiledClass, pCode As %Stream.TmpCharacter, pDocument As %XGEN.AbstractDocument) As %Status { do pCode.WriteLine(..%Indent()_"$$$AddLog(""Action: ""_$i(actionCounter))") if ..Type="call" { do pCode.WriteLine(..%Indent() _ "do $classmethod("_$$$quote(..Class)_", "_$$$quote(..Method)_", "_..Args_")") } elseif ..Type="return" { do pCode.WriteLine(..%Indent() _ "quit ") } Quit $$$OK } } 

Now we need a class that will be a template for describing the rules, and which will be able to compile the resulting XML.

 Class IAT.RuleEngine.Engine Extends %RegisteredObject [ System = 3 ] { XData XMLData [ XMLNamespace = RuleEngine ] { <Definition> </Definition> } ///   ClassMethod Evaluate(context, log) [ CodeMode = objectgenerator ] { ///      Quit ##class(IAT.RuleEngine.Definition).%Generate(%compiledclass, %code, "XMLData") } } 

And now we can create our own class with rules:

 Class IAT.RuleEngine.Test.PatientAlertsRule Extends IAT.RuleEngine.Engine { XData XMLData [ XMLNamespace = RuleEngine ] { <Definition Identifier="PatientAlerts"> <Rule Title="Not young anymore!" Condition="context.Patient.DOB > $horolog-30"> <Action Type="call" Class="IAT.RuleEngine.Test.Utils" Method="SendEmail" Args=""test@server.com","Patient is so old!""/> <Action Type="call" Class="IAT.RuleEngine.Test.Utils" Method="ShowObject" Args="context.Patient"/> <Action Type="return"/> </Rule> </Definition> } } 

After compiling which we get the code:

 zEvaluate(context,log) public { // generated by IAT.RuleEngine.Definition set tSC=1 try { If (context.Patient.DOB > $horolog-30) { set actionCounter=0 set log($i(log))="["_$zdatetime($ztimestamp,3)_"] "_"Rule: Not young anymore! " set log($i(log))="["_$zdatetime($ztimestamp,3)_"] "_"Action: "_$i(actionCounter) do $classmethod("IAT.RuleEngine.Test.Utils", "SendEmail", "test@server.com","Patient is so old!") set log($i(log))="["_$zdatetime($ztimestamp,3)_"] "_"Action: "_$i(actionCounter) do $classmethod("IAT.RuleEngine.Test.Utils", "ShowObject", context.Patient) set log($i(log))="["_$zdatetime($ztimestamp,3)_"] "_"Action: "_$i(actionCounter) quit } } catch ex { set tSC = ex.AsStatus() } quit tSC } 

The complete code can be viewed on GitHub .

Separate file


But the possibilities of the Studio do not end there. I have already told in one of the previous articles about the possibility of creating my own file types. In this case, it is possible to create a new type of XML format, which will also support both autocompletion and compilation of XML into some code, along the same lines. With the current example, there is also my post on the Developer Community .

File Description Class Code
 Class IAT.RuleEngine.EngineFile Extends %Studio.AbstractDocument [ System = 4 ] { Projection RegisterExtension As %Projection.StudioDocument(DocumentDescription = "RuleEngine file", DocumentExtension = "RULE", DocumentNew = 0, DocumentType = "xml", XMLNamespace = "RuleEngine"); Parameter NAMESPACE = "RuleEngine"; Parameter EXTENSION = ".rule"; Parameter DOCUMENTCLASS = "IAT.RuleEngine.Engine"; ClassMethod GetClassName(pName As %String) As %String [ CodeMode = expression ] { $P(pName,".",1,$L(pName,".")-1) } /// Load the routine in Name into the stream Code Method Load() As %Status { Set tClassName = ..GetClassName(..Name) Set tXDataDef = ##class(%Dictionary.XDataDefinition).%OpenId(tClassName_"||XMLData") If ($IsObject(tXDataDef)) { do ..CopyFrom(tXDataDef.Data) } Quit $$$OK } /// Compile the routine Method Compile(flags As %String) As %Status { Set tSC = $$$OK If $get($$$qualifierGetValue(flags,"displaylog")){ Write !,"Compiling document: " _ ..Name } Set tSC = $System.OBJ.Compile(..GetClassName(..Name),.flags,,1) Quit tSC } /// Delete the routine 'name' which includes the routine extension ClassMethod Delete(name As %String) As %Status { Set tSC = $$$OK If (..#DOCUMENTCLASS'="") { Set tSC = $System.OBJ.Delete(..GetClassName(name)) } Quit tSC } /// Lock the class definition for the document. Method Lock(flags As %String) As %Status { If ..Locked Set ..Locked=..Locked+1 Quit $$$OK Set tClassname = ..GetClassName(..Name) Lock +^oddDEF(tClassname):0 If '$Test Quit $$$ERROR($$$CanNotLockRoutineInfo,tClassname) Set ..Locked=1 Quit $$$OK } /// Unlock the class definition for the document. Method Unlock(flags As %String) As %Status { If '..Locked Quit $$$OK Set tClassname = ..GetClassName(..Name) If ..Locked>1 Set ..Locked=..Locked-1 Quit $$$OK Lock -^oddDEF(tClassname) Set ..Locked=0 Quit $$$OK } /// Return the timestamp of routine 'name' in %TimeStamp format. This is used to determine if the routine has /// been updated on the server and so needs reloading from Studio. So the format should be $zdatetime($horolog,3), /// or "" if the routine does not exist. ClassMethod TimeStamp(name As %String) As %TimeStamp { If (..#DOCUMENTCLASS'="") { Set cls = ..GetClassName(name) Quit $ZDT($$$defClassKeyGet(cls,$$$cCLASStimechanged),3) } Else { Quit "" } } /// Return 1 if the routine 'name' exists and 0 if it does not. ClassMethod Exists(name As %String) As %Boolean { Set tExists = 0 Try { Set tClass = ..GetClassName(name) Set tExists = ##class(%Dictionary.ClassDefinition).%ExistsId(tClass) } Catch ex { Set tExists = 0 } Quit tExists } /// Save the routine stored in Code Method Save() As %Status { Write !,"Save: ",..Name set tSC = $$$OK try { Set tClassName = ..GetClassName(..Name) Set tClassDef = ##class(%Dictionary.ClassDefinition).%OpenId(tClassName) if '$isObject(tClassDef) { set tClassDef = ##class(%Dictionary.ClassDefinition).%New() Set tClassDef.Name = tClassName Set tClassDef.Super = ..#DOCUMENTCLASS } Set tIndex = tClassDef.XDatas.FindObjectId(tClassName_"||XMLData") If tIndex'="" Do tClassDef.XDatas.RemoveAt(tIndex) Set tXDataDef = ##class(%Dictionary.XDataDefinition).%New() Set tXDataDef.Name = "XMLData" Set tXDataDef.XMLNamespace = ..#NAMESPACE Set tXDataDef.parent = tClassDef do ..Rewind() do tXDataDef.Data.CopyFrom($this) set tSC = tClassDef.%Save() } catch ex { } Quit tSC } Query List(Directory As %String, Flat As %Boolean, System As %Boolean) As %Query(ROWSPEC = "name:%String,modified:%TimeStamp,size:%Integer,directory:%String") [ SqlProc ] { } ClassMethod ListExecute(ByRef qHandle As %Binary, Directory As %String = "", Flat As %Boolean, System As %Boolean) As %Status { Set qHandle = "" If Directory'="" Quit $$$OK // get list of classes Set tRS = ##class(%Library.ResultSet).%New("%Dictionary.ClassDefinition:SubclassOf") Do tRS.Execute(..#DOCUMENTCLASS) While (tRS.Next()) { Set qHandle("Classes",tRS.Data("Name")) = "" } Quit $$$OK } ClassMethod ListFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = ListExecute ] { Set qHandle = $O(qHandle("Classes",qHandle)) If (qHandle '= "") { Set tTime = $ZDT($$$defClassKeyGet(qHandle,$$$cCLASStimechanged),3) Set Row = $LB(qHandle _ ..#EXTENSION,tTime,,"") Set AtEnd = 0 } Else { Set Row = "" Set AtEnd = 1 } Quit $$$OK } /// Return other document types that this is related to. /// Passed a name and you return a comma separated list of the other documents it is related to /// or "" if it is not related to anything<br> /// Subclass should override this behavior for non-class based editors. ClassMethod GetOther(Name As %String) As %String { If (..#DOCUMENTCLASS="") { // no related item Quit "" } Set result = "",tCls=..GetClassName(Name) // This changes with MAK1867 If $$$defClassDefined(tCls),..Exists(Name) { Set:result'="" result=result_"," Set result = result _ tCls _ ".cls" } Quit result } } 

After that, it is possible to select our new file type * .rule, and select the file that is actually selected as a successor to our template class, which compiles our XML.

image

image

If another code is displayed in the XML editing mode, the same class will be displayed. In this way, we were able to edit only one XML, and at the output we get working code ready for the rules.

Atelier


Studio is no longer the only official Caché development environment. Now we have Atelier. How about supporting such features in Atelier? So far there is no such support, just as there is no information about when it will appear and whether it will appear at all in the future. This applies to both autocompletion and native file types. But Atelier was developed on the Eclipse platform, respectively, this feature can be realized not only in InterSystems and added as a plugin.

Source: https://habr.com/ru/post/279579/


All Articles