Custom Studio file

Developing an idea with RuleEngine in XData, we could even refuse from editing full class and edit only valuable xml. Sometime ago I have already written an article(in russian) about such possibility, even more, in that article I wrote about compilable JavaScript to Caché. 

Opening File

This possible with %Studio.AbstractDocument. While extending this class, developer should add a Projection.

Projection RegisterExtension As %Projection.StudioDocument(
                                DocumentDescription = "RuleEngine file", 
                                DocumentExtension = "RULE", 
                                DocumentNew = 0, 
                                DocumentType = "xml", 
                                XMLNamespace = "RuleEngine");

Where, you could define some properties, and DocumentExtension is required:

  • DocumentDescription - Description of this document type;
  • DocumentExtension - Name of the extension for this routine type, e.g. 'ABC';
  • DocumentIcon - Integer to modify the icon that Studio uses to display documents of this type;
  • DocumentNew - Set to new to make Studio automatically adding a 'new XYZ' item for this document type;
  • DocumentType - Type of the document, this varies how Studio views this document. Possible values include:
    • 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
  • XMLNamespace - Schema associated with this document type, used by Studio Assist, while DocumentType is XML;

As an example I choose, RULE as an extension for our file. Name for our file will be the same as a classname, but with different extension. DocumentType - XML. DocumentNew - 0, we will implement creating new file in other way, with a wizard.

Then implement a List query, which returns a list of files processed with this class. You could use any available way to store data in Caché. In our case we just produces  a list of classes that are subclasses of our RuleEngine class - IAT.RuleEngine.Engine. As well we will implement some more methods:

  • Load - Load the routine in Name into the stream Code. Here we will load an XData from our class;
  • Save - Save the routine stored in Code. We will save our XML to XData block in our class;
  • Compile - Compile the routine. Just compile connected class;
  • Delete - Delete the routine 'name' which includes the routine extension. Delete connected class;
  • Exists - Return 1 if the routine 'name' exists and 0 if it does not. Check if connected class exists;
  • Lock/Unlock - Lock/Unlock current routine, default method just locks the ^ROUTINE global with the name of the routine. We will lock ^oddDEF with connected classname, to prevent simultaneous editing our rule file and class at the same time;
  • GetOther - Return other document types that this is related to (Ctrl+Shift+V). We just return our connected classname;

And full source for our class, will looks like

