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:
[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.
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.
[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
Post a Comment