由Spark产生的一点感想


关注Spark有一段时间了。看着这个开源的软件在社区的力量下一点点完善,感觉就是不一样。不过感觉这个开源软件的源代码控制管理还是有问题,关于代码的patch都只能通过论坛提交,不能直接操作SVN库,如果初级开发者能够提交SVN库,然后经验者Check之后,真正反映到SVN库就好了。这样子也能提高大家的代码贡献积极性么。

前一段时间参考psi的源码,给Spark增加了一个简单的聊天记录搜索功能,并且贡献给了SparkDev。后来在论坛讨论,说我的那种实现方法在大量的聊天记录的时候存在性能问题,确实是这样,也一直在考虑是否有合适的XML文本搜索技术或者能够使用简单、小巧的数据库来实现聊天记录的保存。不知key-value类型的数据库是否能够适用,一些疑问还没有解决,继续努力学习吧。

前几天Spark的一次更新令我感到很欣慰,看到了一个关于软件可用性方面的更新。文字输入区域的大小在输入时不再被重置了,在单次会话期间,一直保留用户的设置。自己在发布内部使用版本的时候有改过这个问题,但是考虑到使用习惯可能不同,所以没有提交相关的Patch。看来是我多想了。

关于Spark及其相关的Smackx和Openfire项目,其实这三个项目都值得做Java的人一直跟进,Smackx和Openfire这两个项目的活跃程度远远高于Spark,毕竟一个是XMPP客户端应用开发的API,一个是服务器。时间和能力有限,我也只能先从Spark入手了。

除了Spark,也在关注Launchpad上的一些项目,使用Ubuntu过程中出现了Bug,也会及时报告。相对于Launchpad上的项目,Spark在项目管理上还有很长的路要走。Bug报告机制,代码贡献方法、评审机制,项目计划及跟进等等。

从Ubuntu开始,了解了开源项目的运作方式,学习了Bug调查和报告的方法,尤其要感谢Launchpad上的人,耐心教导像我这样的 新手应该如何报告Bug,并且及时回复每个问题。对于我自己,我也会在自己成长的过程中,尽自己所能回报开源社区。

Advertisements

Openfire Project使用SLF4J日志工具


之前做项目的时候用到的是Log4j,当然也从来就没有考虑过不同的用户可能对于日志输出系统的要求还不一样,假如项目是产品类型的,A用户要求使用Log4j,卖给B用户的时候B用户要求使用jdk log,如果没有引入SLF4J,那么修改日志输出代码也是个足够麻烦的事情。说到这里,有些人可能已经猜到SLF4J是这个什么工具。

SLF4J的全称是Simple Logging Facade for Java。SLF4J是其他Log框架的(log4j, java.util.logging, Simple logging and NOP)抽象,提供统一的API,使最终用户能够在部署的时候配置所希望的Log实现。

要使用SLF4J替换Log4j也很简单,使用log4j-over-slf4j.jar替换Log4j.jar然后修改获得Log对象实例的代码:
private static final org.slf4j.Logger Log = org.slf4j.LoggerFactory.getLogger(ClassName.class);

SLF4J支持参数化的log字符串,组装消息被推迟到了它能够确定是不是要显示这条消息的时候,避免了之前为了减少字符串拼接的性能损耗而不得不写的if(logger.isDebugEnable()),现在可以直接写:logger.debug(“Temperature set to {}. Old temperature was {}.”, t, oldT);同时,日志中的参数若超过三个,则需要将参数以数组的形式传入。

SLF4J主页地址:http://www.slf4j.org/

给Spark添加聊天记录搜索功能


How to add Search-history-message-function in Spark.

———————————————
很幸运,这个功能的Patch已经被Spark Dev开发小组加入到SVN,大家从SVN库Check out 代码就能获得这个功能了。
相关信息:http://www.igniterealtime.org/issues/browse/SPARK-896
———————————————

目标:实现简单的文本搜索功能。

Spark的聊天记录处理程序主要由org.jivesoftware.sparkimpl.plugin.transcripts.ChatTranscriptPlugin类提供UI界面,
org.jivesoftware.sparkimpl.plugin.transcripts.ChatTranscript类提供历史记录
本次功能的添加,就是通过修改这两个类来实现的。

未完成的问题:搜索框提示信息没有添加进入in18资源文件,这一步修改比较简单,就不再赘述。

