XData Studio Assist — автодополнение в XData блоках классов InterSystems Caché

    Эта статья – перевод моей статьи, опубликованной на новом портале 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 и добавлена в виде плагина.
    InterSystems
    76,00
    Вендор: СУБД Caché, OLAP DeepSee, шина Ensemble
    Поддержать автора
    Поделиться публикацией

    Похожие публикации

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

      0
      Крутая, конечно, вещь. Но сколько людей делает свои XData в классах? Примеры есть?
        +1
        Надеюсь после моей статьи, таких людей станет больше.
          0
          Это да! А все же — может есть пример?

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое