openfire综合介绍

更新时间:2024-04-03 03:41:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

基础简介

XMPP

Extensible Messaging and Presence Protocol,简单的来讲,它就是一个发送接收处理消息的协议,但是这个协议发送的消息,既不是二进制的东东也不是字符串,而是XML。正是因为使用了XML作为消息传递的中介,Extensible 才谈的上,不是么?

IM

Instant Messenger,及时通信软件,就是大家使用的QQ、MSN Messenger和Gtalk等等。其中Gtalk 就是基于XMPP 协议的一个实现,其他的则不是。当前IM 几乎作为每个上网者必然使用的工具,在国外的大型企业中有一些企业级的IM应用,但是其商业价值还没完全发挥出来。设想既然XMPP 协议是一个公开的协议,那么每个企业都可以利用它来开发适合本身企业工作,提高自身生产效率的IM;甚至,你还可以在网络游戏中集成这种通信软件,不但让你可以边游戏边聊天,也可以开发出适合游戏本身的IM 应用,比如说一些游戏关键场景提醒功能,团队语音交流等等都可以基于IM来实现。

Spark Smack 和 Openfire

开源界总是有许多有趣的东东,这三个合起来就是一个完整的XMPP IM 实现。包括服务器端——Openfire,客户端——Spark,XMPP 传输协议的实现——Smack(记住,XMPP是一个协议,协议是需要实现的,Smack起到的就是这样的一个作用)。三者都是基于Java 语言的实现。

Spark 提供了客户端一个基本的实现,并提出了一个很好的插件架构,这对于开发者来说不能不说是一个福音。我强烈建议基于插件方式来实现你新增加的功能,而不是去改它的源代码,这样有利于你项目架构,把原始项目的影响降到最低。

Openfire 是基于XMPP 协议的IM 的服务器端的一个实现,虽然当两个用户连接后,可以通过点对点的方式来发送消息,但是用户还是需要连接到服务器来获取一些连接信息和通信信息的,所以服务器端是必须要实现的。Openfire 也提供了一些基本功能,但真的很基本的!庆幸的是,它也提供插件的扩展,像Spark 一样,同样强烈建议使用插件扩展的方式来增加新的功能,而不是修改人家的源代码。

Smack 是一个XMPP 协议的Java 实现,提供一套可扩展的API,不过有些时候,你还是不得不使用自己定制发送的XML 文件内容的方式来实现自己的功能

下图展示了三者之间的关系:

从图上可以了解到,client 端和server端都可以通过插件的方式来进行扩展,smack是二者传递数据的媒介。

Apache MINA

Openfire的通信处理基于Apache MINA框架实现。Apache MINA是一个网络应用程序框架,用来帮助用户简单地开发高性能和高可靠性的网络应用程序。它提供了一个通过Java NIO在不同的传输例如TCP/IP和UDP/IP上抽象的事件驱动的异步API。

Apache MINA 也称为: ● NIO 框架库

● 客户端服务器框架库 ● 一个网络套接字库

MINA虽然简单但是仍然提供了全功能的网络应用程序框架: ● 为不同的传输类型提供了统一的API:

○ 通过Java NIO提供TCP/IP 和 UDP/IP支持 ○ 通过RXTX提供串口通讯(RS232) ○ In-VM管道通讯

○ 你能实现你自己的API!

● 过滤器作为一个扩展特性; 类似Servlet过滤器 ● 低级和高级的API:

○ 低级: 使用字节缓存(ByteBuffers)

○ 高级: 使用用户定义的消息对象(objects)和编码(codecs) ● 高度定制化线程模型: ○ 单线程 ○ 一个线程池

○ 一个以上的线程池(也就是SEDA)

● 使用Java 5 SSL引擎提供沙盒(Out-of-the-box) SSL ? TLS ? StartTLS支持 ● 超载保护和传输流量控制 ● 利用模拟对象进行单元测试 ● JMX管理能力

● 通过StreamIoHandler提供基于流的I/O支持 ● 和知名的容器(例如PicoContainer、Spring)集成

● 从Netty平滑的迁移到MINA, Netty是MINA的前辈。

命名规则

Openfire中常见的类名后缀命名包括Starter、Plugin、Listener、Dispatcher、Handler、Manager、Provider,通常情况下,这些命名类包括如下意义:

XXStarter

系统启动类,如org.jivesoftware.openfire.starter.ServerStarter,调用其start()方法可启动系统应用。

XXListener

业务的最终处理类。

XXDispatcher

调度类,其中有很多关键方法,如addListener(),以组合的方式,为类内定义的静态Set实例添加XXListener对象。以便调用dispatchEvent(String property, EventType eventType, Map params)方法遍历处理Set集中的XXListener对象(通过调用XXListener对象的各实际方法完成实际业务)。

XXPlugin

实现Plugin接口的插件类,需实现initializePlugin(PluginManager manager, File pluginDirectory)方法和destroyPlugin()方法。在其初始化方法中调用Dispatcher实现类的addListener()方法如PropertyEventDispatcher.addListener(this)。

XXProvider

实现面向接口编程方式的接口类,通过反射机制创建具体实现类的对象,反射类名配置在ofproperty表对应的记录propvalue属性中。若没有相关配置,则调用默认实现类,默认实现类类名命名规则为DefaultXXProvider。

XXHandler

实际处理类,以ConnectionHandler为例,在org.jivesoftware.openfire.spi. ConnectionManagerImpl类的startClientSSLListeners(String localIPAddress)方法中,有这样一段代码: sslSocketAcceptor.bind(new InetSocketAddress(bindInterface, port), new ClientConnectionHandler(serverName));其中bind方法的第二个参数是新创建的一个ClientConnectionHandler的实例,而它就是ConnectionHandler的一个子类。

系统配置项

Openfire的系统配置项采用文件结合数据库表的方式配置,也有部分默认配置项通过Java硬编码方式配置(如org.jivesoftware.openfire. ConnectionManager接口类中定义的DEFAULT_PORT、DEFAULT_SSL_PORT、DEFAULT_COMPONENT_PORT等),Openfire中比较重要的配置位置包括:

一、 src/conf目录下的openfire.xml配置文件。该配置文件为系统核心配

置文件。在第一次启动Openfire并通过管理控制台完成安装配置后会往该配置文件中填入相应的配置信息。

二、 plugin.xml配置文件。该配置文件为各插件包下的核心配置文件,由它

确定插件核心处理类和相应页面插件的展现等。配置项及含义详见官方插件开发说明部分。

三、 web.xml和web-custom.xml配置文件。用于配置servlet和用户自定义

servlet(插件页面用,放在插件对应目录下)。 四、 ofproperty中的各条记录,该表中包括两个字段name和propvalue,分

别代表配置项名和配置项值。

系统启动流程

系统启动时调用ServerStarter类中的start()方法,通过反射加载org.jivesoftware.openfire.XMPPServer类文件,创建实例时调用其构造函数,在其构造函数中调用其start()方法实际启动服务应用程序。Start()方法中首

先调用verifyDataSource()方法验证并确保数据库可以访问,然后会调用

