Search ADF Faces Tree – show only nodes satisfying Query Criteria

We are still refining and enriching our ADF Faces application and in particular the Tree based pages. One of the more advanced user requests we have to deal with, is the desire to be able to search the tree in its entirety. That is: provide a search criterium, press a button and get the tree displayed with only the nodes that satisfy the search criterium – and all their ancestors in order to make it still a tree. The end result would have to look a little like this:

Search ADF Faces Tree - show only nodes satisfying Query Criteria adftreeSearch 

In this article I will discuss how I implemented this search the tree feature

....
 

When I started to analyze the requirement, I concluded that there are at least three approaches, each with their own merits and their own disadvantages:

  • Pass the search criterium to the Model and have it apply directly to the queries under the ViewObjects that feed the tree. While this has the advantage of hiding the details mostly from the application and only querying from the database what we really need, it would give us rather complex set of queries – as each node does not only have to check if it satisfies the query conditions itself but also if any of its children do so! Furthermore, this approach would lead to a rather specialistic solution, not one easy to be reused.
  • Extend the FacesModel that the FacesCtrlHierBinding class sets up and have it wrap the tree model in a layer that filters the nodes that satisfy the query. Well, this approach is neither efficient nor uncomplicated.
  • Apply the search criteria in the Render Phase – just not displaying the nodes we do not want there. While this is a relatively simple approach with the added benefit of being generic and easily reusable, it is hardly efficient as we will have to query each and every node in the entire tree from the database before deciding whether we want it or not. For large data-sets, trees with thousands or more nodes when fully expanded, this will be a costly and rather unperforming solution. Besides, it requires the use of client side JavaScript – not ideal.

However, of these three approaches, given the tree I am looking at – which is relatively small even when fully expanded – and the ViewObjects – which are already quite complex as it is – I decided on the third option.

The tree I start out with for this article – the one I want to add search capabilities to – is the HRM tree (based on EMP and DEPT) shown here:

Search ADF Faces Tree - show only nodes satisfying Query Criteria adfTreeNoSearch 

I want to be able to just type in a String and see all nodes whose label contains that String. If I type a Y, I want to see all nodes that have a Y in their label – and their ancestors to preserve the proper tree format.

The steps to achieve this functionality:

  1. Add an InputItem for the Search Criterium as well as CommandButtons for the Search to start and end
  2. Add a Managed Bean with a doSearch method that is linked through an EL expression to the action attribute of the Search button as well as an undoSearch method that is bound to the action property of the End Search button
  3. Bind the tree to the managed bean, to make the component and its TreeModel available when the doSearch method is invoked
  4. Implement the doSearch method: have it traverse the tree and find out which nodes satisfy the query criteria; all paths to the ancestors of these nodes are added to the tree’s TreePath, the collection of all Expanded Nodes in the tree; this helps ensure that nodes that do not themselves satisfy the query but are required to help display those that do, are in fact available and expanded
  5. Bind the shortDesc attribute with an EL expression to a RenderNode property in the managed bean. Implement the getRenderNode() method – have it verify whether the current node either satisifies the query criteria or is in the TreePath collection
  6. Implement a JavaScript function, to be called onLoad of the afh:body, that locates all links with a title (derived from shortDesc) identifying them as unqualified nodes. Hide all those nodes by setting the (CSS) ClassName property of one of their ancestor elements to a CSS style that makes the element invisible (display:none)

In somewhat more detail:

 

1. Add an InputItem for the Search Criterium as well as CommandButtons for the Search to start and end

Add something like the following JSF fragment to the tree-page:

 <afh:rowLayout >
  <af:panelGroup>
    <af:panelHorizontal>
    <af:inputText value="#{HrmTreeTree.searchCriterium}"  label="Search For"
                  autoSubmit="true" columns="10"
                  shortDesc="Enter the search criterium. The search is case insensitive, does not support wild cards and will check the labels of all nodes for compliance."/>
    <af:objectSpacer width="5"/>
    <af:commandButton  text= "Search" onclick="searchPressed()"/>
    <af:objectSpacer width="5"/>
    <af:commandButton action="#{HrmTreeTree.undoSearch}"     text= "Undo Search"/>
    <af:objectSpacer width="5"/>
    </af:panelHorizontal>

  <af:tree value="#{bindings.HrmTreeTree.treeModel}" var="node"
           binding="#{HrmTreeTree.tree}"
           focusRowKey="#{HrmTreeTree.focusRowKey}" varStatus="nodeStatus">
    <f:facet name="nodeStamp" >
      <af:switcher facetName="#{node.hierType.name}"  >
 

 

