Tree From Database, With Zones

Here we demonstrate Tapestry's Tree component built from data in the database. When you click on a leaf, its info is displayed in a Zone.

The data is a partial list of the Dewey Decimal Classifications. It is stored in table Classification.
Each Classification can have "child" Classifications. By definition, the leaf Classifications have no children, and the root Classifications have no parent.
ClassificationNode is a convenience wrapper around a Classification, summarising whether it has children or is instead a leaf.
Clear Expansions


Refresh
Tree is actually an AJAX component. When you expand or collapse a node it sends event EXPAND_CHILDREN or MARK_COLLAPSED via AJAX.
If you have provided a selectionModel (we have not), when you click a leaf the Tree sends event NODE_SELECTED or NODE_UN_SELECTED.
These events bubble up, so you can add your own event handlers for them.

In this example we did not use a selectionModel. Instead, we overrode the label block with our own and embedded an AJAX EventLink because:
References: Tree, TreeModel, DefaultTreeModel, TreeModelAdapter, ValueEncoder, @InjectComponent, EventLink, Ajax and Zones, Zone, AjaxResponseRenderer.

Home


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<!-- Based on an example kindly provided by George Christman and Lance Java. -->

<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd" xmlns:p="tapestry:parameter">
<body class="container">
    <h3>Tree From Database, With Zones</h3>
    
    <noscript class="js-required">
        ${message:javascript_required}
    </noscript>     

    Here we demonstrate Tapestry's Tree component built from data in the database. When you click on a leaf, its info is displayed in a Zone.<br/><br/>
    
    The data is a partial list of the <a href="http://en.wikipedia.org/wiki/List_of_Dewey_Decimal_classes">Dewey Decimal Classifications</a>. 
    It is stored in table Classification. <br/>
    Each Classification can have "child" Classifications. By definition, the leaf Classifications have no children, and the root Classifications have no parent.<br/>
    ClassificationNode is a convenience wrapper around a Classification, summarising whether it has children or is instead a leaf.
    
    <div class="eg">
        <table>
        <tr>
        
            <td id="treeSide">
                <t:if test="hasResults">
                    <t:eventLink event="clearExpansions" async="true" href="#">Clear Expansions</t:eventLink><br/><br/>

                    <t:zone t:id="treeZone" id="treeZone">
                        <t:tree t:id="Tree" model="treeModel" node="treeNode" value="classificationNode">
                            <p:label>
                                <t:if test="treeNode.leaf">
                                    <t:eventLink event="leafSelected" context="classificationNode.classification.id" 
                                        async="true" class="prop:leafClass" href="#">
                                        ${treeNode.label}
                                    </t:eventLink>
                                </t:if>
                                <t:if test="!treeNode.leaf">
                                    ${treeNode.label}
                                </t:if>
                            </p:label>
                        </t:tree>
                    </t:zone><br/>

                    <t:eventLink event="refresh" async="true" href="#">Refresh</t:eventLink>
                </t:if>
                <t:if test="!hasResults">
                    <span style="color: red;">Database data has not been set up!</span>
                </t:if>
            </td>

            <td id="selectedSide">

                <t:zone t:id="selectedZone" id="selectedZone">
                    <t:if test="selectedClassification">
                        You selected:<br/>
                        ${selectedClassification.label}
                    </t:if>
                </t:zone>

            </td>
            
        </tr>
        </table>
    </div>
    
    Tree is actually an AJAX component. When you expand or collapse a node it sends event EXPAND_CHILDREN or MARK_COLLAPSED via AJAX.<br/>
    If you have provided a selectionModel (we have not), when you click a leaf the Tree sends event NODE_SELECTED or NODE_UN_SELECTED.<br/>
    These events bubble up, so you can add your own event handlers for them.<br/><br/>
    
    In this example we did not use a selectionModel. Instead, we overrode the label block with our own and embedded an AJAX EventLink because:<br/>
    <ul>
        <li>EventLink renders returned Zones, whereas Tree ignores Zones returned from NODE_SELECTED and NODE_UN_SELECTED.</li>
        <li>Tree's selectionModel enables multiple nodes to be selected. Its purpose seems to be to gather your selections until a submit.</li>
    </ul> 
    
    References: 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/Tree.html">Tree</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/tree/TreeModel.html">TreeModel</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/tree/DefaultTreeModel.html">DefaultTreeModel</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/tree/TreeModelAdapter.html">TreeModelAdapter</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/ValueEncoder.html">ValueEncoder</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/annotations/InjectComponent.html">@InjectComponent</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/EventLink.html">EventLink</a>, 
    <a href="http://tapestry.apache.org/ajax-and-zones.html">Ajax and Zones</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/Zone.html">Zone</a>, 
        <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/ajax/AjaxResponseRenderer.html">AjaxResponseRenderer</a>.<br/><br/> 

    <a t:type="eventlink" t:event="Home" href="#">Home</a><br/><br/>

    <t:tabgroup>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/ajax/TreeFromDatabaseWithZones.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/ajax/TreeFromDatabaseWithZones.java"/>
        <t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/treefromdatabasewithzones.css"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/models/examples/tree/ClassificationTreeModelAdapter.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/business/domain/dewey/iface/ClassificationNode.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/business/domain/dewey/DeweyFinderService.java"/>
        <t:sourcecodetab src="/business/src/main/java/jumpstart/business/domain/dewey/Classification.java"/>
    </t:tabgroup>
