将Openfire中的MUC改造成类似QQ群一样的永久群

一、思路

1、新建立永久房间,同时保证房间中的成员存储到“ofmucmember”表当中。

2、新建一个用户登陆监听的插件,这个插件监听用户的登陆行为,同时在用户登陆时查询“ofmucmember”表,查询用户所在的房间。

3、将房间信息封装到自定义的IQ包中,发送到客户端。

4、不管使用Android客户端,还是对Spark进行二次开发,对Openfire发送过来的自定义IQ包进行解析。

5、显示群信息。


二、难点

关于“将Openfire中的MUC改造成类似QQ群一样的永久群”类似的文章不多,其中可见的就是“ 改造MUC实现Openfire群”和“ spark openfire conference修改使群组编程持久的。 类似qq群(1)”这两篇文章,但是如果你按照这两篇文章实现来改造Openfire的话,基本上是实现不了QQ群的功能的,但是还是感谢两位作者提供的思路。两位作者的思路都是可以的,但是都用一个共同的问题,就是没有提到如何将房间成员的关系信息存储到“ofmucmember”中。
虽然Openfire所遵循的 XMPP文档( 中文版,感谢 如意通团队翻译)中的有对room的说明如下:

Persistent Room A room that is not destroyed if the last occupant exits; antonym: Temporary Room.

也就是说,可以建立一个持久的room,但是这个room不同于其他room的特点是,当我们都离开这个Persistent  room之后,其不会消失,其会存储在“ofmucroom”表中,而其他room不会存储在这个表中。同时这个Persistent  room和其他的room一样都不会存储
room中用户的信息到“ofmucmember”表中。具体在Openfire的论坛中, 有很多人都在问为什么ofmucmember这个表是空的,但是,没有人回答,为什么是空的,什么时候想数据库里面插入数据。包括博主自己在读完org.jivesoftware.openfire.muc、org.jivesoftware.openfire.muc.cluster、org.jivesoftware.openfire.muc.spi这三个与MUC相关的三个包中的类之后,虽然在org.jivesoftware.openfire.muc.spi.MUCPersistenceManager类中有
 private static final String ADD_MEMBER =
        "INSERT INTO ofMucMember (roomID,jid,nickname) VALUES (?,?,?)";
向ofmucmember插入数据的代码,同时顺藤摸瓜,你也可以找到一些列的调用函数。但是真正在运行的时候,我并没有找到什么时候能够调用这段代码实现向ofmucmember插入数据的操作。于是,解决对于持久room当中用户的持久化问题,就是解决整个改造问题的关键。

三、Openfire端修改方案


3.1 建立永久房间

打开Spark新建一个房间,同时将房间设置为固定的如下图:
将Openfire中的MUC改造成类似QQ群一样的永久群_第1张图片

验证是否创建成功SQL语句如下:
SELECT * FROM `openfire`.`ofmucroom`
结果如下:

如图,我们已经创建了一个名为csdn的房间。接下来就是修改Openfire的部分代码,实现对对持久房间的用户插入ofmucmember功能。

3.2 实现插入ofmucmember功能

在org.jivesoftware.openfire.muc.spi.MUCPersistenceManager类中增加方法public static void saveMember(LocalMUCRoom localMUCRoom, JID bareJID, String resource) 如下:
    public static void saveMember(LocalMUCRoom localMUCRoom, JID bareJID,
			String resource) {
		 Connection con = null;
         PreparedStatement pstmt = null;
         try {
             con = DbConnectionManager.getConnection();
             pstmt = con.prepareStatement(ADD_MEMBER);
             pstmt.setLong(1, localMUCRoom.getID());
             pstmt.setString(2, bareJID.toBareJID());
             pstmt.setString(3, resource);
             pstmt.executeUpdate();
         }
         catch (SQLException sqle) {
             Log.error(sqle.getMessage(), sqle);
         }
         finally {
             DbConnectionManager.closeConnection(pstmt, con);
         }
		
	}
