Эта статья – перевод моей статьи, опубликованной на новом портале InterSystems Developer Community. В ней рассказывается о ещё одной возможности в Studio — поддержке автодополнения при создании XML документов в XData. Эта статья развивает идею, поднятую Альбертом Фуэнтесом, об использовании XData и кодогенераторов, для упрощенного создания неких правил. Вы уже могли сталкиваться с автодополнением в XData при разработке ZEN приложения, %Installer-манифеста или REST брокера. Называется это Studio Assist. Я расскажу, как можно настроить и использовать такую возможность.


Автодополнение 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.

image

image

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

Atelier


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

Комментарии (3)