/* Copyright (c) 2008 Bluendo S.r.L.
* See about.html for details about license.
*
* $Id: Contact.java 1597 2009-06-19 11:54:12Z luca $
*/
package it.yup.xmpp;
import it.yup.util.Utils;
import it.yup.xml.Element;
import it.yup.xmpp.packets.Iq;
import it.yup.xmpp.packets.Message;
import it.yup.xmpp.packets.Presence;
import it.yup.xmpp.packets.Stanza;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.Vector;
/**
* The Xmpp Contact
*/
public class Contact extends IQResultListener {
/* possible availability status */
public static final int AV_CHAT = 0;
public static final int AV_ONLINE = 1;
public static final int AV_DND = 2;
public static final int AV_AWAY = 3;
public static final int AV_XA = 4;
public static final int AV_UNAVAILABLE = 5;
/* reason for status change */
public static final int CH_MESSAGE_NEW = 0;
public static final int CH_MESSAGE_READ = 1;
public static final int CH_STATUS = 2;
public static final int CH_TASK_NEW = 3;
public static final int CH_TASK_REMOVED = 4;
public static final int CH_CONTACT_REMOVED = 5;
public static final int CH_GROUP = 6;
/*
* The last resource associated to this user that sent a message
*/
/** mapping presence availability constants */
public static String availability_mapping[] = { Presence.SHOW_CHAT, // AV_CHAT
Presence.SHOW_ONLINE, // AV_ONLINE
Presence.SHOW_DND, // AV_DND
Presence.SHOW_AWAY, // AV_AWAY
Presence.SHOW_XA, // AV_XA
Presence.T_UNAVAILABLE // AV_UNAVAILABLE
};
/**
* Get the icon for a presence show
*
* @param i
* a {@link@Presence} AV_* constant
* @return
*/
/** the messages history */
//private Vector conv = null;
Vector convs = new Vector();
/** pending commands */
private Vector tasks = null;
// XXX to array
/** the command list; array of String pairs (node/name) */
public String cmdlist[][] = null;
private String queryCapNode = null;
private String queryCapVer = null;
public boolean pending_tasks = false;
public boolean unread_msg() {
Enumeration en = this.convs.elements();
while (en.hasMoreElements()) {
Object[] ithCouple = (Object[]) en.nextElement();
if (((Vector) ithCouple[1]).size() > 0) return true;
}
return false;
}
/*
* A Calendar used to signal the date of message arriving
*/
static Calendar cal = Calendar.getInstance();
public String jid;
public String name;
private String[] groups;
public String subscription;
Presence[] resources = null;
/** cached availability: cached for speeding up sorting */
int availability = AV_UNAVAILABLE;
/*
* the last (in time order) resource associated to this user
*/
public String lastResource = null;
public Contact(String jid, String name, String subscription,
String groups[]) {
this.jid = jid;
if (name == null) {
this.name = "";
} else {
this.name = name;
}
if (subscription == null || "".equals(subscription)) {
this.subscription = "none";
} else {
this.subscription = subscription;
}
if (groups == null) {
this.groups = new String[0];
} else {
this.groups = groups;
}
// add mySelf to all my groups
// I don't add the "ungrouped" group to myself; I only add myself to the ungrouped group
if (this.groups.length == 0) {
((Group) Group.getGroup(Roster.unGroupedCode)).addElement(this.jid);
} else {
for (int i = 0; i < this.groups.length; i++) {
((Group) Group.getGroup(this.groups[i])).addElement(this.jid);
}
}
// System.out.println("name ---->" + this.name + "(" + this.jid + ")");
}
public Element store() {
Element el = new Element(Roster.NS_IQ_ROSTER, "item");
el.setAttributes(new String[] { "jid", "name", "subscription" },
new String[] { jid, name, subscription });
for (int i = 0; i < this.groups.length; i++) {
if (this.groups[i].equals(Roster.unGroupedCode) == false) {
el.addElement(null, "group").addText(this.groups[i]);
}
}
// #mdebug
//@ System.out.println("Dump: " + jid);
// #enddebug
return el;
}
public void addMessageToHistory(String preferredResource, Message msg) {
// the default presence is the first one
if (preferredResource == null) {
preferredResource = (resources != null ? this.resources[0]
.getAttribute(Message.ATT_FROM) : this.jid);
}
String type = msg.getAttribute(Message.ATT_TYPE);
if (type == null) type = Message.NORMAL;
Enumeration en = this.convs.elements();
boolean found = false;
while (en.hasMoreElements()) {
Object[] convCouple = (Object[]) en.nextElement();
if (((String) convCouple[0]).equals(preferredResource)) found = true;
}
if (found == false) {
convs.addElement(new Object[] { preferredResource, new Vector() });
}
compileMessage(preferredResource, msg, type);
}
protected void compileMessage(String preferredResource, Message msg,
String type) {
String body = msg.getBody();
String to = msg.getAttribute(Stanza.ATT_TO);
String from = msg.getAttribute(Stanza.ATT_FROM);
Element error = msg.getChildByName(null, Message.ERROR);
if (error != null) {
String code = error.getAttribute("code");
if (code != null) {
String mappedCode = XMPPClient.getErrorString (code);
body = body + " - Error:" + mappedCode;
}
Element text = error.getChildByName(null, "text");
if (text!=null){
body = body + ". "+ text.getText();
}
}
// muc can have a delay child that modifies the from
Element delay = msg.getChildByName(null, XMPPClient.DELAY);
if (delay != null) {
String delayFrom = delay.getAttribute(Message.ATT_FROM);
if (delayFrom != null) {
from = Contact.userhost(from) + "/" + Contact.user(delayFrom);
}
}
Element subjectEl = msg.getChildByName(null, "subject");
String subject = null;
if (subjectEl != null) subject = subjectEl.getText();
if (subject != null && subject.length() > 0) {
body = subject + ": " + body;
}
long date;
String arriveTime = "";
date = System.currentTimeMillis();
cal.setTime(new Date(date));
int hour = cal.get(Calendar.HOUR_OF_DAY);
int minute = cal.get(Calendar.MINUTE);
arriveTime = new String((hour < 10 ? "0" : "") + hour + ":"
+ (minute < 10 ? "0" : "") + minute);
if (from != null) lastResource = from;
if (body != null && body.length() > 0) {
getMessageHistory(preferredResource).addElement(
new String[] { to, body, lastResource, arriveTime, type });
}
}
public Vector getMessageHistory(String preferredResource) {
Enumeration en = this.convs.elements();
while (en.hasMoreElements()) {
Object[] convCouple = (Object[]) en.nextElement();
if (((String) convCouple[0]).equals(preferredResource)) return (Vector) convCouple[1];
}
return null;
}
public Vector getAllConvs() {
return this.convs;
}
public int getHistoryLength(String preferredResource) {
Vector messageHistory = getMessageHistory(preferredResource);
if (messageHistory == null) { return 0; }
return messageHistory.size();
}
public void resetMessageHistory(String preferredResource) {
getMessageHistory(preferredResource).removeAllElements();
}
public String getPrintableName() {
if (!"".equals(name)) {
return name;
} else {
return jid;
}
}
/**
* Returns the user full jid (if online) or null if offline. The presence
* with the highest pririty is used
*
* @return The user full Jid.
*/
public String getFullJid() {
if (resources != null) { return resources[0]
.getAttribute(Stanza.ATT_FROM); }
return null;
}
public boolean isVisible() {
if (unread_msg()) {
/* if the contact has messages in his chat, it will be shown */
return true;
}
if (resources != null) { return true; }
if (subscription == null) return false;
// if ("none".equals(subscription)) {// || "to".equals(subscription)) {
// // ignoring the contacts whose status hasn't to be displayed
// // XXX: we should let the user configure this behavior
// return false;
// }
return false;
}
public void addTask(Task t) {
if (tasks == null) {
tasks = new Vector();
pending_tasks = true;
}
if (!tasks.contains(t)) {
tasks.addElement(t);
pending_tasks = true;
}
}
public void removeTask(Task t) {
if (tasks == null) { return; // shouldn't happen, but...
}
tasks.removeElement(t);
if (tasks.size() == 0) {
tasks = null;
pending_tasks = false;
}
}
public Task[] getTasks() {
Task[] tasks;
if (this.tasks != null) {
tasks = new Task[this.tasks.size()];
this.tasks.copyInto(tasks);
} else {
tasks = new Task[0];
}
return tasks;
}
/**
* Get the presence of the best resource (in order of visibility and
* priority)
*
* @return
*/
public Presence getPresence() {
if (resources == null) return null;
return resources[0];
}
/**
* Get all presences for each resource
*
* @return
*/
public Presence[] getAllPresences() {
// XXX it should be safer to return a copy
return resources;
}
/**
* Update the presence for the resource that has sent it
*
* @param p
*/
public void updatePresence(Presence p) {
// look for nickname
// many gw have wrong nickname!
if (p.getAttribute(Iq.ATT_FROM).indexOf("@") >= 0) {
Element x = p.getChildByName(XMPPClient.NS_VCARD_UPDATE, "x");
if (x != null) {
Element nickname = x.getChildByName(null, "nickname");
if (nickname != null) {
String nickNameText = nickname.getText();
if (nickNameText != null && nickNameText.length() > 0) {
this.name = nickNameText;
}
}
}
}
if (Presence.T_UNAVAILABLE.equals(p.getAttribute(Stanza.ATT_TYPE))) {
if (resources == null) {
return;
} else if (resources.length == 1) {
// remove the resource array if the only resource is going
// offline
String old_jid = resources[0].getAttribute(Stanza.ATT_FROM);
if (old_jid.equals(p.getAttribute(Stanza.ATT_FROM))) {
resources = null;
availability = AV_UNAVAILABLE;
}
return;
} else {
updateExistingPresence(p);
}
} else {
// available presence, update the list and resort
if (resources == null) {
// first resource create the list
resources = new Presence[] { p };
} else {
// add or update and finally sort
String jid = p.getAttribute(Stanza.ATT_FROM);
boolean found = false;
// check if we can just update
for (int i = 0; i < resources.length; i++) {
if (jid.equals(resources[i].getAttribute(Stanza.ATT_FROM))) {
resources[i] = p;
found = true;
break;
}
}
if (!found) {
// new resource found, add it
Presence v[] = new Presence[resources.length + 1];
v[0] = p;
for (int i = 0; i < resources.length; i++) {
v[i + 1] = resources[i];
}
resources = v;
}
// presence order may have changed sort the resources
if (resources.length > 1) {
for (int i = 0; i < resources.length - 1; i++) {
for (int j = 1; j < resources.length; j++) {
Presence ri = resources[i];
Presence rj = resources[j];
int diff = mapAvailability(ri.getShow())
- mapAvailability(rj.getShow());
if (diff > 0) {
resources[i] = rj;
resources[j] = ri;
} else if (diff == 0) {
// check priority
int pdiff = ri.getPriority() - rj.getPriority();
// higher priority comes first
if (pdiff < 0) {
resources[i] = rj;
resources[j] = ri;
}
}
}
}
}
}
}
// check the capabilities
// pass them to the roster that checks the db
Element capNode = p.getChildByName(null, "c");
if (capNode != null) {
queryCapNode = capNode.getAttribute("node");
queryCapVer = capNode.getAttribute("ver");
Element cap = Config.getInstance().getCapabilities(queryCapNode,
queryCapVer);
if (cap == null) {
this.askCapabilities(p);
}
}
// cache the new availability
availability = mapAvailability(resources[0].getShow());
capNode = p.getChildByName(null, "c");
if (capNode != null) {
String node = capNode.getAttribute("node");
String ver = capNode.getAttribute("ver");
Element cap = Config.getInstance().getCapabilities(node, ver);
if (cap != null) {
Element identity = cap.getChildByName(null, "identity");
if (identity != null) {
String name = identity.getAttribute("name");
if (name != null && name.compareTo("Lampiro") == 0) {
p.pType = Presence.PHONE;
}
}
}
}
}
public Element getCapabilities(Presence p) {
if (p == null) return null;
Element c = p.getChildByName(XMPPClient.NS_CAPS, "c");
if (c == null) { return null; }
String node = c.getAttribute("node");
String ver = c.getAttribute("ver");
return Config.getInstance().getCapabilities(node, ver);
}
public void askCapabilities(Presence p) {
Element c = null;
if (p != null) {
c = p.getChildByName(XMPPClient.NS_CAPS, "c");
}
Iq iq = new Iq(p.getAttribute(Message.ATT_FROM), Iq.T_GET);
Element query = iq.addElement(XMPPClient.NS_IQ_DISCO_INFO, Iq.QUERY);
if (c != null) {
query.setAttribute("node", c.getAttribute("node") + "#"
+ c.getAttribute("ver"));
}
XMPPClient.getInstance().sendIQ(iq, this);
}
protected void updateExistingPresence(Presence p) {
// More than one resource, remove only that resource (if
// present)
String from = p.getAttribute(Stanza.ATT_FROM);
String from_cache[] = new String[resources.length];
boolean found = false;
for (int i = 0; i < resources.length; i++) {
from_cache[i] = resources[i].getAttribute(Stanza.ATT_FROM);
if (from.equals(from_cache[i])) {
found = true;
}
}
if (found) {
Presence v[] = new Presence[resources.length - 1];
for (int i = 0, j = 0; i < resources.length; i++) {
if (!from.equals(from_cache[i])) {
v[j++] = resources[i];
}
}
resources = v;
}
// got to the end of the method, must update availability
}
/**
* Compare the contatcs for sorting them in the roster. The precedence is
* given by:
* <ol>
* <li> having unread messages </li>
* <li> having pending tasks </li>
* <li> the presence </li>
* <li> alphabetical order </li>
* </ol>
*/
public int compareTo(Contact d) {
// check first unread messages
if (d.unread_msg() && !unread_msg()) {
return -1;
} else if (unread_msg() && !d.unread_msg()) { return 1; }
// then check for pending tasks
if (d.pending_tasks && !this.pending_tasks) {
return -1;
} else if (!d.pending_tasks && this.pending_tasks) { return 1; }
int avDiff = this.availabilityDiff(this, d);
if (avDiff != 0) return avDiff;
// finally use the name if all the other tests failed
return d.getPrintableName().toLowerCase().compareTo(
getPrintableName().toLowerCase());
}
private int availabilityDiff(Contact left, Contact right) {
if ((right.availability >= 2 && left.availability <= 1)
|| (right.availability >= 3 && left.availability <= 2)
|| (right.availability == 5 && left.availability <= 4)) return 1;
if ((right.availability <= 1 && left.availability >= 2)
|| (right.availability <= 2 && left.availability >= 3)
|| (right.availability <= 4 && left.availability == 5)) return -1;
return 0;
}
/**
* Get the availability of the highest scrored resource
*
* @return one of the possible AV_* constants
*/
public int getAvailability() {
return availability;
}
/**
* Get the availability of a given reosurce
*
* @return one of the possible AV_* constants
*/
public int getAvailability(String jid) {
for (int i = 0; resources != null && i < resources.length; i++) {
Presence p = this.getPresence(jid);
if (p != null) return mapAvailability(p.getShow());
}
return AV_UNAVAILABLE;
}
public Presence getPresence(String jid) {
if (resources != null) {
if (jid == null) return resources[0];
for (int i = 0; i < resources.length; i++) {
String ijid = resources[i].getAttribute(Stanza.ATT_FROM);
if (jid.equals(ijid)) { return resources[i]; }
}
return resources[0];
}
return null;
}
// XXX -> move to the Utils?
public static String userhost(String jid) {
int spos = jid.indexOf('/');
if (spos > 0) {
return jid.substring(0, spos);
} else {
return jid;
}
}
public static String resource(String jid) {
int spos = jid.indexOf('/');
if (spos > 0) {
return jid.substring(spos + 1);
} else {
return null;
}
}
public static String user(String jid) {
int spos = jid.indexOf('@');
if (spos > 0) {
return jid.substring(0, spos);
} else {
return null;
}
}
public static String domain(String jid) {
String uh = Contact.userhost(jid);
int spos = uh.indexOf('@');
if (spos > 0) {
return uh.substring(spos + 1);
} else {
return null;
}
}
/**
* Getting the constant for a given availability string
*
* @param s
* the availability string (if null mapped to "online")
* @return
*/
int mapAvailability(String s) {
if (s == null) s = "online";
for (int i = 0; i < availability_mapping.length; i++) {
if (availability_mapping[i].equals(s)) { return i; }
}
return -1;
}
public void handleError(Element e) {
// TODO Auto-generated method stub
}
public void handleResult(Element e) {
Element query = e.getChildByName(XMPPClient.NS_IQ_DISCO_INFO, Iq.QUERY);
String fullNode = query.getAttribute("node");
if (fullNode == null) {
if (queryCapVer != null && queryCapNode != null) fullNode = queryCapNode
+ "#" + queryCapVer;
}
if (fullNode != null) {
Vector fn = Utils.tokenize(fullNode, '#');
String node = (String) fn.elementAt(0);
String ver = (String) fn.elementAt(1);
Config.getInstance().saveCapabilities(node, ver, query);
this.updatePresence(this.resources[0]);
}
}
/**
* @param groups the groups to set
*/
public boolean setGroups(String[] newGroups) {
// first I check if the new groups are different to the old ones
// in that case i re-check the groups structures
boolean retVal = false;
if (groups.length != newGroups.length) retVal = true;
else {
for (int i = 0; i < this.groups.length; i++) {
String ithOldGroup = groups[i];
boolean found = false;
for (int j = 0; j < newGroups.length; j++) {
String ithNewGroup = newGroups[j];
if (ithNewGroup.equals(ithOldGroup)) {
found = true;
break;
}
}
if (found == retVal) return true;
}
}
if (retVal) {
for (int i = 0; i < this.groups.length; i++) {
Group.getGroup(groups[i]).removeElement(this.jid);
}
for (int i = 0; i < newGroups.length; i++) {
Group.getGroup(newGroups[i]).addElement(this.jid);
}
}
this.groups = newGroups;
return retVal;
}
/**
* @return the groups
*/
public String[] getGroups() {
return groups;
}
public boolean supportsMUC(Presence p) {
if (p == null)
return false;
/*
* Gmail supports only the old "deprecated" format with CAPS Extension
*
*/
Element c = p.getChildByName(XMPPClient.NS_CAPS, "c") ;
if (c!=null){
String ext = c.getAttribute("ext");
if (ext != null && ext.indexOf("pmuc-v1")>=0)
return true;
}
/*
* While this is the suggested one
*/
Element caps = this.getCapabilities(p);
if (caps == null)
return false;
Element[] features= caps.getChildrenByName(null, "feature");
for (int i = 0; i < features.length; i++) {
Element ithFeature = features[i];
if (ithFeature.getAttribute("var").equals(XMPPClient.NS_MUC))
return true;
}
return false;
}
}