I was asked to provide a quick solution for presenting search results in a tree structure fashion rather than the classic paged flat list. Considered the premises, read limited time, I needed to find something somewhat already cut for the job and that I could tweak just a little and be done with. So, while searching for that I came across this blog post: Basic Org Charts With xe:navigator and xe:beanTreeNode.
That post brought close enough to what I needed but it presented a limitation: the way it was initialized and managed – managed bean – would not allow me to reset it and present new content after a new search query. However it had some potential, so I started digging a little to work around the problem.
What I came up with was making use of the binding property to interact with the object more flexibly.
Consider the following snippet:
<xe:navigator id="navigator" binding="#{ctrl.resultTreeNavigator}"
expandable="true" expandLevel="0" styleClass="nav-condensed"
rendered="#{not empty ctrl.results}" />
As you can see, I didn’t declare the <xe:beanTreeNode>
tag. That’s because I want to interact with the navigator data more flexibly and that is done through the binding. The binding is important in order to gain easy access to the navigator and inject data directly from our controller class rather than burdening the beanTreeNode class and instructing it on how to find our controller class.
By means of the controller class #{ctrl}
we bind the navigator object through the methods setResultTreeNavigator
and getResultTreeNavigator
.
The controller class
public class MyPageController extends StandardXPageController {
private static final long serialVersionUID = 1L;
private enum PageVariable implements ScopeVariable {
RESULT_TREE_NAVIGATOR;
XPagesScope s;
private PageVariable() {
this(XPagesScope.REQUEST);
}
private PageVariable(XPagesScope s) {
this.s = s;
}
public XPagesScope getScope() {
return this.s;
}
}
private List<MyDTO> results;
private static final String[] levels = { "level1" , "level2" , "level3" };
public UIOutlineNavigator getResultTreeNavigator() {
UIOutlineNavigator nav = (UIOutlineNavigator) getScopeVariable(PageVariable.RESULT_TREE_NAVIGATOR);
return nav;
}
public void setResultTreeNavigator(UIOutlineNavigator iterator) {
setScopeVariable(PageVariable.RESULT_TREE_NAVIGATOR, iterator);
}
public List<MyDTO> getResults() {
return results;
}
public void search() {
MyDAO dao = Factory.getMyDAO();
results = dao.search(/* my logic, not important for this example*/);
Map tree = new TreeMap<String, Object>();
/*
* This methods reads the flat list of results and reorganizes them
* as nested maps according to the number of levels defined
*/
populateTree(tree, 0, f);
UIOutlineNavigator nav = getResultTreeNavigator();
// TreeNodes might not have been initialized
if (nav.getTreeNodes() != null) {
nav.getTreeNodes().clear();
}
nav.addNode(new ResultTreeNode(collectionTree));
}
@SuppressWarnings("unchecked")
private void populateTree(Object o, int level, MyDTO my) {
if (level < levels.length) {
String label = my.getString(levels[level]);
Map<String, Object> m = (Map) o;
if (!m.containsKey(label)) {
m.put(label, level < levels.length - 1 ? new TreeMap<String, Object>()
: new ArrayList<MyDTO>());
}
populateTree(m.get(label), level + 1, my);
} else {
List<MyDTO> l = (List) o;
l.add(my);
}
}
}
Assume the search
method is being invoked by a search button on the page. Now, after the data has been fetched and stored in the results
variable, what I’m going to do is to arrange the result list in a way that can be easily understood by the TreeNode class without it knowing anything about it was composed. Such arrangement is taken care by the populateTree
method.
Virtually, this design allows for as many nested levels as you want. Their number depends on the private variable levels
Now that the tree
variable is ready I serve it to my BasicNodeList implementation class ResultTreeNode
.
The BasicNodeList implementation class
public class ResultTreeNode extends BasicNodeList implements StateHolder {
private static final long serialVersionUID = 1L;
public ResultTreeNode(Map<String, Object> map) {
init(map);
}
private void init(Map<String, Object> map) {
try {
if (map != null && !map.isEmpty()) {
for (Entry<String, Object> entry : map.entrySet()) {
populate(entry, null);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@SuppressWarnings("unchecked")
private void populate(Entry<?, ?> entry, BasicTreeNode parent) {
if (entry.getValue() instanceof Map) {
BasicTreeNode node = new BasicContainerTreeNode();
node.setLabel((String) entry.getKey());
if (parent == null) {
addChild(node);
} else {
((BasicContainerTreeNode) parent).addChild(node);
}
for (Entry<?, ?> subEntry : ((Map<?, ?>) entry.getValue()).entrySet()) {
populate(subEntry, node);
}
} else if (entry.getValue() instanceof List) {
BasicTreeNode node = new BasicContainerTreeNode();
node.setLabel((String) entry.getKey());
((BasicContainerTreeNode) parent).addChild(node);
for (Object o : (List<?>) entry.getValue()) {
MyDTO my = (MyDTO) o;
BasicLeafTreeNode child = new BasicLeafTreeNode();
child.setLabel(my.getString("my_label"));
child.setHref(/*whatever*/);
((BasicContainerTreeNode) node).addChild(child);
}
}
}
@Override
public boolean isTransient() {
// TODO Auto-generated method stub
return true;
}
@Override
public void restoreState(FacesContext paramFacesContext, Object paramObject) {
// Do nothing
}
@Override
public Object saveState(FacesContext paramFacesContext) {
return null;
}
@Override
public void setTransient(boolean paramBoolean) {
// Do nothing
}
}
The result
The following is a screenshot I have taken from the application that implemented the solution I illustrated:
Final notes
The code hasn’t been tested for robustness or having in mind performance. Take it therefore for what it is, a proof of concept.