[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 &lt;=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>
      &#064;Entity
      &#064;Table("NODE")
      class Item {
  
          &#064;Id &#064;GeneratedValue
          &#064;Column(name = "ID")
          private Long id;
  
          &#064;ManyToOne
          &#064;JoinColumn(name = "PARENT")
          private Item parent;
  
          &#064;OneToMany(mappedBy = "parent")
          private Set&lt;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>
      &#064;Entity
      &#064;Table("NODE")
      class Item extends AbstractNestedSetNode&lt;Item> {
  
          &#064;Id &#064;GeneratedValue
          &#064;Column(name = "ID")
          private Long id;
  
          &#064;ManyToOne
          &#064;JoinColumn(name = "PARENT")
          private Item parent;
  
          &#064;OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
          &#064;org.hibernate.annotations.OnDelete(action = org.hibernate.annotations.OnDeleteAction.CASCADE)
          private Set&lt;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>
      &lt;persistence-unit ...>
          ...
          &lt;properties>
              &lt;property name="hibernate.ejb.event.post-insert"
                        value="nestedset.NestedSetPostInsertEventListener"/>
              &lt;property name="hibernate.ejb.event.post-delete"
                        value="nestedset.NestedSetPostDeleteEventListener"/>
  
          &lt;/properties>
      &lt;/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 &lt; :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&lt;Item> startNodeWrapper = new NestedSetNodeWrapper&lt;Item>(startNode, comparator);
      nestedSetQuery.setResultTransformer( new NestedSetResultTransformer&lt;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