📜 ⬆️ ⬇️

Abnormal Programming in InterSystems Caché

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

The article was prepared on version 2014.1, but I suppose it should work on earlier versions.
In the SAMPLES area, you can find an example of working with custom file types. The example suggests opening a document like “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 our own. The type of the .JS file in the studio is already taken, and the JavaScript, the support of which we want to implement, is not a cake at all but original JavaScript. Let's call it CacheJavaScript, and the file type will be .CJS. Let's create the class% CJS.StudioRoutines as a successor of the class% Studio.AbstractDocument and, for a start, we will write in it support for a new file type.

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

Now we will implement all the necessary methods to correctly support the new type of source code in the Studio.
The ListExecute and ListFetch methods are used to get a list of available files in the region and to display them in the open file 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 global rCJS, respectively, the ListFetch method bypasses this global, and returns rows that contain: name, date and size of the found file. In order for the results to appear in the dialog, you need to describe the Exists method, which checks whether a file with the specified name exists or not.
/// Return 1 if the routine is '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 the routine 'name' in% TimeStamp format. This is used to determine if the routine has
/// been updated on the server. 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 load 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 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 the most interesting: compilation of our program. We will compile into INT code, 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: variable declaration (var), read (read) and output (println).
/// CompileDocument is when compiled
/// It has already been called the 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 view the generated INT code. To do this, you must implement the GetOther method. It's pretty simple - it should return a list of programs, separated by commas, that were generated for the source code.
/// Return other document types.
/// Passed a comma
/// or "" if it is not related to anything. Note
/// for example if your test.XXX your document creates a test.INT
/// 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 program blocking method 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 program removal method.
/// 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;
/// 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
Method Unlock (flags As% String) As% Status
{
Lock - ^ rCJS (.. Name )
Quit $$$ OK
}

So, we have implemented a class that allows you to work with our type of program. But so far there is no possibility to create such a program in the Studio. Fix it. To do this, the studio has the ability to define templates. At the moment, there are 3 ways to define a template: a simple CSP file of a specific format, a CSP class derived from the% CSP.StudioTemplateSuper class, and finally a ZEN page derived from% ZEN.Template.studioTemplate. In this case, we will use the last option, since he is easier. Templates also come in 3 types: to create new objects, just code templates and add-ons (Add Inns), which do not generate any output.
In our case, we need a template for creating new objects. Create a class% CJS.RoutineWizard: its contents are quite simple, you just need to describe the field for entering the program name, and in the% OnTemplateAction method describe the studio name of the new program and its mandatory content.
Hidden text
  /// 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
 /// in Studio this template is dispayed on.  If none specified then
 /// it displays on the 'Custom' tab.
 Parameter TEMPLATEGROUP As STRING;

 /// This XML block is the 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;
 /// 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 when the template is complete.  Any
 /// output to the Studio device.
 Method% OnTemplateAction () As% Status
 {
	 Set tRoutineName = ..% GetValueByName ("RoutineName")
	
	 Set% session.Data ("Template", "NAME") = tRoutineName _ ". CJS"
	 Write "//" _tRoutineName ,!
	 Quit $$$ OK
 }

 }

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

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

image
Open another source, we will see this 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 another code

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


In this way, you can describe any language (within the limits of the possible, of course) that you like the most and encode the server business logic for the Caché DBMS. It is clear that there will be problems with its illumination, if this language is not supported in the studio. This example shows how to work with programs, but naturally you can create Caché classes in the same way. So the possibilities are almost endless: it remains only to write a lexical parser, a syntax 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 done with any other programs in Caché.

For those who want to "repeat the experience at home," the source code is available here .

Source: https://habr.com/ru/post/222563/


All Articles