Tigase插件 – 编写插件

本文翻译自 – http://www.tigase.org/content/writing-plugin-code
上一篇文章描述了XMPP stanza如何在session manager当中被处理。处理分为四个步骤,每一个步骤都有相对应类型的插件负责处理。 java

  1. 第一步 – 预处理 – XMPPPreprocessorIfc:这是预处理器插件须要实现的接口
  2. 第二步 – 处理 – XMPPProcessorIfc:这是处理器插件须要实现的接口
  3. 第三步 – 投递 – XMPPPostProcessorIfc:这是投递处理器插件须要实现的接口
  4. 第四步 – 过滤 – XMPPPacketFilterIfc:这是结果过滤器插件须要实现的接口

若是你已经看过这四个接口的代码,你会发现每一个接口都只有一个方法须要实现。没错,这个方法就是处理packet的地方它们具备很是类似的入口参数,下面对这些参数进行介绍: node

  • Packet packet – 须要被处理的packet,这个参数不能够为null。即便这个对象不是immutable的,在方法里也不能对它进行修改。它的任何一个变亮都不能发生改变。
  • XMPPResourceConnection session – session里面包含全部的用户会话数据和访问用户数据库的方法。它容许向持久化数据库中存储信息,但若是用户在线只容许向内存中存储数据。在方法调用时,若是没有在线的用户会话,那么这个参数能够为null。
  • NonAuthUserRespository repo – 当上面的参数-即用户会话为空的时候,这个参数一般用来存储用户数据。它只容许很是有限的数据访问。好比在用户离线时存储用户的离线消息(对已经存在的数据不容许覆写),好比读取用户的公共Vcard信息。
  • Queue<Packet> results – 这是处理产生的结果packet队列。无论怎样,都必须对输入的packet进行备份,并把备份存储到结果队别里面。
  • Map<String, Object> setting – map里面保存着tigase服务器专为插件准备的配置信息。在大多数状况下,插件并不须要这些配置信息,但若是某个插件须要访问外部数据库,那么tigase服务器能够经过这个参数向它传递数据库的链接字符串。

若是仔细得看一下上面的这些接口,会发现它们还extend XMPPImplIfc接口。XMPPImplIfc定义了一些能够得到插件基础meta信息的接口。请参考下面的源码: 数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
 * 须要添加XMPPImplIfc接口的描述
 */
public interface XMPPImplIfc {
    int concurrentQueuesNo();
 
    @Deprecated
    int concurrentThreadsPerQueue();
 
    /**
     * id()返回插件的惟一ID。每个插件都拥有惟一ID:它在配置文件中用来指定哪一个插件须要加载,哪一个不须要。
     * 在大多数状况,ID就是该插件感兴趣的packet的XMLNS。
     *
     * @return id,字符串格式
     */
    String id();
 
    /**
     * init()方法在插件被加载到内存以后当即被执行,检查数据库是否可用和其余的初始化过程均可以写到这个方法里。
     * 这对于那些经过非标准存储方式访问数据库或须要对数据库scheme进行升级的插件来讲很是有用。
     *
     * @param settings 初始化配置信息
     * @throws TigaseDBException
     */
    void init(Map settings)throwsTigaseDBException;
 
    //~--- get methods ----------------------------------------------------------
 
    /**
     * isSupporting方法传入元素的名称和命名空间,返回这个元素是否被该插件“感兴趣”
     *
     * @param elem 元素名称,字符串格式
     * @param ns   命名空间,字符串格式
     * @return 一个布尔类型,true:感兴趣;false:不感兴趣
     */
    boolean isSupporting(String elem, String ns);
 
    //~--- methods --------------------------------------------------------------
 
    /**
     * supDiscoFeatures()方法向请求的发起者返回一个XML元素数组格式的服务发现(service discovery)特性信息。
     * 返回的服务发现特性取决于该插件支持哪些服务。
     *
     * @param session 一个XMPPResourceConnection实例
     * @return 一个XML元素数组
     */
    Element[] supDiscoFeatures(XMPPResourceConnection session);
 
    /**
     * supElements()方法返回该插件“感兴趣”的XML元素名数组,数组当中的每个元素名都依次对应着supNamespaces()返回的命名空间
     *
     * @return 字符串数组
     */
    String[] supElements();
 
    /**
     * supNamespaces()方法返回该插件“感兴趣”的stanza命名空间,数组当中的每个命名空间都依次对应着supElements()方法返回的XML元素名
     *
     * @return 字符串数组
     */
    String[] supNamespaces();
 
    /**
     * supStreamFeatures()方法对请求的发起者返回一个XML元素数组格式的流特性信息。
     * 返回的流特性取决于该插件支持哪些特性。
     *
     * @param session 一个XMPPResourceConnection实例
     * @return XML元素数组
     */
    Element[] supStreamFeatures(XMPPResourceConnection session);
}