loadModules();initModules();startModules();方法来对Module接口的实现类的各子类进行操作,依次完成模块的加载、初始化和启动操作。loadModules()方法中会调用loadModule(String module)方法通过反射加载各模块类,参数字符串module为对应的模块核心处理类的类名,如AdHocCommandHandler。现以AdHocCommandHandler为例对接下来的处理流程进行说明。通过loadModule创建AdHocCommandHandler类实例时调用其构造函数,在构造函数中初始化了其私有AdHocCommandManager对象。在initModules()时调用AdHocCommandHandler实例的initialize(XMPPServer server)方法对其私有属性对象进行初始化。然后调用start()方法,调用addDefaultCommands方法添加命令并启动命令(通过调用startCommand(AdHocCommand command)方法实现)。

网络处理 消息监听服务

SSL

等监听服务的调度在

ConnectionManagerImpl

类中实现。

ConnectionManagerImpl.createClientSSLListeners()方法启动SSL监听

消息封装

信息处理采用XML节的方式传递信息,消息封装通常采用IQ、Message、Presence。

Openfire消息包接受处理流程

数据库处理

Openfire的数据库处理采用直接调用JDBC 的方式。核心类为org.jivesoftware.database.DbConnectionManager。数据库的处理与业务处理耦合,没有划分出专门的业务逻辑层。 ConnectionProvider

此类为数据库提供者接口,如需连接mysql、hsqldb等数据库,需首先实现些接口,

处理方式

通常直接调用XXManager中的实例方法,XXManager中又调用的是对应的接口XXProvider的方法,实际操作在该接口的实现类中实现。实现类是动态绑定的(默认的实现类通常命名规则为DefaultXXProvider),在运行时根据ofproperty表中对应配置项值选择。下面以添加用户组为例进行说明。

首先获得GroupManager的一个实例,在调用其构造函数时调用initProvider()方法,在该方法中获取数据库中配置项的值,若不为空则根据该值通过反射机制获取GroupProvider接口的实现类实例对象;若为空则以DefaultGroupProvider作为GroupProvider接口的实现类并创建实例对象,然后调用GroupProvider. createGroup(String name)方法完成业务操作。

常用类

org.jivesoftware.database.DbConnectionManager 连接管理类

org.jivesoftware.util.JiveGlobals 通常用于操作ofproperty表中记录

openfire数据结构

数据库表

以下是一个说明每个表格的Openfire数据库架构。黄色行表示主键。

? ofGroup ? ofGroupProp ? ofGroupUser ? ofID ? ofOffline ? ofPresence ? ofPrivate ? ofUser ? ofUserProp ? ofUserFlag ? ofRoster ? ofRosterGroups ? ofPrivacyList ? ofVCard ? ofVersion ? ofProperty ? ofExtComponentConf ? ofRemoteServerConf ? ofSecurityAuditLog ? ofMucService ? ofMucServiceProp ? ofMucRoom ? ofMucRoomProp ? ofMucAffiliation ? ofMucMember ? ofMucConversationLog ? ofPubsubNode ? ofPubsubNodeJIDs ? ofPubsubNodeGroups ? ofPubsubAffiliation ? ofPubsubItem ? ofPubsubSubscription ? ofPubsubDefaultConf ofGroup (用户组的数据) 列名 groupName description 类型 VARCHAR VARCHAR 长度 50 255 描述 组名称(主键) 组描述 ofGroupProp (名称值协会为一组) 列名 groupName name propValue 类型 VARCHAR VARCHAR VARCHAR 长度 50 100 4000 描述 组名称(主键) 组属性名称(主键) 组属性值 ofGroupUser (组成员) 列名 groupName username administrator 类型 VARCHAR VARCHAR NUMBER 长度 50 100 n/a 描述 组名称(主键) 用户名(主键) 是否为管理员(布尔)(主键) ofID (用于唯一ID序列生成) 列名 idType id 类型 NUMBER NUMBER 长度 n/a n/a 描述 证件类型(例如,组,用户名册)(主键) 下一个可用块编号的(用于数据库独立编号) ofOffline (离线邮件存储) 列名 username messageID creationDate messageSize 类型 VARCHAR NUMBER VARCHAR NUMBER 长度 32 n/a 15 n/a 更改 用户名(主键) 存储信息的编号(主键) 日期信息存储 邮件的大小以字节为单位 stanza TEXT n/a 消息文本 ofPresence (离线的存在) 列名 username offlinePresence offlineDate 类型 VARCHAR TEXT CHAR 长度 64 n/a 15 更改 用户名(主键) 存在的信息设置为用户注销 信息存储日期 ofPrivate (私人数据存储) 列名 username name namespace privateData 类型 VARCHAR VARCHAR VARCHAR TEXT 长度 32 100 200 n/a 描述 用户名(主键) 姓名私营项(主键) 名字空间私营项(主键) 价值的私人数据 ofUser (用户数据) 列名 username plainPassword encryptedPassword name email creationDate modificationDate 类型 VARCHAR VARCHAR VARCHAR VARCHAR VARCHAR VARCHAR VARCHAR 长度 32 32 255 100 100 15 15 描述 用户名(主键) 纯文字密码数据 加密的密码数据(默认) 名字 电邮地址 创建日期 最后更新日期 ofUserProp (名称值协会针对用户) 列名 username name propValue 类型 VARCHAR VARCHAR VARCHAR 长度 32 100 4000 描述 用户名(主键) 用户属性名称(主键) 用户属性值 ofUserFlag (用户类型标识(如残疾人)) 列名 username name startTime endTime 类型 VARCHAR VARCHAR CHAR CHAR 长度 64 100 15 15 描述 用户名(主键) 用户属性名称(主键) 国旗的时候,开始被有效(无效的'现在' ) 当时国旗是结束有效(无效的'永远' ) ofRoster (好友列表) 列名 rosterID 类型 NUMBER 长度 n/a 描述 编号名册(主键)

