Ненормальное программирование в 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 — номер иконки нумеруется с нуля и варианты доступных иконок: d2cd69e893d32d22194b1a2a13e32ab9.pngDocumentType — тип будет использоваться для подсветки кода и ошибок, доступные типы:

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 CodeMethod 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 CodeMethod 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 pointMethod 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 extensionClassMethod 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 $$$OKMethod 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 routineMethod 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 в Студии.55dc66a1eb0140f70a26269322428f13.pngНазовем её 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 _»!»,!

Скриншот с другим кодом 3e4d117d934dd94a5398f612bc999153.png И теперь его можно выполнить в терминале USER>d ^hello Hello World! What is your name? daimor Hello daimor! Таким образом можно описать любой язык (в пределах возможного, конечно), который вам больше всего нравится и кодировать на нем серверную бизнес-логику для СУБД Caché. Понятно, что будут проблемы с его подсветкой, если этот язык не поддерживается в студии. Данный пример показывает работу с программами, но естественно можно создавать и классы Caché таким же образом. Так что возможности почти безграничны: остается только написать лексический парсер, синтаксический парсер и полноценный компилятор и придумать соответствие всем системным функциям Caché и специфическим конструкциям в новом языке. Также такие программы можно экспортировать и импортировать с компиляцией, как это делается с любыми другими программами в Caché.

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

© Habrahabr.ru