联系人与状态
JID类的实现
在第二章中已经描述过:XMPP协议中JID所代表的是一个登录端点。在本应用中,JID可以
被抽象为一个Jid
类:
在图\ref{fig:ch07.jid.class.png}中所展示的结构中local、domain、resource这三个 成员是每个Jid实例都包含的,是典型的成员变量。另外两个是静态成员:
-
fromString()
是静态方法,功能是把字符串转为Jid实例。 -
jidPattern
是用来匹配Jid格式的正则表达式。
通过对正则表达式的匹配,可以很方便地从字符串构建出Jid实例。
val jidPattern = ("""(((\w+([-_\.]\w+)*)@)?)""" + """(\w+([-_\.]\w+)*)((/(\w+([-_\.]\w+)*))?)""").r /* create instance from String */ def fromString(str: String): Option[Jid] = str match { case jidPattern(a,b,local,c,domain,d,e,f,resource,g) => Some(Jid(lo,dom,rec)) case _ => None }
伴生对象
Java中可以通过关键字static
声明成员为静态成员,Scala中不支持static
语法,而是
采用单例对象来实现静态成员的。通过object
关键字就可以生成一个单例对象,而且
如果单例对象与某个类名相同的话,就自动成为这个类的「伴生对象」。Scala中的类可以
自由访问伴生对象中的私有成员:
/* class */ class Jid(val local: String, val domain: String, val resource: String) { } /* object */ object Jid { val jidPattern = /* ... */ def fromString(str: String): Option[Jid] = /* ... */ }
工厂方法
在Scala语言中,伴生对象的apply()
方法被视为该类的静态工厂方法。工厂方法以后,
可以通过「类名()
」的形式得到类的实例,而不用通过关键字new
调用构造函数。
object Jid { def apply(val local: String, val domain: String, val resource: String) = { new Jid(local, domain, resource) } }
Option类型
方法fromString()
的作用是从字符串创建一个Jid实例:
def fromString(str: String): Option[Jid] = /* ... */
注意这个方法的返回类型不是Jid
而是Option[Jid]
。在Scala语言中Option
类型相当
于一个容器,表示该值的内容可能为空。Option
类型存在的意义就是在调用该方法时
提醒程序员,返回值有可能为空。在操作这个返回值前强制程序员先对返回值为空的情况
作出处理,这样就可以避免发生NullPointerException
异常抛出引发程序中断。
比较常见的处理方法就是使用getOrElse()
来定义没有返回值时的默认行为。
Option[Jid]
表示期待返回的值是Jid
实例,但不保证一定会返回Jid
实例:因为
参数str
是字符串类型,并一定都是格式正确的JID
,所以方法的返回值有可能为空。
Scala的模式匹配与样本类
Jid类型作为用户连接的抽象,在本应用中会非常频繁地被用到。比如从Jid实例中提取 出local、domain、resource的值。
在Java或其他面向对象的语言中一般都是通过成员变量名来访问:
local = jid.getLocal(); domain = jid.getDomain(); resource = jid.getResource();
而在Scala中,由于语言本身提供的模式匹配语法,可以很方便地把Jid实例的成员匹配
到变量上去。首先要加上关键字case
把一个Scala类声明为样本类:
case class Jid(val local: String, val domain: String, val resource: String) { }
声明为样本类后就可以作为模式匹配的条件:
obj match { case Jid(local, domain, resource) => { /* case block */ } case _ => None }
case Jid(l, d, r)
语言起到了两个作用:
- 首先进行类型匹配,如果obj为Jid类的实例,则匹配成功。
-
如果匹配成功,那么按照工厂方法
apply(local,domain,resource)
方法的参数把顺序, 把local
、domain
、resource
三个工厂方法的参数分别绑定到变量l
、d
、r
,这三个变量的作用域为=>
后的语句块。
对于第二个匹配条件case _
,表示匹配所有的情况。如果前一个条件不符合,那么就
自动匹配这个条件。
除了模式匹配,在样本类还有以下这些作用:
-
样本类有自动产生的工厂方法
apply()
。 -
自动包含
copy()
方法可以得到一个副本。 -
编译器为样本类添加了可读性更强的
toString()
方法。 -
自动提供的
hashCode()
和equals()
方法会树型嵌套作用于成员变量。
虽然样本类有很多方便的特性,但并不是所有的类都适合定义为样本类。如果一个样本类
是从其他样本类继承过来的,那么不会自动实现默认的toString()
、 hashCode()
、
equals()
、copy()
方法。而且编译器会提示警告。在以后的Scala版本里可能会禁止
样本类扩展子类。所以,推荐只有最末端的子类是样本类。
抽取器
之前介绍的工厂方法可以根据指定参数来创建实例,与之对应的有抽取器unapply()
方法
根据一个实例来提取出特定的属性值。以Jid
为例:
-
apply()
方法根据local
、domain
、resource
返回Jid
实例。 -
unapply()
方法根据一个Jid
实例返回该实例的local
、domain
、resource
。
在Jid
类的unapply()
方法中,参数是任意类型的Any
。因为模式匹配可以非常方便
地匹配参数的真正类型:
def unapply(obj: Any): Option[(String, String, String)] = { obj match { case Jid(l, d, r) if (isBlank(d)) => None case Jid(l, d, r) if (isBlank(l)) => Some((null, d, null)) case Jid(l, d, r) if (isBlank(r)) => Some((l, d, null)) case Jid(l, d, r) => Some((l, d, r)) case _ => None } }
联系人列表
用Jid类实现了一个JID的抽象以后,还需要一个类作为存放联系人列表的容器。所以在
这里抽象为Roster
类:
-
用户的在线状态可以抽象为内部类
Presense
,其成员jid
为JID实例,priority
为 该登录端点的权重,status
为该登录端点的在线状态。 -
枚举类型
Subscription
抽象了unauth
(对方未同意)和both
(双向订阅)两种 状态。 -
内部类
Member
代表了每一个独立的联系人。 -
Roster
类为联系人的抽象。主要成员是一个HashMap
存放联系人与JID字符串的映射。
向服务器申请联系人列表与联系人的状态
在登录成功以后服务器后会出iq
报文:
<iq id="xqHCu-1" type="result" from="jabber.org"/>
当客户端收到上面例子中这样没有内部元素的iq
报文,就说明可以请求联系人和联系人
状态了。在IQHandler
的process()
方法中,会同时调用requireRoster()
和
requirePresence()
。申请的过程如图\ref{fig:ch07.iqhdl.png}:
requireRoster()
方法会发出如下的报文请求联系人列表:
<iq type="get" id="xqHCu-2"> <query xmlns="jabber:iq:roster"></query> </iq>
requirePresence()
方法如下的报文请求联系人状态:
<presence id="29D92-32"></presence>
解析服务器发送的联系人名单
服务器收到iq/query
请求以后返回联系人格式是一个有query
子元素的iq
节点:
<iq id="xqHCu-2" type="result" to="sorr@jabber.org/jadexmpp"> <query ver="12" xmlns="jabber:iq:roster"> <item subscription="both" name="Jade Shan" jid="ao23@gmail.com"> <group>Buddies</group> </item> <item subscription="both" jid="coru@gmail.com"> <group>Buddies</group> </item> </query> </iq>
如图\ref{fig:ch07.rosteritem.class.png}内部类Item
代表一个服务器发来的消息:
因为返回的联系人名单也是在iq
报文中的,所以只要在处理iq
的处理器中判断是否有
query/item
子元素就可以实现解析联系人名单的功能。如图\ref{fig:ch07.iqphdl.png}
:
process()
方法从XML报文中取出jid
属性、name
属性与group
子元素创建Item
实例,并作为消息发送给Roster
实例。
Roster
类作为一个Actor
实例,如果收到Item
类型的消息就会调用updateMember()
方法更新联系人名单。
解析服务器发送的联系人状态
前一节中已经介绍过如何解析联系人名单,接下来再讨论如何更新联系人状态。服务器在
收到更新联系人状态的请求后返回记录为多个presence
记录。格式为:
<presence id="99jn5-513" to="sorr@jabber.org" from="ao23@gmail.com/androidcHg66345792"> <status>online</status> <priority>0</priority> </presence> <presence id="99jn5-514" to="sorr@jabber.org" from="kod92@gmail.com/androidcHg66345792"> <status>online</status> <priority>0</priority> </presence>
同样可以很简单从服务器的响应建立Presence
实例,和之前的其他服务器响应的处理
方式一样,通过创建一个Presence
标签来处理:
Roster
作为一个Actor
实例,如果收到Presence
类型的消息就会调用
updatePresence()
方法更新联系人的在线状态: