/*
* Copyright 2006-2010 Daniel Henninger. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package net.sf.kraken.roster;
import net.sf.kraken.avatars.Avatar;
import net.sf.kraken.type.NameSpace;
import net.sf.kraken.type.PresenceType;
import org.apache.log4j.Logger;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.NotFoundException;
import org.xmpp.packet.JID;
import org.xmpp.packet.Presence;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* Transport Buddy.
*
* This class is intended to be extended and includes all necessary pieces for
* syncing with a roster. It also handles keeping current statuses and such for
* easy retrieval and only sends presence changes upon status changes. So that
* the base transport can manage this list too sometimes, it is very important
* that the specific transport implementation take into account that there may
* buddy instances that do not know anything about any 'custom' fields the
* transport may be implementing.
*
* @author Daniel Henninger
*/
public abstract class TransportBuddy {
static Logger Log = Logger.getLogger(TransportBuddy.class);
/**
* Default constructor, nothing set up.
*/
public TransportBuddy() {
// Nothing
}
/**
* Creates a TransportBuddy instance.
*
* @param manager Transport buddy manager we are associated with.
* @param contactname The legacy contact name.
* @param nickname The legacy nickname (can be null).
* @param groups The list of groups the legacy contact is in (can be null).
*/
public TransportBuddy(TransportBuddyManager manager, String contactname, String nickname, Collection<String> groups) {
this.managerRef = new WeakReference<TransportBuddyManager>(manager);
this.jid = manager.getSession().getTransport().convertIDToJID(contactname);
this.contactname = manager.getSession().getTransport().convertJIDToID(this.jid);
if (nickname != null) {
this.nickname = nickname;
}
else {
this.nickname = this.contactname;
}
if (groups != null && !groups.isEmpty()) {
this.groups = groups;
}
if (JiveGlobals.getBooleanProperty("plugin.gateway."+getManager().getSession().getTransport().getType()+".avatars", true)) {
try {
this.avatar = new Avatar(this.jid);
this.avatarSet = true;
}
catch (NotFoundException e) {
// Ok then, no avatar, no worries.
}
}
lastActivityTimestamp = new Date().getTime();
}
/**
* The transport buddy manager we are attached to.
*/
private WeakReference<TransportBuddyManager> managerRef = null;
public TransportBuddyManager getManager() {
return managerRef.get();
}
/**
* ID, Screenname, name, whatever the contact name is on the legacy system
*/
public String contactname = null;
/**
* Converted JID for the contact, for caching purposes
*/
public JID jid = null;
/**
* A nickname associated with this contact, if it exists.
*/
public String nickname = null;
/**
* A group associated with this contact, if it exists.
*/
public Collection<String> groups = new ArrayList<String>();
/**
* Specific requested subscription status, if desired.
*/
public RosterItem.SubType subtype = RosterItem.SUB_TO;
/**
* Specific requested ask status, if desired.
*/
public RosterItem.AskType asktype = null;
/**
* Current presence status.
*/
public PresenceType presence = PresenceType.unavailable;
/**
* Current verbose status.
*/
public String verboseStatus = "";
/**
* Avatar instance associated with this contact.
*/
public Avatar avatar = null;
/**
* Has the avatar been set?
*/
public Boolean avatarSet = false;
/**
* Timestamp of last activity
*/
public Long lastActivityTimestamp = null;
/**
* Retrieves timestamp of last activity.
*
* @return Timestamp in milliseconds since the epoch.
*/
public Long getLastActivityTimestamp() {
return lastActivityTimestamp;
}
/**
* Retrieves text event of last activity or null if no event text.
*
* @return Text of last event.
*/
public String getLastActivityEvent() {
return lastActivityEvent;
}
/**
* Text of last activity
*/
public String lastActivityEvent = null;
/**
* Retrieves the name of the contact.
*
* @return Name of contact.
*/
public String getName() {
return contactname;
}
/**
* Retrieves the JID of the contact.
*
*
* @return JID of contact.
*/
public JID getJID() {
return jid;
}
/**
* Sets the name of the contact.
*
* @param contactname Username of the contact.
*/
public void setName(String contactname) {
this.jid = getManager().getSession().getTransport().convertIDToJID(contactname);
this.contactname = getManager().getSession().getTransport().convertJIDToID(this.jid);
}
/**
* Retrieves the nickname of the contact.
*
* @return Nickname of contact.
*/
public String getNickname() {
return nickname;
}
/**
* Sets the nickname of the contact.
*
* @param nickname Nickname of contact.
*/
public void setNickname(String nickname) {
Boolean changed = false;
if (nickname != null) {
if (this.nickname == null || !this.nickname.equals(nickname)) {
changed = true;
}
this.nickname = nickname;
}
else {
if (this.nickname == null || !this.nickname.equals(getName())) {
changed = true;
}
this.nickname = getName();
}
if (changed && getManager().isActivated()) {
Log.debug("TransportBuddy: Triggering contact update for "+this);
getManager().getSession().updateContact(this);
}
}
/**
* Retrieves the groups of the contact.
*
* @return Groups contact is in.
*/
public Collection<String> getGroups() {
return groups;
}
/**
* Sets the list of groups of the contact.
*
* @param groups List of groups the contact is in.
*/
public void setGroups(List<String> groups) {
Boolean changed = false;
if (groups != null && !groups.isEmpty()) {
if (this.groups == null || this.groups.isEmpty() || !groups.containsAll(this.groups) || !this.groups.containsAll(groups)) {
changed = true;
}
this.groups = groups;
}
else {
if (this.groups != null && !this.groups.isEmpty()) {
changed = true;
}
this.groups = null;
}
if (changed && getManager().isActivated()) {
Log.debug("TransportBuddy: Triggering contact update for "+this);
getManager().getSession().updateContact(this);
}
}
/**
* Sets the nickname and list of groups of the contact.
*
* @param nickname Nickname of contact.
* @param groups List of groups the contact is in.
*/
public void setNicknameAndGroups(String nickname, List<String> groups) {
Boolean changed = false;
if (nickname != null) {
if (this.nickname == null || !this.nickname.equals(nickname)) {
changed = true;
}
this.nickname = nickname;
}
else {
if (this.nickname == null || !this.nickname.equals(getName())) {
changed = true;
}
this.nickname = getName();
}
if (groups != null && !groups.isEmpty()) {
if (this.groups == null || this.groups.isEmpty() || !groups.containsAll(this.groups) || !this.groups.containsAll(groups)) {
changed = true;
}
this.groups = groups;
}
else {
if (this.groups != null && !this.groups.isEmpty()) {
changed = true;
}
this.groups = null;
}
if (changed && getManager().isActivated()) {
Log.debug("TransportBuddy: Triggering contact update for "+this);
getManager().getSession().updateContact(this);
}
}
/**
* Retrieves the subscription status for the contact.
*
* @return SubType if set.
*/
public RosterItem.SubType getSubType() {
return subtype;
}
/**
* Sets the subscription status for the contact.
*
* @param substatus Subscription status to be set.
*/
public void setSubType(RosterItem.SubType substatus) {
subtype = substatus;
}
/**
* Retrieves the ask status for the contact.
*
* @return AskType if set.
*/
public RosterItem.AskType getAskType() {
return asktype;
}
/**
* Sets the ask status for the contact.
*
* @param askstatus Ask status to be set.
*/
public void setAskType(RosterItem.AskType askstatus) {
asktype = askstatus;
}
/**
* Retrieves the current status.
*
* @return Current status setting.
*/
public PresenceType getPresence() {
return presence;
}
/**
* Sets the current status.
*
* @param newpresence New presence to set to.
*/
public void setPresence(PresenceType newpresence) {
if (newpresence == null) {
newpresence = PresenceType.unknown;
}
if (newpresence.equals(PresenceType.unavailable)) {
verboseStatus = "";
}
if (!presence.equals(newpresence) && newpresence != PresenceType.unknown) {
Presence p = new Presence();
p.setTo(getManager().getSession().getJID());
p.setFrom(jid);
getManager().getSession().getTransport().setUpPresencePacket(p, newpresence);
if (!verboseStatus.equals("")) {
p.setStatus(verboseStatus);
}
if (avatarSet && avatar != null) {
Element vcard = p.addChildElement("x", NameSpace.VCARD_TEMP_X_UPDATE);
vcard.addElement("photo").addCDATA(avatar.getXmppHash());
vcard.addElement("hash").addCDATA(avatar.getXmppHash());
}
getManager().sendPacket(p);
}
presence = newpresence;
lastActivityTimestamp = new Date().getTime();
}
/**
* Retrieves the current verbose status.
*
* @return Current verbose status.
*/
public String getVerboseStatus() {
return verboseStatus;
}
/**
* Sets the current verbose status.
*
* @param newstatus New verbose status.
*/
public void setVerboseStatus(String newstatus) {
if (newstatus == null) {
newstatus = "";
}
if (!verboseStatus.equals(newstatus)) {
Presence p = new Presence();
p.setTo(getManager().getSession().getJID());
p.setFrom(jid);
getManager().getSession().getTransport().setUpPresencePacket(p, presence);
if (!newstatus.equals("")) {
p.setStatus(newstatus);
}
if (avatarSet && avatar != null) {
Element vcard = p.addChildElement("x", NameSpace.VCARD_TEMP_X_UPDATE);
vcard.addElement("photo").addCDATA(avatar.getXmppHash());
vcard.addElement("hash").addCDATA(avatar.getXmppHash());
}
getManager().sendPacket(p);
}
verboseStatus = newstatus;
lastActivityTimestamp = new Date().getTime();
lastActivityEvent = verboseStatus;
}
/**
* Convenience routine to set both presence and verbose status at the same time.
*
* @param newpresence New presence to set to.
* @param newstatus New verbose status.
*/
public void setPresenceAndStatus(PresenceType newpresence, String newstatus) {
Log.debug("Updating status ["+newpresence+","+newstatus+"] for "+this);
if (newpresence == null) {
newpresence = PresenceType.unknown;
}
if (newstatus == null) {
newstatus = "";
}
if (newpresence.equals(PresenceType.unavailable)) {
newstatus = "";
}
if ((!presence.equals(newpresence) && newpresence != PresenceType.unknown) || !verboseStatus.equals(newstatus)) {
Presence p = new Presence();
p.setTo(getManager().getSession().getJID());
p.setFrom(jid);
getManager().getSession().getTransport().setUpPresencePacket(p, newpresence);
if (!newstatus.equals("")) {
p.setStatus(newstatus);
}
if (avatarSet && avatar != null) {
Element vcard = p.addChildElement("x", NameSpace.VCARD_TEMP_X_UPDATE);
vcard.addElement("photo").addCDATA(avatar.getXmppHash());
vcard.addElement("hash").addCDATA(avatar.getXmppHash());
}
getManager().sendPacket(p);
}
presence = newpresence;
verboseStatus = newstatus;
lastActivityTimestamp = new Date().getTime();
lastActivityEvent = verboseStatus;
}
/**
* Sends the current presence to the session user.
*
* @param to JID to send presence updates to.
*/
public void sendPresence(JID to) {
// TODO: Should figure out best way to handle unknown here.
Presence p = new Presence();
p.setTo(to);
p.setFrom(jid);
getManager().getSession().getTransport().setUpPresencePacket(p, presence);
if (verboseStatus != null && verboseStatus.length() > 0) {
p.setStatus(verboseStatus);
}
if (avatarSet && avatar != null) {
Element vcard = p.addChildElement("x", NameSpace.VCARD_TEMP_X_UPDATE);
vcard.addElement("photo").addCDATA(avatar.getXmppHash());
vcard.addElement("hash").addCDATA(avatar.getXmppHash());
}
getManager().sendPacket(p);
}
/**
* Sends the current presence only if it's not unavailable.
*
* @param to JID to send presence updates to.
*/
public void sendPresenceIfAvailable(JID to) {
if (!presence.equals(PresenceType.unavailable)) {
sendPresence(to);
}
}
/**
* Sends an offline presence for a contact only if it is currently online.
*
* In case you are wondering why, this is useful when a resource goes offline and we want to indicate all contacts are offline.
*
* @param to JID to send presence updates to.
*/
public void sendOfflinePresenceIfAvailable(JID to) {
if (!presence.equals(PresenceType.unavailable)) {
Presence p = new Presence();
p.setType(Presence.Type.unavailable);
p.setTo(to);
p.setFrom(jid);
getManager().sendPacket(p);
}
}
/**
* Retrieves the cached avatar associated with this contact.
*
* @return The avatar associated with this contact, or null if no avatar present.
*/
public Avatar getAvatar() {
return avatar;
}
/**
* Sets the current avatar for this contact.
*
* @param avatar Avatar instance to associate with this contact.
*/
public void setAvatar(Avatar avatar) {
boolean triggerUpdate = false;
if ( (avatar != null && this.avatar == null) ||
(avatar == null && this.avatar != null) ||
(avatar != null && !this.avatar.getXmppHash().equals(avatar.getXmppHash()))) {
triggerUpdate = true;
}
this.avatar = avatar;
this.avatarSet = true;
if (triggerUpdate) {
Presence p = new Presence();
p.setTo(getManager().getSession().getJID());
p.setFrom(jid);
getManager().getSession().getTransport().setUpPresencePacket(p, presence);
if (!verboseStatus.equals("")) {
p.setStatus(verboseStatus);
}
Element vcard = p.addChildElement("x", NameSpace.VCARD_TEMP_X_UPDATE);
if (avatar != null) {
vcard.addElement("photo").addCDATA(avatar.getXmppHash());
vcard.addElement("hash").addCDATA(avatar.getXmppHash());
}
getManager().sendPacket(p);
}
}
/**
* Adds the PHOTO vcard element (representing an avatar) to an existing vcard.
*
* This will add the avatar to a vcard if there's one to add. Otherwise will not add anything.
* If added, a properly formatted PHOTO element with base64 encoded data in it will be added.
*
* param vcard vcard to add PHOTO element to
*/
public void addVCardPhoto(Element vcard) {
if (!avatarSet) {
Log.debug("TransportBuddy: I've got nothing! (no avatar set)");
return;
}
Element photo = vcard.addElement("PHOTO");
if (avatar != null) {
try {
photo.addElement("TYPE").addCDATA(avatar.getMimeType());
photo.addElement("BINVAL").addCDATA(avatar.getImageData());
}
catch (NotFoundException e) {
// No problem, leave it empty then.
}
}
}
/**
* Returns the entire vcard element for an avatar.
*
* This will return a vCard element, filled in as much as possible, regardless of whether we have
* real data for the user. It'll return minimal regardless.
*
* @return vCard element
*/
public Element getVCard() {
Element vcard = DocumentHelper.createElement(QName.get("vCard", NameSpace.VCARD_TEMP));
vcard.addElement("VERSION").addCDATA("2.0");
vcard.addElement("JABBERID").addCDATA(getJID().toString());
vcard.addElement("NICKNAME").addCDATA(getNickname() == null ? getName() : getNickname());
if (JiveGlobals.getBooleanProperty("plugin.gateway."+getManager().getSession().getTransport().getType()+".avatars", true)) {
addVCardPhoto(vcard);
}
return vcard;
}
/**
* Outputs information about the transport buddy in a pretty format.
*/
@Override
public String toString() {
return "{Buddy: "+this.jid+" (Nickname: "+this.nickname+") (Groups: "+this.groups+")}";
}
}