开始作业:
1、在ChatTranscript类中增加一个方法,用于返回包含搜索关键字的聊天记录,代码如下:

    /**
     * Returns messages that included search keywords.
     *
     * @param text search keywords.If the search keywords is null, return all message.
     * @return the messages that included search keywords.
     */
    public List<HistoryMessage> getMessage(String text) {
        if(text == null || "".equals(text)) {
            return messages;
        } else {
            List<HistoryMessage> searchResult = new ArrayList<HistoryMessage>();
            for(HistoryMessage message : messages) {
                // ignore keywords’ case
                if(message.getBody().toLowerCase().indexOf(text.toLowerCase()) != -1) {
                    searchResult.add(message);
                }
            }
            return searchResult;
        }
    }

2、修改ChatTranscriptPlugin类的showHistory方法,增加搜索输入框,增加焦点事件监听,增加回车键时间监听。
在final JPanel mainPanel = new BackgroundPanel();代码和final JEditorPane window = new JEditorPane();代码之间,修改代码如下:

// add search text input
final JPanel topPanel = new BackgroundPanel();
topPanel.setLayout(new GridBagLayout());

final VCardPanel vacardPanel = new VCardPanel(jid);
final JTextField searchField = new JTextField(25);
searchField.setText(Res.getString("message.search.for.history"));
searchField.setToolTipText(Res.getString("message.search.for.history"));
searchField.setForeground((Color) UIManager.get("TextField.lightforeground"));

topPanel.add(vacardPanel, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, new Insets(1, 5, 1, 1), 0, 0));
topPanel.add(searchField, new GridBagConstraints(1, 0, GridBagConstraints.REMAINDER, 1, 1.0, 1.0, GridBagConstraints.SOUTHEAST, GridBagConstraints.NONE, new Insets(1, 1, 6, 1), 0, 0));

mainPanel.add(topPanel, BorderLayout.NORTH);

上面这段代码添加了一个搜索输入框。并且设置了提示信息、默认文本以及默认文本的颜色。另外这个搜索输入框和原来的VCardPanel使用网袋布局管理器布局,以保证窗口放大缩小时控件所在的位置正确。关于网袋布局管理器我之前也没有认真学过,以前用的时候,都是直接用Eclipse设置布局管理器为null,然后画好控件之后直接设置网袋布局管理器就OK了,这次,为了写好这个,还好好学习一把,下面把网袋布局管理器的参数提供给大家,也算是给自己的一个备忘。

Gridx——组件的横向坐标
Girdy——组件的纵向坐标
Gridwidth——组件的横向宽度,也就是指组件占用的列数,这与HTML的colspan类似
Gridheight——组件的纵向长度,也就是指组件占用的行数,这与HTML的rowspan类似
Weightx——指行的权重,告诉布局管理器如何分配额外的水平空间
Weighty——指列的权重,告诉布局管理器如何分配额外的垂直空间
Anchor——告诉布局管理器组件在表格空间中的位置
Fill——如果显示区域比组件的区域大的时候,可以用来控制组件的行为。控制组件是垂直填充,还是水平填充,或者两个方向一起填充
Insets——指组件与表格空间四周边缘的空白区域的大小
Ipadx—— 组件间的横向间距,组件的宽度就是这个组件的最小宽度加上ipadx值
ipady—— 组件间的纵向间距,组件的高度就是这个组件的最小高度加上ipady值

3、搜索输入框做完了之后,添加事件监听,需要监听的有三个事件,首先是当鼠标点入搜索框的时候,默认文本需要消失,并且恢复字体颜色为黑色;其次当然是失去鼠标时,需要把搜索框复原了;最后当然是输入搜索关键字按下回车需要执行搜索功能了。

在showHistory方法最后的代码TaskEngine.getInstance().schedule(transcriptTask, 10);之上添加三个事件监听代码

        searchField.addKeyListener(new KeyListener() {           
            @Override
            public void keyTyped(KeyEvent e) {               
            }
            @Override
            public void keyReleased(KeyEvent e) {
                if(e.getKeyChar() == KeyEvent.VK_ENTER) {
                    TaskEngine.getInstance().schedule(transcriptTask, 10);
                    searchField.requestFocus();
                }
            }
            @Override
            public void keyPressed(KeyEvent e) {
            }
        });
        searchField.addFocusListener(new FocusListener() {
            public void focusGained(FocusEvent e) {
                searchField.setText("");
                searchField.setForeground((Color) UIManager.get("TextField.foreground"));
            }

            public void focusLost(FocusEvent e) {
                searchField.setForeground((Color) UIManager.get("TextField.lightforeground"));
                searchField.setText(Res.getString("message.search.for.history"));
            }
        });