username jid sub ask recv nick VARCHAR TEXT NUMBER NUMBER NUMBER VARCHAR 32 n/a n/a n/a n/a 255 用户名 地址名册入境 认购地位入境 卖出地位入境 检举表明进入名册收到请求 昵称分配给这个名册入境 ofRosterGroups (组的好友名单中的条目) 列名 rosterID rank groupName 类型 NUMBER NUMBER VARCHAR 长度 n/a n/a 255 描述 名册编号(主键) 立场项(主键) 用户定义的名称,这个名册组 ofPrivacyList (用户隐私清单) 列名 username name isDefault list 类型 VARCHAR VARCHAR NUMBER TEXT 长度 32 100 n/a n/a 描述 用户名(主键) 姓名保密清单(主键) 检举指出,如果这是默认隐私的用户名单 XML表示的隐私清单 ofVCard (电子名片的联系信息) 列名 username vcard 类型 VARCHAR TEXT 长度 32 n/a 描述 用户名(主键) 价值的vCard入境 ofVersion (包含产品版本信息) 列名 name version 类型 VARCHAR INTEGER 长度 50 n/a 描述 名称的项目,版本信息正在跟踪的(主键) 版本号 ofProperty (服务器属性) 列名 name propValue 类型 VARCHAR TEXT 长度 100 n/a 描述 属性名称(主键) 进入值 ofExtComponentConf (外部元件配置) 列名 subdomain secret permission 类型 VARCHAR VARCHAR VARCHAR 长度 255 255 10 描述 子的外部元件(主键) 共享密钥的外部元件 许可,表明如果组件是可以连接到服务器 ofRemoteServerConf (远程服务器配置) 列名 xmppDomain remotePort permission 类型 VARCHAR NUMBER VARCHAR 长度 255 n/a 10 描述 域的外部元件(主键) 港口的远程服务器连接到 许可,表明如果远程服务器可以连接到服务器 ofSecurityAuditLog (伐木安全事件) 列名 msgID username entryStamp summary node details 类型 NUMBER VARCHAR NUMBER VARCHAR VARCHAR TEXT 长度 n/a 64 n/a 255 255 n/a 描述 编号审计信息(主键) 使用者谁执行的行动 时间戳当事件发生 总结了发生在事件 节点事件发生 详细的细节,所发生的 ofMucService (甲Groupchat服务) 列名 serviceID subdomain description isHidden 类型 NUMBER VARCHAR VARCHAR NUMBER 长度 n/a 255 255 n/a 描述 编号的服务(收录) 子服务(主键) 服务说明 1 ,如果隐藏的管理界面名单, 0正常 ofMucServiceProp (名称值协会的Groupchat服务) 列名 serviceID name propValue 类型 NUMBER VARCHAR TEXT 长度 n/a 100 n/a 描述 编号的服务(主键) 属性名称(主键) 属性值 ofMucRoom ( Groupchat室内资料) 列名 roomID creationDate modificationDate name naturalName description canChangeSubject maxUsers canChangeSubject publicRoom moderated 类型 NUMBER VARCHAR VARCHAR VARCHAR VARCHAR VARCHAR NUMBER NUMBER NUMBER NUMBER NUMBER 长度 n/a 15 15 50 255 255 n/a n/a n/a n/a n/a 描述 编号的房间(主键) 创建日期 最后更新日期 姓名房间用作公共编号 天然名称室 客房描述 检举指出是否可以改变参与者的主题 马克斯一些房间居住者 检举指出是否与会者可以改变的主题或不 检举指示是否室将在目录中列出或不 检举指示是否室主持或不 membersOnly canInvite roomPassword canDiscoverJID logEnabled subject rolesToBroadcast useReservedNick canChangeNick canRegister NUMBER NUMBER VARCHAR NUMBER NUMBER VARCHAR NUMBER NUMBER NUMBER NUMBER n/a n/a 50 n/a n/a 100 n/a n/a n/a n/a 检举指出是否房间是会员制或不 检举指出是否占用可以邀请其他用户 密码数据加入室 检举指出是否真正JID的居住者是公共或不 检举指出是否房间谈话记录或不 最后为人所知的主题房间 二元代表的作用,以广播 检举指出是否用户只能加入室使用其保留昵称 检举指出是否可以改变其占用的空间昵称 检举显示用户是否被允许登记室 ofMucRoomProp (名称值协会的Groupchat房间) 列名 roomID name propValue 类型 NUMBER VARCHAR VARCHAR 长度 n/a 100 4000 描述 编号的房间(主键) 属性名称(主键) 属性值 ofMucAffiliation (归属的空间用户) 列名 roomID jid affiliation 类型 NUMBER TEXT NUMBER 长度 n/a n/a n/a 描述 编号的房间(主键) 用户JID (主键) 一些代表所属一级 ofMucMember (室成员资料) 列名 roomID jid nickname 类型 NUMBER TEXT VARCHAR 长度 n/a n/a 255 描述 编号的房间(主键) 用户JID (主键) 保留昵称的会员 ofMucConversationLog (室会话日志) 列名 roomID sender nickname logTime subject body 类型 NUMBER TEXT VARCHAR VARCHAR VARCHAR TEXT 长度 n/a n/a 255 15 50 n/a 描述 编号的空间 JID的用户发送邮件的房间 昵称使用时由用户发出的信息 日期的消息时,被送到房间 新的主题改变的信息 消息正文 ofPubsubNode (节点pubsub服务) 列名 类型 长度 描述 serviceID nodeID leaf creationDate modificationDate parent deliverPayloads maxPayloadSize persistItems maxItems notifyDelete notifyRetract presenceBased sendItemSubscribe publisherModel subscriptionEnabled configSubscription accessModel payloadType bodyXSLT dataformXSLT creator description language name replyPolicy associationPolicy maxLeafNodes VARCHAR 100 VARCHAR 100 NUMBER n/a VARCHAR 15 VARCHAR 15 VARCHAR 100 NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a 编号托管服务节点(主键) 编号的节点(主键) 检举表明节点是否是叶或收集节点 创建日期 最后更新日期 编号的父节点(如果有的话) 检举指出是否有效载荷中包含的通知 最大规模的有效载荷的字节 检举表明节点是否将持续出版项目 最大的项目数量将持续 检举指出是否发送通知时,该节点的配置发生了变化 检举指出是否发送通知时,该节点将被删除 检举指出是否发送通知时,发布的项目将被删除 检举指出是否发送通知只有用户才 检举指出是否向去年出版项目,以新用户 Publisher中使用的模式的节点 检举指出是否允许订阅 检举指出是否新的订户必须设定为活跃 访问模型所使用的节点 类型的有效载荷数据将提供在节点 网址的一个XSLT转换有效载荷的格式为一个邮件正文 网址的一个XSLT转化的有效载荷格式的数据形式结果 notifyConfigChanges NUMBER VARCHAR 15 VARCHAR 10 VARCHAR 100 VARCHAR 100 VARCHAR 100 VARCHAR 1024 JID的实体建立了节点 VARCHAR 255 VARCHAR 255 VARCHAR 50 VARCHAR 15 VARCHAR 15 NUMBER n/a 说明节点 默认语言的节点 名称节点 政策界定业主或出版商是否应得到答复项目 政策规定谁可以联系叶节点的集合 马克斯一些叶节点,一个节点可能会收集 ofPubsubNodeJIDs ( JIDs与节点) 列名 serviceID nodeID jid associationType 类型 VARCHAR VARCHAR VARCHAR VARCHAR 长度 100 100 1024 20 描述 编号托管服务节点(主键) 编号的节点(主键) JID实体(主键) 协会类型的节点 ofPubsubNodeGroups (名册集团与节点) 列名 serviceID nodeID rosterGroup 类型 VARCHAR VARCHAR VARCHAR 长度 100 100 100 内容 编号托管服务节点 编号的节点 名册组节点所有者可以签署和检索项目 ofPubsubAffiliation (节点分支机构) 列名 serviceID nodeID jid affiliation 类型 VARCHAR VARCHAR VARCHAR VARCHAR 长度 100 100 1024 10 描述 编号托管服务节点(主键) 编号的节点(主键) JID的子公司(主键) 所属类别 ofPubsubItem (项目发布到节点) 列名 serviceID nodeID id jid creationDate payload 类型 VARCHAR VARCHAR VARCHAR VARCHAR VARCHAR TEXT 长度 100 100 100 1024 15 n/a 描述 编号托管服务节点(主键) 编号的节点(主键) 编号的出版项目(独特的每个节点)(主键) JID出版商 创建日期 XML的有效载荷包括在出版项目 ofPubsubSubscription (订阅节点) 列名 serviceID nodeID id jid owner state deliver digest digest_frequency expire includeBody showValues subscriptionType 类型 长度 描述 编号托管服务节点(主键) 编号的节点(主键) 编号认购(主键) VARCHAR 100 VARCHAR 100 VARCHAR 100 VARCHAR 1024 地址接收通知 VARCHAR 1024 JID的子公司,拥有认购 VARCHAR 15 NUMBER NUMBER NUMBER n/a n/a n/a 国家认购(工作流程中的) 检举指出是否通知或未启用 检举表明一个实体是否希望收到通知摘要 最低数目的毫秒之间发出任何两个通知消化 日期在租赁认购将结束或已经结束 检举表明一个实体是否希望收到邮件正文除了有效载荷格式 存在这些国家的实体希望收到通知 无论是用户订阅的项目或节点(收集节点只) 收到通知的儿童一定深度(收集节点只) VARCHAR 15 NUMBER n/a VARCHAR 30 VARCHAR 10 n/a subscriptionDepth NUMBER

