下载地址:https://www.igniterealtime.org/downloads/index.jsp,当前最新版本为:4.2.3
1、把openfire源文件拷贝到Eclisps的工作目录中,我的目录是D:\eclipse-workspace
2、进入openfire_src>build>eclipse目录下,把里面settings、classpath、project三个文件拷贝到openfire_src目录,并用windows命令行重命名它们:
rename settings .settings、rename classpath .classpath、rename project .project
3、导入工程到eclipse中,发现有些错误,一些jar包missing了。
把报错的jar包remove掉,然后把build>lib下的所有jar包括文件夹里面的jar都导进来。
4、在Build Path配置中把 /openfire_src/src/i18n(支持中文) 、/openfire_src/src/resources/jar 、/openfire_src/build/lib/dist (所有依赖jar包)文件夹添加到 Source 中
5、Ant编译
在“Quick Access”中输入Ant,选择Ant
Add BuildFires
Openfire_src/build/build.xml
双击“openfire”进行编译
6、配置run_configurations
选择Java Application,新建一个配置。Main class选择Search,可以进行查询选择“org.jivesoftware.openfire.starter.ServerStarter”,并且勾选第一个复选框
配置Arguments
-DopenfireHome="${workspace_loc:openfire_src}/target/openfire"
注意:标注红色部分,对于你项目的名称。
配置Common
执行“Apply”,运行“Run”
至此源码编译环境已搭建完毕
1、修改初始数据库文件,路径src/database/openfire_sqlserver.sql,添加ofHistory表
CREATE TABLE ofHistory (
username NVARCHAR(64) NOT NULL,
messageID INTEGER NOT NULL,
creationDate NVARCHAR(64) NOT NULL,
messageSize INTEGER NOT NULL,
stanza TEXT NOT NULL,
CONSTRAINT ofHistory_pk PRIMARY KEY (username, messageID)
);
2、添加保存消息方法
MessageRouter类中133行附近
// 保存消息记录——高明——2018年6月20日 15:45:46
OfflineMessageStore oms = new OfflineMessageStore();
oms.addMessage_toHistory(packet);
3、修改OfflineMessageStore类,添加保存消息记录方法
新添加
//保存消息记录——高明-2018年6月20日 15:43:59
private static final String INSERT_HISTORY = "INSERT INTO ofHistory (username, messageID, creationDate, messageSize, stanza) "
+ "VALUES (?, ?, ?, ?, ?)";
新添加
/**
* 保存消息记录
*
* @author 高明
* @param message
*/
public void addMessage_toHistory(Message message) {
if (message == null) {
return;
}
// ignore empty bodied message (typically chat-state notifications).
if (message.getBody() == null || message.getBody().length() == 0) {
// allow empty pubsub messages (OF-191)
if (message.getChildElement("event",
"http://jabber.org/protocol/pubsub#event") == null) {
return;
}
}
JID recipient = message.getTo();
String username = recipient.getNode();
// If the username is null (such as when an anonymous user), don't
// store.
if (username == null
|| !UserManager.getInstance().isRegisteredUser(recipient)) {
return;
} else if (!XMPPServer.getInstance().getServerInfo().getXMPPDomain()
.equals(recipient.getDomain())) {
// Do not store messages sent to users of remote servers
return;
}
long messageID = SequenceManager.nextID(JiveConstants.OFFLINE);
// Get the message in XML format.
String msgXML = message.getElement().asXML();
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(INSERT_HISTORY);
pstmt.setString(1, username);
pstmt.setLong(2, messageID);
// pstmt.setString(3, StringUtils.dateToMillis(new
// java.util.Date()));
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
pstmt.setString(3, df.format(new Date()).toString());
pstmt.setInt(4, msgXML.length());
pstmt.setString(5, msgXML);
pstmt.executeUpdate();
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
} finally {
DbConnectionManager.closeConnection(pstmt, con);
}
// Update the cached size if it exists.
if (sizeCache.containsKey(username)) {
int size = sizeCache.get(username);
size += msgXML.length();
sizeCache.put(username, size);
}
}
完整OfflineMessageStore.java
/* * Copyright (C) 2004-2008 Jive Software. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.openfire; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.Namespace; import org.dom4j.QName; import org.dom4j.io.SAXReader; import org.jivesoftware.database.DbConnectionManager; import org.jivesoftware.database.SequenceManager; import org.jivesoftware.openfire.container.BasicModule; import org.jivesoftware.openfire.event.UserEventDispatcher; import org.jivesoftware.openfire.event.UserEventListener; import org.jivesoftware.openfire.user.User; import org.jivesoftware.openfire.user.UserManager; import org.jivesoftware.util.JiveConstants; import org.jivesoftware.util.LocaleUtils; import org.jivesoftware.util.StringUtils; import org.jivesoftware.util.XMPPDateTimeFormat; import org.jivesoftware.util.cache.Cache; import org.jivesoftware.util.cache.CacheFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.packet.JID; import org.xmpp.packet.Message; import java.io.StringReader; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Represents the user's offline message storage. A message store holds messages that were * sent to the user while they were unavailable. The user can retrieve their messages by * setting their presence to "available". The messages will then be delivered normally. * Offline message storage is optional, in which case a null implementation is returned that * always throws UnauthorizedException when adding messages to the store. * * @author Iain Shigeoka */ public class OfflineMessageStore extends BasicModule implements UserEventListener { private static final Logger Log = LoggerFactory.getLogger(OfflineMessageStore.class); //保存消息记录——高明-2018年6月20日 15:43:59 private static final String INSERT_HISTORY = "INSERT INTO ofHistory (username, messageID, creationDate, messageSize, stanza) " + "VALUES (?, ?, ?, ?, ?)"; private static final String INSERT_OFFLINE = "INSERT INTO ofOffline (username, messageID, creationDate, messageSize, stanza) " + "VALUES (?, ?, ?, ?, ?)"; private static final String LOAD_OFFLINE = "SELECT stanza, creationDate FROM ofOffline WHERE username=? ORDER BY creationDate ASC"; private static final String LOAD_OFFLINE_MESSAGE = "SELECT stanza FROM ofOffline WHERE username=? AND creationDate=?"; private static final String SELECT_SIZE_OFFLINE = "SELECT SUM(messageSize) FROM ofOffline WHERE username=?"; private static final String SELECT_SIZE_ALL_OFFLINE = "SELECT SUM(messageSize) FROM ofOffline"; private static final String DELETE_OFFLINE = "DELETE FROM ofOffline WHERE username=?"; private static final String DELETE_OFFLINE_MESSAGE = "DELETE FROM ofOffline WHERE username=? AND creationDate=?"; private static final int POOL_SIZE = 10; private Cache
if the message should be stored offline,sizeCache; /** * Pattern to use for detecting invalid XML characters. Invalid XML characters will * be removed from the stored offline messages. */ private Pattern pattern = Pattern.compile("&\\#[\\d]+;"); /** * Returns the instance of OfflineMessageStore being used by the XMPPServer. * * @return the instance of OfflineMessageStore being used by the XMPPServer. */ public static OfflineMessageStore getInstance() { return XMPPServer.getInstance().getOfflineMessageStore(); } /** * Pool of SAX Readers. SAXReader is not thread safe so we need to have a pool of readers. */ private BlockingQueue xmlReaders = new LinkedBlockingQueue<>(POOL_SIZE); /** * Constructs a new offline message store. */ public OfflineMessageStore() { super("Offline Message Store"); sizeCache = CacheFactory.createCache("Offline Message Size"); } /** * Adds a message to this message store. Messages will be stored and made * available for later delivery. * * @param message the message to store. */ public void addMessage(Message message) { if (message == null) { return; } if(!shouldStoreMessage(message)) { return; } JID recipient = message.getTo(); String username = recipient.getNode(); // If the username is null (such as when an anonymous user), don't store. if (username == null || !UserManager.getInstance().isRegisteredUser(recipient)) { return; } else if (!XMPPServer.getInstance().getServerInfo().getXMPPDomain().equals(recipient.getDomain())) { // Do not store messages sent to users of remote servers return; } long messageID = SequenceManager.nextID(JiveConstants.OFFLINE); // Get the message in XML format. String msgXML = message.getElement().asXML(); Connection con = null; PreparedStatement pstmt = null; try { con = DbConnectionManager.getConnection(); pstmt = con.prepareStatement(INSERT_OFFLINE); pstmt.setString(1, username); pstmt.setLong(2, messageID); pstmt.setString(3, StringUtils.dateToMillis(new java.util.Date())); pstmt.setInt(4, msgXML.length()); pstmt.setString(5, msgXML); pstmt.executeUpdate(); } catch (Exception e) { Log.error(LocaleUtils.getLocalizedString("admin.error"), e); } finally { DbConnectionManager.closeConnection(pstmt, con); } // Update the cached size if it exists. if (sizeCache.containsKey(username)) { int size = sizeCache.get(username); size += msgXML.length(); sizeCache.put(username, size); } } /** * 保存消息记录 * * @author 高明 * @param message */ public void addMessage_toHistory(Message message) { if (message == null) { return; } // ignore empty bodied message (typically chat-state notifications). if (message.getBody() == null || message.getBody().length() == 0) { // allow empty pubsub messages (OF-191) if (message.getChildElement("event", "http://jabber.org/protocol/pubsub#event") == null) { return; } } JID recipient = message.getTo(); String username = recipient.getNode(); // If the username is null (such as when an anonymous user), don't // store. if (username == null || !UserManager.getInstance().isRegisteredUser(recipient)) { return; } else if (!XMPPServer.getInstance().getServerInfo().getXMPPDomain() .equals(recipient.getDomain())) { // Do not store messages sent to users of remote servers return; } long messageID = SequenceManager.nextID(JiveConstants.OFFLINE); // Get the message in XML format. String msgXML = message.getElement().asXML(); Connection con = null; PreparedStatement pstmt = null; try { con = DbConnectionManager.getConnection(); pstmt = con.prepareStatement(INSERT_HISTORY); pstmt.setString(1, username); pstmt.setLong(2, messageID); // pstmt.setString(3, StringUtils.dateToMillis(new // java.util.Date())); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); pstmt.setString(3, df.format(new Date()).toString()); pstmt.setInt(4, msgXML.length()); pstmt.setString(5, msgXML); pstmt.executeUpdate(); } catch (Exception e) { Log.error(LocaleUtils.getLocalizedString("admin.error"), e); } finally { DbConnectionManager.closeConnection(pstmt, con); } // Update the cached size if it exists. if (sizeCache.containsKey(username)) { int size = sizeCache.get(username); size += msgXML.length(); sizeCache.put(username, size); } } /** * Returns a Collection of all messages in the store for a user. * Messages may be deleted after being selected from the database depending on * the delete param. * * @param username the username of the user who's messages you'd like to receive. * @param delete true if the offline messages should be deleted. * @return An iterator of packets containing all offline messages. */ public Collection getMessages(String username, boolean delete) { List messages = new ArrayList<>(); SAXReader xmlReader = null; Connection con = null; PreparedStatement pstmt = null; ResultSet rs = null; try { // Get a sax reader from the pool xmlReader = xmlReaders.take(); con = DbConnectionManager.getConnection(); pstmt = con.prepareStatement(LOAD_OFFLINE); pstmt.setString(1, username); rs = pstmt.executeQuery(); while (rs.next()) { String msgXML = rs.getString(1); Date creationDate = new Date(Long.parseLong(rs.getString(2).trim())); OfflineMessage message; try { message = new OfflineMessage(creationDate, xmlReader.read(new StringReader(msgXML)).getRootElement()); } catch (DocumentException e) { // Try again after removing invalid XML chars (e.g. ) Matcher matcher = pattern.matcher(msgXML); if (matcher.find()) { msgXML = matcher.replaceAll(""); } try { message = new OfflineMessage(creationDate, xmlReader.read(new StringReader(msgXML)).getRootElement()); } catch (DocumentException de) { Log.error("Failed to route packet (offline message): " + msgXML, de); continue; // skip and process remaining offline messages } } // if there is already a delay stamp, we shouldn't add another. Element delaytest = message.getChildElement("delay", "urn:xmpp:delay"); if (delaytest == null) { // Add a delayed delivery (XEP-0203) element to the message. Element delay = message.addChildElement("delay", "urn:xmpp:delay"); delay.addAttribute("from", XMPPServer.getInstance().getServerInfo().getXMPPDomain()); delay.addAttribute("stamp", XMPPDateTimeFormat.format(creationDate)); } messages.add(message); } // Check if the offline messages loaded should be deleted, and that there are // messages to delete. if (delete && !messages.isEmpty()) { PreparedStatement pstmt2 = null; try { pstmt2 = con.prepareStatement(DELETE_OFFLINE); pstmt2.setString(1, username); pstmt2.executeUpdate(); removeUsernameFromSizeCache(username); } catch (Exception e) { Log.error("Error deleting offline messages of username: " + username, e); } finally { DbConnectionManager.closeStatement(pstmt2); } } } catch (Exception e) { Log.error("Error retrieving offline messages of username: " + username, e); } finally { DbConnectionManager.closeConnection(rs, pstmt, con); // Return the sax reader to the pool if (xmlReader != null) { xmlReaders.add(xmlReader); } } return messages; } /** * Returns the offline message of the specified user with the given creation date. The * returned message will NOT be deleted from the database. * * @param username the username of the user who's message you'd like to receive. * @param creationDate the date when the offline message was stored in the database. * @return the offline message of the specified user with the given creation stamp. */ public OfflineMessage getMessage(String username, Date creationDate) { OfflineMessage message = null; Connection con = null; PreparedStatement pstmt = null; ResultSet rs = null; SAXReader xmlReader = null; try { // Get a sax reader from the pool xmlReader = xmlReaders.take(); con = DbConnectionManager.getConnection(); pstmt = con.prepareStatement(LOAD_OFFLINE_MESSAGE); pstmt.setString(1, username); pstmt.setString(2, StringUtils.dateToMillis(creationDate)); rs = pstmt.executeQuery(); while (rs.next()) { String msgXML = rs.getString(1); message = new OfflineMessage(creationDate, xmlReader.read(new StringReader(msgXML)).getRootElement()); // Add a delayed delivery (XEP-0203) element to the message. Element delay = message.addChildElement("delay", "urn:xmpp:delay"); delay.addAttribute("from", XMPPServer.getInstance().getServerInfo().getXMPPDomain()); delay.addAttribute("stamp", XMPPDateTimeFormat.format(creationDate)); } } catch (Exception e) { Log.error("Error retrieving offline messages of username: " + username + " creationDate: " + creationDate, e); } finally { // Return the sax reader to the pool if (xmlReader != null) { xmlReaders.add(xmlReader); } DbConnectionManager.closeConnection(rs, pstmt, con); } return message; } /** * Deletes all offline messages in the store for a user. * * @param username the username of the user who's messages are going to be deleted. */ public void deleteMessages(String username) { Connection con = null; PreparedStatement pstmt = null; try { con = DbConnectionManager.getConnection(); pstmt = con.prepareStatement(DELETE_OFFLINE); pstmt.setString(1, username); pstmt.executeUpdate(); removeUsernameFromSizeCache(username); } catch (Exception e) { Log.error("Error deleting offline messages of username: " + username, e); } finally { DbConnectionManager.closeConnection(pstmt, con); } } private void removeUsernameFromSizeCache(String username) { // Update the cached size if it exists. if (sizeCache.containsKey(username)) { sizeCache.remove(username); } } /** * Deletes the specified offline message in the store for a user. The way to identify the * message to delete is based on the creationDate and username. * * @param username the username of the user who's message is going to be deleted. * @param creationDate the date when the offline message was stored in the database. */ public void deleteMessage(String username, Date creationDate) { Connection con = null; PreparedStatement pstmt = null; try { con = DbConnectionManager.getConnection(); pstmt = con.prepareStatement(DELETE_OFFLINE_MESSAGE); pstmt.setString(1, username); pstmt.setString(2, StringUtils.dateToMillis(creationDate)); pstmt.executeUpdate(); // Force a refresh for next call to getSize(username), // it's easier than loading the message to be deleted just // to update the cache. removeUsernameFromSizeCache(username); } catch (Exception e) { Log.error("Error deleting offline messages of username: " + username + " creationDate: " + creationDate, e); } finally { DbConnectionManager.closeConnection(pstmt, con); } } /** * Returns the approximate size (in bytes) of the XML messages stored for * a particular user. * * @param username the username of the user. * @return the approximate size of stored messages (in bytes). */ public int getSize(String username) { // See if the size is cached. if (sizeCache.containsKey(username)) { return sizeCache.get(username); } int size = 0; Connection con = null; PreparedStatement pstmt = null; ResultSet rs = null; try { con = DbConnectionManager.getConnection(); pstmt = con.prepareStatement(SELECT_SIZE_OFFLINE); pstmt.setString(1, username); rs = pstmt.executeQuery(); if (rs.next()) { size = rs.getInt(1); } // Add the value to cache. sizeCache.put(username, size); } catch (Exception e) { Log.error(LocaleUtils.getLocalizedString("admin.error"), e); } finally { DbConnectionManager.closeConnection(rs, pstmt, con); } return size; } /** * Returns the approximate size (in bytes) of the XML messages stored for all * users. * * @return the approximate size of all stored messages (in bytes). */ public int getSize() { int size = 0; Connection con = null; PreparedStatement pstmt = null; ResultSet rs = null; try { con = DbConnectionManager.getConnection(); pstmt = con.prepareStatement(SELECT_SIZE_ALL_OFFLINE); rs = pstmt.executeQuery(); if (rs.next()) { size = rs.getInt(1); } } catch (Exception e) { Log.error(LocaleUtils.getLocalizedString("admin.error"), e); } finally { DbConnectionManager.closeConnection(rs, pstmt, con); } return size; } @Override public void userCreated(User user, Map params) { //Do nothing } @Override public void userDeleting(User user, Map params) { // Delete all offline messages of the user deleteMessages(user.getUsername()); } @Override public void userModified(User user, Map params) { //Do nothing } @Override public void start() throws IllegalStateException { super.start(); // Initialize the pool of sax readers for (int i=0; i true false
otherwise. */ static boolean shouldStoreMessage(final Message message) { // XEP-0334: Implement thehint to override offline storage if (message.getChildElement("no-store", "urn:xmpp:hints") != null) { return false; } switch (message.getType()) { case chat: // XEP-0160: Messages with a 'type' attribute whose value is "chat" SHOULD be stored offline, with the exception of messages that contain only Chat State Notifications (XEP-0085) [7] content // Iterate through the child elements to see if we can find anything that's not a chat state notification or // real time text notification Iterator> it = message.getElement().elementIterator(); while (it.hasNext()) { Object item = it.next(); if (item instanceof Element) { Element el = (Element) item; if (Namespace.NO_NAMESPACE.equals(el.getNamespace())) { continue; } if (!el.getNamespaceURI().equals("http://jabber.org/protocol/chatstates") && !(el.getQName().equals(QName.get("rtt", "urn:xmpp:rtt:0"))) ) { return true; } } } return message.getBody() != null && !message.getBody().isEmpty(); case groupchat: case headline: // XEP-0160: "groupchat" message types SHOULD NOT be stored offline // XEP-0160: "headline" message types SHOULD NOT be stored offline return false; case error: // XEP-0160: "error" message types SHOULD NOT be stored offline, // although a server MAY store advanced message processing errors offline if (message.getChildElement("amp", "http://jabber.org/protocol/amp") == null) { return false; } break; default: // XEP-0160: Messages with a 'type' attribute whose value is "normal" (or messages with no 'type' attribute) SHOULD be stored offline. break; } return true; } }
20180719更新
发现在业务操作中会删除已有的主题订阅
(原因:
* when an event notification was sent to a susbcriber whose address is no longer available.
*
* Answers to authorization requests sent to node owners to approve pending subscriptions)
因此修改部分源码
在 PubSubEngine.java 中 291 行,进行注释
/**
* Handles Message packets sent to the pubsub service. Messages may be of type error
* when an event notification was sent to a susbcriber whose address is no longer available.
*
* Answers to authorization requests sent to node owners to approve pending subscriptions
* will also be processed by this method.
*
* @param service the PubSub service this action is to be performed for.
* @param message the Message packet sent to the pubsub service.
*/
public void process(PubSubService service, Message message) {
if (message.getType() == Message.Type.error) {
// Process Messages of type error to identify possible subscribers that no longer exist
if (message.getError().getType() == PacketError.Type.cancel) {
// TODO Assuming that owner is the bare JID (as defined in the JEP). This can be replaced with an explicit owner specified in the packet
JID owner = message.getFrom().asBareJID();
// Terminate the subscription of the entity to all nodes hosted at the service
//cancelAllSubscriptions(service, owner);
}
else if (message.getError().getType() == PacketError.Type.auth) {
// TODO Queue the message to be sent again later (will retry a few times and
// will be discarded when the retry limit is reached)
}
}
else if (message.getType() == Message.Type.normal) {
// Check that this is an answer to an authorization request
DataForm authForm = (DataForm) message.getExtension("x", "jabber:x:data");
if (authForm != null && authForm.getType() == DataForm.Type.submit) {
String formType = authForm.getField("FORM_TYPE").getValues().get(0);
// Check that completed data form belongs to an authorization request
if ("http://jabber.org/protocol/pubsub#subscribe_authorization".equals(formType)) {
// Process the answer to the authorization request
processAuthorizationAnswer(service, authForm, message);
}
}
}
}