</body>
</html>


// Based on an example kindly provided by George Christman and Lance Java.

package jumpstart.web.pages.examples.ajax;

import java.util.List;

import javax.ejb.EJB;

import jumpstart.business.domain.dewey.Classification;
import jumpstart.business.domain.dewey.iface.ClassificationNode;
import jumpstart.business.domain.dewey.iface.IDeweyFinderServiceLocal;
import jumpstart.web.models.examples.tree.ClassificationTreeModelAdapter;
import jumpstart.web.pages.Index;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.ValueEncoder;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Tree;
import org.apache.tapestry5.corelib.components.Zone;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;
import org.apache.tapestry5.tree.DefaultTreeModel;
import org.apache.tapestry5.tree.TreeModel;
import org.apache.tapestry5.tree.TreeNode;

@Import(stylesheet = "css/examples/treefromdatabasewithzones.css")
public class TreeFromDatabaseWithZones {

    // Screen fields

    private TreeModel<ClassificationNode> treeModel;

    @Property
    private TreeNode<ClassificationNode> treeNode;

    @Property
    private ClassificationNode classificationNode;

    @Property
    private Classification selectedClassification;

    // Generally useful bits and pieces

    @InjectComponent
    private Tree tree;

    @InjectComponent
    private Zone treeZone;

    @InjectComponent
    private Zone selectedZone;

    @Inject
    private AjaxResponseRenderer ajaxResponseRenderer;
    
    @Inject
    private Request request;

    @EJB
    private IDeweyFinderServiceLocal deweyFinderService;

    @Inject
    private ComponentResources componentResources;

    // The code

    void onClearExpansions() {
        tree.clearExpansions();

        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(treeZone).addRender(selectedZone);
        }
    }

    void onRefresh() {
        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(treeZone).addRender(selectedZone);
        }
    }

    void onLeafSelected(Integer classificationId) {
        ClassificationNode classificationNode = deweyFinderService.findClassificationInfo(classificationId);
        selectedClassification = classificationNode.getClassification();
        
        if (request.isXHR()) {
            ajaxResponseRenderer.addRender(treeZone).addRender(selectedZone);
        }
    }

    Object onHome() {
        componentResources.discardPersistentFieldChanges();
        return Index.class;
    }

    // Getters and setters

    public TreeModel<ClassificationNode> getTreeModel() {
        if (treeModel == null) {
            ValueEncoder<ClassificationNode> encoder = new ValueEncoder<ClassificationNode>() {

                @Override
                public String toClient(ClassificationNode node) {
                    return node.getClassification().getId().toString();
                }

                @Override
                public ClassificationNode toValue(String node) {
                    return deweyFinderService.findClassificationInfo(new Integer(node));
                }
            };

            treeModel = new DefaultTreeModel<ClassificationNode>(encoder, new ClassificationTreeModelAdapter(deweyFinderService),
                    deweyFinderService.findRoots());
        }
        return treeModel;
    }

    public List<ClassificationNode> getHasResults() {
        return deweyFinderService.findRoots();
    }

    public String getLeafClass() {
        if (selectedClassification != null && classificationNode.getClassification().equals(selectedClassification)) {
            return "selected";
        }
        else {
            return "";
        }
    }

}


.eg {
                margin: 20px 0;
                padding: 14px;
                color: #888;
                border: 1px solid #ddd;
                border-radius: 6px;
                -webkit-border-radius: 6px;
                -mox-border-radius: 6px;
}

.js-required {
                color: red;
                display: block;
                margin-bottom: 14px;
}

.js-recommended {
                color: red;
                display: block;
                margin-bottom: 14px;
}

