Article
· Apr 27, 2017 7m read

Level up your XDATA

XDATA is used for a whole host of ISC libraries to store things like Zen pages, BPL logic and DTL transformations.

XDATA is the equivalent of XML config files of the JAVA world and JSON config files of the JavaScript / NPM world.

Whilst Atelier looks to shift source code to the disk, XDATA will remain a key component to source control our projects config / meta data.

I come across many developers who overlook XDATA, seeing it as an internal function of Cache and Ensemble and instead use globals to move meta data around, I've been there myself.

It might be because XDATA can feel a little clumsy to use, so here is an approach that I use that makes working with XDATA a breeze.

The first thing that we need is a standard caché class that extends the XML adapter, this is going to describe the meta data that we will store and retrieve. In this example I am defining a simple package manager to go with my source code.

Class Foo.PackageManager Extends (%RegisteredObject, %XML.Adaptor)
{
Property Classes As list Of %String;
Property ClassesIgnore As list Of %String;
Property Routines As list Of %String;
Property RoutinesIgnore As list Of %String;
Property WebFiles As list Of %String;
Property WebFilesIgnore As list Of %String;
}


Next I need a class that will store the XDATA. I'm not aware of a way to dynamically add the XDATA member to an existing class so instead I have a base template class that I dynamically copy. I'll explain that later, for now lets create one by hand.

Class Foo.MyPackage Extends (%RegisteredObject, %XML.Adaptor)
{

XData Package
{
}

}
 


It might be that the XDATA is all handwritten (such as a Zen page) or we may have a development tool such as a BPL editor where the XDATA is dynamically generated. In this instance let consider there is a simple UI form for managing items in our package manager, this will implement code along the following lines...

set myPackage=##class(Foo.PackageManager).%New()
do myPackage.Classes.Insert("Bar.*")
do myPackage.Classes.Insert("Lib.*")
do myPackage.Classes.Insert("Foo.*")
do myPackage.ClassesIgnore.Insert("Foo.Generated.*")
do myPackage.Routines.Insert("Spud*.INC")
do myPackage.RoutinesIgnore.Insert("Spud*.INT")
do myPackage.WebFiles.Insert("/bar/*")
do myPackage.WebFilesIgnore.Insert("/bar/node_modules")

  
Now using a small helper library that I will paste below, all I need to save the object to my XDATA is to use one line of code...

set sc=##class(Cogs.Lib.XData).Save("Foo.MyPackage","Package",myPackage)

The generated XDATA

