Article
John Murray · Mar 3, 2016 2m read

Class Projections and Projection Classes

The purpose of this post is to raise the profile of a powerful mechanism that has long been available to us, and to open a discussion about ways in which it can be used or abused.

You can read more detail about the mechanism here. To summarize, your class definition can use the Projection keyword to reference one or more projection classes. A projection class can implement methods that get invoked at key points in the lifecycle of your class.

A projection class must extend %Projection.AbstractProjection and will typically implement at least one or the following methods:

  • CreateProjection
  • RemoveProjection

CreateProjection will be called after your class has been compiled. RemoveProjection will be called just before your class is recompiled, or just before it is deleted.

I think one of the original uses of this mechanism was to generate Java files that implemented a Java projection of a Caché class. Since then, it has been used more extensively and become more sophisticated. In 2015.2 I counted twenty-four %-classes that derive from %Projection.AbstractProjection.

Apart from how InterSystems uses the mechanism I have also seen it exploited in other ways. Here are a couple of examples:

  • UMLExplorer ships as an XML file containing four classes. One of these is a projection class called ClassExplorer.WebAppInstaller, which ingeniously projects itself:
Class ClassExplorer.WebAppInstaller Extends %Projection.AbstractProjection
{

Projection Reference As WebAppInstaller;
...

So when this class is compiled its CreateProjection method executes, performing whatever steps the developer coded there. In this case it adds a web application called /CacheExplorer, but it could do anything that the permissions of person compiling the class allow.

  • A site using our Deltanji source code management tool created a utility projection class. Whenever they are deploying a piece of work that requires some installation steps to run in a target namespace, they implement those steps in a deployment class (D) that has a projection referencing their utility projection class (P). Then they bundle (D) with the piece of work. When (D) gets loaded and compiled in a target namespace the CreateProjection method of (P) is automatically invoked, and is passed the classname (D), allowing it to invoke methods of (D).

If you've seen projection used in other ways, or if you've devised a novel use yourself, please share it as a comment on this post.

One more thought from me at this point. The mechanism means we should probably think twice before compiling during import an XML file whose contents we aren't sure we can trust. The scope for a malicious projection class is great.

8
0 1,275
Discussion (18)8
Log in or sign up to continue

The same security precautions would be for generator methods which can execute any code as well during compilation.

And for any macro definitions which contain ##Expression, ##Function, #Execute and ##SafeExpression.  Also #If, #IfDef, #IfNDef conditions (any Caché ObjectScript expression) are evaluated at compile time.

Also ##Expression macro preprocessor command can be masked and distributed along several macros (which in turn can be distributed across several inc files). For example consider the following class:

Class Utils.Macro Extends %RegisteredObject
{

/// do ##class(Utils.Macro).Test()
ClassMethod Test()
{
    #define Tab #
    #define Indent $$$Tab$$$Tab
    #define Start Expre
    #define End ssion($$$Call)
    #define Call ##class(Utils.Macro).GenerateNewClassObject(%classname)
    #define All $$$Indent $$$Start$$$End
    set b=$$$All
}

ClassMethod GenerateNewClassObject(cls As %String)
{
    zw
    // custom logic depending on class may be implemented
    q "##class("_cls_").%New()"
}

}

Note, that in this example full-text search for ##Expression would yield nothing.

In here GenerateNewClassObject method would be executed on Utils.Macro class compilation. because $$$All macro would be evaluated to:

##Expression(##class(Utils.Macro).GenerateNewClassObject(%classname)

Which in turn would be evaluated on the last line:  set b=$$$All where GenerateNewClassObject would be called by ##Expression command.

 

So I guess don't compile Caché ObjectScript from untrusted sources at all or check carefully what you compile.

Yes, %Projection mechanism is quite convenient tool to automatically setup things upon simple recompilation.

Nikita Savchenko (author of this mentioned UMLExplorer ) is now working on an article for  HabraHabr site [in Russian] where he explains usage scenarious of that  %Projection facilities in more details. My assumptin is that soon after this post will be translated to English.

Here are few usage cases I was involved lately:

  • in the package manager I want to have easy to use facilitity which will allow to create package definition even for the sources directly loaded from GitHub. Something similar to package.json, but handled by $system.OBJ.Load() . So solution was to create projection class which will load package metadata definition at the end off package compilation - https://github.com/cpmteam/CPM/blob/master/CPM/Sample/PackageDefinition.cls.xml  
    You inherit your class from CPM.Utils.PackageDefinition, insert package.json definition part as XData block, and at the end of compilation stage you not only have your classes compiled, but also have registered them in the package manager as part of package [this will be necessary, for example, for package uninstall] 
  • I've discovered lately that despite the fact that WebSockets could be made working without any installation ste (i.e. you simple call appropriate CSP URL), but for REST handler you have to create web-application (https://community.intersystems.com/comment/3571#comment-3571) and the easiest solution was creation of special projection which would be creating nested web-application upon recompilation - https://github.com/intersystems-ru/iknowSocial/blob/master/TWReader/Setup.cls.xml , and regardless of a namespace it was installed to, and url of default CSP application assigned to this namespace this nested rest handler will still be available handy.

Projections are indeed very powerful. A use case within the product is the generation of JavaScript, CSS and localization files when you compile a Zen or Zen Mojo page.

Projections are a good tool if you have to do something every time you compile a class.

We use a projection to generate and bundle assets for our zen components. So on compile, after css and js get regenerated our projection bundles and minifies it all into one file for the page to use instead of the dozens that otherwise get loaded.

I still remember asking the WRC guys about projections at a global summit a few years back. One of the fellows took me over to a lamp and made a projection with his hands of a dog barking and was like "oh, you mean this projection?" laugh

Two things I'm curious about but never figured out:

  • Is it possible to control the order projections get called in (particularly when inherited)? I have a feeling they're alphabetical, but not quite sure..
  • Aside from the ability to run a projection on class deletion, is there any difference between a projection and a generator method that just runs some code on compile and doesn't actually generate anything?

%ZEN.ObjectProjection demonstrates a few answers to your second question. There are probably more differences/advantages, but here are a few:

  • Projections can call methods of the class that was just compiled, while generator methods can't call methods of the class that's being compiled. Example: %ZEN.ObjectProjection calls the %GetIncludeInfo method of each class that was compiled.
  • Projections can avoid repeated work by queuing classes in CreateProjection and using the EndCompile method (as %ZEN.ObjectProjection does). In the case of the generated JS and CSS files for Zen, multiple classes may contribute content to the same file, so the file must be regenerated when any of these classes is compiled. If many such classes are compiled at the same time, the projection only regenerates each impacted file once. I don't think there's a good way to do the same thing in a generator method.

The difference Tim gave in his first bullet is presumably down to the way that the projection methods get called after the class being projected has been compiled, whereas if you're using generator methods they're executing in the midst of the compilation.

Those are two very good points! Thanks for taking the time to elaborate.

How do you minify and bundle css and js files from Cache?

We implemented an Asset Pipeline (before the days where nice things like webpack existed). It's pretty ugly, only builds on windows and requires a few hooks into the zen page component, but works well and significantly reduces the amount of round trips + makes them fully cacheable. The performance boost is nice, but the drop in "have you cleared your browser cache?" support requests is even better ;)

Asset Pipeline on the left, stock Zen on the right

Asset Pipeline on the left, stock Zen on the right

If you're interested I can add some docs and throw it up on github for you?

No worries, give me a couple of days - I'll let you know when it's up.

Just as a note, this is something you would usually tackle with a gulp or grunt task, e.g. 

https://www.npmjs.com/package/gulp-minify

These watch external files and run a task every time they change. This means a gulp/grunt task can minify JavaScript files after you compiled your Zen/Zen Mojo classes.

Hi, Murray!

Nikita.Savchenko posted longread about Class Projection with examples of real projects using it ;)

A while back, I downloaded Crystal Reports for Eclipse and projected a couple of classes into Java and used them to set up an interface for us to automate emailing, printing, or archiving reports through Cache/IRIS's internal task manager.