#jump-tree-table {
                
}

#treeSide {
                width: 50%;
                background-color: #eee;
                padding: 20px;
}

#treeZone {
                background-color: #eee;
}

.tree-label {
                color: #444;
}

.tree-label a {
                text-decoration: none;
                cursor: pointer;
                color: #444;
}

div.tree-container li.last {
                background-color: #eee;
} /* Without this the last label will be white. */

.selected {
                font-weight: bold;
}

#selectedSide {
                width: 50%;
                background-color: #eee;
                padding: 20px;
                vertical-align: top;
                color: #444;
}

#selectedZone {
                padding: 30px;
                background-color: #eee;
}


// Based on an example kindly provided by George Christman and Lance Java.

package jumpstart.web.models.examples.tree;

import java.util.List;

import jumpstart.business.domain.dewey.iface.ClassificationNode;
import jumpstart.business.domain.dewey.iface.IDeweyFinderServiceLocal;

import org.apache.tapestry5.tree.TreeModelAdapter;

public class ClassificationTreeModelAdapter implements TreeModelAdapter<ClassificationNode> {

    private IDeweyFinderServiceLocal deweyFinderService;

    public ClassificationTreeModelAdapter(IDeweyFinderServiceLocal deweyFinderService) {
        this.deweyFinderService = deweyFinderService;
    }

    public List<ClassificationNode> getChildren(ClassificationNode node) {
        return deweyFinderService.getChildren(node);
    }

    public boolean isLeaf(ClassificationNode node) {
        return node.isLeaf();
    }

    public boolean hasChildren(ClassificationNode node) {
        return node.hasChildren();
    }

    public String getLabel(ClassificationNode node) {
        return node.getClassification().getLabel();
    }
}


// Based on an example kindly provided by George Christman and Lance Java.

package jumpstart.business.domain.dewey.iface;

import java.io.Serializable;

import jumpstart.business.domain.dewey.Classification;

/**
 * ClassificationNode is a convenience wrapper around a Classification, summarising whether it has children or is a leaf.
 */
@SuppressWarnings("serial")
public class ClassificationNode implements Serializable {

    private Classification classification;
    private boolean isLeaf;
    private boolean hasChildren;
    
    @Override
    public String toString() {
        return "ClassificationNode [classification=" + classification + ", isLeaf=" + isLeaf + ", hasChildren=" + hasChildren + "]";
    }

    public Classification getClassification() {
        return classification;
    }

    public void setClassification(Classification classification) {
        this.classification = classification;
    }

    public boolean hasChildren() {
        return hasChildren;
    }

    public void setHasChildren(boolean hasChildren) {
        this.hasChildren = hasChildren;
    }

    public boolean isLeaf() {
        return isLeaf;
    }

    public void setIsLeaf(boolean isLeaf) {
        this.isLeaf = isLeaf;
    }

}


// Based on an example kindly provided by George Christman and Lance Java.

package jumpstart.business.domain.dewey;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.ejb.Local;
import javax.ejb.Remote;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import jumpstart.business.domain.dewey.iface.ClassificationNode;
import jumpstart.business.domain.dewey.iface.IDeweyFinderServiceLocal;
import jumpstart.business.domain.dewey.iface.IDeweyFinderServiceRemote;

@Stateless
@Local(IDeweyFinderServiceLocal.class)
@Remote(IDeweyFinderServiceRemote.class)
public class DeweyFinderService implements IDeweyFinderServiceLocal, IDeweyFinderServiceRemote {

    @PersistenceContext(unitName = "jumpstart")
    private EntityManager em;

    public Classification findClassification(Integer id) {
        return em.find(Classification.class, id);
    }

    public ClassificationNode findClassificationInfo(Integer id) {
        List<Classification> classifications = new ArrayList<Classification>();
        classifications.add(findClassification(id));
        return getClassificationInfo(classifications).iterator().next();
    }

    public List<ClassificationNode> getChildren(ClassificationNode node) {
        List<Classification> classifications = findChildren(node.getClassification().getId());
        return getClassificationInfo(classifications);
    }

    public List<ClassificationNode> findRoots() {
        List<Classification> classifications = findClassificationsWithNoParent();
        if (classifications == null) {
            return new ArrayList<ClassificationNode>();
        }
        return getClassificationInfo(classifications);
    }

    @SuppressWarnings("unchecked")
    private List<Classification> findChildren(Integer id) {
        return em.createQuery("select c from Classification c where c.parent.id = :id").setParameter("id", id)
                .getResultList();
    }

