views:

72

answers:

2

I have a JSF application containing two JSP pages that both display some of the same data from a session-scoped container object. Each page displays the data differently, each in a data table that varies between pages. This all works properly so far.

My problem is that I have been cheating a bit with how I figure out what page was requested from inside my backing bean action methods. On each page, I have used a binding for my data table.

draftReport.jsp:

<t:dataTable 
    border="1"
    id="reportDraftDataTable"
    binding="#{controller.reportDraftDataTable}"
    value="#{sessionData.reportDraftAdapterList}" 
    var="currentRow" 
    rowClasses="dataTableOddRow, dataTableEvenRow">

report.jsp:

<t:dataTable 
    border="1"
    id="reportDataTable"
    binding="#{controller.reportDataTable}"
    value="#{sessionData.reportAdapterList}" 
    var="currentRow" 
    rowClasses="dataTableOddRow, dataTableEvenRow">

I have a request-scoped backing bean (named Controller) with some of the action methods for these pages. Rather than duplicate code on the backing bean (one similar method for each similar JSP page), I wanted to figure out what page was being rendered and use that as a parameter to a generic handler method (that can handle actions from both pages) on the backing bean. So I cheated and did this:

public class Controller {
    ...

    private HtmlDataTable preArrivalReportDataTable;
    private HtmlDataTable preArrivalReportDraftDataTable;
    private static enum ReportType {
        NON_DRAFT,
        DRAFT
    }
    ...

    private ReportType determineReportTypeFromControlBindings() {
        Validate.isTrue(this.preArrivalReportDataTable != null ^
                this.preArrivalReportDraftDataTable != null,
            "Either preArrivalReportDataTable XOR " +
            "preArrivalReportDraftDataTable must be null in " +
            "determineReportTypeFromControlBindings()");
        if (this.preArrivalReportDataTable != null) {
            return ReportType.NON_DRAFT;
        } else {
            return ReportType.DRAFT;
        }
    }
    ...

    public String actionOnReport() {
        ReportType reportType = null;
        reportType = determineReportTypeFromControlBindings();
        handleReportAction(reportType);
        return "REFRESH";
    }
    ...
}

This worked OK inside action methods in my Controller class, but then I needed to add another method that finally broke my hacky code:

    public String getStyleClass() {
        ReportType reportType = determineReportTypeFromControlBindings();
        switch (reportType) {
            case NON_DRAFT:
                return "styleA";
            case DRAFT:
                return "styleB";
            default:
                return null;
        }
    }

In my JSP, the JSF-EL expression is located above the control binding for the data table that I'm using in the backing bean to determine what page I'm on. At that point the determineReportTypeFromControlBindings() throws an exception at the Validate check, presumably because the control binding has not occurred yet.

I'm not surprised that this is happening. It always felt like the wrong way. But my question is:

What is the correct way to determine the currently requested JSP page from a request-scoped backing bean action method?

In case it is relevant, I'm using the MyFaces 1.2 Tomahawk tag library.

+1  A: 

I can think of a few approaches, one is proactive, where the page tells the bean what the view is, a reactive one, where the bean infers the view. And the last one would be to have an abstract bean with concrete implementations for draft and non-draft.

I prefer the last approach, it feels the most Java-like, and the least hacky. But here are some basic ideas on how to do the first two.

Proactive: Call a method during the renderResponse phase to set the report type. I've only done this in session scoped beans, not sure how well it will work in request scoped, you may need to check other phases, or possibly just apply it regardless of the actual phase.

Controller.java

public void draftInitializer(PhaseEvent event) {
  if (event.getPhaseId().equals(PhaseId.RENDER_RESPONSE)) {
    reportType = DRAFT;
  }
} 

draftReport.jsp

<f:view beforePhase="#{controller.draftInitializer}">

Reactive: Get the URL from the request.

Controller.java

  private String getRequestURL(){
    HttpServletRequest request = (HttpServletRequest)FacesContext.getExternalContext().getRequest();
    return request.getRequestURL();
  }

  private boolean isDraft() {
    return getRequestURL().contains(DRAFT_URL_IDENTIFIER);
  }
Naganalf
@Naganalf +1 for your suggestions. I decided to go in another direction that is similar to your "reactive" approach, but uses the UIViewRoot instead of the request URL.
Jim Tough
A: 

So I ended up solving this by inspecting the UIViewRoot object that is obtained from the FacesContext during the request. I replaced my ReportType enum with a RequestedPage enum because it seemed more readable.

public static enum RequestedPage {
    REPORT,
    REPORT_DRAFT,
    UNKNOWN
}

Then I created a couple String constants and a new method inside my Controller class.

private final static String REPORT_DRAFT_JSP_NAME = "draftReport.jsp";
private final static String REPORT_JSP_NAME = "report.jsp";

/**
 * This method should only be invoked from inside an action method.
 * An exception will be thrown if the method is called when either
 * the FacesContext or UIViewRoot are not available. This is normally
 * the case outside of an active request or before the RESTORE_VIEW
 * phase has been completed.
 * 
 * @return A non-null RequestedPage reference
 */
private RequestedPage determineRequestedPageFromViewId() {
    FacesContext facesContext = FacesContext.getCurrentInstance();
    Validate.notNull(facesContext);
    UIViewRoot uiViewRoot = facesContext.getViewRoot();
    Validate.notNull(uiViewRoot);
    String viewId = uiViewRoot.getViewId();
    logger.info("view id: " + viewId);
    RequestedPage requestedPage = null;
    if (viewId.contains(REPORT_DRAFT_JSP_NAME)) {
        requestedPage = RequestedPage.REPORT_DRAFT;
    } else if (viewId.contains(REPORT_JSP_NAME)) {
        requestedPage = RequestedPage.REPORT;
    } else {
        requestedPage = RequestedPage.UNKNOWN;
    }
    return requestedPage;
}

The UNKNOWN enum is meant to cover all the pages whose identity I don't care about in my action methods. As long as I obey the constraints that I mention in my method Javadoc comments, this seems to work fine.

The only disappointing thing about this approach is that I wanted to do the RequestedPage resolution inside the initializer method for my Controller class. Unfortunately, this won't work because the initializer is called before the RESTORE_VIEW phase begins and therefore the UIViewRoot is still null.

Here is the code that will NOT work:

@PostConstruct
public void init() {
    logger.info("init() has been invoked");
    RequestedPage requestedPage = 
        determineRequestedPageFromViewId();
    // An exception is always thrown before I get here...
    this.theRequestedPage = requestedPage;
    logger.info("init() finished");
}

I can live with this, unless someone else has a simple alternative to my approach.

Jim Tough