Ненормальное программирование в InterSystems Caché

    Возможно не все, кто знаком с InterSystems Caché, знают о расширениях Студии по работе с исходным кодом. На самом деле в ней можно создать свой тип исходного кода, компилировать его в интерпретируемый (INT) и объектный код, и даже в некоторых случаях обеспечить и code completion. Т.е. теоретически можно реализовать поддержку в Студии любого языка программирования, который будет исполняться СУБД не хуже Caché ObjectScript. В этой статье я опишу простой пример, как реализовать возможность писать программы на некотором подобии JavaScript в Caché Студии. Если интересно, добро пожаловать под кат.

    Статья готовилась на версии 2014.1, но полагаю это должно работать и на более ранних версиях.
    В области SAMPLES, вы можете найти пример работы с пользовательскими типами файлов. В примере предлагается открыть документ типа «Example User Document (.tst)», и есть только один файл TestRoutine.TST, который на самом деле генерируется на лету. Класс, позволяющий работу с таким типом файлов — Studio.ExampleDocument. Не будем подробно останавливаться на этом примере, а создадим свой. Тип файла .JS в студии уже занят, да и JavaScript, поддержку которого мы хотим реализовать, совсем не торт не совсем оригинальный JavaScript. Назовем его CacheJavaScript, а тип файла будет .CJS. Создадим класс %CJS.StudioRoutines как наследник класса %Studio.AbstractDocument и, для начала, пропишем в нем поддержку нового типа файла.

    /// The extension name, this can be a comma separated list of extensions if this class supports more than one
    Projection RegisterExtension As %Projection.StudioDocument(DocumentDescription = "CachéJavaScript Routine", DocumentExtension = "cjs", DocumentIcon = 1, DocumentType = "JS");
    


    DocumentDescription — отображается в качестве описания для типа, в окне открытия файлов в списке фильтров;
    DocumentExtension — расширение файлов, которые будут обрабатываться данным классом;
    DocumentIcon — номер иконки нумеруется с нуля и варианты доступных иконок:
    DocumentType — тип будет использоваться для подсветки кода и ошибок, доступные типы:
    • INT — Cache Object Script INT code
    • MAC — Cache Object Script MAC code
    • INC — Cache Object Script macro include
    • CSP — Cache Server Page
    • CSR — Cache Server Rule
    • JS — JavaScript code
    • CSS — HTML Style Sheet
    • XML — XML document
    • XSL — XML transform
    • XSD — XML schema
    • MVB — Multivalue Basic mvb code
    • MVI — Multivalue Basic mvi code

    Теперь реализуем все необходимые методы для корректной поддержки нового типа исходного кода в Студии.
    ListExecute и ListFetch методы используются для того, чтобы получить список доступных в области файлов и для отображения их в диалоге открытия файла.
    ClassMethod ListExecute(ByRef qHandle As %Binary, Directory As %String, Flat As %Boolean, System As %Boolean) As %Status
    {
        Set qHandle=$listbuild(Directory,Flat,System,"")
        
    Quit $$$OK

    }

    ClassMethod ListFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = ListExecute ]
    {
        Set Row="",AtEnd=0
        
    If qHandle="" Set AtEnd=1 Quit $$$OK
        If $list
    (qHandle)'=""||($list(qHandle,4)=1) Set AtEnd=1 Quit $$$OK
        set
    AtEnd=1
        
    Set rtnName=$listget(qHandle,5)
        
    For {
            
    Set rtnName=$order(^rCJS(rtnName))    Quit:rtnName=""
            
    continue:$get(^rCJS(rtnName,«LANG»))'=«CJS»
            
    set timeStamp=$zdatetime($get(^rCJS(rtnName,0)),3)
            
    set size=+$get(^rCJS(rtnName,0,«SIZE»))
            
    Set Row=$listbuild(rtnName_".cjs",timeStamp,size,"")
            
    set AtEnd=0
            
    set $list(qHandle,5)=rtnName
            
    Quit
        
    }
        
    Quit $$$OK

    }

    Хранить описание программ будем в глобале rCJS, соответственно метод ListFetch обходит этот глобал, и возвращает строки, которые содержат: имя, дату и размер найденного файла. Для того чтобы результаты отобразились в диалоге, необходимо описать метод Exists, который проверяет, существует или нет файл с указанным именем.
    /// Return 1 if the routine 'name' exists and 0 if it does not.
    ClassMethod Exists(name As %String) As %Boolean
    {
        Set rtnName = $piece(name,".",1,$length(name,".")-1)
        
    Set rtnNameExt = $piece(name,".",$length(name,"."))
        
    Quit $data(^rCJS(rtnName))&&($get(^rCJS(rtnName,«LANG»))=$zconvert(rtnNameExt,«U»))

    }

    Метод TimeStamp должен возвращать дату и время программы, результат также отображается в диалоге открытия файлов.
    /// 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
    {
        Set rtnName = $piece(name,".",1,$length(name,".")-1)
        
    Set timeStamp=$zdatetime($get(^rCJS(rtnName,0)),3)
        
    Quit timeStamp

    }

    Теперь нужно загрузить программу и сохранить изменения в файле. Текст программы хранится построчно все в том же глобале ^rCJS.
    /// Load the routine in Name into the stream Code
    Method Load() As %Status
    {
        set source=..Code
        do
    source.Clear()
        
    set pCodeGN=$name(^rCJS(..ShortName,0))
        
    for pLine=1:1:$get(@pCodeGN@(0),0) {
            
    do source.WriteLine(@pCodeGN@(pLine))
        
    }
        
    do source.Rewind()
        
    Quit $$$OK

    }

    /// Save the routine stored in Code
    Method Save() As %Status
    {
        set pCodeGN=$name(^rCJS(..ShortName,0))
        
    kill @pCodeGN
        
    set @pCodeGN=$ztimestamp
        Set
    ..Code.LineTerminator=$char(13,10)
        
    set source=..Code
        do
    source.Rewind()
        
    WHILE '(source.AtEnd) {
            
    set pCodeLine=source.ReadLine()
            
    set @pCodeGN@($increment(@pCodeGN@(0)))=pCodeLine
        
    }
        
    set @pCodeGN@(«SIZE»)=..Code.Size
        Quit $$$OK

    }

    Теперь самое интересное: компиляция нашей программы. Компилировать будем в INT код, и таким образом получим полную совместимость с Caché. Эта статья только пример, поэтому для компиляции я реализовал совсем немного возможностей языка CachéJavaScript: объявление переменных (var), чтение (read) и вывод данных (println).
    /// CompileDocument is called when the document is to be compiled
    /// It has already called the source control hooks at this point
    Method CompileDocument(ByRef qstruct As %String) As %Status
    {
        Write !,«Compile: „,..Name
        Set
    compiledCode=##class(%Routine).%OpenId(..ShortName_“.INT»)
        
    Set compiledCode.Generated=1
        
    do compiledCode.Clear()
        
        
    do compiledCode.WriteLine(" ;generated at "_$zdatetime($ztimestamp,3))
        
    do ..GenerateIntCode(compiledCode)
        
        
    do compiledCode.%Save()
        
    do compiledCode.Compile()
        
    Quit $$$OK

    }

    Method GenerateIntCode(aCode) [ Internal ]
    {
        set varMatcher=##class(%Regex.Matcher).%New("[ \t]*(var[ \t]+)?(\w[\w\d]*)[ \t]*(\=[ \t]*(.*))?")
        
    set printlnMatcher=##class(%Regex.Matcher).%New("[ \t]*(?:console\.log|println)\(([^\)]+)\)?")
        
    set readMatcher=##class(%Regex.Matcher).%New("[ \t]*read\((.*)\,(.*)\)")
        
        
    set source=..Code
        do
    source.Rewind()
        
    while 'source.AtEnd {
            
    set tLine=source.ReadLine()
            
            
    set pos=1
            
    while $locate(tLine,"(([^\'\""\;\r\n]|[\'\""][^\'\""]*[\'\""])+)",pos,pos,tCode) {
                
    set tPos=1
                
    if $zstrip(tCode,"*W")="" {
                    
    do aCode.WriteLine(tCode)
                    
    continue
                
    }
                
    if varMatcher.Match(tCode) {
                    
    set varName=varMatcher.Group(2)
                    
    if varMatcher.Group(1)'="" {
                        
    do aCode.WriteLine($char(9)_«new „_varName)
                    
    }
                    
    if varMatcher.Group(3)'=“» {
                        
    set expr=varMatcher.Group(4)
                        
    set expr=..Expression(expr)
                        
    do:expr'="" aCode.WriteLine($char(9)_«set „_varName_“ = „_expr)
                    
    }
                    
    continue
                
                
    } elseif printlnMatcher.Match(tCode) {
                    
    set expr=printlnMatcher.Group(1)
                    
    set expr=..Expression(expr)
                    
    do:expr'=“» aCode.WriteLine($char(9)_«Write „_expr_“,!»)
                
                
    } elseif readMatcher.Match(tCode) {
                    
    set expr=readMatcher.Group(1)
                    
    set expr=..Expression(expr)
                    
    set var=readMatcher.Group(2)
                    
    do:expr'="" aCode.WriteLine($char(9)_«read „_expr_“,»_var_",!")
                
    }
            }
        }

    }

    ClassMethod Expression(tExpr) As %String
    {
        set matchers($increment(matchers),«matcher»)="(?sm)([^\'\""]*)\+[ \t]*(?:\""([^\""]*)\""|\'([^\']*)\')([^\'\""]*)"
        
    set matchers(matchers,«replacement»)="$1_""$2$3""$4"

        
    set matchers($increment(matchers),«matcher»)="(?sm)([^\'\""]*)(?:\""([^\""]*)\""|\'([^\']*)\')[ \t]*\+([^\'\""]*)"
        
    set matchers(matchers,«replacement»)="$1""$2$3""_$4"

        
    set matchers($increment(matchers),«matcher»)="(?sm)([^\'\""]*)(?:\""([^\""]*)\""|\'([^\']*)\')([^\'\""]*)"
        
    set matchers(matchers,«replacement»)="$1""$2$3""$4"

        
    set tResult=tExpr
        
    for i=1:1:matchers {
            
    set matcher=##class(%Regex.Matcher).%New(matchers(i,«matcher»))
            
    set replacement=$get(matchers(i,«replacement»))
            
            
    set matcher.Text=tResult
            
            
    set tResult=matcher.ReplaceAll(replacement)
        
    }
        
        
    quit tResult

    }

    Для каждой скомпилированной программы или класса есть возможность посмотреть сгенерированный INT код. Для этого нужно реализовать метод GetOther. Он довольно простой — должен вернуть список программ через запятую, которые были сгенерированы для исходного кода.
    /// 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. Note that this can be passed a document of another type
    /// for example if your 'test.XXX' document creates a 'test.INT' routine then it will also be called
    /// with 'test.INT' so you can return 'test.XXX' to complete the cycle.
    ClassMethod GetOther(Name As %String) As %String
    {
        Set rtnName = $piece(Name,".",1,$length(Name,".")-1)_".INT"
        
    Quit:##class(%Routine).%ExistsId(rtnName) rtnName
        
    Quit ""

    }


    Реализуем метод блокировки программы, чтобы в один момент времени только один разработчик мог редактировать программу или класс на сервере.
    А также не забыть реализовать также метод удаления программы.
    /// Delete the routine 'name' which includes the routine extension
    ClassMethod Delete(name As %String) As %Status
    {
        Set rtnName = $piece(name,".",1,$length(name,".")-1)
        
    Kill ^rCJS(rtnName)
        
    Quit $$$OK

    }

    /// Lock the current routine, default method just unlocks the ^rCJS global with the name of the routine.
    /// If it fails then return a status code of the error, otherise return $$$OK
    Method Lock(flags As %String) As %Status
    {
        Lock +^rCJS(..Name):0 Else Quit $$$ERROR($$$CanNotLockRoutine,..Name)
        
    Quit $$$OK

    }

    /// Unlock the current routine, default method just unlocks the ^rCJS global with the name of the routine
    Method Unlock(flags As %String) As %Status
    {
        Lock -^rCJS(..Name)
        
    Quit $$$OK

    }

    Итак, мы реализовали класс, позволяющий работать с нашим типом программ. Но пока нет возможнсти создать такую программу в Студии. Исправим это. Для этого в студии есть возможность определять шаблоны. На данный момент существуют 3 способа определить шаблон: простой CSP файл определенного формата, CSP-класс наследник от класса %CSP.StudioTemplateSuper, и наконец ZEN страница наследник от %ZEN.Template.studioTemplate. В данном случае будем использовать последний вариант, т.к. он проще. Шаблоны также бывают 3-х типов: для создания новых объектов, просто шаблоны кода и дополнения (Add Inns), которые не генерируют никакого вывода.
    В нашем случае потребуется шаблон для создания новых объектов. Создадим класс %CJS.RoutineWizard: содержимое его довольно простое, нужно просто описать поле для ввода имени программы, и в методе %OnTemplateAction описать для студии имя новой программы и её обязательное содержимое.
    Скрытый текст
    /// Studio Template:<br>
    /// Create a new Cache JavaScript Routine.
    Class %CJS.RoutineWizard Extends %ZEN.Template.studioTemplate [ StorageStrategy = "" ]
    {
    
    Parameter TEMPLATENAME = "Cache JavaScript";
    
    Parameter TEMPLATETITLE = "Cache JavaScript";
    
    Parameter TEMPLATEDESCRIPTION = "Create a new Cache JavaScript routine.";
    
    Parameter TEMPLATETYPE = "CJS";
    
    /// What type of template.
    Parameter TEMPLATEMODE = "new";
    
    /// If this is a TEMPLATEMODE="new" then this is the name of the tab
    /// in Studio this template is dispayed on. If none specified then
    /// it displays on 'Custom' tab.
    Parameter TEMPLATEGROUP As STRING;
    
    /// This XML block defines the contents of the body pane of this Studio Template.
    XData templateBody [ XMLNamespace = "http://www.intersystems.com/zen" ]
    {
    <pane id="body">
    <vgroup labelPosition="left" cellStyle="padding: 2px; padding-left: 5px; padding-right: 5px;">
    <html id="desc" OnDrawContent="%GetDescHTML"/>
    <text label="Routine Name:" 
    	id="ctrlRoutineName"
    	name="RoutineName"
    	size="40"
    	required="true"
    	labelClass="zenRequired"
    	title="Cache JavaScript Routine name" 
    	onchange="zenPage.updateState();"
    />
    </vgroup>
    </pane>
    }
    
    /// Provide contents of description component.
    Method %GetDescHTML(pSeed As %String) As %Status
    {
    	Quit $$$OK
    }
    
    /// This is called when the template is first displayed;
    /// This provides a chance to set focus etc.
    ClientMethod onstartHandler() [ Language = javascript ]
    {
    	// give focus to name
    	var ctrl = zenPage.getComponentById('ctrlRoutineName');
    	if (ctrl) {
    		ctrl.focus();
    		ctrl.select();
    	}
    }
    
    /// Validation handler for form built-into template.
    ClientMethod formValidationHandler() [ Language = javascript ]
    {
    	var rtnName = zenPage.getComponentById('ctrlRoutineName').getValue();
    
    	if ('' == rtnName) {
    		return false;
    	}
    	
    	return true;
    }
    
    /// This method is called when the template is complete. Any
    /// output to the principal device is returned to the Studio.
    Method %OnTemplateAction() As %Status
    {
    	Set tRoutineName = ..%GetValueByName("RoutineName")
    	
    	Set %session.Data("Template","NAME") = tRoutineName_".CJS"
    	Write "// "_tRoutineName,!
    	Quit $$$OK
    }
    
    }
    

    Все. Теперь можно создать свою первую программу на Caché JavaScript в Студии.

    Назовем её hello. А исходный код на CachéJavaScript например такой:
    // hello
    console.log('Hello World!');
    
    var name='';
    read('What is your name? ', name);
    println('Hello ' + name + '!');

    image
    Откроем другой источник, то увидим такой код, уже на COS.
    ;generated at 2014-05-18 20:06:36
        
    Write «Hello World!»,!
        
    new name
        
    set name = ""
        
    read «What is your name? „, name,!
        
    Write “Hello „_ name _“!»,!
    Скриншот с другим кодом

    И теперь его можно выполнить в терминале
    USER>d ^hello
    Hello World!
    What is your name? daimor
    Hello daimor!


    Таким образом можно описать любой язык (в пределах возможного, конечно), который вам больше всего нравится и кодировать на нем серверную бизнес-логику для СУБД Caché. Понятно, что будут проблемы с его подсветкой, если этот язык не поддерживается в студии. Данный пример показывает работу с программами, но естественно можно создавать и классы Caché таким же образом. Так что возможности почти безграничны: остается только написать лексический парсер, синтаксический парсер и полноценный компилятор и придумать соответствие всем системным функциям Caché и специфическим конструкциям в новом языке. Также такие программы можно экспортировать и импортировать с компиляцией, как это делается с любыми другими программами в Caché.

    Для желающих «повторить опыт у себя дома», исходные коды доступны по ссылке.
    InterSystems
    97.58
    Вендор: СУБД Caché, OLAP DeepSee, шина Ensemble
    Support the author
    Share post

    Comments 4

      +2
      Дмитрий, Вы суперкруты! Очень интересная вещь.
      А можно ли расширять уже стандартный COS новыми элементами, делать вставки на другим языке наподобие &html<> или
      MyMethod() [ Language = basic ]
      Если нет, что этому мешает?
        +1
        Нет, о таких возможностях пока не знаю, но думаю что такое невозможно.
        Знаю только о возможности расширения специальных глобалов, на вроде ^$GLOBAL, ^$JOB, ^$LOCK и ^$ROUTINE
        и можно сделать свой по типа ^$ZTEST, и по нему можно будет обходить через $order, так же возможны $get, $data
          +2
          но никто не мешает, добавить свой тип программ, который будет на COS, но с некими вашими улучшениями, тогда его по большей части нужно будет весь транслировать как есть можно и в MAC код, который потом скомпилируется в INT, и можно будет посмотреть все варианты кода.
          +2
          Дмитрий, отличная статья, спасибо.
          К тому же, налицо наглядный пример работы с регулярными выражениями в Caché.

          Only users with full accounts can post comments. Log in, please.