在org.jivesoftware.openfire.muc.spi.LocalMUCRoom类下的public LocalMUCRole joinRoom(String nickname, String password.......)方法中增加如下代码:
                }

		finally {
			lock.writeLock().unlock();
		}
lock锁结束之前,也就是说在lock锁内部加入以下代码:
			try {
				if (isPersistent()) {
					MUCPersistenceManager.saveMember(this, bareJID,
							bareJID.getNode());
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
也就说,如果这个room是Persistent  room,那么,每个用户过来,我都让其保存到ofMucMember表中,如果这个用户已经在这个表中,会报主键冲突的错误,如果不想看到这个错误,可以再插入用户之前先进行查询操作,查找用户是否在表中。如果不在的话,就直接插入。
CREATE TABLE `ofmucmember` (
	`roomID` BIGINT(20) NOT NULL,
	`jid` TEXT NOT NULL,
	`nickname` VARCHAR(255) NULL DEFAULT NULL,
	`firstName` VARCHAR(100) NULL DEFAULT NULL,
	`lastName` VARCHAR(100) NULL DEFAULT NULL,
	`url` VARCHAR(100) NULL DEFAULT NULL,
	`email` VARCHAR(100) NULL DEFAULT NULL,
	`faqentry` VARCHAR(100) NULL DEFAULT NULL,
	PRIMARY KEY (`roomID`, `jid`(70))
)
通过定义可以知道ofmucmember表的定义中roomID和jid为联合主键,不会产生每次SQL查询都只是用一个主键的效率问题。
好了,以上就是实现插入ofmucmember功能的主要代码。以上这个方法只是一个临时方法,如果,网友有更好的方案,欢迎留言探讨。

3.3 新建一个用户登陆监听的插件

关于如何创建Openfire插件的工作,可以在网上搜索一下,有很多博主都有对其详细的描述,在这里就不再一一描述,在这里只是将主要代码贴下来:

3.3.1 创建plugin.xml文件



	
	com.yang.plugin.MUCPersistencePlugin
	
	SimplePlugin
	This is the Yang sample plugin.
	YangZhilong
 
	1.0
	15/11/2014
	http://localhost:9090/openfire/plugins.jsp
	3.4.1
	gpl
 
	    
    

3.3.2 创建MUCDao.java文件

package com.yang.plugin.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jivesoftware.database.DbConnectionManager;
/**
 * @Title: MUCDao.java 
 * @Package com.yang.plugin.dao 
 * @Description: 根据用户名查询出用户所在群组的信息
 * @author Yang  
 * @blog http://blog.csdn.net/yangzl2008
 * @date 2013年11月27日 下午7:16:22 
 * @version V1.0
 */
public class MUCDao {

	public static List> getMUCInfo(String jid) {
		
		List> list = new ArrayList>();
		String sql = "select ofmucroom.serviceID, ofmucroom.name, ofmucroom.roomid ,ofmucmember.nickname from "
				+ "ofmucroom join ofmucmember on ofmucroom.roomID = ofmucmember.roomID and ofmucmember.jid = ?";
		Connection connection = null;
		PreparedStatement statement = null;
		ResultSet resultSet = null;
		Map map = null;
		try {
			connection = DbConnectionManager.getConnection();
			statement = connection.prepareStatement(sql);
			statement.setString(1, jid);
			resultSet = statement.executeQuery();
			while (resultSet.next()) {
				map = new HashMap();
				map.put("serviceID", resultSet.getString(1));
				map.put("name", resultSet.getString(2));
				map.put("roomid", resultSet.getString(3));
				map.put("nickname", resultSet.getString(4));
				list.add(map);
			}
		} catch (Exception e1) {
			e1.printStackTrace();
		} finally {
			DbConnectionManager.closeConnection(resultSet, statement,
					connection);
		}
		return list;
	}
}
这个代码的主要作用就是,根据用户名查询出用户所在群组的信息。

3.3.3 创建MUCPersistencePlugin

package com.yang.plugin;

import java.io.File;
import java.util.List;
import java.util.Map;

import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.jivesoftware.openfire.IQRouter;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.event.SessionEventDispatcher;
import org.jivesoftware.openfire.event.SessionEventListener;
import org.jivesoftware.openfire.handler.IQHandler;
import org.jivesoftware.openfire.muc.spi.LocalMUCRoom;
import org.jivesoftware.openfire.muc.spi.MultiUserChatServiceImpl;
import org.jivesoftware.openfire.session.Session;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;

import com.yang.plugin.dao.MUCDao;
/**
 * 
 * @Title: MUCPersistencePlugin.java 
 * @Package com.yang.plugin 
 * @Description: 监听用户登录信息,用户登录之后,查询用户所在room的信息,将room信息发送到客户端。
 * @author Yang 
 * @blog http://blog.csdn.net/yangzl2008
 * @date 2013年11月27日 上午9:32:21 
 * @version V1.0
 */
public class MUCPersistencePlugin implements Plugin, SessionEventListener {

	private XMPPServer server;
	private MultiUserChatServiceImpl mucService;
	
	private IQRouter router;
	@Override
	public void sessionCreated(Session session) {
		JID userJid = session.getAddress();
		joinRooms(userJid);
	}

	@Override
	public void sessionDestroyed(Session session) {
	}

	@Override
	public void resourceBound(Session session) {
	}

	@Override
	public void anonymousSessionCreated(Session session) {
	}

	@Override
	public void anonymousSessionDestroyed(Session session) {
	}

	@Override
	public void initializePlugin(PluginManager manager, File pluginDirectory) {
		server = XMPPServer.getInstance();
		SessionEventDispatcher.addListener(this);
		System.out.println("Join room plugin is running!");
		
		IQHandler myHandler = new MyIQHander();
		IQRouter iqRouter = XMPPServer.getInstance().getIQRouter();
		iqRouter.addHandler(myHandler);
		
		router = XMPPServer.getInstance().getIQRouter();
	}

	public void joinRooms(JID userJid) {
		List> data = MUCDao.getMUCInfo(userJid.toBareJID());

		if (data == null || data.isEmpty()) {
			return;
		}
		Map map = null;
		
		/**
		 * 构建iq的扩展包,用于发送用户所在房间的名称。
		 */
		Document document = DocumentHelper.createDocument();
		Element iqe = document.addElement("iq");
		iqe.addAttribute("type", "result");
		iqe.addAttribute("to", userJid.toFullJID());
		iqe.addAttribute("id", "YANG");
		
		Namespace namespace = new Namespace("""YANG");
		Element muc = iqe.addElement("muc");
		muc.add(namespace);
		
		
		for (int i = 0, len = data.size(); i < len; i++) {
			map = data.get(i);

			String serviceID = map.get("serviceID");
			mucService = (MultiUserChatServiceImpl) server
					.getMultiUserChatManager().getMultiUserChatService(
							Long.parseLong(serviceID));
			String roomName = map.get("name");
			LocalMUCRoom room = (LocalMUCRoom) mucService.getChatRoom(roomName);

			//增加room和account信息
			Element roome = muc.addElement("room");
			roome.setText(room.getJID().toBareJID());
			roome.addAttribute("account", userJid.toFullJID());
		}
		//最后发送出去!
		IQ iq = new IQ(iqe);
		System.out.println("iq " + iq.toXML());
		router.route(iq);
	}

	@Override
	public void destroyPlugin() {
		SessionEventDispatcher.removeListener(this);
		server = null;
		mucService = null;
	}
}
辅助打印Openfire出入数据类MyIQHander如下:
package com.yang.plugin;

import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.handler.IQHandler;
import org.xmpp.packet.IQ;
/**
 * @Title: MyIQHander.java 
 * @Package com.yang.plugin 
 * @Description: TODO
 * @author Yang 
 * @blog http://blog.csdn.net/yangzl2008
 * @date 2013年11月27日 下午9:56:27 
 * @version V1.0
 */
public class MyIQHander extends IQHandler {
	
	private static final String MODULE_NAME = "group tree handler";
	private static final String NAME_SPACE = "com:im:group";
	private IQHandlerInfo info;
	
	public MyIQHander() {
		super(MODULE_NAME);
		info = new IQHandlerInfo("gruops", NAME_SPACE);
	}

	@Override
	public IQHandlerInfo getInfo() {
		return info;
	}

	@Override
	public IQ handleIQ(IQ packet) throws UnauthorizedException {
		IQ reply = IQ.createResultIQ(packet);
		System.out.println("XML " + reply.toXML());
		return reply;
	}
}

四、修改基于smack客户端

所有基于smack的代码都可以使用以下方案实现接收Openfire发送过来的自定义IQ包。其中主要是MUCPacketExtensionProvider类和对这个Provoider的注册操作最为重要。MUCPacketExtensionProvider代码如下:
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.provider.IQProvider;
import org.xmlpull.v1.XmlPullParser;

import ouc.sei.suxin.android.data.Application;
import ouc.sei.suxin.android.data.entity.MUCInfo;
import ouc.sei.suxin.android.utils.Logger;

public class MUCPacketExtensionProvider implements IQProvider {

	@Override
	public IQ parseIQ(XmlPullParser parser) throws Exception {
		int eventType = parser.getEventType();
		MUCInfo info = null;
		while (true) {
			if (eventType == XmlPullParser.START_TAG) {
				if ("room".equals(parser.getName())) {
					String account = parser.getAttributeValue("", "account");
					String room = parser.nextText();
					
					info = new MUCInfo();
					info.setAccount(account);
					info.setRoom(room);
					info.setNickname(account);
					Logger.d("account is " + account + " and room is " + room);
					
					Application.getInstance().addMUCInfo(info);
				}
			} else if (eventType == XmlPullParser.END_TAG) {
				if ("muc".equals(parser.getName())) {
					break;
				}
			}
			eventType = parser.next();
		}
		return null;
	}

}
对这个Provoider的注册操作代码如下:
ProviderManager.getInstance().addIQProvider("muc", "YANG", new MUCPacketExtensionProvider());
以上这段代码的意思是每当遇到标签为muc同时命名空间为YANG的IQ包时,交由MUCPacketExtensionProvider处理。 以上代码建议添加在Application的onCreate方法中
辅助存储信息的MUCInfo如下:
/**
 * @Title: MUCInfo.java
 * @Package ouc.sei.suxin.android.data.entity
 * @Description: 用于传输从Server端传输过来的MUC的信息
 * @author Yang Zhilong
 * @blog http://blog.csdn.net/yangzl2008
 * @date 2013年11月27日 上午9:27:25
 * @version V1.0
 */
public class MUCInfo {
	private String account;
	private String room;
	private String nickname;

	public String getAccount() {
		return account;
	}

	public void setAccount(String account) {
		this.account = account;
	}

	public String getRoom() {
		return room;
	}

	public void setRoom(String room) {
		this.room = room;
	}

	public String getNickname() {
		return nickname;
	}

	public void setNickname(String nickname) {
		if (nickname.contains("@")) {
			this.nickname = nickname.substring(0, account.indexOf("@"));
			return;
		}
		this.nickname = nickname;
	}

	@Override
	public String toString() {
		return "MUCInfo [account=" + account + ", room=" + room + ", nickname="
				+ nickname + "]";
	}

}
通过以上的代码我们就可以接受到服务器端发送而来的登陆用户的房间信息,关于这个信息的处理,方式就很灵活了。如果是Android可以里ListView的形式显示给用户,如果是Spark,可以对Spark进行二次开发,显示给用户群信息。

四、文中提到的Openfire中的插件下载地址


下载地址



你可能感兴趣的:(Java)