Skip to main content

XPages Application Framework (Part 2 of ?)

In my last post, I offered a very rough overview of the framework I've been building and adapting. Full credit where it's due, I've been borrowing and adapting from Jesse Gallagher's XPages Scaffolding - an awesome project that I've learned a great deal dissecting. This is one of those projects that, if you make the effort to truly understand it, will give you entirely new tools and approaches for problems. As usual, I've learned the most changing things and breaking them. My project differs from Jesse's mostly in that it supports 8.5.3 and doesn't require any relaxed security permissions - because I'm on 8.5.3 and because the admins here will not grant any relaxed permissions.
One of the big changes I've made is to eliminate the role of the ControllingViewHandler. To rehash it's purpose: it determines which controller should be used for the XPage being generated, and binds the event handlers to the JSF lifecycle. In it's place, I have a ControllerPhaseListener and an XPageControllerFactory.

Let's take a quick look at those:

ControllerPhaseListener.java
package itd.common.jsf;

import itd.common.mvc.controller.XPageControllerFactory;
import itd.common.utils.JSF;
import itd.logging.Logger;

import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpServletRequest;

public class ControllerPhaseListener implements PhaseListener {
    private static final long serialVersionUID = 1L;
    private static final Logger logger = Logger.getLogger(ControllerPhaseListener.class.getName());

    public PhaseId getPhaseId() {
        return PhaseId.ANY_PHASE;
    }

    public void beforePhase(final PhaseEvent event) {
        PhaseId phaseId = event.getPhaseId();
        JSF.setCurrentPhaseId(phaseId);
        if (PhaseId.RESTORE_VIEW.equals(phaseId)) {
            XPageControllerFactory.getController().beforeRestoreView();
        }
        else if (PhaseId.APPLY_REQUEST_VALUES.equals(phaseId)) {
            XPageControllerFactory.getController().beforeApplyRequest();
        }
        else if (PhaseId.PROCESS_VALIDATIONS.equals(phaseId)) {
            XPageControllerFactory.getController().beforeProcessValidations();
        }
        else if (PhaseId.UPDATE_MODEL_VALUES.equals(phaseId)) {
            XPageControllerFactory.getController().beforeUpdateModelValues();
        }
        else if (PhaseId.INVOKE_APPLICATION.equals(phaseId)) {
            XPageControllerFactory.getController().beforeInvokeApplication();
        }
        else if (PhaseId.RENDER_RESPONSE.equals(phaseId)) {
            XPageControllerFactory.getController().beforeRenderResponse();
        }
        logPhase("BEGIN PHASE [" + event.getPhaseId().getOrdinal() + "] ", event);
    }

    public void afterPhase(final PhaseEvent event) {
        logPhase("END PHASE [" + event.getPhaseId().getOrdinal() + "] ", event);

        PhaseId phaseId = event.getPhaseId();
        if (PhaseId.RESTORE_VIEW.equals(phaseId)) {
            XPageControllerFactory.getController().afterRestoreView();
        }
        else if (PhaseId.APPLY_REQUEST_VALUES.equals(phaseId)) {
            XPageControllerFactory.getController().afterApplyRequest();
        }
        else if (PhaseId.PROCESS_VALIDATIONS.equals(phaseId)) {
            XPageControllerFactory.getController().afterProcessValidations();
        }
        else if (PhaseId.UPDATE_MODEL_VALUES.equals(phaseId)) {
            XPageControllerFactory.getController().afterUpdateModelValues();
        }
        else if (PhaseId.INVOKE_APPLICATION.equals(phaseId)) {
            XPageControllerFactory.getController().afterInvokeApplication();
        }
        else if (PhaseId.RENDER_RESPONSE.equals(phaseId)) {
            XPageControllerFactory.getController().afterRenderResponse();
            clearFlashScope(event);
        }
        JSF.clearCurrentPhaseId();
    }

    private void logPhase(final String prefix, final PhaseEvent event) {
        // LogMan.getLogger(AppControlPhaseListener.class.getName());
        String msg = prefix;
        if (event.getPhaseId().equals(PhaseId.RESTORE_VIEW)) {
            msg += "RESTORE_VIEW";
        }
        else if (event.getPhaseId().equals(PhaseId.APPLY_REQUEST_VALUES)) {
            msg += "APPLY_REQUEST_VALUES";
        }
        else if (event.getPhaseId().equals(PhaseId.PROCESS_VALIDATIONS)) {
            msg += "PROCESS_VALIDATIONS";
        }
        else if (event.getPhaseId().equals(PhaseId.UPDATE_MODEL_VALUES)) {
            msg += "UPDATE_MODEL_VALUES";
        }
        else if (event.getPhaseId().equals(PhaseId.INVOKE_APPLICATION)) {
            msg += "INVOKE_APPLICATION";
        }
        else if (event.getPhaseId().equals(PhaseId.RENDER_RESPONSE)) {
            msg += "RENDER_RESPONSE";
        }
        logger.debug(msg);
    }

