A lazy tree model for PrimeFaces

Enhance your JSF application with lazy loading!

PrimeFaces and Disy

Primefaces is one of the leading JSF component libraries. At Disy we use this framework for our metadata management solution Preludio. We currently use Version 5.3, which this post is based on.

Primefaces offers a wide variety of components. For a quick overview have a look at the PrimeFaces Showcase.

PrimeFaces TreeTable, Tree and HorizontalTree

To display hierarchical data PrimeFaces offers three components:

TreeTable, utilizing a tabular format

Tree, similar to a directory view

HorizontalTree, a horizontal representation

All of these components come with a wide range of extension points suitable for their different use cases. Have a look at the links for sample implementations.

The basic data model used for all three components is the interface TreeNode. Let’s have a look how the default implementation of this interface can be adapted to support lazy loading.

Lazy Loading

Consider a scenario where a large amount of data exists within a hierarchical structure. Think of a very deep tree with a lot of nodes. For instance, the navigator view of your IDE: Hierarchical view on source code in Eclipse

To provide PrimeFaces with this tree upfront might be time and resource consuming. Furthermore, users may browse only a small portion of the complete tree rendering any additional data unnecessary. In this scenario it might be a good idea to only fetch the data needed to display the current status of the tree component. Depending on the actions of the user addtitional data is then loaded when nodes are expanded.

The Tree Model

Lets have a look at TreeNode and its methods used to determine the tree structure:

public interface TreeNode { //... public int getChildCount (); public List < TreeNode > getChildren (); public TreeNode getParent (); public void setParent ( TreeNode treeNode ); public boolean isExpanded (); public boolean isLeaf (); //... }

As you can see, every TreeNode knows about its parent and children. Rendering starts from the root node. For non-expanded nodes isLeaf() is called to determine whether a control for the expansion is to be rendered. For expanded nodes getChildren() is called and all children are rendered as well.

The main idea behind lazy loading is to only retrieve the data needed to render all expanded nodes and their immediate children. The last possible moment to do so is when getChildren() is called on the current node. However, to give the correct values for isLeaf() or getChildCount() it might be required to retrieve the children even when the node is not expanded but its parent is.

The Backend

Let us say we can retrieve our data from a very simple backend service:

public interface BackendService { List < Item > findWithParent ( String parentId ); }

An Item of our data has a unique identifier of type String (any other suitable type is also possible). Our backend supports the retrival of a collection of Item with a given parent identifier. If the parent idenfier is set to null we will get the root items.

We now need to use that service to dynamically construct a PrimeFaces tree model with the strategy we outlined in the last section.

Constructing the Model

PrimeFaces offers a convenient default implementation of TreeNode : DefaultTreeNode . We will extend that class to implement the lazy loading:

public class LazyLoadingTreeNode extends DefaultTreeNode { private BackendService service ; private boolean childrenFetched ; public LazyLoadingTreeNode ( Item data , Service service ) { super ( Item . class . getSimpleName (), data , null ); this . service = service ; } @Override public List < TreeNode > getChildren () { ensureChildrenFetched (); return super . getChildren (); } @Override public int getChildCount () { ensureChildrenFetched (); return super . getChildCount (); } @Override public boolean isLeaf () { ensureChildrenFetched (); return super . isLeaf (); } private void ensureChildrenFetched () { if (! childrenFetched ) { childrenFetched = true ; String parentId = Optional . ofNullable ( getData ()) . map ( Item :: getIdentifier ) . orElse ( null ); List < LazyLoadingTreeNode > childNodes = service . findWithParent ( parentId ) . stream () . map ( item -> new LazyLoadingTreeNode ( item , service )) . collect ( Collectors . toList ()); super . getChildren (). addAll ( childNodes ); } } }

This is a pretty straight forward implementation. Children are retrieved to determine whether a node is a leaf and to provide a correct child count. Actually, this implementation retrieves a little bit too much data for non-expanded nodes. It eagerly fetches the children of an unexpanded node when its parent is expanded.

All we need to do now is to construct the root node within the backing bean of the view. This can be done by new LazyLoadingTreeNode(null, service) . Bind the PrimeFaces component to this TreeNode and you have a lazy tree model.

A big advantage of this implementation is that only minimum assumptions on the data model (the Item class) and the service backend were made. Hence it is very flexible and can be used in many different scenarios where the data ist loaded, for instance, from a database, a directory or a web service.

Note: Item can also be a JPA-Entity that already knows about its children via a OneToMany relation. Then you just need to call Item.getChildren() provided you use the default FetchType.LAZY . In that case no service is needed within our LazyLoadingTreeNode .

n+1 Problem

Our implementation has so far been very simple. However, it does put a lot of stress on the BackendService . Whenever a node is expanded a call to BackendService.findWithParent(...) is made for each child of the expanded node. This is called the n+1-Problem, sinces you need n+1 service calls to provide the expanded node.

This behaviour can be especially problematic if you expected a large number of children to a single node. It may be fine if you have only a small number of children to each node, but beware that the balance of the child count may change over time when your application is used.

If you want to use this solution, you will probably have to enhance the service interface or the Item model. One way to reduce the number of service calls is an additional method to retrieve the isLeaf() status for all children of a node in a single call. This would be the bare minimum of information needed by the PrimeFaces component.

Wrap Up

PrimeFaces offers some nice components for the visualization of hierarchical data. All of them are based on the same model interface TreeNode . We have shown how the default implementation can be altered to allow lazy loading of tree nodes. The presented implementation is easily adaptable for almost every type of backend. If you like our work feel free to use it with your own applications.

The title image Singing Ringing Tree, Panopticon for Burnley was published by David Dixon under CC‑BY 2.0. We cropped it.