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
 
}
相關文章
相關標籤/搜索