    @SuppressWarnings("unchecked")
    private List<Classification> findClassificationsWithNoParent() {
        return em.createQuery("select c from Classification c where c.parent is null").getResultList();
    }

    /**
     * Given a list of Classifications, returns a list of ClassificationNodes. Each one represents a Classification and some info about
     * the Classification: specifically whether it has children or is a leaf.
     */
    @SuppressWarnings("unchecked")
    private List<ClassificationNode> getClassificationInfo(List<Classification> classifications) {
        Map<Integer, ClassificationNode> classificationNodesById = new LinkedHashMap<Integer, ClassificationNode>();

        // Build a map of skeleton ClassificationNodes by Classification id - one entry per given Classification.

        for (Classification classification : classifications) {
            ClassificationNode classificationNode = new ClassificationNode();
            classificationNode.setClassification(classification);
            classificationNodesById.put(classification.getId(), classificationNode);
        }

        if (!classificationNodesById.isEmpty()) {

            // Query whether each Classification has children.

            StringBuilder buf = new StringBuilder();

            // This JPQL query should have worked but Hibernate translates the count to "count(.)" which is invalid SQL
            // (a Hibernate bug?)...
            // buf.append("select c1.id, count(c1.children) from Classification c1");
            // buf.append(" where c1.id in (:catIds) ");
            // buf.append(" group by c1.id");
            // Query q = em.createQuery(buf.toString());

            // ...so we use a native query instead
            buf.append("select c1.id, count(c2.id) from Classification c1");
            buf.append(" left join Classification c2 on c2.parentId = c1.id");
            buf.append(" where c1.id in (:catIds) ");
            buf.append(" group by c1.id");
            Query q = em.createNativeQuery(buf.toString());

            q.setParameter("catIds", classificationNodesById.keySet());
            List<Object[]> l = q.getResultList();

            // Update each Classification's corresponding ClassificationNode in the map with whether it has children or is a leaf.

            for (Object[] result : l) {
                Integer classificationId = (Integer) result[0];
                int childCount = ((Number) result[1]).intValue();

                ClassificationNode classificationNode = classificationNodesById.get(classificationId);
                classificationNode.setHasChildren(childCount != 0);
                classificationNode.setIsLeaf(childCount == 0);
            }
        }

        return new ArrayList<ClassificationNode>(classificationNodesById.values());
    }

}


// Based on an example kindly provided by George Christman and Lance Java.

package jumpstart.business.domain.dewey;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Version;

/**
 * Represents a Dewey Decimal Classification. See http://en.wikipedia.org/wiki/List_of_Dewey_Decimal_classes.
 */
@Entity
@SuppressWarnings("serial")
public class Classification implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(nullable = false)
    private Integer id;

    @Version
    @Column(nullable = false)
    private Integer version;

    private String label;

    /**
     * Classification is in an AGGREGATION relationship with itself. Here are its child classifications and parent
     * Classification. Be careful: cycles are not allowed.
     */

    // Do not cascade REMOVE because this is only an AGGREGATION relationship, not a COMPOSITION relationship.
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    private Set<Classification> children = new HashSet<Classification>();

    @ManyToOne
    @JoinColumn(name = "parentId")
    private Classification parent;

    public String toString() {
        final String DIVIDER = ", ";

        StringBuilder buf = new StringBuilder();
        buf.append(this.getClass().getSimpleName() + ": ");
        buf.append("[");
        buf.append("id=" + id + DIVIDER);
        buf.append("version=" + version + DIVIDER);
        buf.append("label=" + label);
        buf.append("]");
        return buf.toString();
    }

    // The need for an equals() method is discussed at http://www.hibernate.org/109.html

    @Override
    public boolean equals(Object obj) {
        return (obj == this) || (obj instanceof Classification) && id != null
                && id.equals(((Classification) obj).getId());
    }

    // The need for a hashCode() method is discussed at http://www.hibernate.org/109.html

    @Override
    public int hashCode() {
        return id == null ? super.hashCode() : id.hashCode();
    }

    public Integer getId() {
        return id;
    }

    public Integer getVersion() {
        return version;
    }

    public Set<Classification> getChildren() {
        return children;
    }

    public void setChildren(Set<Classification> children) {
        this.children = children;
    }

    public Classification getParent() {
        return parent;
    }

    public void setParent(Classification parent) {
        this.parent = parent;
    }

    public String getLabel() {
        return label;
    }

    public void setLabel(String label) {
        this.label = label;
    }
}