2. Add a Managed Bean with a doSearch method

The initial implementation of the Managed Bean is like this: 

package nl.amis.view;
...
public class MyTreeBean {
    public MyTreeBean() {
    }
    String searchCriterium;
    private CoreTree tree;

    boolean inSearch = false;

    public String undoSearch() {
        inSearch = false;
        return null;
    }

    public void setSearchCriterium(String searchCriterium) {
        this.searchCriterium = searchCriterium;
    }

    public String getSearchCriterium() {
        return searchCriterium;
    }

  public void setTree(CoreTree tree)
  {
    this.tree = tree;
    if (isShowExpanded())
    {
      expandAll(null);
    }
  }

  public CoreTree getTree()
  {
    return tree;
  }

...
}

 

The bean needs to be configured in the faces-config.xml file of course:

<managed-bean>
  <managed-bean-name>HrmTreeTree</managed-bean-name>
  <managed-bean-class>nl.amis.view.MyTreeBean</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
</managed-bean>
 

3. Bind the tree to the managed bean

See the JSF fragment under 1. where the tree has its binding attribute refer to the HrmTreeTree.tree property.

4. Implement the doSearch method

Have it traverse the tree and find out which nodes satisfy the query criteria; all paths to the ancestors of these nodes are added to the tree’s TreePath, the collection of all Expanded Nodes in the tree; this helps ensure that nodes that do not themselves satisfy the query but are required to help display those that do, are in fact available and expanded

    public String doSearch() {
        JUCtrlHierNodeBinding rootnode =
            (JUCtrlHierNodeBinding)getSelectedNode();
        inSearch = true;
        getTree().setTreeState(new PathSet(false));

        List path = new ArrayList();
        // now inspect nodes, find the ones that satisfy the query conditions,
        // add them to the Qualifying Nodes set
        // add their parents to the TreeState to make them expanded
        List children = rootnode.getParent().getChildren();
        for (int i = 0; i < children.size(); i++) {
            path.add(0, Integer.toString(i));
            if (inspectNode((JUCtrlHierNodeBinding)children.get(i), path)) {
                // add node to treestate
                getTree().getTreeState().getKeySet().add(path);
            }
            path.remove(0);
        }
        return "";
    }
    private boolean doesNodeQualify(JUCtrlHierNodeBinding node) {
        return ((String)node.getAttribute("NodeLabel")).toLowerCase().indexOf(searchCriterium.toLowerCase()) >
            -1;
    }

    private boolean inspectNode(JUCtrlHierNodeBinding node, List treePath) {
        boolean expandParent = false;
        // check if node satisfies query condition
        // if it does, add to qualifying nodes and set expandParent to true
        if (doesNodeQualify(node)) {
            expandParent = true;
        }
        //next: inspect children; expandParent = expandParent || inspectNode (children)
        boolean expandNode = false;
        if ((node.getChildren() != null) && (node.getChildren().size() > 0)) {
            List children = node.getChildren();
            expandNode = false;
            for (int i = 0; i < children.size(); i++) {
                treePath.add(Integer.toString(i));
                // note: not the short circuit OR (||): once we have expandNode == true,
                // we would not process the children of subsequent nodes if were to use the || operator
                expandNode =
                        expandNode | inspectNode((JUCtrlHierNodeBinding)children.get(i),
                                                 treePath);
                treePath.remove(treePath.size() - 1);

            } //for
            if (expandNode) {
                // add node to treestate
                getTree().getTreeState().getKeySet().add(treePath);
            }
        }
        return expandParent || expandNode;
    }
 

5. Bind the shortDesc attribute with an EL expression to a RenderNode property in the managed bean

 

 <f:facet name="HrmTreeSecondLevelView1Node">
        <af:commandLink text="#{node.NodeLabel}"
           immediate="true"  onclick="return alertForChanges()"
           action="#{TreeHelper.action}"  shortDesc="queryQualified=#{HrmTreeTree.renderNode}"
           >
      <af:setActionListener  

 