4、事件监听代码也添加完了,还忘了件事情,就是把取得Message信息的方法修改成我们自己添加的方法。
把之前的代码

final List<HistoryMessage> list = transcript.getMessage();

修改成

final List<HistoryMessage> list = transcript.getMessage(
                                                            Res.getString("message.search.for.history").equals(searchField.getText())
                                                                ? null : searchField.getText());

5、给所有的i18n资源文件添加message.search.for.history = Search History值,有多国语言能力的还可以翻译成其他语种。

6、一切OK,看效果图
没有获得焦点时
image 
获得焦点后
 image
搜索效果
image

发布自己的Spark版本


1. 安装客户端控制插件(Client Control),在这个插件的帮助下,我们可以把新的Spark版本上传到服务器,然后用户就会收到更新通知。

image

2. 增加自定义设置。
在Spark中指定聊天服务器地址:打开org.jivesoftware.resource包中的default.properties文件,设置HOST_NAME属性即可。
修改User Guide手册地址:org.jivesoftware.MainWindow中的viewHelpGuideAction内部类即可。
另外,还可以添加自定义的菜单等等。

3. 上面的修改完了之后,修改org.jivesoftware.sparkimpl.settings.JiveInfo中的版本号,使新创建的版本高于已经安装的版本。

关于版本号,Spark在判断自身的版本时,如果是beta版,版本号有一定的规则限制:最后一个小数点右边的版本只能是一位,如果是两位,也只会取出一位来。比如2.6.0.01 Beata 2,最后得到的版本号2.6.0.0,为了避免出现这个问题,一种方法就是把Beta字样从getVersion()方法的返回值里移除。这样版本比较的逻辑不会运行到会出问题的代码就返回结果了。另外一个就是修改问题代码——CheckUpdates的getVersion()方法
int lastIndexOf = version.lastIndexOf(“.”);
if (lastIndexOf != -1) {
return version.substring(0, lastIndexOf);
}
有兴趣的可以看看怎么改。

4. ant运行build.xml,编译,生成target目录。
build.xml存放在build目录下。

5. 下载install4j 4.0.8
地址:http://www.ej-technologies.com/download/install4j/version40.html

关于install4j,这是一个跨平台的编译平台,可以在windows, unix, linux, mac操作系统使用,他可以在这些平台上编译运行于windows, linux, unix, mac的安装包。在编译spark的时候,碰到了一点小问题,从网站上下载最新的install4j 4.2.4编译好了之后,在安装的最后一步会报java.lang.abstractMethodError错误。换成4.0.8版本的就好了。

6. 使用install4j打开Spark项目用spark.install4j文件。
该文件存放在buildinstaller

7. 在install4j中下载JRE,并指定安装包使用的JRE版本。
下载JRE,找到Download按钮,一步步执行就可以了。
下载完了之后,在Media选项卡中设置Windows和Unix Archive用到JRE。如下图
image
8. 修改install4j项目设置中的版本号
org.jivesoftware.sparkimpl.settings.JiveInfo的getVersion()方法返回的版本需要和install4j的版本号一致。
客户端在判断是否有新版本的时候,本地的版本号从JiveInfo的getVersion()方法取得。
服务器上的版本通过文件名区分,而文件名是在install4j中通过设置版本号来完成的,如下图:
下图的设置生成的文件名是spark_2_6_0_01

image

9. 上传,设置更新信息,更新信息在用户下载的时候,会出现在下载的会话框中。

image

10. 客户端收到更新提示

image

11. 点击yes后,客户端开始下载新版本,之前设置的更新信息也出现在会话框中

image

12. 下载完了之后提示用户是否安装

image

13. 安装完成之后,用户就用上了我们自己打包的Spark.

SSL/TLS Support for Sparkweb


Openfire开启HTTP绑定支持,然后修改SparkWeb.html中的jive_sparkweb_getConfig方法如下:

function jive_sparkweb_getConfig()
{
return {
server: “im.uniqueme.cn”,
location: window.location.href,
connectionType: “https”,
port: “7443”,
autoLogin: “false”,
policyFileURL: “xmlsocket://im.uniqueme.cn:5229”
};
}

修改完了之后,通过HTTPS协议打开SparkWeb.html即可。