Class IAT.RuleEngine.EngineFile Extends %Studio.AbstractDocument [ System = 4 ]
{

Projection RegisterExtension As %Projection.StudioDocument(DocumentDescription = "RuleEngine file", DocumentExtension = "RULE", DocumentNew = 0, DocumentType = "xml", XMLNamespace = "RuleEngine");

Parameter NAMESPACE = "RuleEngine";

Parameter EXTENSION = ".rule";

Parameter DOCUMENTCLASS = "IAT.RuleEngine.Engine";

ClassMethod GetClassName(pName As %String) As %String [ CodeMode = expression ]
{
$P(pName,".",1,$L(pName,".")-1)
}

/// Load the routine in Name into the stream Code
Method Load() As %Status
{
    Set tClassName = ..GetClassName(..Name)
    
    Set tXDataDef = ##class(%Dictionary.XDataDefinition).%OpenId(tClassName_"||XMLData")
    If ($IsObject(tXDataDef)) {
        do ..CopyFrom(tXDataDef.Data)
    }
    
    Quit $$$OK
}

/// Compile the routine
Method Compile(flags As %String) As %Status
{
    Set tSC = $$$OK

    If $get($$$qualifierGetValue(flags,"displaylog")){
        Write !,"Compiling document: " _ ..Name
    }
    Set tSC = $System.OBJ.Compile(..GetClassName(..Name),.flags,,1)
    
    Quit tSC
}

/// Delete the routine 'name' which includes the routine extension
ClassMethod Delete(name As %String) As %Status
{
    Set tSC = $$$OK
    If (..#DOCUMENTCLASS'="") {
        Set tSC = $System.OBJ.Delete(..GetClassName(name))
    }
    Quit tSC
}

/// Lock the class definition for the document.
Method Lock(flags As %String) As %Status
{
    If ..Locked Set ..Locked=..Locked+1 Quit $$$OK
    Set tClassname = ..GetClassName(..Name)
    Lock +^oddDEF(tClassname):0
    If '$Test Quit $$$ERROR($$$CanNotLockRoutineInfo,tClassname)
    Set ..Locked=1
    Quit $$$OK
}

/// Unlock the class definition for the document.
Method Unlock(flags As %String) As %Status
{
    If '..Locked Quit $$$OK
    Set tClassname = ..GetClassName(..Name)
    If ..Locked>1 Set ..Locked=..Locked-1 Quit $$$OK
    Lock -^oddDEF(tClassname)
    Set ..Locked=0
    Quit $$$OK
}

/// 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
{
    If (..#DOCUMENTCLASS'="") {
        Set cls = ..GetClassName(name)
        Quit $ZDT($$$defClassKeyGet(cls,$$$cCLASStimechanged),3)
    }
    Else {
        Quit ""
    }
}

/// Return 1 if the routine 'name' exists and 0 if it does not.
ClassMethod Exists(name As %String) As %Boolean
{
    Set tExists = 0
    Try {
        Set tClass = ..GetClassName(name)
        Set tExists = ##class(%Dictionary.ClassDefinition).%ExistsId(tClass)
    }
    Catch ex {
        Set tExists = 0
    }
    
    Quit tExists
}

/// Save the routine stored in Code
Method Save() As %Status
{
    Write !,"Save: ",..Name
    set tSC = $$$OK
    try {
        Set tClassName = ..GetClassName(..Name)
        
        Set tClassDef = ##class(%Dictionary.ClassDefinition).%OpenId(tClassName)
        if '$isObject(tClassDef) {
            set tClassDef = ##class(%Dictionary.ClassDefinition).%New()
            Set tClassDef.Name = tClassName
            Set tClassDef.Super = ..#DOCUMENTCLASS
        }
        
        Set tIndex = tClassDef.XDatas.FindObjectId(tClassName_"||XMLData")
        If tIndex'="" Do tClassDef.XDatas.RemoveAt(tIndex)
        
        Set tXDataDef = ##class(%Dictionary.XDataDefinition).%New()
        Set tXDataDef.Name = "XMLData"
        Set tXDataDef.XMLNamespace = ..#NAMESPACE
        Set tXDataDef.parent = tClassDef
        do ..Rewind()
        do tXDataDef.Data.CopyFrom($this)
        
        set tSC = tClassDef.%Save()
    catch ex {
    }
    Quit tSC
}

Query List(Directory As %String, Flat As %Boolean, System As %Boolean) As %Query(ROWSPEC = "name:%String,modified:%TimeStamp,size:%Integer,directory:%String") [ SqlProc ]
{
}

ClassMethod ListExecute(ByRef qHandle As %Binary, Directory As %String = "", Flat As %Boolean, System As %Boolean) As %Status
{
    Set qHandle = ""
    If Directory'="" Quit $$$OK
    
    // get list of classes
    Set tRS = ##class(%Library.ResultSet).%New("%Dictionary.ClassDefinition:SubclassOf")

    Do tRS.Execute(..#DOCUMENTCLASS)
    While (tRS.Next()) {
        Set qHandle("Classes",tRS.Data("Name")) = ""
    }
    
    Quit $$$OK
}

ClassMethod ListFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = ListExecute ]
{
    Set qHandle = $O(qHandle("Classes",qHandle))
    If (qHandle '= "") {
        
        Set tTime = $ZDT($$$defClassKeyGet(qHandle,$$$cCLASStimechanged),3)
        Set Row = $LB(qHandle _ ..#EXTENSION,tTime,,"")
        Set AtEnd = 0
    }
    Else {
        Set Row = ""
        Set AtEnd = 1
    }
    Quit $$$OK
}

/// 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<br>
/// Subclass should override this behavior for non-class based editors.
ClassMethod GetOther(Name As %String) As %String
{
    If (..#DOCUMENTCLASS="") {
        // no related item
        Quit ""
    }
    
    Set result = "",tCls=..GetClassName(Name)
    
    // This changes with MAK1867
    If $$$defClassDefined(tCls),..Exists(Name) {
        Set:result'="" result=result_","
        Set result = result _ tCls _ ".cls"
    }
    
    Quit result
}

}

And now, in Studio Open dialog, we can choose our type, and select our file.

Creating a new File

But what to do to create a new Rule file. We could do it by setting DocumentNew=1 in Projection. But in this case it is just add a new Item in New File menu with a fixed name.  We will add a simple in one page ZEN Wizard, where we could choose Package and Name for new Rule. For that task we will use %ZEN.Template.studioTemplate class. This class supports not only new file creations, you can find more information about Studio templates in documentation

Our class is quite simple, with some important parameters:

  • TEMPLATETYPE - which says what type of code this template generates;
  • TEMPLATEMODE - Specifies what type of template this is: 'template', 'new', or 'addin';
  • GLOBALTEMPLATE - true if this template available globally, by mapping or as a system class;

While wizard finished, it call %OnTemplateAction method, where we define initial content and name of our file.

Class IAT.RuleEngine.EngineFileWizard Extends %ZEN.Template.studioTemplate
{

Parameter TEMPLATENAME = "Rule Engine";

Parameter TEMPLATETITLE = "Rule Engine Wizard";

Parameter TEMPLATEDESCRIPTION = "Create a new RuleEngine class.";

Parameter TEMPLATETYPE = "xml";

/// What type of template.
Parameter TEMPLATEMODE = "new";

/// Domain used for localization.
Parameter DOMAIN = "RuleEngine";

/// If this is true then even if this template is tied to a specific namespace it
/// will be available in any namespace and it will be up to the template to make sure
/// it looks for any specific data in the target namespace.
Parameter GLOBALTEMPLATE As BOOLEAN = 0;

/// This Style block contains page-specific CSS style definitions.
XData Style
{
<style type="text/css">

#svgFrame {
    border: 1px solid darkblue;
}
.radioSetCaption {
    font-size: 0.8em;
}

/* @doc="Style for disabled radio captions." */
.radioSetCaptionDisabled {
    font-size: 0.8em;
}

</style>
}

/// 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"/>
<dataCombo label="Package Name:"
        id="ctrlPackage"
        name="Package"
        required="true"
        labelClass="zenRequired"
        title="Package name for the new page"
        editable="true"
        unrestricted="true"
        searchKeyLen="0"
        maxRows="500"
        size="60"
        OnCreateResultSet="CreatePackageRS"
        onchange="zenPage.updateState();"
/>

<text label="Class Name:" 
    id="ctrlClassName"
    name="ClassName"
    size="40"
    required="true"
    labelClass="zenRequired"
    title="Class name for the new page" 
    onchange="zenPage.updateState();"
/>

</vgroup>
</pane>
}

