Foundation
Project Documentation

Introduction

The goal of the XMLMenuModel is to allow application developers to create a hierarchical representation of a menu in XML format external to the page metadata. Having menus external to the page allows for better reuse and easier management of the menu metadata. By creating a hierarchical representation of the menu, developers will be able to visualize the relationship of the nodes in the menu but not tie the menu to any one UI at runtime.

The XMLMenuModel defines an implementation of a Trinidad MenuModel, which is described in another chapter of the Trinidad developer's guide. You might want to read that chapter over first, but much of the goal of the XMLMenuModel is to hide the complexity of building a MenuModel. As an introduction, originally navigations menus did not have a separate model and menu items were tied directly to each page and individual elements within each page. Global changes to the menus of an application required each page to be changed. Attribute values such as "selected" had to be set individually on each page. This way is obviously not easily maintained. What if a page is moved from one place to another in the tree?

What was needed is a model object that knows the application hierarchy and also knows how to say what is the current "focus path" in that hierarchy. With the current model, changes are isolated to the model, which will be reflected throughout all pages of an application using this model.

Example

Here is an example of a tree structure representing a menu:

menu tree example

metadata

The metadata for such a structure would look like this:

<?xml version="1.0" encoding="iso-8859-1"?>
<menu xmlns="http://myfaces.apache.org/trinidad/menu">
  <itemNode id="gin0" label="Global 0" action="goToGlobal0"
            focusViewId="/menuDemo/global0.jspx">
    <itemNode id="in1" label="Tab 1" action="goToTab1"
              focusViewId="/menuDemo/tab1.jspx">
      <itemNode id="in11" label="Subtab 1" action="goToSubTab1"
                focusViewId="/menuDemo/subtab1.jspx"/>
      <itemNode id="in12" label="Subtab 2" action="goToSubTab2"
                focusViewId="/menuDemo/subtab2.jspx"
                customAttribute1="myCustomAttrValue"/> <!-- example custom attribute -->
    </itemNode>
    <itemNode id="in2" label="Tab 2" destination="/faces/menuDemo/tab2.jspx"/
              focusViewId="/menuDemo/tab2.jspx"/>
  </itemNode>
  <itemNode id="gin1" label="Global 1" icon="/components/images/globalhelp.gif"
            action="goToGlobal1"
            focusViewId="/menuDemo/global1.jspx"/>
  <itemNode id="gin2" label="Global 2" icon="/components/images/globalprefs.gif
            destination="/faces/menuDemo/global2.jspx"
            focusViewId="/menuDemo/global2.jspx"/>
</menu>

Referring to the XMLMenuModel

The menu model is specified in the tr:navigationPane (for each area: global, primary tabs, menubar, and navbar/list) and tr:breadCrumbs components of the .jspx file as follows:


                       .
                       .
                       .
          <!-- Navbar or list -->
          <f:facet name="navigation3">
            <tr:navigationPane var="foo" value="#{root_menu}" level="3" hint="list">
              <f:facet name="nodeStamp">
                <tr:commandNavigationItem
                  text="#{foo.label}"
                  action="#{foo.doAction}"/>
              </f:facet>
            </tr:navigationPane>
          </f:facet>

          <!-- Breadcrumbs -->
          <f:facet name="location">
            <tr:breadCrumbs var="foo" value="#{root_menu}">
              <f:facet name="nodeStamp">
                <tr:commandNavigationItem
                  text="#{foo.label}"
                  action="#{foo.doAction}"/>
              </f:facet>
            </tr:breadCrumbs>
          </f:facet>

Creating the XMLMenuModel

The XMLMenuModel is created as a simple managed bean. The EL expression of the "value" attribute, "#{root_menu}", references an entry in the faces-config.xml file, which would look like:


      <!-- managed bean menu model -->
     <managed-bean>
       <managed-bean-name>root_menu</managed-bean-name>
       <managed-bean-class>org.apache.myfaces.trinidad.model.XMLMenuModel</managed-bean-class>
       <managed-bean-scope>request</managed-bean-scope>
       <managed-property>
         <property-name>source</property-name>
         <value>/WEB-INF/menu-metadata.xml</value>
       </managed-property>
     </managed-bean>

