Skip navigation.
Arch2Arch Tab BEA.com

Advanced Page Flows: Nesting, Exception Handling and Global.app

by Rich Feit
01/29/2004

The basic page flow framework gives you the ability to centralize navigational state and logic for groups of pages in a Web project. While this is a clear improvement over earlier Web development patterns, with navigational logic spread throughout pages and state stored in clouds of session objects, some of the more advanced page flow features can enable you to build projects that are even more elegant and powerful.

This article assumes you are familiar with building and running page flows in WebLogic Workshop. In it, we'll introduce three features: nesting, declarative exception handling, and Global.app

Nesting


By default, executing an action in a new page flow causes the current page flow to be discarded. This behavior allows you to create separate controllers for different sections of your project, and it minimizes the amount of data kept in the user session at one time. Each page flow manages its own state and logic. In the WebLogic Workshop IDE, transferring control to another page flow is represented with a dimmed, terminal node representing the external page flow:

1


Want to learn more about advanced page flows? Download the author's sample application, which accompanies this article. Page flow nesting gives you an even greater ability to break up your project into separate, self-contained bits of functionality. At its heart, it is a way of pushing aside the current page flow temporarily and transferring control to another page flow with the intention of coming back to the original one.

So when would you use this? Nesting is useful when you want to do one of the following tasks:

  • gather data from the user, for use in the current page flow
  • allow the user to correct errors or supply additional information en route to executing a desired action
  • show an alternate view of data represented in the current page flow
  • show the user information that will be useful in the current page flow (e.g., help screens)

Let's start with a simple example. In this scenario, the user is asked to choose a color and is redirected to a page depending on the color chosen. Without nesting, the flow (/noNesting/noNestingController.jpf) might look like this:

1


If, however, the color-choosing area needs to be more complex (for instance, if it requires a confirmation before proceeding), you could replace chooseColor.jsp with a nested page flow that fulfills the same purpose (in /simpleNesting/simpleNestingController.jpf):

1


An important characteristic of nested page flows is that they can act much like pages. Nested page flows raise actions and can even "post," or be initialized with, forms. In general you can always replace a single page with a nested page flow. Here is the flow view for the nested page flow /chooseColor/chooseColorController.jpf:

1


This page flow allows the user to choose a color, requests confirmation, and then raises an action on the original page flow. While this page flow is active, the original page flow is stashed away (in a stack) in the user session. When this page flow is finished, the original one is restored and either the "chooseRed" or "chooseBlue" action is raised on it.

Creating and using a nested page flow is fairly straightforward. The Page Flow Wizard offers a "make this a nested page flow" option, which causes the new page flow to be defined as a nested page flow in the class-level @jpf:controller annotation:


  /**
   * @jpf:controller nested="true"
   */
  public class chooseColorController extends PageFlowController


To cause the nested page flow to raise an action on the original page flow, you forward to an "exit node" (the red square "Exit" item in the Page Flow Palette), which looks like this in an action method:


  /**
   * @jpf:action
   * @jpf:forward name="red" return-action="chooseRed"
   * @jpf:forward name="blue" return-action="chooseBlue"
   */
  public Forward done(ChooseColorForm form)
  {
      if ( form.getChosenColor().equals( "Red" ) )
      {
          return new Forward( "red" );
      }
      else
      {
          return new Forward( "blue" );
      }
  }


The nested page flow is exited by forwarding to a @jpf:forward that defines a return-action instead of a path.

A slightly more involved example is shown below. In /demoNesting/demoNestingController.jpf, the user is forwarded to a nested page flow (/chooseAirport/chooseAirportController.jpf), which is a wizard that helps the user find an airport. The nested page flow returns ("posts") the chosen airport to the original page flow, which continues with its sequence.

1


This page flow demonstrates two new features related to nesting:

  • If the nested page flow raises a "chooseAirportCancelled" action, the page flow will go back to the previous page shown to the user. This is accomplished by using the return-to attribute on @jpf:forward:

    /*
     * @jpf:action
     * @jpf:forward name="previousPage" return-to="page"
     */
    protected Forward chooseAirportCancelled()
    {
        return new Forward( "previousPage" );
    }


  • The nested page flow returns a form (FormData class) when it raises the "chooseAirportDone" action. This is discussed further below, but it is important to note this page flow handles the returned form the same way it would handle a form posted from a page.

Here is the nested page flow, /chooseAirport/chooseAirportController.jpf:

1


The only new thing here is the returning of a form along with the "chooseAirportDone" action that is raised on the original page flow. This is accomplished using the return-form attribute on @jpf:forward:


    /*
     * @jpf:action
     * @jpf:forward name="done"
     *              return-action="chooseAirportDone"
     *              return-form="_currentResults"
     */
    protected Forward confirmResults()
    {
        return new Forward( "done" );
    }