keyword VARCHAR 200 关键字活动必须符合 ofPubsubDefaultConf (默认配置节点) 列名 serviceID leaf deliverPayloads maxPayloadSize persistItems maxItems notifyDelete notifyRetract presenceBased sendItemSubscribe publisherModel subscriptionEnabled accessModel language replyPolicy associationPolicy maxLeafNodes 类型 长度 描述 编号托管服务节点(主键) 检举指出是否配置属于叶或收集节点(主键) 检举指出是否有效载荷中包含的通知 最大规模的有效载荷的字节 检举表明节点是否将持续出版项目 最大的项目数量将持续 检举指出是否发送通知时,该节点的配置发生了变化 检举指出是否发送通知时,该节点将被删除 检举指出是否发送通知时,发布的项目将被删除 检举指出是否发送通知只有用户才 检举指出是否向去年出版项目,以新用户 Publisher中使用的模式的节点 检举指出是否允许订阅 访问模型所使用的节点 默认语言的节点 政策界定业主或出版商是否应得到答复项目 政策规定谁可以联系叶节点的集合 马克斯一些叶节点,一个节点可能会收集 VARCHAR 100 NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER NUMBER n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a notifyConfigChanges NUMBER VARCHAR 15 VARCHAR 10 VARCHAR 255 VARCHAR 15 VARCHAR 15 NUMBER n/a

WEB服务器

Openfire采用内置的jetty作web服务器,在启动AdminConsolePlugin插件时调用startup()方法启动jetty服务器,9090为其明文端口,9091为其加密端口。

页面处理

Openfire没有采用现在很流行的技术架构(SSH),只使用JSP+JavaBean,但是它有自己的系统设计,就连日志都是自己做的,没有使用我们熟悉的log4j。

现有的Openfire管理控制台可采用插件方式进行扩展(详见插件开发说明部分介绍),页面采用Jsp方式实现,页面直接调用业务处理逻辑类(通常命名为XXManager)的实例方法,通常通过request对象封装的方式传递页面展现判定变量,常出现本页跳转。每个插件可定义自己的Servlet类和web.xml及

web-custom.xml配置文件。 采用装饰框架方式展现页面,decorator页面有两个,即src/web/decorators目录下的两个页面main.jsp和setup.jsp。采用自定义的admin标签实现,标签库admin.tld放置在src/web/WEB-INF目录下,标签解析类放置在org.jivesoftware.admin包下,有SidebarTag、SubnavTag、SubSidebarTag、TabsTag四个解析类。在调用loadPlugin()方法进行插件加载时,解析插件的plugin.xml配置文件,将获取的相关信息封装在AdminConsole类的generatedModel对象中,后期通过插件解析类提取该对象中的数据并配合sitemesh装饰器进行页面展现。详见“使用dom4j设计Openfire式导航菜单”部分相关介绍。

插件开发

Openfire Plugins加载流程

官方插件开发说明

所有插件都存放在openfire根下的plugins目录下。当一个插件被以JAR或WAR文件发布时,他自动扩展为一个文件夹。插件目录结构如下所示: Plugin Structure myplugin/

|- plugin.xml <- 插件定义文件

|- readme.html <- 可选的插件自己述文件,它将被显示给最终用户。 |- changelog.html <-可选的插件版本日志文件,它将被展现给最终用户。 |- logo_small.gif <- 可选的与插件关联的小图标(16x16)文件(也能为png文件) |- logo_large.gif <-可选的与插件关联的大图标(32x32)文件(也能为png文件)

|- classes/ <- 你的插件需要的资源文件(如properties文件) |- database/ <- 可选的你的插件需要的数据库schema文件 |- i18n/ <- 可选的i18n文件,它们为插件提供国际化支持 |- lib/ <- 你的插件需要的库(JAR文件)

|- web <- 需要集成到管理控制台中的各类资源(如果有的话) |- WEB-INF/

|- web.xml <- 配置jsp调度的web.xml配置文件

|- web-custom.xml <- 可选的用户定义的web.xml文件,用于调度自定义servlets |- images/

若插件需要为Openfire的管理控制台添加内容,则web文件夹必须存在。具体内容详述如下。

Plugin.xml文件指定了主插件类,下面是一个例子。

Itvi plugin.xml

plugin.Itvi

TestPlugin

This is an example plugin. Jive Software

1.0 07/01/2006

http://www.igniterealtime.org/projects/openfire/plugins.jsp

3.0.0 gpl

description=\to administer settings for my plugin\ />

各元数据域能在plugin.xml文件中进行设置:

? ? ? ? ? ? ?

?

name – 插件名.

description – 插件描述. author – 插件作者. version – 插件版本.

date – 插件版本生成日期。该日期必须为MM/dd/yyyy格式, 如07/01/2006.

url – 关于该插件的更多详细信息可以从该地址获取.

minServerVersion – 为运行该插件所需的最低的Openfire软件版本(Openfire 2.1.2及后续版本支持该选项).如果服务器版本低于需要的最低版本,插件将不会启动.

databaseKey – 如果插件需要它自己的数据库表,必须将databaseKey元素设置为一个schema key name(通常与插件同名). 然后需将所有需支持数据库类型的数据库schema 文件放到插件的database文件夹下. 例如,给一个关键字“foo”,那么schema文件需命名为\\等.我们建议你在命名你的表时加上前缀\

(openfire),以避免与其他可能使用同一数据库的其他应用系统需要的表产生冲突.关于版本的描述信息需添加到ofVersion表中,以对应的key

做标识,这样可以跟踪schema版本信息,如:

INSERT INTO ofVersion (name, version) VALUES ('foo', 0);

?

databaseVersion – 数据库schema版本(如果定义了数据库schema)。有数据库schema的新插件的版本从0开始编号。如果以后的插件版本需要更新schema,这些更新能通过在database/upgrade目录下为各版本创建子目录的方式来定义。例如目录database/upgrade/1和

database/upgrade/2将包括如 \和\这样的包含各版本相应的数据库改变信息的脚本.这些脚本中均需要更新ofVersion表的信息,如:

UPDATE ofVersion set version=1 where name='foo';