遇到的问题:在Firefox3.5.2中按下Login按钮之后可以正常登录,但是在XP SP3 IE7中按下Login按钮之后,没有反应,Openfire服务器也没有收到登录的请求。

Openfire Fastpath插件安装备忘


环境:
Ubuntu Server 9.04
Openfire 3.6.4
Fastpath 4.0.0

安装前的准备
确认Openfire使用的域名,我设置的域名是im.uniqueme.cn
/etc/hosts文件里追加域名规则:127.0.0.1       im.uniqueme.cn

安装Fastpath Service插件和Fastpath Webchat插件

安装完成之后在Fastpath插件页面的Tools里面设置Host和端口,在这里需要注意的是Host名称需要和Openfire使用的域名一样,否则会出现,创建组之后不会在/webchat/页面上显示,所以Host名称我设置成了im.uniqueme.cn实际会通过/etc/hosts文件指向127.0.0.1

现在基本的安装就完成了。可以通过https://im.uniqueme.cn:9091/webchat/或者http://im.unqiueme.cn:9090/webchat/访问了。

更多的设置可以参考Fastpath指南

参考:
Fastpath指南——http://www.igniterealtime.org/community/docs/DOC-1876

 

How to reset admin password in openfire?


安装好Openfire之后,忘记了管理员密码,怎么办?下面就来说说怎么重置Openfire的管理员密码。

在Openfire中,密码都是以加密形式存储在数据库中的(假定使用Mysql作为Openfire的用户和组的存储服务),登录openfire的后台数据库,通过下面的语句就能看到。

mysql> SELECT * FROM ofUser WHERE username='admin';
+----------+---------------+--------------------------------------------------+---------------+-------------------+-----------------+------------------+
| username | plainPassword | encryptedPassword                                | name          | email             | creationDate    | modificationDate |
+----------+---------------+--------------------------------------------------+---------------+-------------------+-----------------+------------------+
| admin    | NULL          | df8d8aa53e956defc1d83aa7612194fd610897dc271e475a | Administrator | admin@uniqueme.cn | 001248259126800 | 0                |
+----------+---------------+--------------------------------------------------+---------------+-------------------+-----------------+------------------+
1 row in set (0.00 sec)

mysql> _

encryptedPassword字段存储的就是管理员的密码,不过都已经变成16进制的字符了,但是plainPassword为什么是NULL呢?

如果你用过Openfire的User Import Export插件就会发现,在导入或者导出用户的数据的时候,密码都是明文,而且导入的用户,如果还没有登录过,密码都是明文存储在plainPassword字段的。登录过一次之后,plainPassword字段的密码被清空了,只留下了加密的密码。
利用这个特性,我们可以把encryptedPassword字段设置成NULL,然后给plainPassword字段重新设置明文密码。

mysql> UPDATE ofUser SET plainPassword='test123', encryptedPassword=NULL WHERE username='admin';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM ofUser WHERE username='admin';
+----------+---------------+-------------------+---------------+-------------------+-----------------+------------------+
| username | plainPassword | encryptedPassword | name          | email             | creationDate    | modificationDate |
+----------+---------------+-------------------+---------------+-------------------+-----------------+------------------+
| admin    | test123       | NULL              | Administrator | admin@uniqueme.cn | 001248259126800 | 0                |
+----------+---------------+-------------------+---------------+-------------------+-----------------+------------------+
1 row in set (0.00 sec)

mysql> _

好了。现在密码被重置了。我们去控制台试试,是不是登录成功了呢?再看一下数据库的状况。

mysql> SELECT * FROM ofUser WHERE username='admin';
+----------+---------------+--------------------------------------------------+---------------+-------------------+-----------------+------------------+
| username | plainPassword | encryptedPassword                                | name          | email             | creationDate    | modificationDate |
+----------+---------------+--------------------------------------------------+---------------+-------------------+-----------------+------------------+
| admin    | NULL          | dc94e8d99302d76ba02213b1f0c95088b58945b6110d5579 | Administrator | admin@uniqueme.cn | 001248259126800 | 0                |
+----------+---------------+--------------------------------------------------+---------------+-------------------+-----------------+------------------+
1 row in set (0.00 sec)mysql> _

如果你用的是Openfire 3.6.4,别忘了在更新了plainPassword和encryptedPassword之后,先重启一下Openfire服务,然后再登录。
为什么要重启再登录?这是Openfire 3.6.4的一个Bug,将在3.6.5中修复,详细请看:http://www.igniterealtime.org/issues/browse/JM-1537