The "setSource" method of the XMLMenuModel will be called with the location of the menu model's metadata, as specified in the "managed-property" element. IMPORTANT NOTE: the scope for the menu model must be "request".

Metadata Reference

There are four possible, different node types in a menu model's metdata:

  1. menu
  2. groupNode
  3. itemNode
  4. sharedNode

The following are descriptions, attributes and value types for each node type. Please note that almost every groupNode and itemNode attribute corresponds to an equivalent attribute of the tr:commandNavigationItem.

menu

Top-level, root-node of the model. This is a required node and only one per model is allowed. This node is not rendered/visible. Attributes are:

  • xmlns: must be "http://myfaces.apache.org/trinidad/menu"
  • resourceBundle: resource bundle to be loaded for visible text, e.g. tab labels.
  • var: String id for the resource bundle. Used in EL expressions to identify the resource bundle.

Attributes Common to groupNodes and itemNodes

  • id: a required, user-defined, string value identifier for the component, which must be unique in the entire menu.
  • label: a user-defined, string value for the text that appears for the menu item. EL expression is OK.
  • accessKey: One of the characters in the label used to activate the menu item via the keyboard. Also known as a "mnemonic". EL expression is OK.
  • labelAndAccessKey: a user-defined, string value for the text that appears for the menu item. EL expression is OK.
  • icon: a URI value reference to the file used to display the icon. EL expression is OK.
  • rendered: a boolean value whose default value is true. EL expression is OK.
  • disabled: a boolean value whose default value is false. EL expression is OK.
  • readOnly: a boolean value whose default value is false. EL expression is OK.

groupNode

This type of node simply points to child node through its "idref" attribute. When selected by a user, the implementation follows the "idref" attribute value to a referree child node. The referree can be also be a groupNode, which also refers to one of its children, until an itemNode is reached. A groupNode only navigates indirectly, it does not have an "action", "destination", or "focusViewId" attribute. The node that actually results in navigation (the only node that performs a GET or POST) is the itemNode.

groupNode-specific Attributes:

  • idref: a user-defined, string value reference to the "id" of one its child node or nodes. The referred to node can either be another group node or an itemNode.

itemNode

Performs navigation upon user selection. When the "destination" attribute is used, menu item selection performs a GET and directly navigates to the destination attribute value. When the "action" attribute is used, menu item selection performs a POST and navigation is done via an outcome. If both attributes are set on an itemNode, the "destination" attribute takes precedence and a GET is done.

itemNode-specific Node Attributes:

  • action: a user-defined, string value reference to:
    1. an EL method binding expression. This method should return a string reference (from-outcome) to a navigation case/navigation rule in faces-config.xml.
    2. a from-outcome for a navigation-rule in a navigation-case in faces-config.xml
  • destination: a user-defined, string value reference to:
    1. a URI of the page that will be navigated to when the menu item is clicked. IMPORTANT NOTE: if the destination is a "faces" page, the URI should begin with "/faces...", or whatever specific FacesServlet mapping you're using.
    2. an EL value binding expression. This method must return a URI as described in item #1 above.
  • defaultFocusPath: a boolean value whose default value is false, used to specify the focus path to be used by the model when navigation into the model occurs from outside the model. EL expression is OK.
  • focusViewId: a required URI that must match the node's navigational result.

    When using the "destination" attribute, this should map to the target viewId. For example, if you're using "/faces/*" as the FacesServlet mapping and the destination was "/faces/foo.jspx", the viewId should be "/foo.jspx". But ifthe destination's URI is to a page outside of the menu, e.g. http://www.yahoo.com, the focusViewId should be left blank.

    When using the "action" attribute, the focusViewId should be the viewId that will result after navigating, if it is known, or left blank if it is not.

  • actionListener: an EL expression that is a method reference to an action listener.
  • launchListener: an EL expression that is a method reference to a launch listener.
  • returnListener: an EL expression that is a method reference to a return listener.
  • immediate: boolean value or EL expression returning a boolean value that indicates whether or not data validation (client-side or server-side) should take place when events are generated by this component.
  • useWindow: boolean value or EL expression returning a boolean value indicating whether to use a new window when launching dialogs.
  • windowHeight: int or EL expression returning an int which is the height of the window, if this command is used to launch a window.
  • windowWidth: int or EL expression returning an int which is the width of the window, if this command is used to launch a window.