In this case, _currentResults is a member variable in the nested page flow. As an alternative, if you want to initialize a form locally (and avoid using a member variable), you can declare the form type with the return-form-type attribute, and attach the form to the returned Forward object:


    /*
     * @jpf:action
     * @jpf:forward name="done"
     *              return-action="chooseAirportDone"
     *              return-form-type="ChooseAirportForm"
     */
    protected Forward confirmResults( ConfirmationForm confirmForm )
    {
        ChooseAirportForm returnForm = new ChooseAirportForm( … );
        return new Forward( "done", returnForm );
    }


Aside from the @jpf:controller nested="true" declaration, and the patterns for raising exit actions and returning forms, nested page flows are built just like non-nested page flows.

Other Notes

  • Forwarding/redirecting to a nested page flow, or raising any action on it, causes nesting to occur. The original page flow is pushed onto the "nesting stack" until the nested page flow raises a return action, using the return-action attribute on @jpf:forward.
  • Page flows can nest themselves
  • While in an action method in a nested page flow, you can get a reference to the original page flow by calling PageFlowUtils.getNestingPageFlow( getRequest() );
  • A nested page flow (as well as any other page flow) can accept a FormData argument on its begin action method:

    /**
     * @jpf:action
     * @jpf:forward name="firstPage" path="index.jsp"
     */
    protected Forward begin( InitForm form )
    {
        …
    }


To initialize the nested page flow, the original page flow may forward to it like this:


    /**
     * @jpf:action
     * @jpf:forward name="nest" path="/nested/nestedController.jpf"
     */
    protected Forward goNested()
    {
        nested.nestedController.InitForm initForm
                = new nested.nestedController.InitForm();
        initForm.setValue( … );
        return new Forward( "nest", initForm );
    }


  • A nested page flow defines concretely the form (FormData class) that it will return. This is different than the behavior of a page, which simply posts data as request parameters, which are set (or cajoled) into the form bean associated with the action being raised. The stricter behavior of nested page flow return-form allows you to nest two page flows that raise similarly-named actions. Handling the return actions from the two nested page flows might look like this:


  /**
   * @jpf:action
   * @jpf:forward …
   */
  public Forward done( nested1.nested1Controller.OutputForm form )
  {
      …
  }

  /**
   * @jpf:action
   * @jpf:forward …
   */
  public Forward done( nested2.nested2Controller.DoneForm form )
  {
    …
  }


Exception Handling


While you can always handle exceptions programmatically within your action methods, page flows give you a powerful framework for handling exceptions declaratively. On action methods and at the class level, you can handle an exception by forwarding directly to a page (or a nested page flow), or by invoking an exception handler method, which chooses the next page to display.

Forwarding directly to a URI is the simplest way to handle an exception:


  /**
   * @jpf:catch type="Exception" path="/error.jsp"
   * @jpf:catch type="NotLoggedInException"
   *            path="/login/loginController.jpf"
   */
  public class exampleController extends PageFlowController


In the above example, when a NotLoggedInException is thrown, the user is forwarded to /login/loginController.jsp. When any other Exception is thrown, the user is forwarded to /error.jsp (note that the page flow exception handler always tries to match the most specific exception type first). The exception is automatically stored in the request for the <netui:exceptions> JSP tag, which can be used to display the exception message and/or stack trace:


  <netui:exceptions showMessage="true" showStackTrace="false" />


You can also handle an exception with an exception-handler method. To do this, use the method attribute on @jpf:catch:


  * @jpf:catch type="ExampleException"
  *            method="handleExampleException"
  *            message-key="myCustomMessage"


The method attribute refers to an exception-handler method, described below, and the message-key resolves to a message in a resource bundle declared at the class level using a @jpf:message-resources tag (this particular one finds resources in /WEB-INF/classes/exceptions/Messages.properties):


  * @jpf:message-resources resources="exceptions.Messages"


The exception-handler method itself is defined like this:


    /**
     * @jpf:exception-handler
     * @jpf:forward name="errorDetailsPage" path="errorDetails.jsp"
     */
    protected Forward handleExampleException( ExampleException ex,
                                              String actionName,
                                              String message,
                                              FormData form )
    {
        getRequest().setAttribute( "exceptionType",
                                   ex.getClass().getName() );
        getRequest().setAttribute( "customMessage", message );
        return new Forward( "errorDetailsPage" );
    }


In this example, the method is storing the exception type and the custom message (defined by the message-key attribute on @jpf:catch) in the request so that errorDetails.jsp can display them.

