One of the very-long-term side projects I have going on is a rewrite of OpenNTF's site. While the one we have has served us well, we have a lot of ideas about how we want to present projects differently and similar changes to make, so this is as good a time as any to go whole-hog.
The specific hog in question involves an opportunity to use modern Jakarta EE technologies by way of my Domino Open Liberty Runtime project, as I do with my blog here. And that means I can, also like this blog, use the delightful Jakarta MVC Spec.
However, when moving to JEE 9.1, I ran into some trouble with the current Open Liberty beta and its handling of JSP as the view template engine. At some point, I plan to investigate to see if the bug is on my side or in Liberty's (it is beta, in this case), but in the mean time it gave my brain an opportunity to wander: in theory, I could use ERB (a Ruby-based templating engine) for this purpose. I started to look around, and it turns out the basics of such a thing aren't difficult at all, and I figure it's worth documenting this revelation.
The way the MVC spec works, you have a JAX-RS endpoint that returns a string or is annotated with a view-template name, and that's how the framework determines what page to use to render the request. For example:
Here, the controller method does a little work to load required model data for the page and then hands it off to the view engine, identified here by returning
"home.jsp", which is in turn loaded from
WEB-INF/views/home.jsp in the app.
In the framework, it looks through instances of
ViewEngine to find one that can handle the named page. The default spec implementation ships with a few of these, and
JspViewEngine is the one that handles view names ending with
.jspx. The contract for
ViewEngine is pretty simple:
So basically, one method to check whether the engine can handle a given view name and another one to actually handle it if it returned
Writing A Custom Engine
With this in mind, I set about writing a basic
ErbViewEngine to see how practical it'd be. I added JRuby to my dependencies and then made my basic class:
At the top, you see how a custom
ViewEngine is registered: it's done by way of making your class a CDI bean in the application scope, and then it's good form to mark it with a
@Priority of the application level stored in the interface. Extending
ViewEngineBase gets you a handful of utility classes, so you don't have to, for example, hard-code
WEB-INF/views into your lookup code. The bit with
ServletContext is there because it becomes useful in implementation below - it's not part of the contractual requirement.
And that's basically the gist of hooking up your custom engine. The devil is in the implementation details, for sure, but that
processView is an empty canvas for your work, and you're not responsible for the other fiddly details that may be involved.
First-Pass ERB Implementation
Though the above covers the main concept of this post, I figure it won't hurt to discuss the provisional implementation I have a bit more. There are a couple ways to use JRuby in a Java app, but the way I'm most familiar with is using JSR 223, which is a generic way to access scripting languages in Java. With it, you can populate contextual objects and settings and then execute a script in the target language. The Krazo MVC implementation actually comes with a generic
Jsr223ViewEngine that lets you use any such language by extension.
In my case, the task at hand is to read in the ERB template, load up the Ruby interpreter, and then pass it a small script that uses the in-Ruby
ERB class to render the final page. This basic implementation looks like this:
resolveView methods come from
ViewEngineBase and do basically what their names imply. The rest of the code here reads in the ERB template file and passes it to the script engine. This is extremely similar to the generic JSR 223 implementation, but diverges in that the actual Ruby code is always the same, since it exists just to evaluate the template.
If I continue down this path, I'll put in some time to make this more streamable and to provide access to CDI beans, but it did the job to prove that it's quite doable.
All in all, I found this exactly as pleasant and straightforward as it should be.