Struts2 Tree标签的使用

  在项目中有很多地方使用了树形结构,主要有几个地方:素材和软件等软件资源表,人员权限的权限资源表、人员权限的角色,套餐及产品包,现以类别表为例,说明树形结构的数据库存储、利用Struts2 中dojo的Tree标签来完成前端展现。

1、 数据库结构

CREATE TABLE `oss_category` (
  `category_id` int(11) NOT NULL auto_increment,
  `parent_id` int(11) default '-1',
  `level` smallint(6) default NULL ,
  `is_leaf` tinyint(1) default NULL,
  `category_title` varchar(100) default NULL,
  `category_name` varchar(100) default NULL,
  `category_code` varchar(100) default NULL,
  `category_type` varchar(30) default NULL,
  `image` varchar(255) default NULL,
  `status` varchar(20) default NULL,
  `creator` varchar(50) default NULL,
  `create_date` datetime default NULL,
  `modify_user` varchar(50) default NULL,
  `modify_date` datetime default NULL,
  `description` text,
  PRIMARY KEY  (`category_id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=gbk;

测试数据:

INSERT INTO `oss_category` VALUES ('1', '-1', '1', '0', 'root', 'root', 'root', 'product_type', null, '1', null, null, null, null, null);
INSERT INTO `oss_category` VALUES ('2', '1', '2', '0', '<input type=\'checkbox\' name=\'product_type1\'  id=\'product_type1\'  /><b>product_type1</b>', 'product_type1', 'product_type1', 'product_type', null, '1', null, null, null, null, null);
INSERT INTO `oss_category` VALUES ('3', '1', '2', '0', '<input type=\'checkbox\' name=\'product_type2\' id=\'product_type2\'  class=\'treeCheckBox\'/><b>product_typ', 'product_type2', 'product_type2', 'product_type', null, '1', null, null, null, null, null);
INSERT INTO `oss_category` VALUES ('4', '3', '3', '0', '<input type=\'checkbox\' name=\'product_type21\'  id=\'product_type21\'  /><b>product_type21</b>', 'product_type21', 'product_type21', 'product_type', null, '1', null, null, null, null, null);
INSERT INTO `oss_category` VALUES ('5', '2', '3', '0', '<input type=\'checkbox\' name=\'product_type11\'  id=\'product_type11\'  /><b>product_type11</b>', 'product_type11', 'product_type11', 'product_type', null, '1', null, null, null, null, null);
INSERT INTO `oss_category` VALUES ('6', '5', '4', '1', '<input type=\'checkbox\' name=\'product_type111\'  id=\'product_type111\'  /><b>product_type111</b>', 'product_type111', 'product_type111', 'product_type', null, '1', null, null, null, null, null);
INSERT INTO `oss_category` VALUES ('7', '3', '2', '0', '<input type=\'checkbox\' name=\'product_type22\'  id=\'product_type22\'  /><b>product_type22</b>', 'product_type22', 'product_type22', 'product_type', null, '1', null, null, null, null, null);
INSERT INTO `oss_category` VALUES ('8', '2', '3', '0', '<input type=\'checkbox\' name=\'product_type12\'  id=\'product_type12\'  /><b>product_type12</b>', 'product_type12', 'product_type12', 'product_type', null, '1', null, null, null, null, null);
INSERT INTO `oss_category` VALUES ('9', '4', '4', '1', '<input type=\'checkbox\' name=\'product_type211\'  id=\'product_type211\'  /><b>product_type211</b>', 'product_type211', 'product_type211', 'product_type', null, '1', null, null, null, null, null);
INSERT INTO `oss_category` VALUES ('10', '7', '4', '1', '<input type=\'checkbox\' name=\'product_type221\'  id=\'product_type221\'  /><b>product_type221</b>', 'product_type221', 'product_type221', 'product_type', null, '1', null, null, null, null, null);

 

注意:

  • 为了支持在树形结构的节点前有checkbox框,需要在树形结构的title(对应s:tree中的nodeTitleProperty)前增加checkbox树形,因此在数据库中category_title字段的格式采用如上的形式,例如:

<input type=’checkbox’ name=’product_type1′  id=’product_type1′  /><b>product_type1</b>

  • 对于有序的树形结构,为了标识left和right,可以增加ordering字段,或者采用类似嵌套树的结构,例如
CREATE TABLE nested_category (
 category_id INT AUTO_INCREMENT PRIMARY KEY,
 name VARCHAR(20) NOT NULL,
 lft INT NOT NULL,
 rgt INT NOT NULL
);
但采用嵌套树的方式,在变动一个节点时候需要对相关的节点进行更新,不是很适合。但效率较高。可以参看:
http://dev.mysql.com/tech-resources/articles/hierarchical-data.html
http://www.sitepoint.com/print/hierarchical-data-database
  • mysql对树形结构没有原生的支持,Oracle和DB2可以支持,Oracle通过CONNECT BY,DB2通过With方式。

2、Model层

OssCategory.java

直接用myeclipse生成后,增加了如下内容:

    private OssCategory[] childCategories;
    private OssCategory parentCategory;

    public OssCategory[] getChildCategories() {
        return childCategories;
    }

    public void setChildCategories(OssCategory[] childCategories) {
        this.childCategories = childCategories;
    }

    public OssCategory getParentCategory() {
        return parentCategory;
    }

    public void setParentCategory(OssCategory parentCategory) {
        this.parentCategory = parentCategory;
    }

3、DAO层

OssCategoryDAO.java

直接用myeclipse生成,没有调优,增加如下内容:

    public OssCategory[] getAllCategory() {
        ArrayList<OssCategory> resultList=new ArrayList<OssCategory>();
        String queryString="from OssCategory as category where category.parentId=1";
        List<OssCategory> queryList =getHibernateTemplate().find(queryString);
        Iterator iterator=queryList.iterator();
        while(iterator.hasNext()){
            OssCategory ossCategory=(OssCategory)iterator.next();
            OssCategory[] childrenArray=getChildCategoriesById(ossCategory.getCategoryId());
            logger.debug("getAllCategory() - OssCategory[] childrenList=" + childrenArray.length);
            ossCategory.setChildCategories(childrenArray);
            resultList.add(ossCategory);

        }

        OssCategory[] resultArray=(OssCategory[])resultList.toArray(new OssCategory[resultList.size()]);
        return resultArray;

    }

    public OssCategory[] getChildCategoriesById(int categoryId) {
        String queryString="from OssCategory as category where category.parentId="+categoryId;
        List<OssCategory> queryList =getHibernateTemplate().find(queryString);
        logger.debug("getChildCategoriesById(int) - List<OssCategory> queryList=" + queryList.size());
        ArrayList<OssCategory> resultList=new ArrayList<OssCategory>();
        Iterator iterator=queryList.iterator();
        while(iterator.hasNext()){
            OssCategory ossCategory=(OssCategory)iterator.next();
            OssCategory[] childrenList=getChildCategoriesById(ossCategory.getCategoryId());
            logger.debug("getChildCategoriesById(int) - OssCategory[] childrenList=" + childrenList+"parentid is "+categoryId); //$NON-NLS-1$
            ossCategory.setChildCategories(childrenList);
            resultList.add(ossCategory);

        }
        OssCategory[] resultArray=(OssCategory[])resultList.toArray(new OssCategory[resultList.size()]);
        return resultArray;
    }
注意:
  •  getAllCategory和getChildCategoriesById采用了递归调用方式,应该可以一次性取出指定节点的所有子节点到结果集合List,然后对List在内存中进行遍历,但比较麻烦,先采用此种方式。
  •  struts2的tree应该支持List,而不一定要采用数组方式,只不过没有测试
 

4、Service层

TreeService.java

有些方法尚未实现

package com.mobilesoft.framework.tree.service;

import java.util.List;
import java.util.Set;

import com.mobilesoft.framework.tree.model.OssCategory;

public interface TreeService {

    /**
     * @return 获取下级子节点
     */
  //  public Set getChildren(int rootId);

    /**
     * @return 递归指定级别的所有子节点
     *      
     */
  //  public Set getChildrenByLevel(int rootId,int level);

    /**
     * @return 获取指定类型的所有子节点
     * @param type:节点的类型
     * @param levle:节点相对于所属类型的节点的级别 -1表示获取所属类型的根节点的所有级别的子节点
     */
 //   public Set getChildrenByType( String type,int level);

    /**
     * @return 获取指定根节点的所有子节点
     */
    public OssCategory[]  getAllCategory();

}

TreeServiceImpl.java

 

package com.mobilesoft.framework.tree.service.impl;

import java.util.List;
import java.util.Set;

import com.mobilesoft.framework.tree.dao.hibernate.OssCategoryDAO;
import com.mobilesoft.framework.tree.model.OssCategory;
import com.mobilesoft.framework.tree.service.TreeService;

public class TreeServiceImpl implements TreeService {
    OssCategoryDAO ossCategoryDAO;
    public Set getChildren(int rootId) {
        // TODO Auto-generated method stub
        return null;
    }

    public Set getLevelChildren(int rootId, int level) {
        // TODO Auto-generated method stub
        return null;
    }

    public OssCategory[]   getAllCategory(){
        return ossCategoryDAO.getAllCategory();
    }
    public OssCategoryDAO getOssCategoryDAO() {
        return ossCategoryDAO;
    }

    public void setOssCategoryDAO(OssCategoryDAO ossCategoryDAO) {
        this.ossCategoryDAO = ossCategoryDAO;
    }

}

5、Action层

package com.mobilesoft.esales.webapp.action;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;

import com.mobilesoft.framework.tree.model.FileWrapper;
import com.mobilesoft.framework.tree.model.OssCategory;
import com.mobilesoft.framework.tree.service.TreeService;

public class TreeAction extends BaseAction {
    /**
     * Logger for this class
     */
    private static final Logger logger = Logger.getLogger(TreeAction.class);

    private OssCategory root;
    private TreeService treeService;
    private FileWrapper fileroot;

    public String execute() {
        root = new OssCategory();
        root.setCategoryId(new Integer(1));
        root.setParentId(-1);
        root.setCategoryTitle("Root");
        root.setCategoryName("Root");
        OssCategory[] childCategories = treeService.getAllCategory();

        root.setChildCategories(childCategories);
        visitTree(childCategories);
        getRequest().setAttribute("root", root);
        // fileroot = new FileWrapper(getSession().getServletContext().getRealPath("/"));
        return SUCCESS;
    }

    public String treeWithCheckbox() {
        Map map = getRequest().getParameterMap();
        logger.fatal("the tree map");
        Set set = map.entrySet();
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry) iterator.next();

            logger.fatal("the tree key is: " + entry.getKey() + " ,and value is: "
                    + entry.getValue());
        }

        return SUCCESS;
    }

    public TreeService getTreeService() {
        return treeService;
    }

    public void setTreeService(TreeService treeService) {
        this.treeService = treeService;
    }

    public OssCategory getRoot() {
        return root;
    }

    public void setRoot(OssCategory root) {
        this.root = root;
    }

    public void visitTree(OssCategory[] tree) {

        for (int i = 0; i < tree.length; i++) {
            OssCategory node = (OssCategory) tree[i];
            logger.fatal("visitTree:The treenode parentid is :"
                    + node.getParentId() + " ,treenode id is "
                    + node.getCategoryId() + ",and the treenode name is "
                    + node.getCategoryName());
            visitTree(node.getChildCategories());
        }

    }
}

6、页面

treetest.jsp

<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ page language="java" errorPage="/error.jsp" pageEncoding="GBK" contentType="text/html;charset=GBK" %>
<html>
<head>
    <title>tag list</title>
    <%@ include file="/common/meta.jsp" %>

<style>
    .treeCheckBox {
        height: 14px;
        margin: 0px;
        padding: 0px;
        border: 1px;
        vertical-align: middle;
    }
</style>
<s:head theme="ajax" debug="true" />
</head>
<body >

<script type="text/javascript">
function treeNodeSelected(arg) {
    //alert("id["+arg.source.widgetId+"], name["+ arg.source.title+ "] selected");
}

function treeNodeExpanded(arg) {
    //alert("id["+arg.source.widgetId+"], name["+ arg.source.title+ "] expanded");
}

function treeNodeCollapsed(arg) {
    //alert("id["+arg.source.widgetId+"], name["+ arg.source.title+ "] collapsed");
}

dojo.addOnLoad(function() {
    var t = dojo.widget.byId('root');
    dojo.event.topic.subscribe(t.eventNames.expand, treeNodeExpanded);
    dojo.event.topic.subscribe(t.eventNames.collapse, treeNodeCollapsed);

    var s = t.selector;   

});

</script>

<s:form name="form1" action="treeWithCheckbox">
<div style="float:left; margin-right: 50px;" >
<s:tree
    theme="ajax"
    id="root"
    rootNode="#request.root"
    childCollectionProperty="childCategories"
    nodeIdProperty="categoryId"
    nodeTitleProperty="categoryTitle"
    treeSelectedTopic="treeSelected">
</s:tree>
</div>     

<div style="float:left; margin-right: 50px;">
<s:submit/>
</div>
</s:form>

</body>
</html>
 

注意:

  • 在s:tree中不能使用label标签,使用后树形结构出不来。
  • childCollectionProperty=”childCategories”对应Category.java中的
    private OssCategory[] childCategories;
    
    public OssCategory[] getChildCategories() {
        return childCategories;
    } 
    
    public void setChildCategories(OssCategory[] childCategories) {
        this.childCategories = childCategories;
    }
    
  • nodeIdProperty=”categoryId”和nodeTitleProperty=”categoryTitle” 对应OssCategory[] childCategories中每一个Category的categoryId和categoryTitle

  • rootNode=”#request.root”,采用rootNode=”root”方式好像取不到数据,按理说应该可以通过ognl自动调用getRoot方法得到

  • 结果提交后,根据对应checkbox的name的on属性来判断是否选中,因此在数据库中对节点的命名需要有一定规则,以便方便选择

尚需要完善的地方:

  • 增加对下拉框树形结构实现的支持,以处理项目中大量诸如产品类型、资源类型等类型。

在实现上,为了避免由于浏览器兼容性问题,在形式上是树形结构,实际上就是从字典表(目前树形结构仍然采用oss_category作为字典表)动态取出数据,然后拼凑成字符串形式展现,例如:

<select>
    <option value="1">1</option>
    <option value="11">&nbsp;&nbsp;|-11</option>
    <option value="12">&nbsp;&nbsp;|-12</option>
    <option value="2">2</option>
    <option value="21">&nbsp;&nbsp;|-21</option>
    <option value="22">&nbsp;&nbsp;|-22</option>
</select>
  • 进一步优化递归算法,一次性从数据库取出需要的数据,在内存中完成树形结构的构造,避免对数据库的查询,在目前情况下,采用递归方式问题也不大。
  • 由于Struts2.0采用dojo来实现其标签,尽管dojo与jquery相比较很重,但由于与Struts2.0较好支持,在架构统一上和使用上还是较为方便的,因此有空需要研究一下dojo库。

7、参考资料

http://www.ajaxtree.com/

http://struts.apache.org/2.x/docs/tree.html

http://ait.web.psi.ch/js/dojo/tests/widget/tree/

http://exjava.spaces.live.com/blog/cns!A43FBDAF9245A0BC!199.entry

http://www.stack.be/~roel/blog/archives/the-dojo-tree-control-for-beginners-part-1/

http://www.stack.be/~roel/blog/archives/the-dojo-tree-control-for-beginners-part-2

http://www.codepencil.com/index.php/struts2-dojo-dynamic-tree/

http://dojotoolkit.org/book/dojo-book-0-9/part-2-dijit/advanced-editing-and-display/tree

 

Technorati 标签: ,,,,

站内标签:,,,,

6 Comments so far

  1. aaa on 八月 7th, 2008

    hao thank you!!!

  2. oozoh on 八月 24th, 2009

    非常感谢楼主的分享。收藏了。

  3. BadGirl76 on 十月 22nd, 2009

    Mark,It’s not Google’s fault that there’s not a proper app. ,

  4. No_limits58 on 十月 23rd, 2009

    I have been working in a temporary part time position for over a year. ,

  5. hw on 十一月 2nd, 2009

    代码写的够烂

  6. 新建文件夹 on 十一月 20th, 2009

    有问题:

    Expression stack.findValue(parameters.nodeTitleProperty) is undefined on line 23, column 48 in template/ajax/treenode-include.ftl.
    The problematic instruction:
    ———-
    ==> ${stack.findValue(parameters.nodeTitleProperty)} [on line 23, column 46 in template/ajax/treenode-include.ftl]
    in include “/${parameters.templateDir}/ajax/treenode-include.ftl” [on line 124, column 5 in template/ajax/tree.ftl]
    ———-

    Java backtrace for programmers:
    ———-
    freemarker.core.InvalidReferenceException: Expression stack.findValue(parameters.nodeTitleProperty) is undefined on line 23, column 48 in template/ajax/treenode-include.ftl.
    at freemarker.core.TemplateObject.assertNonNull(TemplateObject.java:124)
    at freemarker.core.Expression.getStringValue(Expression.java:118)
    at freemarker.core.Expression.getStringValue(Expression.java:93)
    at freemarker.core.DollarVariable.accept(DollarVariable.java:76)
    at freemarker.core.Environment.visit(Environment.java:208)
    at freemarker.core.MixedContent.accept(MixedContent.java:92)
    at freemarker.core.Environment.visit(Environment.java:208)
    at freemarker.core.Environment.include(Environment.java:1477)
    at freemarker.core.Include.accept(Include.java:169)
    at freemarker.core.Environment.visit(Environment.java:208)
    at freemarker.core.MixedContent.accept(MixedContent.java:92)

    ….(省略更多堆栈信息)

    怎么解决呢?

Leave a reply