Business Components Without the Business: Part III

Over the last two weeks, I’ve been talking about model layer code to facilitate integration of database-level validation logic into an ADF application. This week, let’s finish off the project by writing the controller-layer code that will turn these custom exceptions into nice, user-friendly error messages. This post builds upon the previous two, so I’d recommend looking them over before you try to understand the following.

Getting Started

We’ll handle our custom exceptions by overriding the addError() method in the class oracle.adf.controller.faces.lifecycle.FacesPageLifecycle. The general process of creating a custom subclass of FacesPageLifecycle and getting your application to use it are covered well in the ADF Developer’s Guide for Forms/4GL Developers, so I’m going to assume you have such a subclass, and that your application is set up to use it.

Now to override the addError() method:

protected void addError(FacesContext context, List addedErrors, int messageNum,
                        int messageLev, Throwable ex) {
    if (ex instanceof DBValException) {       // Corresponds to a custom trigger error
        addDBValEx(context, addedErrors, messageNum, messageLev, (DBValException) ex);
    } else if (ex instanceof BundledDBValException) {
        Object[] details = ((BundledDBValException)ex).getDetails();
        if (details != null {
            for (int detailNum = 0; detailNum < details.length; detailNum++) {
                addError(context, addedErrors,
                         messageNum + detailNum, messageLev,  (Throwable)details [detailNum]);
            }
        }
    } else {
        super.addError(context, addedErrors, num, lev, ex);
    }
}

This code basically divides errors (not including instances of AttrValException; these are dealt with separately and added to the addedErrors parameter) into three groups:

  • DBValException instances, which it handles via a method called addDBValEx(), which we’ll write below
  • BundledDBValException instances, whose detail exceptions it loops through and handles (at the same level)
  • Everything else, which it lets the superclass method deal with

Now the Fun Begins

OK, time to start getting into the meat of things. We need to write that handleDBValEx() method to, well, handle our DBValException instances. These are, after all, the nice meaty ones that wrap our database trigger messages.

private void addDBValEx(FacesContext context, List addedErrors, int messageNum,
                        int messageLev, DBValEx ex) {
    String colName = ex.getColumnName();
    if (colName == null) {   // Trigger errors not associated with particular columns
        addMesssage(context, null, ex);
    } else {                 // Trigger errors associated with particular columns
        AttributeBinding binding = findBinding(context, colName, tabName);
        addMessage(context, binding, ex);
    }
}

That’s a short method, but not a trivial one. It splits into two cases based on whether the original DB error message specified a column or not. If not, it just calls the method FacesPageLifecycle.addMessage(), which handles the actual transformation of an exception’s message into a JSF message. The null in the second parameter means that there’s no particular attribute binding this message relates to, so there’s no need to create a nice LabeledFacesMessage with a link to (and additional marking beside) a UI control; a plain old FacesMessage will do fine.

If, on the other hand, the message did specify a column name, there might well be a particular attribute binding for this page that the message relates to. So first, we have to try to find the attribute binding, and pass what we find (or null, if we find nothing) as the second attribute to addMessage(). We have to write the code to find the attribute binding, though.

Writing the Code to Find the Attribute Binding

To find the appropriate attribute binding, we could cycle through every attribute binding on the page, testing each to see if its underlying column and table match the underlying column and table for the exception. And that’s basically what we will do–but we certainly don’t want to do it for every single exception on the page. What if there are many of them? So we’re going to do it in a lazy-initialized map, like so:

private Map<String[], AttributeBinding> getTableColumnMap(FacesContext context) {
    Map requestMap = context.getExternalContext().getRequestMap();
    Map<String[], AttributeBinding> tableColumnMap = (Map<String[], AttributeBinding>)
        requestMap.get("tableColumnMap");
    DCBindingContainer bc = (DCBindingContainer)
        context.getApplication().createValueBinding("#{bindings}").getValue(context);
    if (tableColumnMap == null) {
        tableColumnMap = createTableColumnMap(bc);
        requestMap.put("tableColumnMap", tableColumnMap);
    }
    return tableColumnMap;
}

That checks to see whether a request attribute called “tableColumnMap” has been set, and if not, it calls the following to set it:

private Map<String[], AttributeBinding> createTableColumnMap(DCBindingContainer bc) {
    Map<String[], AttributeBinding> tableColumnMap = new HashMap<String[], AttributeBinding>();
    List<AttributeBinding> attrBindings = bc.getAttributeBindings();
    for (AttributeBinding currentAttrBinding : attrBindings) {
        JUCtrlValueBinding valueBinding = (JUCtrlValueBinding) currentAttrBinding;
        AttributeDef underlyingVoAttr = valueBinding.getAttributeDef(0);
        String underlyingColumn = underlyingVoAttr.getColumnName();
        ErrorReportingViewObject vo = (ErrorReportingViewObject) valueBinding.getViewObject();
        String underlyingTable = vo.getUnderlyingTable(underlyingVoAttr.getIndex());
        String[] key = new String[] {underlyingTable, underlyingColumn};
        if (tableColumnMap.get(key) == null) {
            tableColumnMap.put(key, current);
        }
    }
    return tableColumnMap;
}

Now that we have that, writing findBinding() is actually pretty easy–and it’s efficient, too:

private AttributeBinding findBinding(context, colName, tabName) {
    return getTableColumnMap(context).get(new String[] {tabName, colName})
}

…and there you go. A fair bit of work, all told, but if you’ve put this all in framework classes, it’s work that only has to be done once. And getting your DB trigger errors to show up nicely for your users is worth it.

Leave a Reply

Your email address will not be published. Required fields are marked *