JSP and MVC Support in the XPages JEE Project
Dec 20, 2021, 11:20 AM
- Updating The XPages JEE Support Project To Jakarta EE 9, A Travelogue
- JSP and MVC Support in the XPages JEE Project
- Migrating a Large XPages App to Jakarta EE 9
- XPages Jakarta EE Support 2.2.0
- DQL, QueryResultsProcessor, and JNoSQL
- Implementing a Basic JNoSQL Driver for Domino
- Video Series On The XPages Jakarta EE Project
- JSF in the XPages Jakarta EE Support Project
- So Why Jakarta?
- Adding Concurrency to the XPages Jakarta EE Support Project
- Adding Transactions to the XPages Jakarta EE Support Project
- XPages Jakarta EE 2.9.0 and Next Steps
Over the weekend, I wrapped up the transition to
jakarta.* for my XPages JEE Support project and uploaded it to OpenNTF.
With that in the bag, I decided to investigate adding some other things that I had been itching to get working for a while now: JSP and MVC.
JSP? Isn't That, Like, A Billion Years Old?
Okay, first: shut up.
Expanding on that point, it is indeed pretty old - arriving in 1999 - and its early form was pretty bad. It was designed as an answer to things like PHP and ASP and bore all those scars: it used actual Java syntax on the page to control output, looping, conditionals, and the like. It even had special directives to import Java classes for the page! All that stuff is still in there, too, which isn't great.
However, JSP used judiciously - focusing on JSTL tags for control/looping and EL references to CDI beans for data access - is a splendid little thing, and it has the advantage that it remains part of the JEE spec.
Domino flirted with JSP for a long time. It's what Garnet was all about and was part of how OpenNTF got off the ground. IBM did eventually ship the custom tags, and they ship with Domino to this day, sitting in the data/domino/java directory, gathering dust. Domino also inherited JSP from WebSphere as part of XPages... kind of. It has hooks for using JSP files in Expeditor-container webapps, but the implementation is conspicuously missing - present only in Notes, presumably for some sort of Social nonsense reason.
For better or for worse, none of that matters now anyway: it's all crusty and old and, critically, uses
javax.*. I had to go a different route.
From what I gather, there's basically only one real open-source JSP implementation: Jasper, which is a part of Tomcat. Basically everyone just uses that, and that works well enough. There are various re-bundlings of it to remove the Tomcat dependencies, and I went with the GlassFish one, since it was pretty clean.
Diving into it, there were a few things that were potential and actual problems.
First, JSP files aren't evaluated directly. Instead, they're compiled into
Servlet class implementations, either on the fly or ahead of time. This process is basically the same as how XPages work: the JSP is translated into a Java file, which is then compiled into a class, which is then reused by the runtime for subsequent requests. Jasper has a dependency on Eclipse JDT, which worried me: when I looked into this in the past, I found that JDT (at least how it was used for JSP) makes a lot of assumptions about working with the actual filesystem. I lucked out here, though: Jasper actually uses the
JavaCompiler API, which is more flexible. The JDT dependency seems like either a vestige of an older version or a fallback option.
However, despite the fact that
JavaCompiler can work purely in memory, Jasper does do a lot of filesystem-bound work when it comes to loading tag libraries, such as JSTL. I ended up having to deploy a bunch of stuff to the filesystem. Ideally, I'll find a better way around this.
Hooking It Up To Domino
Having a JSP interpreter is one thing, but having it respond to URLs like "http://example.com/foo.nsf/bar.jsp" is another, especially if that should also participate in the XPages class space of the NSF.
I originally considered an
HttpService implementation that would accept incoming *.jsp URLs. This could work, but it would be less than ideal: the
HttpService, while working in the XPages OSGi layer, wouldn't know about the internal layout of the NSF. I'd have to either reinvent it or wrangle my way over to the active
NSFService (the one that runs XPages), find or load the NSF's module, and root around in there. Possible, but not ideal.
Fortunately, I lucked out tremendously: the
NSFService class has an
addHandledExtensions static method that I can just call to tell it that incoming ".jsp" requests should go to the XPages runtime. This looks like it was added for more Social-nonsense reasons, but I'm happy it's there regardless. Better still, when the runtime finds a URL it was told to handle, it queries
IServletFactory implementations like those you can use in an NSF for custom servlets. I already had one in place for JAX-RS, so I made another one (refactored since that commit) for JSP.
Once that was in place (plus some other fiddly details), I got to what I wanted: writing JSPs inside an NSF and having them share the XPages class space:
Next Up: MVC
Once I had JSP in place (and after some troublesome fiddling with JSF), I decided to take a swing at adding my beloved MVC to the stack.
This had its own complications, this time for the inverse problem as JSP. While Jasper is a creature of the early 2000s and uses older, less-flexibile Java APIs that I had to write around, MVC is the opposite. It's a pure child of the modern, CDI-based world and thus does everything through CDI and ServiceLoaders. However, though I've had CDI support in the project for a long time, actually tying together anything to do with CDI or ServiceLoaders in OSGi is eternally difficult, especially on Domino.
I had to wrangle for this for a while, but I eventually came up with a functional-but-odd workaround: I made use of my own custom
ServiceParticipant extension capability that lets me have an object perform pre/post behavior around each JAX-RS request in order to futz with the
ClassLoader. I had trouble where the NSF ClassLoader didn't find classes from the MVC implementation, though it should have, so I ended up overriding the
ClassLoader to first look explicitly there. It's not pretty, but it works and at least it doesn't require filesystem stuff.
Servlets and Request Dispatchers
Another aspect of being a more-modern child than Jasper is that Krazo makes ready use of Servlet capabilities that have been there for a while but which don't exist on Domino.
For example, Krazo uses a
ServletContainerInitializer instance to do pre-research in the app to find classes that should get MVC behavior. Without this scan, MVC won't be applied. This is a Servlet 3.0 feature dating to 2009 and Domino doesn't support it - or any kind of annotation-based classpath scanning, for that matter.
Fortunately, I didn't really need to fully support this concept - I really just needed to make sure this ran whenever the JAX-RS support was being loaded for an NSF. So I made it possible to contribute these via an extension point and added my own scanning implementation to gather the applicable types. Essentially, a backport of this feature to apply in an NSF. With that in place, I was able to register the initializer and have it do its work.
My next hurdle was to do with the way Krazo delegates to JSPs. Specifically, it queries the
ServletContext (essentially, the app container) for Servlet registrations that can handle the desired extensions (".jsp" and ".jspx" here) and routes to that using a
RequestDispatcher. Well, Domino supports none of this. Trying to get a
RequestDispatcher is hard-coded to throw an exception saying "Domino doesn't support this" and the bit about getting
ServletRegistrations was new in 3.0. Originally, I stubbed these out, but I decided to give a swing at backporting this as well.
While an NSF doesn't have "Servlet registrations" as such, it does have a list of the aforementioned
IServletFactory instances, so I decided to write my own. I wrote a
getRequestDispatcher implementation that queries the current module's Servlet factories for a match and, when found, return a basic implementation. Then, I wrote a custom subtype of
IServletFactory to provide additional information and made use of that to emulate the Servlet 3+ behavior, at least well enough to let Krazo do what it needs.
Seeing It Together
Once I figured out all these hurdles, I got to what I wanted: I can make a JAX-RS service in an NSF that acts as an MVC controller:
Neat! There are still some rough edges to clean, but it's great to see in action.
Conclusion and Next Steps
So why is this good? Well, there's a certain amount of box-checking going on: the more JEE specs I can get going, the better.
But beyond that, this is helping to crystallize some of my thinking about what Domino (web) developers are even supposed to freaking do nowadays. This remains an extremely-vexing problem, but I know the answer isn't XPages as it exists now. Maybe the answer is to move XPages to a better container or maybe it's to add a better container to Domino (or both of those, I guess). This is another option, one that preserves the "just fire up Designer and edit some code" niceties of the XPages experience while gaining better, more modern capabilities. I could see writing an app with this, doing all my work in CDI beans and using JSP as the front end - pure open-source solutions with active developers - all inside the NSF. Is it the real best answer? I don't know. Maybe. It's something, though, and specifically something worth trying.
Jesper Kiaer - Dec 20, 2021, 4:09 PM
Georg K. - Dec 27, 2021, 6:35 AM
Great Work :)