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;
}
}