Эта статья – перевод моей статьи, опубликованной на новом портале InterSystems Developer Community. В ней рассказывается о ещё одной возможности в Studio — поддержке автодополнения при создании XML документов в XData. Эта статья развивает идею, поднятую Альбертом Фуэнтесом, об использовании XData и кодогенераторов, для упрощенного создания неких правил. Вы уже могли сталкиваться с автодополнением в XData при разработке ZEN приложения, %Installer-манифеста или REST брокера. Называется это Studio Assist. Я расскажу, как можно настроить и использовать такую возможность.
Существует несколько способов реализации автодополнения для XML. Но все они в той или иной мере сводятся к использованию класса %Studio.SASchemaClass. Некоторые схемы описаны не через классы а в виде одного файла, примеры этих файлов можно увидеть в папке с установленным Cache /dev/studio/saschema. Например здесь располагается файл схемы описания роутинга для используемый в %CSP.REST, в этом классе определена схема XML но используется она только для парсинга UrlMap. Формат достаточно простой, в нем описана xml namespace и префикс. Далее описана иерархия тегов, с аттрибутами и их значениями.
Но в данном случае это подойдет только в качестве помощника в студии, нам же еще нужно добавить кодогенерацию на основе XML. Помогут нам в этом классы из пакета %XGEN. К сожалению данные классы помечены как не рекомендуемые к использованию, так как могут быть удалены из будущих версий, а могут и нет, и рекомендуется обратиться в InterSystems если вам они нужны. Таким образом, теперь для описания схемы нам нужно создать ряд классов: под каждый тег в нашем XML, нужно создать по отдельному классу, еще один класс, который будет компилировать все наши правила, будет суперклассом для новых правил. Я немного модифицировал XML формат для правил из статьи Альберта, и в итоге у нас корневой тег Definition, который может содержать теги Rule, а те в свою очередь любое количество тегов Action. Ниже пример XML который у нас должен получится.
Далее нам нужно сгенерировать код на основе такого XML, который будет проверять условие (Condition) в правиле (Rule), и выполнять действия описанные в этом правиле.
Благодаря %XGEN мы не только получаем автодополнение в XData, но и возможность генерировать код на его основе. Наши классы для тегов получают несколько методов, позволяющих сгенерировать код под конкретный тег. Это методы %OnGenerateCode, %OnBeforeGenerateCode и %OnAfterGenerateCode.
Классы для корневого тега Definition:
Следом, тег Rule:
И последний тег Action:
Теперь нам нужен класс, который будет шаблоном для описания правил, и который сможет компилировать полученный XML.
И теперь мы можем создать свой класс с правилами:
После компиляции которого получим код:
Полностью код можно посмотреть на GitHub.
Но на этом возможности Studio не заканчиваются. Я уже рассказывал в одной из предыдущих статей о возможности создавать свои типы файлов. В данном случае есть возможность создать новый тип формата XML, который так же будет поддерживать и автодополнение и компиляция XML в некий код, по той же схеме. С текущим пример так же есть мой пост и на Developer Community.
После этого появляется возможность выбрать наш новый тип файла *.rule, и выбрать файл, который на самом деле отобран как наследник нашего класса шаблона, который компилирует наш XML.
Если в режиме редактирования XML отобразить другой код, то будет отображен все тот же класс. Таким образом мы получили возможность редактировать только один XML, а на выходе получать рабочий готовый к выполнению правил код.
Studio теперь уже не единственная официальная среда для разработки на Cache. Теперь у нас есть и Atelier. Как насчет поддержки таких возможностей в Atelier? Пока такой поддержки нет, так же как и нет информации о том, когда появится и появится ли вообще в будущем. Это касается как автодополнения, так и собственных типов файлов. Но Atelier разработан на Eclipse платформе, соответственно, такая возможность может быть реализована не только в InterSystems и добавлена в виде плагина.
Автодополнение XML в XData
Существует несколько способов реализации автодополнения для XML. Но все они в той или иной мере сводятся к использованию класса %Studio.SASchemaClass. Некоторые схемы описаны не через классы а в виде одного файла, примеры этих файлов можно увидеть в папке с установленным Cache /dev/studio/saschema. Например здесь располагается файл схемы описания роутинга для используемый в %CSP.REST, в этом классе определена схема XML но используется она только для парсинга UrlMap. Формат достаточно простой, в нем описана xml namespace и префикс. Далее описана иерархия тегов, с аттрибутами и их значениями.
# 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
Но в данном случае это подойдет только в качестве помощника в студии, нам же еще нужно добавить кодогенерацию на основе XML. Помогут нам в этом классы из пакета %XGEN. К сожалению данные классы помечены как не рекомендуемые к использованию, так как могут быть удалены из будущих версий, а могут и нет, и рекомендуется обратиться в InterSystems если вам они нужны. Таким образом, теперь для описания схемы нам нужно создать ряд классов: под каждый тег в нашем XML, нужно создать по отдельному классу, еще один класс, который будет компилировать все наши правила, будет суперклассом для новых правил. Я немного модифицировал XML формат для правил из статьи Альберта, и в итоге у нас корневой тег Definition, который может содержать теги Rule, а те в свою очередь любое количество тегов Action. Ниже пример XML который у нас должен получится.
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>
}
Далее нам нужно сгенерировать код на основе такого XML, который будет проверять условие (Condition) в правиле (Rule), и выполнять действия описанные в этом правиле.
Благодаря %XGEN мы не только получаем автодополнение в XData, но и возможность генерировать код на его основе. Наши классы для тегов получают несколько методов, позволяющих сгенерировать код под конкретный тег. Это методы %OnGenerateCode, %OnBeforeGenerateCode и %OnAfterGenerateCode.
Классы для корневого тега Definition:
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
}
}
Следом, тег Rule:
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
}
}
И последний тег Action:
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
}
}
Теперь нам нужен класс, который будет шаблоном для описания правил, и который сможет компилировать полученный 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")
}
}
И теперь мы можем создать свой класс с правилами:
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>
}
}
После компиляции которого получим код:
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 }
Полностью код можно посмотреть на GitHub.
Отдельный файл
Но на этом возможности Studio не заканчиваются. Я уже рассказывал в одной из предыдущих статей о возможности создавать свои типы файлов. В данном случае есть возможность создать новый тип формата XML, который так же будет поддерживать и автодополнение и компиляция XML в некий код, по той же схеме. С текущим пример так же есть мой пост и на Developer Community.
Код класса описания файла
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
}
}
После этого появляется возможность выбрать наш новый тип файла *.rule, и выбрать файл, который на самом деле отобран как наследник нашего класса шаблона, который компилирует наш XML.
Если в режиме редактирования XML отобразить другой код, то будет отображен все тот же класс. Таким образом мы получили возможность редактировать только один XML, а на выходе получать рабочий готовый к выполнению правил код.
Atelier
Studio теперь уже не единственная официальная среда для разработки на Cache. Теперь у нас есть и Atelier. Как насчет поддержки таких возможностей в Atelier? Пока такой поддержки нет, так же как и нет информации о том, когда появится и появится ли вообще в будущем. Это касается как автодополнения, так и собственных типов файлов. Но Atelier разработан на Eclipse платформе, соответственно, такая возможность может быть реализована не только в InterSystems и добавлена в виде плагина.