Skip to main content

XPages Application Framework (Part 1?)

Note: I changed projects and priorities after my last POI article, resulting in a long hiatus. I anticipate using that framework on my current project and will likely refine and complete my related article series.

For the past several months, I've been fortunate enough to lead a project overhaul from traditional Domino Webapp to XPages. I had a few goals in mind, but the top three were integrating Bootstrap, embracing MVC principles, and eliminating all SSJS. I imagine the motivation for Bootstrap integration is self-explanatory. There is an OpenNTF project out there called Bootstrap4XPages. I didn't use that for a number of reasons - the main being a policy in the current environment. But you may ask yourself (if you didn't follow that link), why so much hatred for SSJS?

I have a litany of reasons. It impacts readability of the XPage source; it hurts maintainability when logic is scattered across dozens of XPages, custom controls, and script libraries; it mixes logic and presentation on a single document; and it can create errors that are very difficult to troubleshoot. It also doesn't have the compiler-level protections from certain types of bugs.

So, instead I've crafted a pure-Java solution. Which, incidentally, gave me huge grief with errors that were very difficult to troubleshoot during framework development. But I have those issues resolved. And, while I was mindful of MVC principles, I did make some decisions which were not "pure". Perhaps if the scale of the application was larger, I'd have hewn more closely to that ideal, but I found myself creating classes and methods and interfaces that owed more to ideology than practicality. I changed course and rejected those design elements.

First, some shout-outs. Most likely, none of this would have been possible without reading Jesse Gallagher's blog and looking through his XPages Scaffolding source. Sadly, I couldn't make more use of it because we run on 8.5.3 for now, and because of the same policies that prevent the use of Bootstrap4XPages. Additionally, I'd like to take a moment to remember Tim Tripcony. I learned more working with him and asking him questions than I could've learned in 5 years on my own. All of my work builds on the foundational knowledge I gained from him.

Without more ado, let's talk about how I accomplished these goals. It might be illustrative to start talking about Java packages as a rough outline of how everything comes together. There are several utility classes/packages, but the key elements are:
  • org.common.mvc.controller
    •  (Interface) JSFController - defines a controller as something with event handlers for beforeRenderResponse, afterRenderResponse, and afterRestoreView.
    • (Class implements JSFController) XPageController - A default implementation of JSFController. This does nothing more than log (when enabled) the invocation of the event handlers.
  • org.common.mvc.model
    • (Abstract Class implements DataObject) BaseDataModel - This is a wrapper for a Map to use as a data bean. It adds two critical methods, plus a few supporting/utility methods.

      In the bastardized EL implementation used with XPages, you cannot pass an argument through Expression Language. The customGetter function is called by the getValue function before checking the underlying map to allow more complex Expression Language ability. The default example is determining whether a property is read-only or not. The customGetter checks to see if the property starts with "isReadOnly" and, if so, strips that portion from the property name and passes the rest to an isReadOnly call. So if I write #{dataObject.isReadOnlyName}, customGetter intercepts that and returns isReadOnly('name').[1]

      The second critical addition is a normalizeKey function. Domino is case-agnostic toward data, but Java is not. This function takes a getter key that is going to be passed to the map, uses equalsIgnoreCase to see if some form of the key already exists, and uses that capitalization to get the value. I don't perform this function for the setter function.
    • (Abstract Class extends BaseDataModel) DocumentDataModel - This adds support for document edit mode, document UNID, and loading/saving a Domino Document. It also specifies a getFormName method which returns the name of the form to be used for the data documents the model supports.
  • org.project.mvc.controller
    •  (Class implements JSFController, Map[2]) ApplicationController - This is the global "controller". Any controller functionality that doesn't belong to a specific XPage JSF lifecycle event goes here. It has it's own beforeRenderResponse, afterRenderResponse, and afterRestoreView event handlers so that, for example, the currentUser bean can be constructed from any entry point into the application.
    • (Class extends XPageController) <PageName>Controller - This is a specific controller to be invoked when accessing <PageName>.xsp. So far, the only thing this is being used for is to look for a document unid to be passed as a URL parameter, and load up that document if one is found.
  • org.project.mvc.model
    •  (Class extends DocumentDataModel) <ClassName> - This is where I put my data models. They are registered in faces-config.xml and so the class name is irrelevant. Each model implements getFormName() and can override customGetter(). ActionEvent listeners also go here.[3]
  • org.project.jsf
    •  (Class implements PhaseListener) AppControlPhaseListener - This is a massively useful class while developing JSF applications. It allows you to know exactly where you are in the lifecycle when you are getting/setting various values. It also invokes the call to ApplicationController.beforeRenderResponse() before the RenderResponse phase.
    • (Class extends ViewHandleExImpl) ControllingViewHandler - This is registered in faces-config and overrides the createView method. It instantiates the generic XPageController or the custom controller for the XPage being loaded and binds the JSF events to the view so that they are invoked automatically.
[1] My original implementation of this made heavy use of Reflection to avoid an ugly block of "if (key.equals('foo')){} else if (key.equals('bar')){} else...". While I did get this to work, the code wound up being ugly and prone to errors, and I had to introduce a lot of overhead to protect the developer from the IDE being blind to what was actually being called. I recommend the if-block.

[2] Why the heck does this implement Map??? I'm not completely certain why this was necessary, but the answer is I'm hijacking the default PropertyResolver to be able to use Map.get() and Map.put(). This shouldn't be necessary, because the BeanELResolver should have no problem accessing getPropertyName() and setPropertyName(), but my setter wasn't being recognized and an input field was being marked as read-only. Implementing Map worked around this problem.

[3] This is one place I didn't strictly follow MVC conventions. These listeners should be in a controller class. But I ran into a lot of issues and overhead implementing this. The primary issue is that the controllers need to have their data object as a managed property. XPages doesn't allow the @PostConstruct annotation from JSF1.2, so sometimes I'd be using a controller and the data object it was trying to access wasn't instantiated yet. So then I had to write a parser to reach into faces-config.xml, find the managed property class, name, and scope, and instantiate them. And that worked, but sometimes this method led to a new JSF lifecycle. (In fact, I ran into this bug a lot during this project, not just from this.) I didn't spend enough time to determine exactly what the problem was, but this class of bug was massively difficult to troubleshoot and fix. My solution is to have exactly one controller for an XPage rather than mapping them one controller to one model. Which means EventListeners needed to go with the model they pertained to. As an added bonus, I eliminated the need to tediously register all of these controllers, along with their managed property bean, in faces-config.

Comments

Popular posts from this blog

Pass data between XPages and existing LS scripts

I'm working on modernizing a fairly hefty application with a lot of existing script libraries which we want to leverage within the XPages environment. Here is a technique that works very well. First, create an in-memory document in SSJS. We can set any input values needed for the back end. Then we pass that document to a LS Agent which can work it's magic using the values set in SSJS and use the same document to return values back to the XPage. Here is how it works in detail:

Quick tip: Convert a number to String in EL

I just had a need to do this and a Google search didn't immediately turn up a solution. So I thought for a couple of minutes and came up with this: value="0#{numberVar}" This takes advantage of the way Java auto-converts objects to strings when doing a concatenation. So if your number is 13, Java EL turns this into new String("0"+13), which becomes "013". You can then strip off the leading zero or just parse the string back into a number.

Project in Review - Part 3: What didn't work

Of course, not everything was an unmitigated success. I tried many things that didn't work out. Much of which I've removed and forgotten about, but a few things remain - either scarred into my psyche or woven too deeply to fix. What didn't work Storing my entire configuration in application.properties Using properties files is great. It let me get configuration out of a profile document and into something much easier to edit - particularly configuration that users will never see or maintain (and thus there is no need for an interface for). But I took it too far. The paths to the other databases are there, and that's good. But view aliases are also there, and that was a mistake. I already have a ViewDefinition enum that describes each view and all the information I need to know about it. I could have set view names there, but instead I'm reading them from the application config. I can change where a view is pointing without having to go into my code. Except of co