Clear Expansions
 Refresh  | 
			
				 | 
			
		
<!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   
     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;
    }
}