Implement the getRenderNode() method – have it verify whether the current node either satisifies the query criteria or is in the TreePath collection

 public boolean getRenderNode() {
   JUCtrlHierNodeBinding currentNode =
     (JUCtrlHierNodeBinding)getTree().getRowData();
   if (inSearch) {
     return (getTree().getTreeState().isContained() ||
             doesNodeQualify(currentNode));
   } else
       return true;
}
 

6. Have JavaScript function hide the nodes that should not be displayed 

Implement a JavaScript function, to be called onLoad of the afh:body, that locates all links with a title (derived from shortDesc) identifying them as unqualified nodes. Hide all those nodes by setting the (CSS) ClassName property of one of their ancestor elements to a CSS style that makes the element invisible (display:none)

function hideUnselected() {
  links = document.getElementsByTagName('a');
  for (var i=0; i<links.length; i++) {
    if (links[i].title.indexOf('queryQualified=false') != -1) {
     links[i].parentNode.parentNode.parentNode.className = "invisible";
    }
  }// for
}
 

Add the style invisible to one of the CSS stylesheets linked in with the tree page. Add the JavaScript functions to a JS library imported by the tree-page.
 

Note: the search button has an onclick event handler that calls the JS function searchPressed():

function searchPressed() {
  links = document.getElementsByTagName('a');
  for (var i=0; i<links.length; i++) {
    if (links[i].title.indexOf('search') != -1) {
     doEventDispatch(links[i]);
     return;
    }
  }
}
/* Dispatch a click event into the document tree
      *
      * Note: I would have called this function fireEvent, or
      *       dispatchEvent, however, this would have resulted in the
      *       browser-supplied functions (former in IE, latter in DOM-
      *       compliant browsers) being called. Be sure to avoid that.
      */
     function doEventDispatch(elm) {
       var evt = null;

       if(document.createEvent) {
         evt = document.createEvent('MouseEvents');
       }
       if(elm && elm.dispatchEvent && evt && evt.initMouseEvent) {
         evt.initMouseEvent(
           'click',
           true,     // Click events bubble
           true,     // and they can be cancelled
           document.defaultView,  // Use the default view
           1,        // Just a single click
           0,        // Don't bother with co-ordinates
           0,
           0,
           0,
           false,    // Don't apply any key modifiers
           false,
           false,
           false,
           0,        // 0 - left, 1 - middle, 2 - right
           null);    // Click events don't have any targets other than
                     // the recipient of the click
         elm.dispatchEvent(evt);
       }
     }  

This function in turns tries to locate a link with title equal to search. This link is defined in the node facet for the root-nodes of the tree. The reason for this complex construction is that we need access to the Nodes in the doSearch method and for some reason that only seems to work from actions invoked from within the tree itself.

The command link is defined in the JSF page as follows:

<f:facet name="HrmTreeFirstLevelView1Node" >
  <h:panelGroup>
     <af:outputText value="#{node.NodeLabel}"  />
     <af:commandLink text=" "  action="#{HrmTreeTree.doSearch}"  shortDesc="search">
        <af:setActionListener from="#{node}"
                              to="#{HrmTreeTree.selectedNode}"/>
     </af:commandLink>
  </h:panelGroup>
</f:facet>

Search ADF Faces Tree - show only nodes satisfying Query Criteria adftreeSearch2 

What’s Next?

It is very simple to extend the search facilities to a more elaborate query engine. We can have support for wild cards and regular expressions, have the user search for a specific type of nodes instead of all nodes, allow the user to search on other node attributes than just the label as we have been doing here.

The main things to work on are: add query properties in the TreeBean and the Tree page and link them together and impelment  a more advanced doesNodeQualify() method to leverage the more complex query criteria.

6 Comments

  1. david December 19, 2011
  2. tranhieu5959 April 12, 2010
  3. Amir Ehsan Shahmirzaloo January 25, 2009
  4. Grant Ronald February 12, 2007
  5. Michael A. Fons February 5, 2007
  6. Jimmy November 1, 2006