sharedNode

Not a true node. It is tantamount to an include of another menu metadata model. This will result in another "submenu" menu model being created, attached to the main menu model, and rendered. Its sole attribute is:

  • ref: a required, user-defined, string value reference to another menu model. The menu constructed by the referenced menu model will be a submenu of the current menu. It references the Menu Model used, e.g. "#{xmlMenuModel_shared}".

A sharedNode can be added anywhere within a menu hierarchy. The following adds a submenu as a global menu item to the main menu because the sharedNode is at the same level as the global itemNodes (level 0). Its usage is as follows:

<?xml version="1.0" encoding="iso-8859-1"?>
<menu xmlns="http://myfaces.apache.org/trinidad/menu">
  <itemNode id="gin1" label="one" action="goToPageOne"
            focusViewId="/menuDemo/page_one.jspx"/>
  <sharedNode ref="#{xmlMenuModel_subMenu}"/>
  <itemNode id="gin2" label="two" action="goToPageTwo"
            focusViewId="/menuDemo/page_two.jspx"/>
</menu>

Localizing an XMLMenuModel

Strings that are viewed in the application at runtime should not be "hard-coded" in the metadata. In the sample menudata above the "label" attribute values of the nodes should not hard-coded for a real application. Strings such as these must be externalized in a resource bundle so that they can be translated. A mechanism for loading a resource bundle to obtain attribute value strings is implemented by using the "resourceBundle" and "var" attributes of the top-level, root node of the model. Values can be obtained from the resource bundle through the use of the appropriate EL expression.

Typically, an attribute whose value needs to be externalized for translation is the "label" attribute. The following is a localized version of the original metadata sample:

<?xml version="1.0" encoding="iso-8859-1"?>
<menu xmlns="http://myfaces.apache.org/trinidad/menu"
      resourceBundle="org.apache.myfaces.trinidaddemo.xmlMenuModelDemo.resource.MenuBundle"
      var="bundle"/>
  <itemNode id="gin0" label="#{bundle.GLOBAL_TAB_0}" action="goToGlobal0"
            focusViewId="/menuDemo/global0.jspx">
    <itemNode id="in1" label="#{bundle.PRIMARY_TAB_0}" action="goToTab1"
              focusViewId="/menuDemo/tab1.jspx">
      <itemNode id="in11" label="#{bundle.LEVEL2_TAB_0}" action="goToSubTab1"
                viewId="/menuDemo/subtab1.jspx"/>
      <itemNode id="in12" label="#{bundle.LEVEL2_TAB_1}" action="goToSubTab2"
                focusViewId="/menuDemo/subtab2.jspx"/>
    </itemNode>
    <itemNode id="in2" label="#{bundle.PRIMARY_TAB_1}" destination="/faces/menuDemo/tab2.jspx"
              focusViewId="/menuDemo/tab2.jspx"/>
  </itemNode>
  <itemNode id="gin1" label="#{bundle.GLOBAL_TAB_1}" icon="/components/images/globalhelp.gif"
            action="goToGlobal1"
            focusViewId="/menuDemo/global1.jspx"/>
  <itemNode id="gin2" label="#{bundle.GLOBAL_TAB_2}" icon="/components/images/globalprefs.gif
            destination="/faces/menuDemo/global2.jspx"
            focusViewId="/menuDemo/global2.jspx"/>
</menu>

Multiple menu models may be nested through the use of the "ref" attribute of a sharedNode. It is quite possible that these nested models will use the same value for the "var" attribute of the top-level, root menu node. This is handled during parsing where it is ensured that each resource bundle has a unique hash key, so that the correct resource bundle is always being used.

Custom Node Attributes

