Эта статья – перевод моей статьи, опубликованной на новом портале InterSystems Developer Community. В ней рассказывается о ещё одной возможности в Studio — поддержке автодополнения при создании XML документов в XData. Эта статья развивает идею, поднятую Альбертом Фуэнтесом, об использовании XData и кодогенераторов, для упрощенного создания неких правил. Вы уже могли сталкиваться с автодополнением в XData при разработке ZEN приложения, %Installer-манифеста или REST брокера. Называется это Studio Assist. Я расскажу, как можно настроить и использовать такую возможность.
Автодополнение XML в XData
Существует несколько способов реализации автодополнения для XML. Но все они в той или иной мере сводятся к использованию класса %Studio.SASchemaClass. Некоторые схемы описаны не через классы а в виде одного файла, примеры этих файлов можно увидеть в папке с установленным Caché /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 теперь уже не единственная официальная среда для разработки на Caché. Теперь у нас есть и Atelier. Как насчет поддержки таких возможностей в Atelier? Пока такой поддержки нет, так же как и нет информации о том, когда появится и появится ли вообще в будущем. Это касается как автодополнения, так и собственных типов файлов. Но Atelier разработан на Eclipse платформе, соответственно, такая возможность может быть реализована не только в InterSystems и добавлена в виде плагина.
ссылка на оригинал статьи https://habrahabr.ru/post/279579/
Добавить комментарий