    private void clearFlashScope(final PhaseEvent event) {
        FacesContext context = event.getFacesContext();
        HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
        if (!request.getMethod().equals("POST")) {
            JSF.getFlashMap().clear();
        }
    }
}
A quick rundown of optional features:
  • JSF.setCurrentPhaseId() / JSF.clearCurrentPhaseId() : Before the first before[PhaseEvent] method is invoked, I'm setting a requestScope variable to the current PhaseId. It is occasionally useful to know or restrict the event a method is running in.
  • logPhase() : This logs the JSF lifecycle. It lets me know which events are running, and can show me at a glance when certain code is executed, or perhaps why it isn't performing as expected in certain cases.
  • clearFlashScope() : A more in-depth discussion of flashScope can be found elsewhere, but in short it is a JSF 2.0 construct that is unavailable in XPages but is very easy to implement (in fact, this is the extent of the implementation other than declaring it in faces-config.xml).
That leaves the only required component: before and after each phase event, we are running XPageControllerFactory.getController() to get the controller class, and then executing the appropriate event for that controller. Let's have a quick look at that:

XPageControllerFactory.java
package itd.common.mvc.controller;

import itd.common.utils.JSF;
import itd.common.utils.Strings;
import itd.logging.Logger;

import java.util.Map;

import javax.faces.context.FacesContext;

public final class XPageControllerFactory {
    private static final Logger logger = Logger.getLogger(XPageControllerFactory.class.getName());

    protected final static String BEAN = "controller";

    private XPageControllerFactory() {

    }

    public static final XPageController getController() {
        XPageController controller = null;
        String key = XPageController.class.getName();
        Map<String, Object> requestMap = JSF.getRequestMap();
        if (requestMap.containsKey(key)) {
            return (XPageController) requestMap.get(key);
        }
        else {
            Class<? extends XPageController> controllerClass = null;
            FacesContext context = JSF.getFacesContext();
            String controllerName = JSF.getAppConfig().cachedGet("controller.package") + "."
                    + Strings.upperFirst(JSF.getPageName()) + JSF.getAppConfig().cachedGet("controller.suffix");
            try {
                logger.trace("Getting controller: " + controllerName);
                controllerClass = (Class<? extends XPageController>) Class.forName(controllerName);
            }
            catch (ClassNotFoundException cnfe) {

                String baseController = JSF.getAppConfig().cachedGet("controller.baseclass");
                controllerName = JSF.getAppConfig().cachedGet("controller.package") + "."
                        + JSF.getAppConfig().cachedGet("controller.baseclass");
                logger.trace("No page controller. Getting base controller: " + controllerName);
                try {
                    controllerClass = (Class<? extends XPageController>) Class.forName(controllerName);
                }
                catch (ClassNotFoundException cnfe2) {
                    logger.trace("No base controller, getting generic controller");
                    controllerClass = XPageController.class;
                }
            }
            try {
                controller = controllerClass.newInstance();
            }
            catch (IllegalAccessException e) {
                logger.fatal("Unable to access page controller class.", e);
                throw new RuntimeException(e);
            }
            catch (InstantiationException e) {
                logger.fatal("Unable to instantiate page controller class.", e);
                throw new RuntimeException(e);
            }
            requestMap.put(key, controller);
            JSF.getViewRoot().getViewMap().put(BEAN, controller);
            return controller;
        }
    }
}
Key features:
  • The controller is cached in the requestScope, which means we only create it once per request.
  • The controller is added to the ViewRoot ViewMap, which allows us to use #{controller} in our EL expressions.
  • The package in which Controllers are define is configurable in application.properties, as is the suffix used to name the controller ('Controller' in this case). Also the ControllerBase class (I'll explain that momentarily).
  • If a class is found named [controller.package][XPage][controller.suffix] (e.g. app.controllers.HomeController), it is instantiated and used as the controller.
  • If that class does not exist, the ControllerBase is used. ControllerBase is where any ActionListeners and EventHandlers that are not page-specific are added. All page controllers extend and call the super() methods of this class so that their unique functionality is added to the core event handlers.
  • If that class does not exist, the XPageController class is used, which is really just a fail-safe. All events are implemented with a simple return; statement.
So the hierarchy goes like this:
  1. XPageController is generic and universal to all applications. It has no built-in functionality and primarily exists to provide default handling of events we aren't interested in.
  2. ControllerBase is application-specific and application-wide. If an ActionListener or EventHandler pertains to more than one page in the application (e.g. navigation), it goes here.
  3. Specific XPage controller is invoked only for the named page. It can handle, for example, parsing URL parameters to load a specific document.
This change has resulted in the elimination of SecurityManager exceptions related to context.getContextClassLoader. Also, this eliminated an entire class of bugs and problems with my Logger where it was sometimes invoked outside of the JSF lifecycle and I was unable to access my logger.properties configuration.

Up next: the specific case of the XAgent controller.

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