Customers using the menu model may want to add custom attributes to nodes in their metadata. This can only be done on itemNodes. SharedNodes and menuNodes are not real nodes, so they cannot have custom attributes. GroupNodes point to child nodes, which are either groupNodes or itemNodes. The itemNodes are the nodes that do that actual navigation, so they are the nodes that can use custom attributes.

Custom attributes appear as any other pre-defined attribute (described above), a name followed by a value, e.g. 'context="Human Resources"'. Custom attributes support values of type String or an EL expressions returning a String. It will be up to the customer to convert the string to the typed desired by the customer (e.g. (String) "true" -> (boolean) true).

Customers of the menu model may have different uses and purposes for custom attributes. The menu model currently has one API, getCustomProperty(), that takes a node and a name of a custom attribute as arguments, returning the value of the named attribute. Typical scenarios for finding a node or nodes with a particular custom attribute could be:

  • Get the tree from the menu model by caling its getWrappedData() API. Traverse the tree, calling getCustomProperty() on each node to find nodes that contain the desired custom attribute.
  • Get the tree from the menu model by calling its getWrappedData() API. Call the getFocusRowKey() API to get the current focus path. Use this focus path to traverse the tree and return a list of nodes in the focus path (See below for sample code). Test one or more of these nodes for custom attribute(s) by calling the getCustomProperty() API.

Customers needing custom behavior from the menu model will probably have to extend XMLMenuModel and operate on the model's tree to get the desired behaviors.


   /**
    * Returns the nodes corresponding to a focus path
    * 
    * @param tree
    * @param focusPath
    */
    public List getNodesFromFocusPath(TreeModel tree, ArrayList focusPath)
    {
      if (focusPath == null || focusPath.size() == 0)
        return null;

      // Clone the focusPath cause we remove elements
      ArrayList fp = (ArrayList) focusPath.clone();
  
      // List of nodes to return
      List nodeList = new ArrayList<Object>(fp.size());
  
      // Convert String rowkey to int and point to the
      // node (row) corresponding to this index
      int targetNodeIdx = Integer.parseInt((String)fp.get(0));
      tree.setRowIndex(targetNodeIdx);

      // Get the node
      Object node = tree.getRowData()

      // put the Node in the List
      nodeList.add(node);

      // Remove the 0th rowkey from the focus path
      // leaving the remaining focus path
      fp.remove(0);
  
      // traverse into children 
      if (   fp.size() > 0
          && tree.isContainer() 
          && !tree.isContainerEmpty()
         )
      {
        tree.enterContainer();
  
        // get list of nodes in remaining focusPath
        List childList = getNodesFromFocusPath(tree, fp);
  
        // Add this list to the nodeList
        nodeList.addAll(childList);
  
        tree.exitContainer();
      }
  
      return nodeList;
    } 

  public String getElementLabel(XMLMenuModel model, Object myVal, String myProp)
  {
     TreeModel tree = model.getWrappedData();
     
     Object node = findNodeByPropertyValue(tree, myVal, myProp);

     FacesContext context = FacesContext.getCurrentInstance();
     PropertyResolver resolver = context.getApplication().getPropertyResolver();
     
     String label = (String) resolver.getValue(node, _LABEL_ATTR);
     
     return label;
  }

  public Object findNodeByPropertyValue(TreeModel tree, Object myVal, String myProp)
  {
    FacesContext context = FacesContext.getCurrentInstance();
    PropertyResolver resolver = context.getApplication().getPropertyResolver();
    
    for ( int i = 0; i < tree.getRowCount(); i++)
    {
      tree.setRowIndex(i);
subsection
      // Get a node
      Object node = tree.getRowData();
      
      // Get the value of the attribute of the node
      Obect propVal = resolver.getValue(node, myProp);
      
      if (propVal == myVal)
      { 
        return node;
      }        
      
      if (tree.isContainer() && !tree.isContainerEmpty())
      {
        tree.enterContainer();
        node = findNodeByPropertyValue(tree, myVal, myProp);

        if (node != null)
          return node;

        tree.exitContainer();
      }
    }
    return null;
  }