parentPlugin – 父插件名 (\插件相应的为\当一个插件有一个父插件,将不会创建一个新的class loader,相应地替换为使用父插件的class loader。这使得插件间能更紧密地联合工作。子插件不能脱离父插件独立工作。

? licenseType – 指定许可类型信息。 有效值包括:

o \插件被发布为商业性插件。

o \插件被发布为遵循GNU Public License (GPL)协议。 o \插件被发布为Apache license系列

o \插件仅供内部使用,且不能被重新发布。

o \插件被发布为与其他目录下的版本限制不同。License

许可信息需在插件自述文件中进行详细描述。

?

如果许可类型未设置,默认为other。

为了给最终用户提供插件的额外信息,可以为插件添加一些额外文件(全放置在插件的主目录下):

readme.html – 可选的插件自述文件,它的信息将展现给最终用户。 ? changelog.html --可选的插件版本日子文件,它将被展现给最终用户。 ? logo_small.png --可选的与插件关联的小图标(16x16)文件(也能为gif文件)。

? logo_large.png --可选的与插件关联的大图标(32x32)文件(也能为gif文件)。

?

插件必须实现Plugin接口,且有一个默认构造函数。Plugin接口包含了初始化和销毁插件的方法。

Sample plugin implementation package org.example;

import org.jivesoftware.openfire.container.Plugin;

import org.jivesoftware.openfire.container.PluginManager;

import java.io.File; /**

* A sample plugin for Openfire. */

public class ExamplePlugin implements Plugin {

public void initializePlugin(PluginManager manager, File pluginDirectory) {

// Your code goes here

}

public void destroyPlugin() { // Your code goes here } }

Openfire插件开发人员指南

导言

插件是增强Openfire功能。这份文件是一个开发人员创建插件指南。

结构的一个插件

插件存放在插件openfireHome目录。当部署一个jar或war插件文件,它会自动解压安装。该文件在插件目录结构如下: 插件结构

myplugin /

| - plugin.xml 插件定义文件

| - readme.html 任择自述文件的插件,它会显示给最终用户

| - changelog.html 任择修改文件的插件,它会显示给最终用户

| - icon_small.gif 可选小( 16x16 )图标与插件(也可以是 PNG文件)

| - icon_large.gif 可选大( 32x32 )图标与插件(也可以是 PNG文件)

| classes/ 资源的插件需要(即属性文件)

| -database/ 可选数据库架构文件,你需要插件

| -i18n/ 插件国际化的语言配置。

| -lib/ 您的插件的jar包

| -web 资源的管理控制台集成,如果有的话

| - WEB-INF/

| - web.xml 生成web.xml中含有编译JSP的条目

| -web-custom.xml 可选用户自定义的web.xml中的自定义servlets

| -images/ 图片文件存放的目录

web目录存在的插件,需要添加到Openfire管理控制台。进一步的细节如下。

在plugin.xml文件规定的主要插件类。样本文件看起来可能如下: 样本plugin.xml

org.example.ExamplePlugin

Example Plugin

This is an example plugin. Jive Software

1.0 07/01/2006

http://www.igniterealtime.org/projects/openfire/plugins.jsp

3.0.0

gpl

该元数据的领域,可以设置在plugin.xml文件:

? name -插件的名称。 ? description -插件的说明。 ? author -插件的作者。 ? version -该插件的版本。

? date -发布日期如2006年7月1日。 ? url -插件网址。

? minServerVersion -最低Openfire版本

? databaseKey -如果插件需要它自己的数据表,该databaseKey内容应设立

一个架构主要名称(通常是相同名称的插件)。数据库架构文件为每个支持的数据库,然后放置在数据库目录下的插件。例如, “foo”,架构文件将被称为“ foo_mysql.sql ” , “ foo_oracle.sql ”等等,我们建议您,您的表前缀of ,以避免可能的冲突与其他应用程序安装在同一数据库。脚本应该进入ofVersion表使用的关键,这样的架构版本信息可跟踪,例如:

INSERT INTO ofVersion (name, version) VALUES ('foo', 0); databaseVersion -数据

库版本号(如果数据库模式的定义)。新的插件与数据库架构应该开始在版本。如果将来插件版本的需要更新,这些更新可以定义创建子目录中的升级数据库目录为每个版本。例如,目录database/upgrade/1和database/upgrade/2将包含脚本,如“ foo_mysql.sql ”和“ foo_oracle.sql ”中包含相关的数据库,为每一个版本的变化。每个脚本应该更新版本中的信息ofVersion表,例如:

UPDATE ofVersion set version=1 where name='foo';

? parentPlugin -父层插件(作为“foo”的“ foo.jar ”插件)。当一个插件有一个父插

件,插件的类加载器将被使用来而不是建立一个新的类加载器。这可让插件更加紧密地协同工作。子插件将不会影响其父插件。

? “licenseType”:显示许可协议,该插件是由。有效值如下:

o “commercial”:commercial “商业” :插件是下发布的商业许可协议。 o “gpl”: “通用公共许可证” :插件发布使用GNU公共授权( GPL )。 o “apache” :该插件发布的Apache许可证。

o “internal” :(内部)插件是供内部使用的一个组织只,并不会重新分配。 o “other” :(其他)插件是许可下发布agrement不属于其中的其他类别。许

可协议的细节应该在插件的自述。 如果许可证类型未设置,这是假定其他。

一些额外的文件都可以在该插件提供更多资料,以最终用户(所有放置在主要的插件目录):

? readme.html -可选自述文件的插件,它会显示给最终用户。 ? changelog.html -可选修改文件的插件,它会显示给最终用户。

? icon_small.png -可选小( 16x16 )图标相关插件。也可以是。 GIF文件。 ? icon_large.png -可选大( 32x32 )图标相关插件。也可以是。 GIF文件。

interface from the Openfire API as well as have a default (no argument) contructor.您的插件类必须执行插件接口从Openfire的API ,以及有一个缺省(无参数) contructor 。插件接口方法初始化和销毁插件。

采样插件执行

package org.example;

import org.jivesoftware.openfire.container.Plugin;

import org.jivesoftware.openfire.container.PluginManager;

import java.io.File; /**

* A sample plugin for Openfire. */

public class ExamplePlugin implements Plugin {

public void initializePlugin(PluginManager manager, File pluginDirectory) { //您的代码 }

public void destroyPlugin() { //您的代码 } }

General Plugin Best Practices一般插件最佳实践

在选择软件包的名称为您的插件,我们建议您选择一些与众不同的您和/或您的组织,以帮助避免冲突尽可能。例如,如果每个人去与org.example.PluginName ,即使PluginName是不同的,您可能会开始投放到一些冲突在这里和那里的类名。尤其是当工作与聚类。 。

修改管理控制台

插件可以添加标签,区段,和网页的管理控制台。有几个步骤,以完成这项:

? 第一个必须添加到plugin.xml文件。

? JSP的文件必须编制和实施的类路径的插件。阿web.xml中的汇编文件,其中包

含的JSP servlet的条目必须付诸网页/目录下的插件。注: Openfire建立脚本可以协助编制JSPs和创造web.xml中。这是详细说明如下。

? 任何图像所需的JSP页面必须住在网页/图像/目录。只有GIF和PNG图像的支

持。

该部分plugin.xml定义额外的标签,科和项目管理控制台框架。抽样

plugin.xml文件看起来可能如下:

样本plugin.xml

org.example.ExamplePlugin

description=\

在这个例子中,我们定义一个新的标签“范例” ,一个工具栏目“我的插件”和一个网页“我的插件管理” 。我们已经注册的,插件admin.jsp页面。您可以覆盖现有的标签,区段,项目利用现有的ID属性值在自己的定义。 管理控制台最佳实践

有几种最佳做法时,需要考虑变更Openfire管理控制台通过一个插件。总的主题是无缝集成的插件应该:

? 集成到现有的标签和侧栏节只要有可能不是创建自己的。只有创造新标签的非常

重要的新功能。

? 不要用“插件”的名称,标签, sidebars和项目。例如,有一个项目叫做“网关插

件” ,也可能是所谓的“网关设置” 。

? 尝试以符合现有的用户界面管理控制台在您的自定义插件的网页。 ? 没有必要建立一个管理控制台进入查看插件元数据。相反,让Openfire告知用

户有关该插件安装,并提供插件管理。

写作网页管理控制台

Openfire使用Sitemesh框架装饰网页的管理控制台。全球定义的装修适用于每个网页,以便使最终产出,如下面的图:

建立网页,与Sitemesh是容易的。只要创建有效的HTML页面,然后使用中继标记来传送指示Sitemesh 。当渲染输出, Sitemesh将使用您所提供的指示,使装修的任何内容,在您的HTML网页。以下元标记可用于:

? pageID -的ID的网页,其中必须符合入境管理控制台中的XML上文所述。要么pageID

或subPageID 必须指定。

? subPageID -的ID小组网页,其中必须符合入境管理控制台中的XML上文所述。小

组网页用于行政行为涉及到父页面编号。例如,编辑或删除某一特定群体。要么pageID或subPageID 必须指定。

? extraParams (可选) -额外的参数,应通过网页。例如,在网页上删除一组可能的

ID集团。参数必须是URL编码。

? 装修(可选) -覆写Sitemesh装修使用的网页。装修没有命名,将可提供简单的网页

没有装修。

下面的HTML代码段显示了有效的网页: 范例

My Plugin Page

Body here!

在您使用本土化插件

这有可能把你的插件成多种语言化( i18n )。为此,请使用下列程序进行:

? 创建一个“i18n”目录的根目录中的插件。

? 购买每项资源文件,并使用%[plugin_name]%_i18n \。属性”

命名约定,在那里[ plugin_name ]的名字是插件目录中。见翻译指南 ,详细了解资源包。

? 字符串转换在您的JSP的文件指的是国际钥匙。例如:

<%@ taglib uri=\prefix=\

<%@ taglib uri=\prefix=\ ...

? 国际化在你的Java文件使用LocaleUtils class:

org.jivesoftware.util.LocaleUtils.getLocalizedString(\\

? 国际化plugin.xml文件在您使用$ (叶)格式:

${plugin.description}

使用Openfire构建脚本

在Openfire建立脚本将帮助您建立和发展插件。它看起来的插件开发目录格式如下: 插件结构

myplugin/

|- plugin.xml <-插件定义文件 |- readme.html <-插件自述文件 |- changelog.html <-插件修改的日志 |- icon_small.gif <- 缩略图片(16x16) |- icon_large.gif <-图片 (32x32)

|- classes/ <-资源的插件需要(即属性文件) |- lib/ <- 包 |- src/

|- database <-可选的数据库脚本的插件 |- java <- 插件的Java源代码 | |- com

| |- mycompany | |- *.java |- web

|- *.jsp <- jsp页面 |- images/ <- 图片文件 |- WEB-INF

|- web.xml <-可选的文件自定义servlets可以注册

构建脚本将编译源文件和JSPs ,并建立有效的插件结构和JAR文件。把你的插件目录中的 src

/插件目录下的源分布,然后用antplugins来建立您的插件。

任何JAR文件的插件需要在汇编应放到lib目录。这些JAR文件也将被复制到插件的生成lib目录的构建过程的一部份。

如果您创建一个src/web/WEB-INF/web.xml,注册servlets初始化时启动插件。只有注册的servlet和servlet的映在web.xml中的文件。注:此功能是通过合并执行您的自定义web.xml中的档案文件的web.xml中所产生的JSP的汇编过程。

执行您的插件

插件完全进入Openfire的API 。这提供了一个巨大的灵活性,什么插件可以完成。然而,有几个集成点,这是最常见的:

1. 注册一个插件作为一个组成部分 。元件接收所有数据包给某一特定子网域。例如,

test_component.example.com。因此,数据包发送到

joe@test_component.example.com将交付给该组件。请注意,子域定义为组件无关的

DNS条目的子域。所有的XMPP协议路由在套接字级别是使用主服务器域( example.com在上面的例子) ;子域仅用于路由的XMPP协议的服务器。

2. 注册一个插件作为IQHandler 。智商处理回应智商包特别元素的名称和命名空间。下面

的代码片断演示了如何注册一个IQHandler : IQHandler myHandler = new MyIQHander();

IQRouter iqRouter = XMPPServer.getInstance().getIQRouter(); iqRouter.addHandler(myHandler);

3. 注册一个插件作为PacketInterceptor接收所有数据包被通过该系统,并有选择地予以拒

绝。例如,一个拦截可以拒绝所有的邮件或载亵渎国旗他们审查的管理员。

4. 您可以储存持续插件设置Openfire属性使用JiveGlobals.getProperty (字符串)和

JiveGlobals.setProperty (字符串,字符串)的方法。让您的插件属性听众收听改变其性质,执行org.jivesoftware.util.PropertyEventListener方法。您可以注册您的插件作为一个听者使用PropertyEventDispatcher.addListener

( PropertyEventListener )方法。一定要注册您的插件作为听众在您的插件的destroyPlugin ()方法。

插件常见问题解答

我是否可以部署一个插件目录不是一jar?

不,所有的插件必须部署Jar文件或WAR文件。当插件的一个jar或war不存在, Openfire假设该文件已被删除,而用户要销毁插件,所以它也删除了目录。

通常的插件开发经验

当为你的插件包命名时,我们建议你选择你或你的单位相关的唯一性信息作为包名以尽可能地避免冲突。例如,如果所有人都以“org.example.插件名”为插件类命名,即使插件名不同, 你也可能在不同的地方遇到类名冲突的情况。这种情况在联合开发时特别容易碰到。

Openfire和MINA

看了几天的openfire,网上的资料太少了,只有一个国外的网站不错,http://www.igniterealtime.org/ ,其他的只能自己摸索了。 openfire启动:

ServerStarter

会加载 org.jivesoftware.openfire.XMPPServer 在XMPPServer会加载一系列模块

其中的ConnectionManagerImpl 是连接模块

// Load this module always last since we don't want to start listening for clients

// before the rest of the modules have been started loadModule(ConnectionManagerImpl.class.getName()); ConnectionManagerImpl 会启动一系列的监听。

其中的createClientListeners和startClientListeners是我比较关心的,先看看在这里面openfire都做了什么!

private void createClientListeners() {

// Start clients plain socket unless it's been disabled.

if (isClientListenerEnabled()) {

// Create SocketAcceptor with correct number of processors socketAcceptor = buildSocketAcceptor();

// Customize Executor that will be used by processors to process incoming stanzas

ExecutorThreadModel threadModel = ExecutorThreadModel.getInstance(\ ); int eventThreads =

JiveGlobals.getIntProperty(\ , 16); ThreadPoolExecutor eventExecutor = (ThreadPoolExecutor)threadModel.getExecutor();

eventExecutor.setCorePoolSize(eventThreads + 1); eventExecutor.setMaximumPoolSize(eventThreads + 1); eventExecutor.setKeepAliveTime(60, TimeUnit.SECONDS );

socketAcceptor .getDefaultConfig().setThreadModel(threadModel);

// Add the XMPP codec filter

socketAcceptor .getFilterChain().addFirst(\ , new ProtocolCodecFilter(new XMPPCodecFactory()));

// Kill sessions whose outgoing queues keep growing and fail to send traffic

socketAcceptor .getFilterChain().addAfter(\ , \ \new StalledSessionsFilter()); } }

对了这里就是和用的mina框架去做联网处理,首先设置mina框架的线程池,然后把由XMPPCodecFactory做为 ProtocolCodecFilter的chain添加到FilterChain中! 然后

private void startClientListeners(String localIPAddress) {

// Start clients plain socket unless it's been disabled. if (isClientListenerEnabled()) {

int port = getClientListenerPort(); try {

// Listen on a specific network interface if it has been set.

String interfaceName =

JiveGlobals.getXMLProperty(\ InetAddress bindInterface = null; if (interfaceName != null) {

if (interfaceName.trim().length() > 0) {

bindInterface = InetAddress.getByName(interfaceName); } }

// Start accepting connections socketAcceptor

.bind(new InetSocketAddress(bindInterface, port), new ClientConnectionHandler(serverName));

ports.add(new ServerPort(port, serverName, localIPAddress, false, null, ServerPort.Type.client));

List params = new ArrayList(); params.add(Integer.toString(port));

Log.info(LocaleUtils.getLocalizedString(\n\ }

catch (Exception e) {

System.err.println(\port \

e.getMessage());

Log.error(LocaleUtils.getLocalizedString(\.socket-setup\ } } }

socketAcceptor

.bind(new InetSocketAddress(bindInterface, port), new ClientConnectionHandler(serverName)); 将ClientConnectionHandler作为数据处理 服务器去监听5222端口去了,mina真方便!

关于MINA框架可以去网上找找资料,这里就不说了。

这里要说下MINA中的IoHandler这个接口,IoHandler是最终面对用户的接口,看下这个接口中的方法:

public interface IoHandler { /** * Invoked from an I/O processor thread when a new connection has been

created.

* Because this method is supposed to be called from the same thread that

* handles I/O of multiple sessions, please implement this method to perform * tasks that consumes minimal amount of time such as socket parameter * and user-defined session attribute initialization. */

void sessionCreated(IoSession session) throws Exception;

/**

* Invoked when a connection has been opened. This method is invoked after

* {@link #sessionCreated(IoSession)}. The biggest difference from * {@link #sessionCreated(IoSession)} is that it's invoked from other thread

* than an I/O processor thread once thread model is configured properly. */

void sessionOpened(IoSession session) throws Exception;

/**

* Invoked when a connection is closed. */

void sessionClosed(IoSession session) throws Exception;

/**

* Invoked with the related {@link IdleStatus} when a connection becomes idle.

* This method is not invoked if the transport type is UDP; it's a known bug,

* and will be fixed in 2.0. */

void sessionIdle(IoSession session, IdleStatus status) throws Exception;

/**

* Invoked when any exception is thrown by user {@link IoHandler} * implementation or by MINA. If cause is an instance of * {@link IOException}, MINA will close the connection automatically. */

void exceptionCaught(IoSession session, Throwable cause) throws Exception;

/**

* Invoked when a message is received. */

void messageReceived(IoSession session, Object message) throws Exception;

/**

* Invoked when a message written by {@link IoSession#write(Object)} is

* sent out. */ void messageSent(IoSession session, Object message) throws Exception; }

在mina中实现这个接口的类是IoHandlerAdapter 这个类

/**

* An abstract adapter class for {@link IoHandler}. You can extend this * class and selectively override required event handler methods only. All

* methods do nothing by default. *

* @author The Apache MINA Project (dev@mina.apache.org)

* @version $Rev: 671827 $, $Date: 2008-06-26 10:49:48 +0200 (jeu, 26 jun 2008) $ */

public class IoHandlerAdapter implements IoHandler {

private final Logger logger = LoggerFactory.getLogger(getClass());

public void sessionCreated(IoSession session) throws Exception { }

public void sessionOpened(IoSession session) throws Exception { }

public void sessionClosed(IoSession session) throws Exception { }

public void sessionIdle(IoSession session, IdleStatus status) throws Exception { }

public void exceptionCaught(IoSession session, Throwable cause) throws Exception {

if (logger.isWarnEnabled()) {

logger.warn(\ + getClass().getName()

+ \for proper handling:\cause); } }

public void messageReceived(IoSession session, Object message) throws Exception { }

public void messageSent(IoSession session, Object message) throws Exception { } }

接下来转到openfire,在openfire中ConnectionHandler类继承自IoHandlerAdapter,也就充当着最后要面对用户的角色。

public abstract class ConnectionHandler extends IoHandlerAdapter

这个是ConnectionHandler的类图,可以看出他有3个子类,而其中的ClientConnectionHandler是处理客户端和服务器连接用到的,也是我先要说明的。

当有客户端进行连接的时候MINA框架会调用IoHandler的sessionOpened(),

public void sessionOpened(IoSession session) throws Exception { // Create a new XML parser for the new connection. The parser will be used by the XMPPDecoder filter.

XMLLightweightParser parser = new XMLLightweightParser(CHARSET );

session.setAttribute(XML_PARSER, parser);

// Create a new NIOConnection for the new session

NIOConnection connection = createNIOConnection(session); session.setAttribute(CONNECTION , connection); session.setAttribute(HANDLER , createStanzaHandler(connection));

// Set the max time a connection can be idle before closing it int idleTime = getMaxIdleTime(); if (idleTime > 0) {

session.setIdleTime(IdleStatus.READER _IDLE, idleTime); } }

可以看到这里分别创建了XMLLightweightParser,NIOConnection,ClientStanzaHandler实例并将它们放到了session中

当客户端往服务器发送消息的时候会调用IoHandler的messageReceived(IoSession session, Object message)

public void messageReceived(IoSession session, Object message) throws Exception {

// Get the stanza handler for this session StanzaHandler handler = (StanzaHandler) session.getAttribute(HANDLER ); // Get the parser to use to process stanza. For optimization there is going

// to be a parser for each running thread. Each Filter will be executed

// by the Executor placed as the first Filter. So we can have a parser associated

// to each Thread

int hashCode = Thread.currentThread().hashCode(); XMPPPacketReader parser = parsers .get(hashCode); if (parser == null ) {

parser = new XMPPPacketReader(); parser.setXPPFactory(factory );

parsers .put(hashCode, parser); }

// Update counter of read btyes updateReadBytesCounter(session);

System.out .println(\ + message);

// Let the stanza handler process the received stanza try {

handler.process((String) message, parser); } catch (Exception e) {

Log.error(\connection due to error while processing message: \ + message, e);

Connection connection = (Connection) session.getAttribute(CONNECTION ); connection.close(); } }

在这里得到了XMPPPacketReader 然后处理过来的XML数据。

到这里就完成了C------>S的数据传输,数据通过MINA层来到了XMPP层。 上面写了数据由MINA层到了XMPP层,接下来看看数据(XMPP)是在OPENFIRE的XMPP层里都被做什么处理了,由于研究的时间比较短,只能大概写一下。 可以看到数据在messageReceived()中被StanzaHandler处理了。

上面是类图,只画了C-S的(ClientStanzaHandler)的。

首先如果是有新客户端连接的话会调用sessionOpened().然后创建一个ClientStanzaHandler。

接着数据在MINA框架经过第一次解码(XMPPCodecFactory)后传到ConnectionHandler中的

messageReceived(),然后处理客户端发来的XMPP包,StanzaHandler 会解析数据包(process),

如果是\开头的话会为此客户端创建一个clientsession(createSession()). 下面是代码片段:

boolean initialStream = stanza.startsWith(\ ) || stanza.startsWith(\ );

if (!sessionCreated || initialStream) { if (!initialStream) {

// Allow requests for flash socket policy files directly on the client listener port

if (stanza.startsWith(\ )) { String crossDomainText = FlashCrossDomainServlet.CROSS_DOMAIN_TEXT +

XMPPServer.getInstance().getConnectionManager().getClientListenerPort() +

FlashCrossDomainServlet.C ROSS_DOMAIN_END_TEXT + '\\0' ;

connection.deliverRawText(crossDomainText); return; }

else {

// Ignore

// Found an stream:stream tag... if (!sessionCreated ) { sessionCreated = true;

MXParser parser = reader.getXPPParser(); parser.setInput(new StringReader(stanza)); createSession(parser); }

以后再有 xmpp数据过来的话,会送到clientSession中去处理,比如给好友发消息,到clientSession中解析完了后,会调用deliver()由NIOConnection发到MINA层中。

至此,数据从C----->S的简单流程就完事了,以后有时间会写S----->S的数据处理!

使用dom4j设计Openfire式导航菜单

通过上一篇文章(dom4j实战(一)——使用dom4j从XML中读取数据源配置),使我们对dom4j有了一些初步的认识和了解,也掌握了基本的操作方法,本文将承接前篇,借鉴Openfire项目中菜单设计的优点,结合我们自己的需求,通过一个小实例,来说明一下如何使用dom4j来实现这个功能。

可以在http://www.igniterealtime.org/downloads/index.jsp 这里下载到Openfire的发布版和源码版,Openfire的介绍在这里不作描述,网上相关的资料有很多,有兴趣的朋友可以下载源码研究一下,其实不了解Openfire也没关系,因为我们只是借鉴其中的一些思想,等做完了实例,就会有一个比较直观的认识。

大象建议先在最后面下载源码和必要的JAR包,让程序运行起来看下效果,再看下面的详细说明。

开发环境:Eclipse 3.2.1 MyEclipse 5.10GA Tomcat 6.10 dom4j-1.6.1.jar jaxen-1.1-beta-7.jar sitemesh-2.2.1.jar 1、创建tag-console.xml

在src目录下建一个tag-console.xml文件,这个配置文件中写的是菜单信息,内容如下:

菜单导航demo ver 1.0 菠萝大象

上面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 = \

ResourceManage前面加载资源的部分和上一篇,后来修改过的DataBaseConnect类一样,只需把Constant.DB_CONFIG换成

Constant.TAG_CONFIG就行了。接下来,在类中加入几个读取XML中基本信息的方法:

菜单导航demo ver 1.0 菠萝大象

这里只举出取得appname元素值的方法,其它的几个都很相似,请查看源代码。

/**

* 得到应用程序名称 */

public static String getAppName(){ Element appName = (Element)

coreModel.selectSingleNode(\ 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(\ }

这里用到了XPATH语法,根据传入的id值,在整个文档中查找id属性值与此一致的元素对象。用下面的代码举例说明:

description=\深入详解\ 当传入的id属性值为\时,那么我们就会得到对应这个id值的book元素对象,id属性值在整个配置文件中就是一个key关键字,起到定位的作用。

根据id属性值查找上下文中对应的catalog元素:

/**

* 根据传入的id查找上下文中对应的catalog元素 * @param id 待查找的id属性值

* @return 返回id值所在的catalog元素对象 */

public static Element getElementByID(String id) {

return (Element) coreModel.selectSingleNode(\ + \}

ancestor是XPATH语法中轴的概念,我引用网上官方文档中的说明:“ancestor轴(axis)包含上下节点的祖先节点,该祖先节点由其上下文节点的父节点以及父节点的父节点等等诸如此类的节点构成,所以ancestor轴总是包含有根节点,除非上下文节点就是根节点本身。”这句话的意思其实就是向上查找节点,直到找到根节点为止。对于ancestor::catalog来说,就是向上查找直到catalog节点为止。所以getElementByID这个方法是根据传入的id属性值在上下文中查找节点,直到找到这个id值所在的上下文catalog节点为止。当传入的id属性值为\时,我们会得到id=\这个catalog节点元素,而不会得到id=\这个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(\ 将所有的catalog元素取出放到List集合中,这里是只取得catalog这一层级的元素,实质就是catalogs中只有两个对象,一个是id=\,

另一个是id=\,使用dom4j我们会发现处理元素非常容易,API相当的丰富,想写成什么样完全凭你自己的想法。 //将所有的catalog元素取出放到List集合

List catalogs =

ResourceManage.getCoreModel().selectNodes(\ 看看前面介绍的getElementByID这个方法,这个currentCatalog所表示就是pageID所在的catalog元素。上面的代码是为了和下面的代码结合来判断当前的菜单是否为选中,加入CSS样式显示。

//pageID所在的catalog元素,主要用来判断当前菜单是否被选中 Element currentCatalog =

(Element)ResourceManage.getElementByID(pageID); 从BodyContent中将标签体缓存流读取出来,标签在WebRoot/decorators/main.jsp中:

onmouseover=\onmouseout=\

String value = getBodyContent().getString(); //得到标签体 Catalogs里面是两个catalog对象,循环遍历取出,将标签体中的[id]、[url]、[name]、[description]替换为XML文件中的属性值,这样主菜单标签就生成了。

for (int i=0; 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, \ .getContextPath()

+ \ value = StringUtils.replace(value, \

value = StringUtils.replace(value, \ attributeValue(\

}

}

String css = getCss();

//对当前选中菜单添加CSS样式

if (catalog.equals(currentCatalog)) { css = getCurrentcss(); }

buf.append(\if (i > 0) {

buf.append(\}

buf.append(value).append(\

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元素,如果为691245,则subnav为id=\的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,添加如下代码:

sitemesh

com.opensymphony.module.sitemesh.filter.PageFilter

sitemesh /*

然后在WEB-INF下新建decorators.xml文件,内容如下:

/index.jsp /book_*.jsp

请注意defaultdir后面的值,这是你放装饰器页面的目录位置。本例中,在WebRoot目录下新建decorators文件夹,再在里面新建main.jsp,这个就是装饰器页面了。之间的内容就是需要被装饰的页面,*号是通配符,可以代替任何字符。/book_*.jsp表示:使用main.jsp装饰WebRoot目录下所有以book_开头的页面,这里定义的name=\,可以在装饰器页面中使用,因为不一定只有一个装饰器页面,可能会有很多个。因此,在装饰器页面中为了布局效果会联合使用多个装饰器来修饰页面,以达到简化布局、降低维护难度、提高工作效率的作用。另外在使用时,请注意被装饰页面与装饰器页面之间的相对位置。 6、main.jsp

在页面中引用被装饰页面的page对象:

使用decoratedPage取得被装饰页面中meta标签的content值,再将它放到request请求中,这样在自定义标签类中我们使用pageContext对象得到的请求就是这个。

<%

request.setAttribute(\

decoratedPage.getProperty(\ %>

显示被装饰页面之间的标题: 显示被装饰页面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=\

Struts2 深入详解

Struts2 深入详解

其余的几个页面内容大致一样,把content值、标题、以及

之间的内容换成book元素中定义的属性值即可。

demo.css

在WebRoot目录下新建css文件夹,再在里面创建demo.css文件。我直接把Openfire的样式表COPY过来。然后把里面没用的部分删除了,体积小了不少。 图片

我从Openfire中只取了本例用到的图片,如果是专业美工,完全可以设计出自己的菜单风格。 8、发布项目

我们在web.xml中可以加入下面一段代码,将index.jsp作为我们的默认显示页面:

index.jsp

把demo部署到%TOMCAT_HOME%webapps目录下,启动tomcat,在地址栏中输入:http://localhost:8080/demo 看看效果是不是和下面的一样:

本文来源:https://www.bwwdw.com/article/bqfr.html

Top