Wednesday, October 6, 2010

Manage Hierarchical data using Spring, JPA and Aspects




Managing hierarchical data using two dimentional tables is a pain. There are some patterns to reduce this pain. One such solution is described at here. This article is about implementing the same using Spring, JPA, Annotations and Aspects. Please go through the is link to better understand this solution described. The purpose is to come up with a component that will remove the boiler-plate code in business layer to handle hierarchical data.


Summary
  • Create base class for Entities used to represent Hierarchical data
  • Create annotation classes
  • Code the Aspect that will execute addional steps for managing Hierarchical data. (Heart of the solution)
  • Now the Aspect can be used everywhere Hierarchical data is used.


Detail
  • Create base class for Entities used to represent Hierarchical data.
    The purpose of the super class is to encapsulate all the common attrubutes and operations required for managing hierarchical data in a table. Please note that the class is annotated as @MappedSuperclass.
    The methods are meant to generate queies required to perform CRUD operations on the Table. Their use will be more clear later in the article when we will revisit HierarchicalEntity.

    Now any Entity that extends this class will have all the attribues required to manage hierarchical data.


    import com.es.clms.aspect.HierarchicalEntity;
    import javax.persistence.EntityListeners;
    import javax.persistence.MappedSuperclass;

    @MappedSuperclass
    @EntityListeners({HierarchicalEntity.class})
    public abstract class AbstractHierarchyEntity 

    implements Serializable {

        protected Long parentId;
        protected Long lft;
        protected Long rgt;

        public String getMaxRightQuery() {
            return "Select max(e.rgt) from " this.getClass().getName() " e";
        }

        public String getQueryForParentRight() {
            return "Select e.rgt from " this.getClass().getName()
                    " e where e.id = ?1";
        }

        public String getDeleteStmt() {
            return "Delete from " this.getClass().getName()
                    " e Where e.lft between ?1 and ?2";
        }

        public String getUpdateStmtForFirst() {
            return "Update " this.getClass().getName()
                    " e set e.lft = e.lft + ?2 Where e.lft >= ?1";
        }

        public String getUpdateStmtForRight() {
            return "Update " this.getClass().getName()
                    " e set e.rgt = e.rgt + ?2 Where e.rgt >= ?1";
        }
        .
        .
        .//Getter and setters for all the attributes.
    }


  • Create annotation classes

    Following is a annotation class that will be used to annotate the methods that performs CRUD operations on hierarchical data. It is followed by a enum that will decided the type of curd operation to be performed.
    These classes will make more sense after the next section.

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface HierarchicalOperation {

        HierarchicalOperationType operationType();
    }


    /**
     * Enum - Type of CRUD operation.
     */
    public enum HierarchicalOperationType {

        SAVE,
        DELETE;
    }


  • Code the Aspect that will execute addional steps for managing Hierarchical data.
    HierarchicalEntity is an aspect that performs the additional logic required to manage the hierarchical data as descriped in the article here.
    This is the first time I am using Aspect. Therefore I am sure that there are better ways to do this. Those of you, who are good at it, please improve this part of code.

    This class is annotated as @Aspect. The pointcut will intercept any method anotated with HierarchicalOperation and has a input of type AbstractHierarchyEntity. A sample its usage is in next section.

    operation method is anotated to be executed before the pointcut. Based on the HierarchicalOperationType passed, this method will either execute the additional tasks required to save or delete the hierarchical record. This is where the methods defined in AbstractHierarchyEntity for generating JPA Queries are used.

    GenericDAOHelper is a utility class for using JPA.

    import com.es.clms.annotation.HierarchicalOperation;
    import com.es.clms.annotation.HierarchicalOperationType;
    import com.es.clms.common.GenericDAOHelper;
    import com.es.clms.model.AbstractHierarchyEntity;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.stereotype.Service;
    import org.springframework.beans.factory.annotation.Autowired;

    @Aspect
    @Service("hierarchicalEntity")
    public class HierarchicalEntity {

        @Autowired
        private GenericDAOHelper genericDAOHelper;

        @Pointcut(value = "execution(@com.es.clms.annotation.HierarchicalOperation * *(..)) "
        "&& args(AbstractHierarchyEntity)")
        private void hierarchicalOps() {
        }

        /**
         
         @param jp
         @param hierarchicalOperation
         */
        @Before("hierarchicalOps() && @annotation(hierarchicalOperation) ")
        public void operation(final JoinPoint jp,
                final HierarchicalOperation hierarchicalOperation) {
            if (jp.getArgs().length != 1) {
                throw new IllegalArgumentException(
                        "Expecting only one parameter of type AbstractHierarchyEntity in "
                        + jp.getSignature());
            }
            if (HierarchicalOperationType.SAVE.equals(
                    hierarchicalOperation.operationType())) {
                save(jp);
            else if (HierarchicalOperationType.DELETE.equals(
                    hierarchicalOperation.operationType())) {
                delete(jp);
            }
        }

        /**
         
         @param jp
         */
        private void save(JoinPoint jp) {

            AbstractHierarchyEntity entity =
                    (AbstractHierarchyEntityjp.getArgs()[0];
            if (entity == null)
              return;
            if (entity.getParentId() == null) {
                Long maxRight = (LonggenericDAOHelper.executeSingleResultQuery(
                        entity.getMaxRightQuery());

                if (maxRight == null) {
                    maxRight = 0L;
                }
                entity.setLft(maxRight + 1);
                entity.setRgt(maxRight + 2);
            else {
                Long parentRight = (LonggenericDAOHelper.executeSingleResultQuery(
                        entity.getQueryForParentRight(), entity.getParentId());

                entity.setLft(parentRight);
                entity.setRgt(parentRight + 1);

                genericDAOHelper.executeUpdate(
                        entity.getUpdateStmtForFirst(), parentRight, 2L);
                genericDAOHelper.executeUpdate(
                        entity.getUpdateStmtForRight(), parentRight, 2L);
            }
        }

        /**
         *
         @param jp
         */
        private void delete(JoinPoint jp) {

            AbstractHierarchyEntity entity =
                    (AbstractHierarchyEntityjp.getArgs()[0];

            genericDAOHelper.executeUpdate(
                    entity.getDeleteStmt(), entity.getLft(), entity.getRgt());

            Long width = (entity.getRgt() - entity.getLft()) 1;

            genericDAOHelper.executeUpdate(
                    entity.getUpdateStmtForFirst(), entity.getRgt(), width * (-1));
            genericDAOHelper.executeUpdate(
                    entity.getUpdateStmtForRight(), entity.getRgt(), width * (-1));
        }
    }


  • Sample Usage
    From this point on you donot have to worry about the additional tasks required for managing the data. Just use the HierarchicalOperation anotation with appropriate HierarchicalOperationType.
    Below is a sample use of the code developed so far.
        @HierarchicalOperation(operationType = HierarchicalOperationType.SAVE)
        public long save(VariableGroup group) {

            entityManager.persist(group);
            return group.getId();
        }


        @HierarchicalOperation(operationType = HierarchicalOperationType.DELETE)
        public void delete(VariableGroup group) {
            entityManager.remove(entityManager.merge(group));
        }

Wicket: Lazy Loading TreeTable




I followed the steps mentioned below in order to enable lazy loading of child nodes in TreeTable in Wicket. I did not find a component in Wicket to do this directly. (I may be wrong, as I am new to Wicket)



It seems javax.swing.tree.DefaultTreeModel and javax.swing.tree.DefaultMutableTreeNode are generally used for creating the TreeTable.


The two important things to be done are overidding the isLeaf() method of DefaultMutableTreeNode and then call setAllowsChildren(true) for nodes that will contain children nodes when expanded. It is explained in detail below.



org.apache.wicket.extensions.markup.html.tree.table.TreeTable uses isLeaf() method of javax.swing.tree.DefaultMutableTreeNode to find if the node needs to be rendered as a folder or a leaf. isLeaf() method returns true only if the node has a child node. This behavior needs to be changed for lazy loading. The method needs to return true for a node that can potentially contain a child node and false otherwise. So, in the below code DefaultMutableTreeNode is extended and isLeaf() method is overridden.


public class MutableTreeNode extends DefaultMutableTreeNode {
    .
    .
    .
    public boolean isLeaf() {
        return !getAllowsChildren();
    }
}

Therefore, for any node that can potentially have a child, setAllowsChildren() method should be called


Now create the TreeTable as usual using MutableTreeNode (created earlier) instead of DefaultMutableTreeNode. Just make sure to call the setAllowsChildren(true) for nodes that will have child node loaded when expanded.

The following is some sample code for creating a TreeTable.

MutableTreeNode rootNode = new MutableTreeNode();

for (VariableGroup group : grps) {

    //Create initial nodes.
    child = new MutableTreeNode(group);
    //The following line will make sure that isLeaf() method will return true for all initial nodes.
    child.setAllowsChildren(true);
    rootNode.add(child);
}

//Create the treemodel
DefaultTreeModel model = new DefaultTreeModel(rootNode);
model.setAsksAllowsChildren(true);
//Now create the TreeTable.
varGrpTree = new TreeTable("treeVariableGroup"new CompoundPropertyModel(model), columns) {
      
        //Override the onJunctionLinkClicked callback method to retrieve the child records and add them as child
        protected void onJunctionLinkClicked(AjaxRequestTarget target, javax.swing.tree.TreeNode node){

            //Return if the Children are already added.
            if (node.getChildCount() != 0) {
                return;
            }

            MutableTreeNode treeNode = (MutableTreeNodenode;

            VariableGroup parentGrp = (VariableGrouptreeNode.getUserObject();

            //Retrieve the recorders from Database
            List childGrps = variableManager.fetchChildroup(parentGrp.getId());
            MutableTreeNode child = null;

            for (VariableGroup group : childGrps) {

                child = new MutableTreeNode(group);
                //Business rule says if group.getLft().equals(group.getRgt() - 1 is false then the record will have child record.
                child.setAllowsChildren(!group.getLft().equals(group.getRgt() 1));
                treeNode.add(child);
            }
            addChildGroups(treeNode);
            varGrpTree.getTreeState().expandNode(treeNode);               

        }

    };

Code was converted to HTML using Java2html