Ненормальное программирование в 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:
/// 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" ] { } /// 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 + '!');

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

Откроем другой источник, то увидим такой код, уже на 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é.

Для желающих «повторить опыт у себя дома», исходные коды доступны по ссылке.

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

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