接下来,咱们实现一个专门处理<message/> packet的简单插件,插件的工做就是把packet投递到目的地地址。传入packet会被转发给用户,而传出packet会被转发到一个外部目的地地址。这个插件其实已经实现了,它保存在咱们的SVN服务器上(https://svn.tigase.org/reps/tigase-server/trunk/src/main/java/tigase/xmpp/impl/Message.java)。代码当中有一些备注,可是这篇文档会更深刻的介绍实现细节。 数组

在开始以前你须要选择一个插件类型。若是要开发一个处理器插件,那么就须要实现XMPPProcessorIfc接口;若是是预处理插件,就须要实现XMPPPreprocessorIfc接口;固然你也能够实现多个接口,这个取决于你的需求和状况,你也可使用helper抽象类做为基类来实现全部的插件。插件类的声明应该像下面那样(假如你要实现一个处理器插件): 服务器

1
2
publicclassMessageextendsXMPPProcessor
    implementsXMPPProcessorIfc

要作的第一件事情就是肯定插件ID。它是惟一的,须要放到配置文件里面,告诉服务器在启动时加载并使用相对应的插件。若是这个插件只对特定命名空间下特定名称的元素“感兴趣”,在多数状况下,能够直接以命名空间来做为ID,固然了谁也没法保证这个名称的元素不会出如今其余的packet里面。由于咱们想开发一个可以处理全部的的处理器插件,可是又不想花费一成天来考虑如何为这个插件起一个很酷的ID,因此咱们干脆就叫它“message”吧。 session

用下面的代码来声明插件的ID: dom

1
2
private static final String ID ="message";
public String id() {returnID; }

就像以前咱们描述的那样,插件只接收并处理它“感兴趣”的packet。咱们的插件只对“jabber:client”命名空间下的元素感兴趣。声明插件所感兴趣的东西,须要添加两个方法: svn

1
2
3
4
5
6
7
public String[] supElements() {
  returnnewString[] {"message"};
}
 
public String[] supNamespaces() {
  return newString[] {"jabber:client"};
}

如今咱们已经准备好了把插件加载到tigase服务器。下一步就是实现packet处理的方,请参考SVN中的代码(https://svn.tigase.org/reps/tigase-server/trunk/src/main/java/tigase/xmpp/impl/Message.java)。我只会在容易形成困惑的代码上面添加注释,而后添加一两行代码帮助你理解。 性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public void process(finalPacket packet,
    final XMPPResourceConnection session,
    final NonAuthUserRepository repo,
    final Queue<Packet> results,
    final Map<String, Object> settings)
    throwsXMPPException {
 
  // 出于性能的考虑,最好在打印日志以前现检查一下日志级别
  if(log.isLoggable(Level.FINEST)) {
    log.finest("Processing packet: "+ packet.toString());
  }
 
  // 若是用户不在线,你也许想跳事后面的处理环节
  if(session ==null) {
    return;
  }// end of if (session == null)
 
  // 当插件在第一次处理这个用户的会话信息的时候,还有另一种方法能够执行必要的操做
  if(session.getSessionData(ID) ==null) {
    session.putSessionData(ID, ID);
    // 你能够把你的代码放到这里
    .....
    // 若是你不但愿终止操做,那么就把return语句去掉
    return;
  }
 
  // 若是用户的会话没有受权,那么每一次调用session.getUserId()方法都会抛出异常
  try{
 
    // 在比较JID以前必定记得要去掉resource部分
    // JID的组成:jid = [ node "@" ] domain [ "/" resource ]
    // 好比:chutianxing@gmail.com/home
    String id = JIDUtils.getNodeID(packet.getElemTo());
    // 检查一下这个packet是不是发给会话的拥有者
    if(session.getUserId().equals(id)) {
      // 若是是,那么这个消息的确是要发送给这个客户端的
      Element elem = packet.getElement().clone();
      Packet result =newPacket(elem);
      // 这里就是咱们为最终收到消息的用户设置客户端组件地址的地方了
      // 在大多数状况,这多是一个可以保持于客户端链接的c2s或Bosh组件
      result.setTo(session.getConnectionId(packet.getElemTo()));
      // 在大多数状况,这一步能够跳过,可是当packet的投递过程出现了什么问题,这么作能够为调用者返回一个错误
      result.setFrom(packet.getTo());
      // 最后不要忘记把结果packet放到结果队列里面去,不然结果会丢失
      results.offer(result);
    }// end of else
 
    // 在比较JID以前必定记得要去掉resource部分
    id = JIDUtils.getNodeID(packet.getElemFrom());
    // 检查一下这个packet是否由会话的拥有者发出
    if(session.getUserId().equals(id)) {
      // 这是一个由客户端发出的packet,最简单的处理就是把packet转发到packet的目的地地址:
      // 简单的对XML元素进行克隆,而后……
      Element result = packet.getElement().clone();
      // 把他放到传出packet队列里面就好了
      results.offer(newPacket(result));
      return;
    }
 
    // 程序真的会运行到这里吗?
    // 是的,一些packet即没有from也没有to地址。最容易理解的一个例子是向服务器发送的获取某些数据的IQ请求。这类packet没有任何地址,而且须要对它作不少复杂的处理
    // 下面的代码展现了如何肯定这个seesion就是请求发起者的session
    id = packet.getFrom();
    // 下面的处理和检查getElementFrom差很少
    if(session.getConnectionId().equals(id)) {
      // 这里须要针对IQ packet作一些特别处理,可是咱们须要处理的是message,因此这里只须要对它进行转发
      Element result = packet.getElement().clone();
      // 若是程序运行到这里说明packet的from地址是没有的,如今对from属性就行设置
      result.setAttribute("from", session.getJID());
      // 最后把传出packet放到结果队列里面就ok乐
      results.offer(newPacket(result));
    }
 
  }catch(NotAuthorizedException e) {
    log.warning("NotAuthorizedException for packet: "  +
      packet.getStringData());
    results.offer(Authorization.NOT_AUTHORIZED.getResponseMessage(packet,
      "You must authorize session first.",true));
  }// end of try-catch
 
}