Class Foo.MyPackage Extends (%RegisteredObject, %XML.Adaptor)
{

XData Package
{
<object className="Foo.PackageManager">
  <Classes>
    <ClassesItem>Bar.*</ClassesItem>
    <ClassesItem>Lib.*</ClassesItem>
    <ClassesItem>Foo.*</ClassesItem>
  </Classes>
  <ClassesIgnore>
    <ClassesIgnoreItem>Foo.Generated.*</ClassesIgnoreItem>
  </ClassesIgnore>
  <Routines>
    <RoutinesItem>Spud*.INC</RoutinesItem>
  </Routines>
  <RoutinesIgnore>
    <RoutinesIgnoreItem>Spud*.INT</RoutinesIgnoreItem>
  </RoutinesIgnore>
  <WebFiles>
    <WebFilesItem>/bar/*</WebFilesItem>
  </WebFiles>
  <WebFilesIgnore>
    <WebFilesIgnoreItem>/bar/node_modules</WebFilesIgnoreItem>
  </WebFilesIgnore>
</object>
}

}  

Using the same helper library we can programmatically open and use the XDATA with one line of code...

set sc=##class(Cogs.Lib.XData).Open("Foo.MyPackage","Package",.myPackage)


Now we can directly access the data properties of myPackage object without having to think about XML deserialization.

The Code

Class Cogs.Lib.XData Extends %RegisteredObject
{

ClassMethod Open(pClassName As %String, pXDataName As %String, Output pObject As %RegisteredObject) As %Status
{
    if '##class(%Dictionary.XDataDefinition).%Exists($listbuild(pClassName_"||"_pXDataName)) quit $$$ERROR($$$GeneralError,"Class or XData does not exist, ensure an XData block exists before opening it")
    set xml=##class(%Dictionary.XDataDefinition).%OpenId(pClassName_"||"_pXDataName,-1,.sc) $$$QuitOnError(sc)
    set header=xml.Data.Read(100)
    do xml.Data.Rewind()
    set className=$piece($piece(header,"className=""",2),"""")
    set reader=##class(%XML.Reader).%New()
    set sc=reader.OpenStream(xml.Data) $$$QuitOnError(sc)
    do reader.Correlate("object",className)
    do reader.Next(.pObject,.sc)
    quit sc
}

ClassMethod Save(pClassName As %String, pXDataName As %String, pObject As %RegisteredObject) As %Status
{
    //if ##class(Cogs.Lib.Dictionary).ClassExtends(pClassName,"%XML.Adaptor")=0 quit $$$ERROR($$$GeneralError,"Class containing XDATA must extend %XML.Adaptor")
    if '##class(%Dictionary.XDataDefinition).%Exists($listbuild(pClassName_"||"_pXDataName)) quit $$$ERROR($$$GeneralError,"Class or XData does not exist, ensure an XData block exists before writting to it")
    set xml=##class(%Dictionary.XDataDefinition).%OpenId(pClassName_"||"_pXDataName)
    set attrs=1,attrs(1)="className",attrs(1,0)=pObject.%ClassName(1)
    set sc=pObject.XMLExportToStream(.xmlStream,"object",,,.attrs) $$$QuitOnError(sc)
    set sc=..Format(.xmlStream,.xmlStreamFormatted) $$$QuitOnError(sc)
    set sc=xml.Data.CopyFrom(xmlStreamFormatted)  $$$QuitOnError(sc)
    quit xml.%Save()
}

ClassMethod Format(pXmlStream As %CharacterStream, Output pXmlStreamFormatted As %CharacterStream) As %Status
{
    set xslt=##class(%Dictionary.XDataDefinition).%OpenId(..%ClassName(1)_"||XSLT",-1,.sc)
    quit ##class(%XML.XSLT.Transformer).TransformStream(pXmlStream,xslt.Data,.pXmlStreamFormatted)
}

XData XSLT
{
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>
 <xsl:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>
</xsl:stylesheet>
}

}

How it works

The save method takes three parameters, a class name and XDATA name (the location where the XDATA XML is stored), and an object that contains the properties which will be serialised into the saved XML.

Since the object extends XML adapter all we need to do is call its XMLExportToStream to create the XML. At this stage the XML does not contain any human readable formatting (line breaks etc), so we implement a little bit of XSLT magic to format the XML in a couple of lines of COS code.

To save the XML to the XDATA block we need to get a handle on its dictionary object. We call the %OpenId method of %Dictionary.XDataDefinition passing in a string that looks like this "Foo.MyPackage||Package" , this is essentially the class name and the package name separated by two pipe characters. All we need to then do is copy the XML stream we created into XDATA data stream and call the objects %Save() method.

Notice that the code adds the class name of the generated XML as an attribute of the XML wrapper element. This is used to self describe the XML and make it easy for anyone wanting to open and use the XDATA.

To open the XDATA we use the same %OpenId to get at the stored XML stream. The code reads a small amount of the stream to extract out the class name attribute. It then creates an XML data reader that is used to correlate the XML stream into an object instance of type class name. Since we only have one instance in the XML it automatically calls Next to get and return that instance.

As you can see this approach makes saving and opening objects to and from XDATA a simple one line operation.

Cloning a Class

I'm not aware of a way to dynamically add an XDATA member to an existing class. If you try and write to an XDATA block that is not there then you will get an error. To get around this I create a dummy class that has an empty XDATA block in it. I can then dynamically clone that class as many times as I want, to do this I use the following approach...

ClassMethod CopyClass(pSourceClassName As %String, pTargetClassName As %String, pDeleteTargetIfExists As %Boolean = 0) As %Status
{
    if ##class(%Dictionary.ClassDefinition).%ExistsId(pSourceClassName)=0 quit $$$ERROR($$$GeneralError,"Source class does not exist")
    if 'pDeleteTargetIfExists,##class(%Dictionary.ClassDefinition).%ExistsId(pTargetClassName) quit $$$ERROR($$$GeneralError,"Target class already exists, use pDeleteTargetIfExists argument to force replacement")
    if pDeleteTargetIfExists,##class(%Dictionary.ClassDefinition).%ExistsId(pTargetClassName) Do ##class(%Dictionary.ClassDefinition).%DeleteId(pTargetClassName)
    set newClass = ##class(%Dictionary.ClassDefinition).%OpenId(pSourceClassName).%ConstructClone(1)
    set newClass.Name = pTargetClassName
    quit newClass.%Save()
}


The code provided here is from a curated library of development tools that I have been using for years. I plan to open source the library soon, in the mean time please feel free to use any code here without any restrictions.

Discussion (4)1
Log in or sign up to continue