/// Provide contents of description component.
Method %GetDescHTML(pSeed As %String) As %Status
{
    Write $$$TextHTML("This wizard creates a new Rule Engine file."),"<br/>"
    Write $$$TextHTML("Fill in the form below and then press Finish.")
    Quit $$$OK
}

Method %OnAfterCreatePage() As %Status
{
    #; plug in default values
    Do %page.%SetValueById("ctrlPackage", "IAT.RuleEngine.Test")
    Quit $$$OK
}

/// Update state of controls on the wizard form.
ClientMethod updateForm() [ Language = javascript ]
{
    zenPage.updateState();
}

/// 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('ctrlClassName');
    if (ctrl) {
        ctrl.focus();
        ctrl.select();
    }
}

ClientMethod hasMultiplePages() [ Language = javascript ]
{
    return false;
}

/// Validation handler for form built-into template.
ClientMethod formValidationHandler() [ Language = javascript ]
{
    return this.validateClassName();
}

ClientMethod validateClassName() [ Language = javascript ]
{
    // test if class name is valid
    var pkgName = zenPage.getComponentById('ctrlPackage').getValue();
    var clsName = zenPage.getComponentById('ctrlClassName').getValue();

    var msg = this.IsValidClassName(pkgName + "." + clsName);

    if ('' != msg) {
        alert(msg);
        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
{
    #dim tStream As %Library.Stream

    Set tPackage = ..%GetValueByName("Package")
    Set tClassName = ..%GetValueByName("ClassName")
    Set tDescription = ..%GetValueByName("Description")

    Set fileName = tPackage_"."_tClassName_".rule"

    Set %session.Data("Template","NAME") = fileName
    
    Write "<?xml version=""1.0"" ?>",!
    Write "<Definition>",!
    Write "</Definition>",!

    Quit $$$OK
}

/// Create the result set used by the Package dataCombo.
Method CreatePackageRS(Output tSC As %Status, pInfo As %ZEN.Auxiliary.QueryInfo) As %ResultSet
{
    Set tRS = ""
    Set tSC = $$$OK

    Set tRS = ##class(%ResultSet).%New()
    Set tRS.ClassName = "%ZEN.Utils"
    Set tRS.QueryName = "EnumeratePackages"
    Quit tRS
}

}

Studio supports GUI editing files, with web editor inside Studio window, as an example you can see BPL files editor in Ensemble. It would be possible to implement it with %ZEN.StudioDocument.AbstractDocument and %ZEN.StudioDocument.AbstractEditor. With such editor class, Studio shows two panes, the upper pane contains HTML content, served by a subclass of this class; the lower pane displays an editable XML representation of the data displayed in the upper pane. But unfortunately such functionality not working as well as wanted.

Comments

This is an excellent tutorial.

Does anyone know whether custom document types will be useful in Atelier?

Its unclear at this time. They will appear in the server explorer view for certain. In the examples above these are just classes in the end and certainly could be edited in Atelier. The rules implementation as an example is very specific to Studio and in Eclipse there are easier and better ways to make wizards and templates natively.