Global.app


Global.app, defined in /WEB-INF/src/global/Global.app, is a both a fallback handler for unhandled actions and exceptions and a place to store session-scoped state. A single instance of this class is stored in the user session upon the first request to any page flow, and it stays there until the session ends.

When an action is raised in a page flow, and the action is not handled by that page flow, Global.app gets a chance to handle it. The same is true for an exception raised within a page flow: if it is unhandled in the page flow, Global.app gets a chance to handle it. The following example Global.app handles any uncaught Exception, and takes care of any "goHome" action that is not handled by the page flow:


  /**
   * @jpf:catch type="Exception" path="/error.jsp"
   */
  public class Global extends GlobalApp
  {
      /**
       * @jpf:action
       * @jpf:forward name="homePage" path="/index.jsp"
       */
      public Forward goHome()
      {
          return new Forward( "homePage" );
      }
  }


As mentioned above, the Global.app instance remains active throughout the lifetime of the user session. To access the instance from a page flow, you can do one of two things:

  • Declare a special member variable protected global.Global globalApp; in the page flow. This will be initialized automatically with the current Global.app instance, and can be used from within page flow action methods and lifecycle methods (e.g., onCreate, beforeAction, afterAction).
  • Call PageFlowUtils.getGlobalApp( getRequest() );

To access Global.app public member data and getters/setters from a JSP, you can use the "globalApp" databinding context:


  <netui:label value="{globalApp.someProperty}"/>


An Advanced Example


The following example demonstrates all of nesting, exception handling, and Global.app. In it, the user can attempt to execute an action ("doit") that requires the user to be logged in. If the user is logged in, flow continues through the "doit" action. If not, an exception is thrown and caught by Global.app, which forwards to a nested login page flow, and then attempts to rerun the "doit" action after the user is logged in. First, here is the main page flow (/demoLogin/demoLoginController.jpf):

1


The only special thing about this page flow is the behavior of the "doit" action:


    /**
     * @jpf:action login-required="true"
     * @jpf:forward name="success" path="success.jsp"
     */
    public Forward doit()
    {
        return new Forward( "success" );
    }


The login-required attribute on @jpf:action causes a NotLoggedInException to be thrown if the user is not logged in. This is logically equivalent to throwing the exception explicitly:


    /**
     * @jpf:action
     * @jpf:forward name="success" path="success.jsp"
     */
    public Forward doit()
        throws NotLoggedInException
    {
        if ( getRequest().getUserPrincipal() == null )
        {
            throw new NotLoggedInException();
        }

        return new Forward( "success" );
    }


When the NotLoggedInException is thrown from doit, it is caught in Global.app, which forwards to the nested page flow /login/loginController.jpf:


  /**
   * @jpf:catch type="NotLoggedInException"
   *            path="/login/loginController.jpf"
   */
  public class Global extends GlobalApp


/login/loginController.jpf looks like this:

1


When the nested page flow raises the "loginOK" action, it is unhandled by /demoLogin/demoLoginController.jpf (which is still the original page flow), and bubbles up to Global.app, which handles it by re-running the previous action on the current page flow, which is /demoLogin/demoLoginController.jpf.


  /**
   * @jpf:catch type="NotLoggedInException"
   *            path="/login/loginController.jpf"
   */
  public class Global extends GlobalApp
  {
      /**
       * @jpf:action
       * @jpf:forward name="previousAction" return-to="action"
       */
      protected Forward loginOK()
      {
          return new Forward( "previousAction" );
      }


The return-to="action" attribute on @jpf:forward causes the most recent action to be run; in this case it is "doit", which will now succeed. Finally, if the nested page flow raises the "loginCancel" action, it similarly bubbles up to Global.app, which handles it by returning to the previously viewed page in the current page flow (again, in /demoLogin/demoLoginController.jpf):


  /**
   * @jpf:catch type="NotLoggedInException"
   *            path="/login/loginController.jpf"
   */
  public class Global extends GlobalApp
  {
      /**
       * @jpf:action
       * @jpf:forward name="previousAction" return-to="action"
       */
      protected Forward loginOK()
      {
          return new Forward( "previousAction" );
      }

      /**
       * @jpf:action
       * @jpf:forward name="previousPage" return-to="page"
       */
      protected Forward loginCancel()
      {
          return new Forward( "previousPage" );
      }
  }


In this case, the user ends up back at the starting page if the login attempt is cancelled.

Article Tools

Email E-mail
Print Print
Blog Blog

Related Products

Check out the products mentioned in this article:

Related Technologies

Bookmark Article

del.icio.us del.icio.us
Digg Digg
DZone DZone
Furl Furl
Reddit Reddit