Abnormal programming at InterSystems Caché

    Perhaps not everyone who is familiar with InterSystems Caché knows about Studio extensions to working with source code. In fact, you can create your own type of source code in it, compile it into interpreted (INT) and object code, and even in some cases provide code completion. Those. theoretically, you can implement support in the Studio of any programming language that will be executed by the DBMS no worse than Caché ObjectScript. In this article I will describe a simple example of how to implement the ability to write programs in some kind of JavaScript in Caché Studio. If interested, welcome to cat.

    The article was prepared on version 2014.1, but I think this should work on earlier versions.
    In the SAMPLES area, you can find an example of working with custom file types. In the example, it is proposed to open a document of the type “Example User Document (.tst)”, and there is only one TestRoutine.TST file, which is actually generated on the fly. The class that allows working with this type of file is Studio.ExampleDocument. We will not dwell on this example in detail, but create your own. The .JS file type in the studio is already taken, and the JavaScript we want to implement support is not a cake at all, not entirely original JavaScript. Let's call it CacheJavaScript, and the file type will be .CJS. Let's create the% CJS.StudioRoutines class as an inheritor of the% Studio.AbstractDocument class and, for starters, we will write support for the new file type in it.

    /// 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 - displayed as a description for the type in the file open window in the filter list;
    DocumentExtension - file extension that will be processed by this class;
    DocumentIcon - the number of the icon is numbered from scratch and options for available icons:
    DocumentType - the type will be used to highlight the code and errors, the available types are:
    • 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

    Now we implement all the necessary methods for the correct support of a new type of source code in the Studio.
    ListExecute and ListFetch methods are used to get a list of files available in the area and to display them in the file open dialog.
    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
    }

    We will store the description of the programs in the rCJS global; accordingly, the ListFetch method bypasses this global and returns lines that contain: name, date and size of the found file. In order for the results to appear in the dialog, you must describe the Exists method, which checks whether or not a file with the specified name 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"))
    }

    The TimeStamp method should return the date and time of the program; the result is also displayed in the file open dialog.
    /// 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
    }

    Now you need to download the program and save the changes to the file. The text of the program is stored line by line, all in the same global ^ 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
    }

    Now for the fun part: compiling our program. We will compile the code in INT, and in this way we will get full compatibility with Caché. This article is just an example, so for compilation I implemented quite a few features of the CachéJavaScript language: declaration of variables (var), reading (read) and data output (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
    }

    For each compiled program or class, it is possible to look at the generated INT code. To do this, you must implement the GetOther method. It is quite simple - it should return a list of programs, separated by commas, that were generated for the source code.
    /// 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 ""
    }


    We implement the method of blocking the program so that at one time only one developer can edit the program or class on the server.
    And also do not forget to implement also the method of uninstalling the program.
    /// 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
    }

    So, we have implemented a class that allows us to work with our type of programs. But while there is no way to create such a program in the Studio. Fix it. To do this, the studio has the ability to define patterns. Currently, there are 3 ways to define a template: a simple CSP file of a certain format, a CSP class inherited from the% CSP.StudioTemplateSuper class, and finally a ZEN page inherited from% ZEN.Template.studioTemplate. In this case, we will use the latter option, because it is simpler. There are 3 types of templates: for creating new objects, just code templates and Add Inns, which do not generate any output.
    In our case, you need a template to create new objects. Let's create the% CJS.RoutineWizard class: its contents are quite simple, you just need to describe the field for entering the program name, and in the% OnTemplateAction method describe the name of the new program and its required contents for the studio.
    Hidden text
    /// 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 } }

    All. Now you can create your first Caché JavaScript program in Studio.

    Let's call it hello. And the source code for CachéJavaScript for example is this:
    // hello
    console.log('Hello World!');
    var name='';
    read('What is your name? ', name);
    println('Hello ' + name + '!');

    image
    If we open another source, then we will see such code, already on 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 _ “!” ,!
    Screenshot with a different code

    And now it can be executed in the terminal
    USER> d ^ hello
    Hello World!
    What is your name? daimor
    Hello daimor!


    In this way, you can describe any language (to the extent possible, of course) that you like best and encode the server business logic for the Caché DBMS on it. It is clear that there will be problems with its backlighting if this language is not supported in the studio. This example shows how to work with programs, but of course you can create Caché classes in the same way. So the possibilities are almost endless: it remains only to write a lexical parser, a syntactic parser and a full-fledged compiler and come up with a correspondence to all Caché system functions and specific constructions in the new language. Also, such programs can be exported and imported with compilation, as is the case with any other programs in Caché.

    For those who wish to “repeat the experience at home”, the source codes are available here .

    Also popular now: