内容如下:
<?xml version="1.0" encoding="GBK"?>
<bookstore>
<global>
<appname>菜单导航demo</appname>
<version>ver 1.0</version>
<creator>菠萝大象</creator>
</global>
<catalog id="catalog-program" name="编程开发" url="index.jsp" description="编程开发">
<item id="item-program-java" name="Java开发" url="index.jsp" description="Java开发">
<book id="695043" name="Struts2 深入详解" url="index.jsp" description="Struts2 深入详解"/>
<book id="691254" name="Ant整合开发" url="book_ant.jsp" description="Ant整合开发"/>
<book id="693668" name="Java编程思想" url="book_java.jsp" description="Java编程思想"/>
</item>
<item id="item-program-database" name="数据库开发" url="book_oracle9.jsp" description="数据库开发">
<book id="691245" name="Oracle 9i数据库宝典" url="book_oracle9.jsp" description="Oracle 9i数据库宝典"/>
<book id="693254" name="SQL Server 2005应用开发" url="book_sqlserver.jsp" description="SQL Server 2005应用开发"/>
<book id="690215" name="Oracle 10g高级开发" url="book_oracle10.jsp" description="Oracle 10g高级开发"/>
</item>
</catalog>
<catalog id="catalog-system" name="系统相关" url="book_vista.jsp" description="系统相关">
<item id="item-system-windows" name="Windows系统" url="book_vista.jsp" description="Windows系统">
<book id="691258" name="Windows Vista入门" url="book_vista.jsp" description="Windows Vista入门"/>
<book id="695489" name="Windows注册表实战" url="book_windows.jsp" description="Windows注册表实战"/>
</item>
<item id="item-system-linux" name="Linux系统" url="book_linux9.jsp" description="Linux系统">
<book id="696598" name="Linux 9.0详解" url="book_linux9.jsp" description="Linux 9.0详解"/>
<book id="694585" name="Linux宝典" url="book_linux.jsp" description="Linux宝典"/>
</item>
</catalog>
</bookstore>
上面XML里面的东西我是随便写的,大家千万不要较真,我用图书来做菜单一是方便大家理解,另一个是简化程序,其实Openfire的服务器端是一个后台管理系统,它是基于XMPP(可扩展消息处理现场协议)开发的,XMPP贯穿整个系统设计,如果你想用它的控制台框架,但又不想用XMPP,请先从网页入口开始,结合页面仔细分析代码,把需要的部分抽取出来就行了,其它的不用去管。大象没有研究过XMPP,只是抽取了控制台框架,对Openfire的源代码也没能深入的研究,最主要还是E文太烂了。^_^
Openfire没有采用现在很流行的技术架构(SSH),只使用JSP+JavaBean,但是它有自己的系统设计,就连日志都是自己做的,没有使用我们熟悉的log4j,真的是太佩服鸟~~~~
2、创建ResourceManage.java
在util包下创建ResourceManage类,这个类主要是用来读取tag-console.xml文件,并取得文件中的基本信息,以及查找元素等操作。
我们先在Constant接口中,增加一个字符串常量:String TAG_CONFIG = "tag-console.xml"
ResourceManage前面加载资源的部分和上一篇,后来修改过的DataBaseConnect类一样,只需把Constant.DB_CONFIG换成Constant.TAG_CONFIG就行了。接下来,在类中加入几个读取XML中基本信息的方法:
<global>
<appname>菜单导航demo</appname>
<version>ver 1.0</version>
<creator>菠萝大象</creator>
</global>
这里只举出取得appname元素值的方法,其它的几个都很相似,请查看源代码。
/**
* 得到应用程序名称
*/
public static String getAppName(){
Element appName = (Element) coreModel.selectSingleNode("//bookstore/global/appname");
if(appName!=null){
return appName.getText();
}else{
return null;
}
}
根据id属性值查找对应的元素:
/**
* 在整个文档节点中查找id属性值为传入id的元素对象
* @param id 待查找的id属性值
* @return 返回找到的元素对象
*/
public static Element getSingleElementById(String id){
return (Element)coreModel.selectSingleNode("//*[@id='"+id+"']");
}
这里用到了XPATH语法,根据传入的id值,在整个文档中查找id属性值与此一致的元素对象。用下面的代码举例说明:
<book id="695043" name="Struts2 深入详解" url="index.jsp" description="Struts2 深入详解"/>
当传入的id属性值为"695043"时,那么我们就会得到对应这个id值的book元素对象,id属性值在整个配置文件中就是一个key关键字,起到定位的作用。
根据id属性值查找上下文中对应的catalog元素:
/**
* 根据传入的id查找上下文中对应的catalog元素
* @param id 待查找的id属性值
* @return 返回id值所在的catalog元素对象
*/
public static Element getElementByID(String id) {
return (Element) coreModel.selectSingleNode("//*[@id='" + id
+ "']/ancestor::catalog");
}
ancestor是XPATH语法中轴的概念,我引用网上官方文档中的说明:“ancestor轴(axis)包含上下节点的祖先节点,该祖先节点由其上下文节点的父节点以及父节点的父节点等等诸如此类的节点构成,所ancestor轴总是包含有根节点,除非上下文节点就是根节点本身。”这句话的意思其实就是向上查找节点,直到找到根节点为止。对于ancestor::catalog来说,就是向上查找直到catalog节点为止。所以getElementByID这个方法是根据传入的id属性值在上下文中查找节点,直到找到这个id值所在的上下文catalog节点为止。当传入的id属性值为"695043"时,我们会得到id="catalog-program"这个catalog节点元素,而不会得到id="catalog-system"这个catalog节点元素。这样说大家大概能明白是什么意思了吧?
可以去这个网站看下XPATH教程:http://www.zvon.org/xxl/XPathTutorial/General_chi/examples.html
3、自定义标签
采用自定义标签的方式来生成菜单,借助ResourceManage类取出XML文件中的信息,将这些内容装载到标签体中,然后在JSP页面中呈现出来。
1、主菜单标签
主菜单有两个,catalog元素这一层表示主菜单。标签的实现类如下:
MainTag.java
建com.demo.tag包,在tag包下创建MainTag类,继承javax.servlet.jsp.tagext.BodyTagSupport类,主要的部分代码如下,完整代码请下载源码包查看。
这些属性与demo.tld中的属性对应,每个属性都有setter和getter方法。
private String css; //菜单的CSS样式
private String currentcss; //当前选中菜单的CSS样式
doStartTag()方法是遇到标签开始时调用的方法,EVAL_BODY_BUFFERED表示创建一个缓冲流,将标签体的内容保存到BodyContent对象中,可以对其内容进行修改。BodyContent继承了javax.servlet.jsp.JspWriter类,BodyContent对象的内容不自动写入servlet的输出流,而是放在一个字符流缓存中。当标签体完成后其对象仍可在doEndTag()方法中应用,由getString()或getReader()方法操作。并在必要时修改及写入恢复的JspWriter输出流。EVAL_BODY_INCLUDE表示将显示标签间的文字。另一个返回值是SKIP_BODY,它表示不显示标签间的文字。
public int doStartTag() throws JspException {
return EVAL_BODY_BUFFERED; //创建保存到BodyContent对象中的缓冲流
}
doEndTag()方法是遇到标签结束时调用的方法,EVAL_PAGE表示处理完标签后继续执行标签之后的JSP页面。另一个返回值SKIP_PAGE表示不处理标签之后的JSP网页。
public int doEndTag() throws JspException {
//代码主体省略,请查看源码
return EVAL_PAGE; //处理完标签后继续执行标签之后的JSP页面
}
doEndTag()方法中部分比较重要的代码说明:
使用pageContext对象在JSP页面上下文取得请求,不过请注意pageContext,它定义在javax.servlet.jsp.tagext.TagSupport中,而不是在BodyTagSupport中,因为BodyTagSupport继承了TagSupport
//使用pageContext对象在JSP页面上下文取得请求
HttpServletRequest request = (HttpServletRequest)pageContext.getRequest();
取得请求中的pageID值,这个pageID值在每个jsp页面中放在meta标签中,通过sitemesh装饰器取出放到request中。
/*
* 从请求中得到pageID值,即每个JSP里meta的content值
* 与XML文件中book元素的id属性值一致
*/
String pageID = (String)request.getAttribute("pageID");
将所有的catalog元素取出放到List集合中,这里是只取得catalog这一层级的元素,实质就是catalogs中只有两个对象,一个是id="catalog-program",另一个是id="catalog-system",使用dom4j我们会发现处理元素非常容易,API相当的丰富,想写成什么样完全凭你自己的想法。
//将所有的catalog元素取出放到List集合
List catalogs = ResourceManage.getCoreModel().selectNodes("//catalog");
看看前面介绍的getElementByID这个方法,这个currentCatalog所表示就是pageID所在的catalog元素。上面的代码是为了和下面的代码结合来判断当前的菜单是否为选中,加入CSS样式显示。
//pageID所在的catalog元素,主要用来判断当前菜单是否被选中
Element currentCatalog = (Element)ResourceManage.getElementByID(pageID);
从BodyContent中将标签体缓存流读取出来,标签在WebRoot/decorators/main.jsp中:<a href="[url]" title="[description]" onmouseover="self.status='[description]';return true;" onmouseout="self.status='';return true;">[name]</a>
String value = getBodyContent().getString(); //得到标签体
Catalogs里面是两个catalog对象,循环遍历取出,将标签体中的[id]、[url]、[name]、[description]替换为XML文件中的属性值,这样主菜单标签就生成了。
for (int i=0; i<catalogs.size(); i++) {
Element catalog = (Element)catalogs.get(i); //catalog元素对象
String value = getBodyContent().getString(); //得到标签体
/*
* 将标签体中的[id]、[url]、[name]、[description]
* 替换为XML文件中的属性值
* attributeValue方法是取属性值
*/
if (value != null) {
value = StringUtils.replace(value, "[url]", request
.getContextPath()
+ "/" + catalog.attributeValue("url"));
value = StringUtils.replace(value, "[name]",catalog.attributeValue("name"));
value = StringUtils.replace(value, "[description]",catalog.
attributeValue("description"));
}
String css = getCss();
//对当前选中菜单添加CSS样式
if (catalog.equals(currentCatalog)) {
css = getCurrentcss();
}
buf.append("<li class=\"").append(css).append("\">");
if (i > 0) {
buf.append(" | ");
}
buf.append(value).append("</li>");
}
2、导航菜单及侧边栏菜单标签
导航菜单每个catalog下都有两个,而侧边栏菜单则在item下定义,这两个标签类与主菜单的标签类没有太大的区别,主要就是生成标签体,匹配CSS样式,因此,代码中相同的部分我不再细述,只说一下不同的地方。
在tag包下创建NavTag类和SideTag类,标签属性与MainTag一样,只是SideTag多了一个headercss属性,这是在页面显示时,加在边栏上当前选中项左侧小箭头的CSS样式,不清楚的话,请运行程序后观察。
NavTag.java
根据pageID找到此元素对象:
//根据pageID找到此元素对象,即book元素对象
Element current = ResourceManage.getSingleElementById(pageID);
如果current不为空,取得父节点,其为item元素。根据pageID值,如果为695043,则subnav为id="item-program-java"的item元素,如果为691245,则subnav为id="item-program-database"的item元素。这个subnav的作用也是用来判断当前的菜单是否为选中,加入CSS样式显示。
Element subnav = null;
if (current != null) {
subnav = current.getParent(); //取得父节点,即item元素
}
SideTag.java
在SideTag中也有上面的代码,但是subnav不再与CSS有关,而是取得它的所有子元素集合,即book元素集合,然后遍历所有book节点,取出属性值放入标签体中再输出到页面。
我注释写得很详细,请查看代码了解细节。
4、创建StringUtils.java
在util包下创建StringUtils类,这个类作为字符串处理类。添加public static String replace(String string, String oldString, String newString)方法,它的作用就是将标签体中的[id]、[url]、[name]、[description]替换为XML文件中的属性值。如果被替换的字符串在标签体中有多个,也能将它全部替换。
/**
* 将string中的oldString全部替换为newString
* @param string 原始字符串
* @param oldString 被替换的字符串
* @param newString 要替换的字符串
* @return 返回替换完后的新string
*/
public static String replace(String string, String oldString, String newString) {
if (string == null) {
return null;
}
int i = 0;
//判断string中是否有被替换的字符串,i其实是索引值
if ((i = string.indexOf(oldString, i)) >= 0) {
char[] string2 = string.toCharArray(); //字符串放入数组
char[] newString2 = newString.toCharArray(); //要替换的字符串
int oLength = oldString.length(); //被替换的字符串的长度
StringBuilder buf = new StringBuilder(string2.length);
/*
* 从索引0开始,按i值的长度在string2数组中截取字符
* 将截取的字符放到buf中,接着再加入要替换的内容
*/
buf.append(string2, 0, i).append(newString2);
i += oLength; //得到被替换字符结束位置的索引
int j = i;
/*
* 查找string中,是否仍然含有被替换字符串
* 使用循环,将所有oldString换成newString
*/
while ((i = string.indexOf(oldString, i)) > 0) {
buf.append(string2, j, i - j).append(newString2);
i += oLength; //得到被替换字符结束位置的索引
j = i;
}
/*
* 截取string2数组中从索引j开始
* string2.length-j的长度加到buf中
* 其实就是在buf中补全标签体
*/
buf.append(string2, j, string2.length - j);
return buf.toString();
}
return string;
}
如果看注释就能懂那最好不过,如果不明白,在这上面打个断点调试一下,就会十分清楚了。这个方法是Openfire中的源代码,不过全是E文,大象先也看不明白,后来调试了一下,知道是怎么回事了,特加上注释和大家一起分享这个好东东。
写完了自定义标签类,我们还需要自定义标签文件,在WEB-INF目录下新建demo.tld,代码不帖出来了,使用源码中的就行。
5、装饰器
后台的Java类,我们全部写完了,现在开始完成前台部分,在页面显示上,Openfire使用了sitemesh装饰器框架,它能帮助我们在由大量页面构成的项目中创建一致的页面布局和外观,如一致的导航条,一致的banner,一致的版权等等。至于怎样使用sitemesh这里不作介绍了,请自行去搜索相关资料,这部分内容网上很多的,sitemesh比较简单,很容易上手。
使用装饰器,需要导入JAR包,在本例中,大象使用的是sitemesh-2.2.1.jar包,将jar包加到WEB-INF/lib目录中,然后修改web.xml,添加如下代码:
<filter>
<filter-name>sitemesh</filter-name>
<filter-class>com.opensymphony.module.sitemesh.filter.PageFilter</filter-class>
</filter >
<filter-mapping>
<filter-name>sitemesh</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
然后在WEB-INF下新建decorators.xml文件,内容如下:
<decorators defaultdir="/decorators">
<decorator name="main" page="main.jsp">
<pattern>/index.jsp</pattern>
<pattern>/book_*.jsp</pattern>
</decorator>
</decorators>
请注意defaultdir后面的值,这是你放装饰器页面的目录位置。本例中,在WebRoot目录下新建decorators文件夹,再在里面新建main.jsp,这个就是装饰器页面了。<pattern></pattern>之间的内容就是需要被装饰的页面,*号是通配符,可以代替任何字符。/book_*.jsp表示:使用main.jsp装饰WebRoot目录下所有以book_开头的页面,这里定义的name="main",可以在装饰器页面中使用,因为不一定只有一个装饰器页面,可能会有很多个。因此,在装饰器页面中为了布局效果会联合使用多个装饰器来修饰页面,以达到简化布局、降低维护难度、提高工作效率的作用。另外在使用时,请注意被装饰页面与装饰器页面之间的相对位置。
6、main.jsp
在页面中引用被装饰页面的page对象:<decorator:usePage id="decoratedPage" />
使用decoratedPage取得被装饰页面中meta标签的content值,再将它放到request请求中,这样在自定义标签类中我们使用pageContext对象得到的请求就是这个。
<%
request.setAttribute("pageID", decoratedPage.getProperty("meta.pageID"));
%>
显示被装饰页面<title></title>之间的标题:<decorator:title />
显示被装饰页面body中的内容,被装饰页面的主体都将在这里显示:<decorator:body/>
除此之外,在main.jsp中,我们还发现大量的使用div来放置元素,并且每个标签中都有id属性,没有看到任何的CSS样式,其实是通过id属性在demo.css文件进行了定义,所有的布局和显示效果都在这个文件中进行了定义,这样就达到了内容呈现与样式布局相分离的结果,方便以后的修改和维护,这种做法用的人现在已经越来越多,大家赶快行动吧!
sitemesh中还有一个sitemesh.xml文件,如果程序中没有特别需求,可以不用加入它,我们也能在sitemesh-2.2.1.jar中找到,com.opensymphony.module.sitemesh.factory目录下有一个sitemesh-default.xml文件,这就是sitemesh默认的配置文件。
7、显示页面
在tag-console.xml的url属性里定义了显示页面,接下来我们把这些页面都做好,内容很简单,本文只是演示Openfire的菜单设计思想,用容易懂的例子来说明,以便大家能够快速了解。
index.jsp页面的代码:
<%@ page contentType="text/html; charset=utf-8" %>
<html>
<head>
<title>Struts2 深入详解</title>
<meta name="pageID" content="695043"/>
</head>
<body>
<center><h1>Struts2 深入详解</h1></center>
</body>
</html>
其余的几个页面内容大致一样,把content值、<title></title>标题、以及<center><h1></h1></center>之间的内容换成book元素中定义的属性值即可。
demo.css
在WebRoot目录下新建css文件夹,再在里面创建demo.css文件。我直接把Openfire的样式表COPY过来。然后把里面没用的部分删除了,体积小了不少。
图片
我从Openfire中只取了本例用到的图片,如果是专业美工,完全可以设计出自己的菜单风格。
8、发布项目
我们在web.xml中可以加入下面一段代码,将index.jsp作为我们的默认显示页面:
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
把demo部署到%TOMCAT_HOME%\webapps目录下,启动tomcat,在地址栏中输入:http://localhost:8080/demo 看看效果是不是和下面的一样:
大家觉得这个菜单的显示方式怎么样呢?偶觉得这用来做后台管理到还是不错滴,如果是其它的信息管理系统,那这个配置文件的内容就会很恐怖了,其实还可以把XML中的中文信息保存到国际化资源文件中,这样可以实现多语种版本以及简化维护。各位有什么好的意见或建议,可以和我留言或E-mail给我。大象也想把自己的一点心得拿出来和大家分享。
点击下载:demo源码
点击下载:dom4j-1.6.1.jar jaxen-1.1-beta-7.jar sitemesh-2.2.1.jar
本文为菠萝大象原创,如要转载请注明出处