XData Studio Asist

от автора

Эта статья – перевод моей статьи, опубликованной на новом портале 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.

image

image


Если в режиме редактирования XML отобразить другой код, то будет отображен все тот же класс. Таким образом мы получили возможность редактировать только один XML, а на выходе получать рабочий готовый к выполнению правил код.

Atelier

Studio теперь уже не единственная официальная среда для разработки на Caché. Теперь у нас есть и Atelier. Как насчет поддержки таких возможностей в Atelier? Пока такой поддержки нет, так же как и нет информации о том, когда появится и появится ли вообще в будущем. Это касается как автодополнения, так и собственных типов файлов. Но Atelier разработан на Eclipse платформе, соответственно, такая возможность может быть реализована не только в InterSystems и добавлена в виде плагина.

ссылка на оригинал статьи https://habrahabr.ru/post/279579/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *