[jboss-cvs] jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset ...
Christian Bauer
christian at hibernate.org
Fri Aug 17 09:00:23 EDT 2007
User: cbauer
Date: 07/08/17 09:00:23
Added: examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset
AbstractNestedSetNode.java package.html
NestedSetPostDeleteEventListener.java
DeleteNestedSetOperation.java
NestedSetResultTransformer.java
NestedSetNodeWrapper.java NestedSetOperation.java
NestedSetPostInsertEventListener.java
InsertNestedSetOperation.java NestedSetNode.java
Log:
Major refactoring of core data schema and some new features
Revision Changes Path
1.1 date: 2007/08/17 13:00:23; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/AbstractNestedSetNode.java
Index: AbstractNestedSetNode.java
===================================================================
/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.seam.wiki.core.nestedset;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
/**
* Utility class that implements the basic {@link NestedSetNode} interface.
* <p>
* This class can be used as a superclass for any entity in your domain model that represents
* a node in a nested set. You will still have to implement the <tt>parent</tt> and
* <tt>children</tt> relationships and map them to the appropriate database foreign key
* columns. However, you are free to pick any collection type for the <tt>children</tt>
* collection.
* <p>
* Use this class if you already have an adjacency list model (parent/children
* relationship mapped with a regular many-to-one property and a one-to-many collection) based
* on a foreign key. You only need to add this superclass to your persistent entity class and
* you will be able to execute nested set queries on your trees and have the event listeners
* update the nested set values of the tree (thread, left, right, of each node) if you add or
* remove nodes.
*
* @author Christian Bauer
*/
@MappedSuperclass
public abstract class AbstractNestedSetNode<N extends NestedSetNode> implements NestedSetNode<N> {
@Column(name = "NS_THREAD", nullable = false, updatable = false)
private Long nsThread = 0l;
@Column(name = "NS_LEFT", nullable = false, updatable = false)
private Long nsLeft = 0l;
@Column(name = "NS_RIGHT", nullable = false, updatable = false)
private Long nsRight = 0l;
public void addChild(N child) {
if (child.getParent() != null) {
child.getParent().getChildren().remove(child);
}
getChildren().add(child);
child.setParent(this);
}
public N removeChild(N child) {
getChildren().remove(child);
child.setParent(null);
return child;
}
public Long getNsThread() {
return nsThread;
}
public void setNsThread(Long nsThread) {
this.nsThread = nsThread;
}
public Long getNsLeft() {
return nsLeft;
}
public void setNsLeft(Long nsLeft) {
this.nsLeft = nsLeft;
}
public Long getNsRight() {
return nsRight;
}
public void setNsRight(Long nsRight) {
this.nsRight = nsRight;
}
public int getDirectChildCount() {
return getChildren().size();
}
public int getTotalChildCount() {
return (int) Math.floor((getNsRight() - getNsLeft()) / 2);
}
public String getTreeSuperclassEntityName() {
return getClass().getSimpleName();
}
}
1.1 date: 2007/08/17 13:00:23; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/package.html
Index: package.html
===================================================================
<html>
<head></head>
<body>
<p>
This package contains the infrastructure for a transparent nested set overlay on an adjacency list representation
of hierarchical data structures.
</p>
<h3>
The Nested Set approach and background information
</h3>
<p>
The nested set approach allows extremely scalably and fast querying of data represented in a hierarchy, a directed
acyclic graph. On the other hand, a nested set approach has extra costs when the tree is modified, so it's only
appropriate for read-mostly trees. Consider the following representation of hierarchical data:
</p>
<pre>
A
/ \
B C
/ \
D E
/\
F G
</pre>
<p>
(Note: This particular graph is a binary tree, however, it doesn't have to be binary or balanced.)
</p>
<p>
This is traditionally implemented with an adjacency list in a SQL DBMS:
</p>
<pre>
NODE
---------------
| ID | PARENT |
---------------
| A | NULL |
| B | A |
| C | A |
| D | C |
| E | C |
| F | D |
| G | D |
---------------
</pre>
<p>
You can now query for the whole subtree of node <tt>C</tt> (or any other node) - this is called a
<i>bill of materials explosion</i> - with the following strategies:
</p>
<ol>
<li>
<p>
<b>Manual Recursion</b>: Select all the children of node <tt>C</tt>, and if these nodes have children, recursively
query until the whole subtree is loaded. This can be implemented with a stored procedure in SQL or by
recursively exeucting <tt>SELECT</tt> statements in the application language. This strategy does not scale.
</p>
</li>
<li>
<p>
<b>Proprietary Recursion:</b> Oracle offers a <tt>CONNECT BY ... PRIOR</tt> extension to standard SQL that executes
a recursive query inside the database, so you only have to execute one <tt>SELECT</tt> in the application language.
This extension is proprietary to Oracle DBMS and has several flaws (conceptually), as documented here:
<a href="http://www.amazon.com/Practical-Issues-Database-Management-Practitioner/dp/0201485559">Practical Issues in
Database Management: A Reference for the Thinking Practitioner by Fabian Pascal</a>.
</p>
</li>
<li>
<p>
<b>Standardized Recursion:</b> The SQL:1999 standard allows a recursive <tt>SELECT</tt> syntax using the
<tt>WITH</tt> clause (subquery factoring). This however also has numerous flaws (see the Pascal book) and
is not implemented by many SQL DBMSs. Furthermore, the implementation is often suboptimal,
with global temporary tables. As a stopgap measure, please remind your SQL DBMS vendor to implement it
(or a proper explode() operator as explained by Pascal), so that workarounds like the nested set or materialized
paths are no longer needed.
</p>
</li>
<li>
<p>
<b>Materialized Path:</b> An additional column named <tt>PATH</tt> is added to the table and the values
are concatenated strings such as <tt>/A/C/D</tt> for the tuple <tt>F</tt>. This path value has to be manipulated
whenever a node is added, deleted, or moved in the tree. You can query for a subtree by using string operations
such as <tt>where NODES.PATH like "/A/C/%"</tt>, which would return all children of <tt>C</tt>. The performance
depends on the query optimizer and index usage for such an operation. The cost of each tree modification is
high, although, it can be implemented with stored procedures in the DBMS.
</p>
</li>
<li>
<p>
<b>Nested Set</b>: A nested set approach is often the most flexible and portable strategy.
</p>
</li>
</ol>
<p>
Consider the following addition of <tt>LEFT</tt> and <tt>RIGHT</tt> values to each node:
</p>
<pre>
NODE
------------------------------
| ID | PARENT | LEFT | RIGHT |
------------------------------
| A | NULL | 1 | 14 |
| B | A | 2 | 3 |
| C | A | 4 | 13 |
| D | C | 5 | 10 |
| E | C | 11 | 12 |
| F | D | 6 | 7 |
| G | D | 8 | 9 |
------------------------------
</pre>
<p>
These values have been created by traversing the tree from top-down from left to right. You can now query for all
children of <tt>C</tt> as follows:
</p>
<pre>
select
count(n1.ID) as NODE_LEVEL,
n1.ID
from
NODE n1, NODE n2
where
n1.LEFT between n2.LEFT and n2.RIGHT
and
n2.LEFT => 4 and n2.RIGHT <=13
group by
n1.ID
order by
n1.LEFT
</pre>
<p>
Which returns the following result:
</p>
<pre>
RESULT
-------------------
| NODE_LEVEL | ID |
-------------------
| 1 | C |
| 2 | D |
| 3 | F |
| 3 | G |
| 2 | E |
-------------------
</pre>
<p>
The disadvantage of the nested set model is the high cost of tree modifications, which require, depending on the
actual data, significant recalculation of left/right values of nodes. This is where the Hibernate implementation
in this package comes into the picture, it can transparently renumber a nested set tree when you modify your
parent/child relationships in Java application code, and it can support you when you query for whole subtrees.
</p>
<p>
(Note: The <tt>PARENT</tt> column of the adjacency list is no longer needed if you have the <tt>LEFT</tt> and
<tt>RIGHT</tt> values of each node - which can also be used to identify the parent of each node. However, the
following examples and the implementation in this package assumes that you keep this column intact and that
you use it for non-nested traversal "up the tree". In other words, this implementation is an overlay
on the adjacency list structure with nested set tree traversal and queries.)
</p>
<h3>
The Hibernate implementation
</h3>
<p>
Assuming that you have an existing adjacency list implementation with a typical parent/child relationship
represented with associations in Java:
</p>
<pre>
@Entity
@Table("NODE")
class Item {
@Id @GeneratedValue
@Column(name = "ID")
private Long id;
@ManyToOne
@JoinColumn(name = "PARENT")
private Item parent;
@OneToMany(mappedBy = "parent")
private Set<Item> children;
// Constructor, getters and setters
...
}
</pre>
<p>
You can now overlay a nested set by either implementing the {@link NestedSetNode} interface or by extending
the default implementation, {@link AbstractNestedSetNode}:
</p>
<pre>
@Entity
@Table("NODE")
class Item extends AbstractNestedSetNode<Item> {
@Id @GeneratedValue
@Column(name = "ID")
private Long id;
@ManyToOne
@JoinColumn(name = "PARENT")
private Item parent;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
@org.hibernate.annotations.OnDelete(action = org.hibernate.annotations.OnDeleteAction.CASCADE)
private Set<Item> children;
// Constructor, getters and setters
...
}
</pre>
<p>
This is a mapped and persistent superclass, so additional columns are either created or required on the <tt>NODE</tt>
table, their default names are <tt>NS_THREAD</tt> (identifies a particular tree), <tt>NS_LEFT</tt>, and
<tt>NS_RIGHT</tt>. You can override using the JPA <tt>@AttributeOverrides</tt> annotation on the <tt>Item</tt> entity
class.
</p>
<p>
It is recommended that you enable <tt>ON DELETE CASCADE</tt> as a foreign key option on the join column of the
adjacency list. With this option, you can easily delete a node in the tree and have the guarantee that all its
children are deleted as well (note that this might conflict with in-memory state if you continue using the
persistence context after deletion or if you have the second-level cache enabled).
</p>
<p>
Enabling cascading persistence on the collection of children is also recommended. If you do not enable it, you need to call
<tt>entityManager.persist(item)</tt> in the right order, e.g.: You need to call persist(A) before you
call persist(B) and persist(C) if B and C are children of A. In any case, the order in which B and C are inserted is
undefined, this is a Set in the example - and it doesn't matter. However, parents need to be inserted before children.
</p>
<p>
The tree is manipulated through the <tt>parent</tt> property and <tt>children</tt> collection of each node. Remember
to always set both references if you link a node to a parent, which is what the default implementations
<tt>addChild()</tt> and <tt>removeChild()</tt> of {@link AbstractNestedSetNode} can do for you. If you want to
save a new item, create it, link it to its parent with <tt>addChild()</tt>, persist it with the
<tt>EntityManager</tt> and flush the persistence context.
If you want to remove an item from the tree, unlink it with <tt>removeChild()</tt> from its parent, remove it
with the <tt>EntityManager</tt>, then flush the persistence context. The nested set tree is automatically
updated by the event listeners in this package, which you have to add to your Hibernate configuration, here
for JPA with <tt>persistence.xml</tt>:
</p>
<pre>
<persistence-unit ...>
...
<properties>
<property name="hibernate.ejb.event.post-insert"
value="nestedset.NestedSetPostInsertEventListener"/>
<property name="hibernate.ejb.event.post-delete"
value="nestedset.NestedSetPostDeleteEventListener"/>
</properties>
</persistence-unit>
</pre>
<p>
Consult the Hibernate documentation if you want to configure them in a different environment. Note that these
new listeners <i>extend</i> the Hibernate EntityManager listeners and that all classes require JDK 5.0. You
can however rewrite the code easily to make it work with plain Hibernate Core in JDK 1.4.
</p>
<p>
To query for a subtree, use the <tt>NestedSetNodeWrapper</tt> and <tt>NestedSetResultTransformer</tt>
convenience classes. An example, loading the whole subtree starting at <tt>startNode</tt> (which would
be an instance of <tt>Item</tt> you have already loaded):
</p>
<pre>
select
count(n1.id) as nestedSetNodeLevel,
n1 as nestedSetNode
from Item n1, Item n2
where
n1.nsThread = :thread and n2.nsThread = :thread
and n1.nsLeft between n2.nsLeft and n2.nsRight
and n2.nsLeft > :startLeft and n2.nsRight < :startRight
group by [allPropertiesOfItem]
order by n1.nsLeft
// Now bind the thread, left, and right values of the startNode as query arguments to the parameters
NestedSetNodeWrapper<Item> startNodeWrapper = new NestedSetNodeWrapper<Item>(startNode, comparator);
nestedSetQuery.setResultTransformer( new NestedSetResultTransformer<Item>(startNodeWrapper) );
nestedSetQuery.list();
</pre>
<p>
You can now traverse the tree by accessing <tt>startNodeWrapper.getWrappedParent()</tt>,
<tt>startNodeWrapper.getWrappedChildren()</tt>, <tt>startNodeWrapper.getLevel()</tt>, and
<tt>startNodeWrapper.getWrappedNode()</tt>. All sub-children are initialized with this single query,
and the <tt>wrappedChildren</tt> collection of the wrapper is sorted with the supplied <tt>Comparator</tt>.
</p>
<p>
Note that moving of nodes between parents is not yet supported by the even listeners. If you remove a node
from a parent, and add it to another parent, the behavior is undefined.
</p>
<p>
Consult the Javadoc of each interface and class for more information. This implementation is licensed under the LGPL,
any modification and distribution of modifications requires distribution of any modified source under the LGPL.
</p>
</body>
</html>
1.1 date: 2007/08/17 13:00:23; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/NestedSetPostDeleteEventListener.java
Index: NestedSetPostDeleteEventListener.java
===================================================================
/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.seam.wiki.core.nestedset;
import org.hibernate.ejb.event.EJB3PostDeleteEventListener;
import org.hibernate.event.PostDeleteEvent;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Executes the nested set tree traversal after a node was deleted.
*
* @author Christian Bauer
*/
public class NestedSetPostDeleteEventListener extends EJB3PostDeleteEventListener {
private static final Log log = LogFactory.getLog(NestedSetPostDeleteEventListener.class);
public void onPostDelete(PostDeleteEvent event) {
super.onPostDelete(event);
if (event.getEntity() instanceof NestedSetNode) {
if (event.getEntity() instanceof NestedSetNode) {
log.debug("executing nested set delete operation, recalculating the tree");
new DeleteNestedSetOperation( (NestedSetNode)event.getEntity() ).execute(event.getSession());
}
}
}
}
1.1 date: 2007/08/17 13:00:23; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/DeleteNestedSetOperation.java
Index: DeleteNestedSetOperation.java
===================================================================
/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.seam.wiki.core.nestedset;
import org.hibernate.Query;
import org.hibernate.StatelessSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.Collection;
/**
* Moves the values of all nodes on the right side of a deleted node.
*
* @author Christian Bauer
*/
class DeleteNestedSetOperation extends NestedSetOperation {
private static final Log log = LogFactory.getLog(DeleteNestedSetOperation.class);
long databaseMoveOffset;
public DeleteNestedSetOperation(NestedSetNode node) {
super(node);
}
protected void beforeExecution() {
databaseMoveOffset = node.getNsRight() - node.getNsLeft() + 1;
log.trace("calculated database offset: " + databaseMoveOffset);
}
protected void executeOnDatabase(StatelessSession ss) {
Query updateLeft =
ss.createQuery("update " + nodeEntityName + " n set " +
" n.nsLeft = n.nsLeft - :offset " +
" where n.nsThread = :thread and n.nsLeft > :right");
updateLeft.setParameter("offset", databaseMoveOffset);
updateLeft.setParameter("thread", node.getNsThread());
updateLeft.setParameter("right", node.getNsRight());
int updateLeftCount = updateLeft.executeUpdate();
log.trace("updated left values of nested set nodes: " + updateLeftCount);
Query updateRight =
ss.createQuery("update " + nodeEntityName + " n set " +
" n.nsRight = n.nsRight - :offset " +
" where n.nsThread = :thread and n.nsRight > :right");
updateRight.setParameter("offset", databaseMoveOffset);
updateRight.setParameter("thread", node.getNsThread());
updateRight.setParameter("right", node.getNsRight());
int updateRightCount = updateRight.executeUpdate();
log.trace("updated right values of nested set nodes: " + updateRightCount);
}
protected void executeInMemory(Collection<NestedSetNode> nodesInPersistenceContext) {
log.trace("updating in memory nodes (flat) in the persistence context: " + nodesInPersistenceContext.size());
for (NestedSetNode n: nodesInPersistenceContext) {
if (n.getNsThread().equals(node.getNsThread())
&& n.getNsRight() > node.getNsRight()) {
n.setNsRight(n.getNsRight() - databaseMoveOffset);
}
if (n.getNsThread().equals(node.getNsThread())
&& n.getNsLeft() > node.getNsRight()) {
n.setNsLeft(n.getNsLeft() - databaseMoveOffset);
}
}
}
}
1.1 date: 2007/08/17 13:00:23; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/NestedSetResultTransformer.java
Index: NestedSetResultTransformer.java
===================================================================
/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.seam.wiki.core.nestedset;
import org.hibernate.transform.ResultTransformer;
import java.util.*;
import java.io.Serializable;
/**
* Transforms a nested set query result into a tree of in-memory linked {@link NestedSetNodeWrapper} instances.
* <p>
* A typical nested set query, for all subtree nodes starting at a particular node, in HQL, looks as follows:
* </p>
* <pre>
* select
* count(n1.id) as nestedSetNodeLevel,
* n1 as nestedSetNode
* from [NestedSetNodeEntityName] n1, [NestedSetNodeEntityName] n2
* where
* n1.nsThread = :thread and n2.nsThread = :thread
* and n1.nsLeft between n2.nsLeft and n2.nsRight
* and n2.nsLeft > :startLeft and n2.nsRight < :startRight
* group by [allPropertiesOfNestedSetNodeEntityClassHierarchy]
* order by n1.nsLeft
* </pre>
* <p>
* The values for <tt>thread</tt>, <tt>startLeft</tt>, and <tt>startRight</tt> parameters are the values of
* the root node of the subtree you want to query for. This start node is not included in the query result.
* </p>
* <p>
* This transformer expects two projected values, with the alias names <tt>nestedSetNodeLevel</tt> and
* <tt>nestedSetNode</tt>. Your query must return these two values in that order. The transformer uses
* these values to build an in-memory tree of linked {@link NestedSetNodeWrapper} instances from the
* flat tree in the query result.
* </p>
* <p>
* You need to manually create a {@link NestedSetNodeWrapper} for the start node of your query (which
* is not included in the query result) and pass it to the constructor of the transformer. For example:
* </p>
* <pre>
* NestedSetNodeWrapper<N> startNodeWrapper = new NestedSetNodeWrapper<N>(startNode, comparator);
* nestedSetQuery.setResultTransformer( new NestedSetResultTransformer<N>(startNodeWrapper) );
* nestedSetQuery.list();
* </pre>
* <p>
* The start node is at level zero. The query returns nothing, the transformer takes each tuple of the
* result and appends it to the tree, on the <tt>startNodeWrapper</tt>. You can now navigate the tree
* you loaded by accessing the <tt>wrappedChildren</tt> collection (recursiveley all the way down) and
* <tt>wrappedParent</tt> property, starting with the <tt>startNodeWrapper</tt> (which doesn't have a
* linked <tt>wrappedParent</tt>). The <tt>wrappedChildrenSorted</tt> colleciton of each wrapper is
* sorted with the given {@link java.util.Comparator}.
* </p>
* <p>
* If you supply a <tt>flattenToLevel</tt> constructor argument, the transformed tree will be flattened
* to the specified level. If, for example, you declare that the tree should be flattened to level 3, all
* nodes that are deeper than level 3 will be appended to the parent in level 3, so that the tree is no
* deeper than 3 levels. This is useful for certain kinds of tree display.
* </p>
* <p>
* A note about restrictions: If the only restriction condition in your query is the one shown above, limiting
* the returned tuples to the nodes of the subtree, you will have a whole and complete subtree, hence, you will
* not have any gaps in the in-memory tree of {@link NestedSetNodeWrapper}s returned by the transformer. However,
* if you add another condition (e.g. "only return tuples <tt>where isMenuItem = true</tt>"), you will have gaps
* in the in-memory tree. These gaps can be recursive, for example, if a subnode B has children C, D, and E, and only
* C, D, and E have the <tt>isMenuItem</tt> flag enabled, they will not be included in the in-memory tree because
* their parent, B, does not have the <tt>isMenuItem</tt> flag enabled. The query won't return B so its children,
* which are returned by the query, can't be linked into the in-memory tree. They will be ignored. This might be
* the correct behavior for building a tree of menu items, but there are certainly situations when you don't want
* these gaps but only restrict what <i>leaf</i> nodes are included in the tree. This is currently not possible with
* the query/transform approach.
* </p>
*
* @author Christian Bauer
*/
public class NestedSetResultTransformer<N extends NestedSetNode> implements ResultTransformer {
Comparator<NestedSetNodeWrapper<N>> comparator;
NestedSetNodeWrapper<N> rootWrapper;
NestedSetNodeWrapper<N> currentParent;
long flattenToLevel = 0;
public NestedSetResultTransformer(NestedSetNodeWrapper<N> rootWrapper, long flattenToLevel) {
this(rootWrapper);
this.flattenToLevel = flattenToLevel;
}
public NestedSetResultTransformer(NestedSetNodeWrapper<N> rootWrapper) {
this.rootWrapper = rootWrapper;
this.comparator = rootWrapper.getComparator();
currentParent = rootWrapper;
}
public NestedSetNodeWrapper<N> getRootWrapper() {
return rootWrapper;
}
public Object transformTuple(Object[] objects, String[] aliases) {
if (!"nestedSetNodeLevel".equals(aliases[0]))
throw new RuntimeException("Missing alias 'nestedSetNodeLevel' as the first projected value in the nested set query");
if (!"nestedSetNode".equals(aliases[1]))
throw new RuntimeException("Missing alias 'nestedSetNode' as the second projected value in the nested set query");
if (objects.length != 2) {
throw new RuntimeException("Nested set query needs to return two values, the level and the nested set node instance");
}
Long nestedSetNodeLevel = (Long)objects[0];
N nestedSetNode = (N)objects[1];
// Connect the tree hierarchically (child to parent, skip child if parent isn't present)
NestedSetNodeWrapper<N> nodeWrapper = new NestedSetNodeWrapper<N>(nestedSetNode, comparator, nestedSetNodeLevel);
if (!nodeWrapper.getWrappedNode().getParent().getId().equals(currentParent.getWrappedNode().getId())) {
NestedSetNodeWrapper<N> foundParent = findParentInTree(nodeWrapper.getWrappedNode().getParent().getId(), currentParent);
if (foundParent != null) {
currentParent = foundParent;
} else {
return null; // Continue
}
}
nodeWrapper.setWrappedParent(currentParent);
currentParent.getWrappedChildren().add(nodeWrapper);
currentParent = nodeWrapper;
return rootWrapper; // Need to return something so that transformList() is called afterwards
}
private NestedSetNodeWrapper<N> findParentInTree(Serializable parentId, NestedSetNodeWrapper<N> startNode) {
if (!parentId.equals(startNode.getWrappedNode().getId()) && startNode.getWrappedParent() != null) {
return findParentInTree(parentId, startNode.getWrappedParent());
} else if (parentId.equals(startNode.getWrappedNode().getId())) {
return startNode;
} else {
return null;
}
}
public List transformList(List list) {
if (flattenToLevel > 0) {
List<NestedSetNodeWrapper<N>> flatChildren = new ArrayList<NestedSetNodeWrapper<N>>();
for(NestedSetNodeWrapper<N> child: rootWrapper.getWrappedChildrenSorted()) {
flattenTree(flatChildren, 1l, child);
}
rootWrapper.setWrappedChildren(flatChildren);
}
return new ArrayList();
}
// Recursively flatten tree
private void flattenTree(List<NestedSetNodeWrapper<N>> flatChildren, long i, NestedSetNodeWrapper<N> wrapper) {
NestedSetNodeWrapper<N> newWrapper = new NestedSetNodeWrapper<N>(wrapper.getWrappedNode(), comparator, i);
flatChildren.add( newWrapper );
if (wrapper.getWrappedChildren().size() > 0 && wrapper.getLevel() < flattenToLevel) {
i++;
for (NestedSetNodeWrapper<N> child : wrapper.getWrappedChildrenSorted()) {
flattenTree(newWrapper.getWrappedChildren(), i, child);
}
} else if (wrapper.getWrappedChildren().size() > 0) {
for (NestedSetNodeWrapper<N> child : wrapper.getWrappedChildrenSorted()) {
flattenTree(flatChildren, i, child);
}
}
}
}
1.1 date: 2007/08/17 13:00:23; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/NestedSetNodeWrapper.java
Index: NestedSetNodeWrapper.java
===================================================================
/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.seam.wiki.core.nestedset;
import java.util.*;
/**
* Wraps a {@link NestedSetNode} and links it into a read-only tree of parent and children.
* <p>
* This wrapper is returned by the {@link NestedSetResultTransformer}. For example,
* you query your tree with a nested set query starting from a particular node. You
* want all children of that start node, including their children, and so on. The
* {@link NestedSetResultTransformer} will handle your query result, which represents
* a flat subtree, and link together the nodes in a hierarchical fashion. You will get
* back your start node in a {@link NestedSetNodeWrapper} and you can access the
* children and their children, and so on, through the <tt>wrappedChildren</tt> collection
* of the wrapper. The regular <tt>children</tt> collection of the wrapped
* {@link NestedSetNode} instances are not initialized! Use the wrapper tree to
* display the data or to work with the whole subtree. As a bonus you also get
* the <tt>level</tt> of each node in the (sub)tree you queried. You can access (but not
* modify) the linked parent of each wrapped node through <tt>wrappedParent</tt>.
* </p>
* <p>
* The <tt>wrappedChildren</tt> of each wrapper are by default in a {@link java.util.List}.
* You can also access the same nodes through the <tt>getWrappedChildrenSorted()</tt> method,
* which returns a {@link java.util.SortedSet} that is sorted with the {@link java.util.Comparator}
* supplied at construction time. This means that in-level sorting (how the children of a particular node
* are sorted) does not occur in the database but in memory. This should not be a performance problem,
* as you'd usually query for quite small subtrees, most of the time to display a
* subtree. The comparator usually sorts the collection by some property of the
* wrapped {@link NestedSetNode}.
* </p>
* <p>
* Note: Do not modify the collections or the parent reference of the wrapper, these
* are read-only results and modifications are not reflected in the database.
* </p>
*
* @author Christian Bauer
*/
public class NestedSetNodeWrapper<N extends NestedSetNode> {
N wrappedNode;
NestedSetNodeWrapper<N> wrappedParent;
List<NestedSetNodeWrapper<N>> wrappedChildren = new ArrayList<NestedSetNodeWrapper<N>>();
Comparator<NestedSetNodeWrapper<N>> comparator;
Long level;
public NestedSetNodeWrapper(N wrappedNode, Comparator<NestedSetNodeWrapper<N>> comparator) {
this(wrappedNode, comparator, 0l);
}
public NestedSetNodeWrapper(N wrappedNode, Comparator<NestedSetNodeWrapper<N>> comparator, Long level) {
this.wrappedNode = wrappedNode;
this.comparator = comparator;
this.level = level;
}
public N getWrappedNode() {
return wrappedNode;
}
public void setWrappedNode(N wrappedNode) {
this.wrappedNode = wrappedNode;
}
public NestedSetNodeWrapper<N> getWrappedParent() {
return wrappedParent;
}
public void setWrappedParent(NestedSetNodeWrapper<N> wrappedParent) {
this.wrappedParent = wrappedParent;
}
public List<NestedSetNodeWrapper<N>> getWrappedChildren() {
return wrappedChildren;
}
public void setWrappedChildren(List<NestedSetNodeWrapper<N>> wrappedChildren) {
this.wrappedChildren = wrappedChildren;
}
public Comparator<NestedSetNodeWrapper<N>> getComparator() {
return comparator;
}
public Long getLevel() {
return level;
}
public SortedSet<NestedSetNodeWrapper<N>> getWrappedChildrenSorted() {
SortedSet<NestedSetNodeWrapper<N>> sortedSet = new TreeSet<NestedSetNodeWrapper<N>>(comparator);
sortedSet.addAll(getWrappedChildren());
return sortedSet;
}
public String toString() {
return "Wrapper on level " + getLevel() + " for: " + getWrappedNode();
}
}
1.1 date: 2007/08/17 13:00:23; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/NestedSetOperation.java
Index: NestedSetOperation.java
===================================================================
/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.seam.wiki.core.nestedset;
import org.hibernate.Session;
import org.hibernate.StatelessSession;
import org.hibernate.event.EventSource;
import org.hibernate.impl.SessionFactoryImpl;
import org.hibernate.util.LazyIterator;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
/**
* The contract of a nested set operation sequence as executed in a Hibernate event listener.
* <p>
* Guarantees that first the database tree nodes are updated, then the in-memory nodes
* currently managed by the persistence context.
* </p>
* To access the database, an operation uses a <tt>StatelessSession</tt> of Hibernate, and
* it obtains a JDBC connection using the Hibernate connection provider. If run in an
* application server with JTS/JTA, the <tt>getConnection()</tt> method
* returns the same connection handle that is already used inside the current transaction.
* This means we run on the same connection and transaction as the rest of the Hibernate flush event
* that executes the <tt>NestedSetOperation</tt>. However, if you run this outside of a managed
* environment, a new JDBC connection might be obtained from the JDBC connection pool.
* In that case, you should enable auto-commit mode in your Hibernate configuration. Or,
* if you want the database tree updates to be atomic and isolated (a good idea), you can
* override the <tt>beforeExecution()</tt> and <tt>afterExecution()</tt> methods and begin
* and commit a database transaction manually. Note that this still would be outside the
* initial connection and transaction, and therefore not be atomic with the overall tree
* manipulation. This can be improved as soon as Hibernate implements a new contract
* for the deprecated <tt>Session#connection()</tt> method.
* </p>
*
* TODO: We should lock the rows we are about to update before the updates run!
*
* @author Christian Bauer
*/
public class NestedSetOperation {
protected NestedSetNode node;
protected String nodeEntityName;
public NestedSetOperation(NestedSetNode node) {
this.node = node;
this.nodeEntityName = node.getTreeSuperclassEntityName();
}
// The main sequence of the operation, override to implement your operation
protected void beforeExecution() {}
protected void executeOnDatabase(StatelessSession statelessSession) {}
protected void executeInMemory(Collection<NestedSetNode> inMemoryState) {}
protected void afterExecution() {}
// The procedure that executes the sequence of the operation
public void execute(EventSource session) {
StatelessSession ss = null;
Connection jdbcConnection = null;
try {
jdbcConnection = getConnection(session);
ss = session.getSessionFactory().openStatelessSession(jdbcConnection);
beforeExecution();
executeOnDatabase(ss);
// Find all NestedSetNode instances in the persistence context
Collection<NestedSetNode> nodesInPersistenceContext = new HashSet<NestedSetNode>();
Iterator contextIterator = new LazyIterator( session.getPersistenceContext().getEntitiesByKey() );
while (contextIterator.hasNext()) {
Object o = contextIterator.next();
if (o instanceof NestedSetNode) nodesInPersistenceContext.add((NestedSetNode)o);
}
executeInMemory(nodesInPersistenceContext);
afterExecution();
} catch (Exception ex) {
throw new RuntimeException(ex);
} finally {
if (ss != null) {
try {
jdbcConnection.close();
ss.close();
} catch(SQLException ex) {
ex.printStackTrace(System.err);
}
}
}
}
protected Connection getConnection(Session session) throws Exception {
// We do not use session.connection() because it conflicts with Hibernates aggressive collection release
return ((SessionFactoryImpl)session.getSessionFactory()).getConnectionProvider().getConnection();
}
}
1.1 date: 2007/08/17 13:00:23; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/NestedSetPostInsertEventListener.java
Index: NestedSetPostInsertEventListener.java
===================================================================
/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.seam.wiki.core.nestedset;
import org.hibernate.ejb.event.EJB3PostInsertEventListener;
import org.hibernate.event.PostInsertEvent;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Executes the nested set tree traversal after a node was inserted.
*
* @author Christian Bauer
*/
public class NestedSetPostInsertEventListener extends EJB3PostInsertEventListener {
private static final Log log = LogFactory.getLog(NestedSetPostInsertEventListener.class);
public void onPostInsert(PostInsertEvent event) {
super.onPostInsert(event);
if (event.getEntity() instanceof NestedSetNode) {
log.debug("executing nested set insert operation, recalculating the tree");
new InsertNestedSetOperation( (NestedSetNode)event.getEntity() ).execute(event.getSession());
}
}
}
1.1 date: 2007/08/17 13:00:23; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/InsertNestedSetOperation.java
Index: InsertNestedSetOperation.java
===================================================================
/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.seam.wiki.core.nestedset;
import org.hibernate.StatelessSession;
import org.hibernate.Query;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.Collection;
/**
* Moves the values of all nodes on the right side of an inserted node.
*
* @author Christian Bauer
*/
class InsertNestedSetOperation extends NestedSetOperation {
private static final Log log = LogFactory.getLog(InsertNestedSetOperation.class);
long spaceNeeded = 2l;
long parentThread;
long newLeft;
long newRight;
public InsertNestedSetOperation(NestedSetNode node) {
super(node);
}
protected void beforeExecution() {
if (node.getParent() == null) {
// Root node of a tree, new thread
parentThread = node.getId();
newLeft = 1l;
newRight = 2l;
} else {
// Child node of a parent
parentThread = node.getParent().getNsThread();
newLeft = node.getParent().getNsRight();
newRight = newLeft + spaceNeeded -1;
}
log.trace("calculated the thread: " + parentThread + " left: " + newLeft + " right: " + newRight);
}
protected void executeOnDatabase(StatelessSession ss) {
Query updateLeft =
ss.createQuery("update " + nodeEntityName + " n set " +
" n.nsLeft = n.nsLeft + :spaceNeeded " +
" where n.nsThread = :thread and n.nsLeft > :right");
updateLeft.setParameter("spaceNeeded", spaceNeeded);
updateLeft.setParameter("thread", parentThread);
updateLeft.setParameter("right", newLeft);
int updateLeftCount = updateLeft.executeUpdate();
log.trace("updated left values of nested set nodes: " + updateLeftCount);
Query updateRight =
ss.createQuery("update " + nodeEntityName + " n set " +
" n.nsRight = n.nsRight + :spaceNeeded " +
" where n.nsThread = :thread and n.nsRight >= :right");
updateRight.setParameter("spaceNeeded", spaceNeeded);
updateRight.setParameter("thread", parentThread);
updateRight.setParameter("right", newLeft);
int updateRightCount = updateRight.executeUpdate();
log.trace("updated right values of nested set nodes: " + updateRightCount);
log.trace("updating the newly inserted row with thread, left, and right values");
Query updateNode =
ss.createQuery("update " + nodeEntityName + " n set " +
" n.nsLeft = :left, n.nsRight = :right, n.nsThread = :thread " +
" where n.id = :nid");
updateNode.setParameter("thread", parentThread);
updateNode.setParameter("left", newLeft);
updateNode.setParameter("right", newRight );
updateNode.setParameter("nid", node.getId());
updateNode.executeUpdate();
}
protected void executeInMemory(Collection<NestedSetNode> nodesInPersistenceContext) {
log.trace("updating in memory nodes (flat) in the persistence context: " + nodesInPersistenceContext.size());
for (NestedSetNode n : nodesInPersistenceContext) {
if (n.getNsThread().equals(parentThread) && n.getNsLeft() > newLeft) {
n.setNsLeft(n.getNsLeft() + spaceNeeded);
}
if (n.getNsThread().equals(parentThread) && n.getNsRight() >= newLeft) {
n.setNsRight(n.getNsRight() + spaceNeeded);
}
}
}
protected void afterExecution() {
// Set the values of the "read-only" properties
node.setNsThread(parentThread);
node.setNsLeft(newLeft);
node.setNsRight(newRight);
}
}
1.1 date: 2007/08/17 13:00:23; author: cbauer; state: Exp;jboss-seam/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/NestedSetNode.java
Index: NestedSetNode.java
===================================================================
/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.seam.wiki.core.nestedset;
import java.util.Collection;
/**
* Interface implemented by domain model classes that represent a node in a nested set.
*
* @author Christian Bauer
*/
public interface NestedSetNode<N extends NestedSetNode> {
/**
* Every node in a nested set needs a stable identifier, the primary key. This currently
* is limited to numeric (long) values because the nested set model uses the identifier
* of a root node (a node with no parent) to also identify a particular tree (the thread).
*
* @return the stable primary key of the nested set node
*/
public Long getId();
/**
* Nested set updates require that direct DML is executed by event listeners, this is
* the name of the entity that is used by the event listeners. You can in most cases
* return the simple class name of an instance, unless you customize your persistent
* entity identifiers or when inheritance is involved. If, for example, you have a
* class named <tt>FileSystemNode</tt> that implements <tt>NestedSetNode</tt>, and this
* class has subclasses <tt>RegularFile</tt> and <tt>Directory</tt>, all instances need
* to return <tt>FileSystemNode</tt> so that all nodes in the tree can be reached when
* the nested set manipulation occurs in event listeners.
*
* @return the persistent entity name of the superclass that implements this interface
*/
public String getTreeSuperclassEntityName();
/**
* Utility method required until TODO: http://opensource.atlassian.com/projects/hibernate/browse/HHH-1615
* is implemented. If you query for nested set subtrees, you need to group by all properties of
* the nested set node implementation class (in fact, the whole hierarchy). So for example,
* this method would need to return all scalar property names and foreign key property names of
* classes <tt>FileSystemNode</tt> and its potential subclasses <tt>RegularFile</tt> and
* <tt>Directory</tt>. Yes, this is not not great.
*
* @return all property names of scalar and foreign key properties of the nested set class hierarchy
*/
public abstract String[] getTreeSuperclassPropertiesForGrouping();
/**
* An implementation must return the parent instance of a node in the tree, this can be mapped
* as a regular many-to-one. This property should be nullable, that is, the root node of a thread
* (a thread represents a single tree) does not have a parent. Although not strictly required by
* the nested set approach (children of a parent can be identified solely by their "left" and
* "right" values), it simplifies regular navigation "up the tree".
*
* @return the parent of this nested set node
*/
public N getParent();
public void setParent(N parent);
/**
* An implementation must return the direct children (the sublevel) of a nested set node. This
* can be mapped as a regular one-to-many collection, however, you are free to chose the type
* of collection: lists, bags, and sets all work. This collection is the collection you need
* to modify if you want to link a child node to a parent (by adding an element) or if you
* delete a node (by removing an element).
*
* @return the immediate children of a nested set node
*/
public Collection<N> getChildren();
/**
* Convenience method that should link a node into the tree by adding it to the <tt>children</tt>
* collection and setting its <tt>parent</tt> to be <i>this</i>.
*
* @param child the child node to be added at this level in the tree
*/
public void addChild(N child);
/**
* Convenience method that should remove a node from this level of the tree by removing it from
* the <tt>children</tt> collection and setting its <tt>parent</tt> to null. Called before a
* node is finally deleted in a persistent fashion, in your application.
*
* @param child the child node that is removed from the tree
* @return the removed child node
*/
public N removeChild(N child);
/**
* The root of a tree is a nested set node without a parent. Its identifier is also the thread
* number of all nodes in that particular tree. So all children nodes (and their children, recursively)
* need to have the same thread number. This should be mapped as a persistent property of the
* implementor of this interface, not nullable and not updatable. Any updates that are required are
* done transparently with event listeners.
*
* @return the non-nullable, persistent, and not updatable mapped persistent identifier for a particular tree
*/
public Long getNsThread();
public void setNsThread(Long nsThread);
/**
* In the nested set model, each node requires two additional attributes right visit and left visit. The tree is
* then traversed in a modified pre-order: starting with the root node, each node is visited twice. Whenever
* a node is entered or exited during the traversal, the sequence number of all visits is saved in
* the current node's right visit and left visit. This is the job of the event listeners, not yours. You can
* retrieve the current value with this method.
*
* @return the left value of a node
*/
public Long getNsLeft();
public void setNsLeft(Long nsLeft);
/**
* In the nested set model, each node requires two additional attributes right visit and left visit. The tree is
* then traversed in a modified pre-order: starting with the root node, each node is visited twice. Whenever
* a node is entered or exited during the traversal, the sequence number of all visits is saved in
* the current node's right visit and left visit. This is the job of the event listeners, not yours. You can
* retrieve the current value with this method.
*
* @return the left value of a node
*/
public Long getNsRight();
public void setNsRight(Long nsRight);
/**
* The number of children of this node, only one level deep.
*
* @return the number of children of this node, one level deep.
*/
public int getDirectChildCount();
/**
* The number of child nodes of this node, total at all sub-levels. This can be calculated by taking
* the left and right values: <tt>Math.floor((getNsRight() - getNsLeft()) / 2)</tt>
*
* @return the total number of children on all sub-levels
*/
public int getTotalChildCount();
}
More information about the jboss-cvs-commits
mailing list