23 October 2008

Expressions in IceFaces Navigation Targets

I must write this lest I forget.

I've been working on an exception handling mechanism for a JSF-based application using IceFaces, and I was thinking... "why oh why can't we dynamically navigate to error pages using a navigation rule?"
Navigation rules in the faces-config.xml look like this:
<navigation-rule>
    <from-view-id>/somePage.xhtml</from-view-id>
    <navigation-case>
        <from-outcome>error</from-outcome>
        <to-view-id>/errorPage.xhtml</to-view-id>
    </navigation-case>
</navigation-rule>
That is fine but very limiting for my navigation purposes. What if I want to navigate to a different page according to the actual error code? In other words, I want to be able to do this:
<navigation-rule>
    <from-view-id>/somePage.xhtml</from-view-id>
    <navigation-case>
        <from-outcome>error</from-outcome>
        <to-view-id>/#{errorBean.errorCode}.xhtml</to-view-id>
    </navigation-case> </navigation-rule>
Well, it seems that I can't do that with the Sun JSF RI or IceFaces, so I decided to make it happen. I figured that if I wanted to add expression evaluation in a navigation target URL I needed to write a view handler. With IceFaces, the application view handler is com.icesoft.faces.facelets.D2DFaceletViewHandler, which is reponsible for setting up the direct-to-DOM rendering etc, so I needed to extend that class and find what methods I needed to override in order to get me to where I wanted to be. After a bit of experimentation I found there are two scenarios:
Scenario #1: Navigation Rule With Redirection
This is where the navigation rule has a <redirect/> tag. The method in D2DFaceletViewHandler that handles this is
public String getActionURL(FacesContext context, String viewId)
Scenario #2: Navigation Rule Without Redirection
This is where the navigation rule does not have a <redirect/> tag. The method in D2DFaceletViewHandler that handles this is
public void renderView(FacesContext context, UIViewRoot viewToRender)
The way I decided to process the expression is very simple, almost elementary:

  1. Parse the URL/ViewId looking for a sub-string that begins with '#{' or '${' and ends with '}' 
  2. capture the sub-string and create a value binding to evaluate the expression 
  3. Replace the expression in the URL/ViewId with the actual value 
  4. Process the newly evaluated URL/ViewId

Before I get any comments on step 1... no, I don't like regex because my brain just doesn't get it, and it takes me considerably longer to figure out a regex pattern to capture such a simple substring than to actually write a few lines of code to do the parsing.

So here's my (edited) code.
/**
* Constructor
*/
public MyViewHandler(ViewHandler delegate) {
    super(delegate);
}

/**
* Processes a view id that may contain an expression, by evaluating the
* expression and replacing the expression tag in the original view id with
* the expression result.
*
* @param context The faces context.
* @param viewId The view id to process.
* @return The processed view id.
*/
private String processViewId(FacesContext context, String viewId) {
    String processedViewId = viewId;

    int startExpression = processedViewId.indexOf("{") - 1;
    if (startExpression > 0) {
        char expChar = processedViewId.charAt(startExpression);

        // expressions start with # or $
        if ((expChar == '#') || (expChar == '$')) {
            int endExpression = processedViewId.indexOf("}", startExpression);

            if (endExpression > startExpression) {
                // viewId contains an expression
                String expression = processedViewId.substring(startExpression, endExpression + 1);

                try {
                    ValueBinding vb = context.getApplication().createValueBinding(expression);

                    if (vb != null) {
                        String evaluatedExpression = vb.getValue(context).toString();

                        // replace the expression tag in the view id
                        // with the expression's actual value
                        processedViewId = processedViewId.replace(expression, evaluatedExpression);
                    }
                }
catch (ReferenceSyntaxException ex) {
                    // do nothing: processedViewId = viewId;
                }
            }
        }
    }

    return processedViewId;
}

/**
* Used to process a URL that may contain an expression. If a navigation
* rule in the faces configuration file has a <redirect> tag, this
* method will be used to process the URL specified in the
* <to-view-id> tag
*
* @see javax.faces.application.ViewHandler#getActionURL(FacesContext, String)
*/
@Override
public String getActionURL(FacesContext context, String viewId) {

    String processedViewId = super.getActionURL(context, viewId);
    processedViewId = this.processViewId(context, processedViewId);

    return processedViewId;
}

/**
* If a navigation rule in the faces configuration file does not have a
* <redirect> tag, this method will be used to process the URL
* specified in the <to-view-id> tag
*
* @see com.icesoft.faces.application.D2DViewHandler#renderView(FacesContext,
* UIViewRoot)
*/
@Override
public void renderView(FacesContext context, UIViewRoot viewToRender)
throws IOException, NullPointerException {

    String viewId = this.processViewId(context, viewToRender.getViewId());
    viewToRender.setViewId(viewId);

    super.renderView(context, viewToRender);
}
To use my spanking new view handler I just have to change the application section in the faces-config.xml file:
<faces-config>
    <application>
        ...
        ...
        <view-handler>
            <!--
            com.icesoft.faces.facelets.D2DFaceletViewHandler
            -->

            myViewHandler
        </view-handler>
    </application>
</faces-config>
M.

No comments: