/*
* 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.session;
import net.sf.kraken.BaseTransport;
import net.sf.kraken.avatars.Avatar;
import net.sf.kraken.muc.MUCTransportSessionManager;
import net.sf.kraken.registration.Registration;
import net.sf.kraken.registration.RegistrationHandler;
import net.sf.kraken.roster.TransportBuddy;
import net.sf.kraken.roster.TransportBuddyManager;
import net.sf.kraken.type.*;
import org.apache.log4j.Logger;
import org.dom4j.Element;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.roster.Roster;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.openfire.vcard.VCardManager;
import org.jivesoftware.util.Base64;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.NotFoundException;
import org.xmpp.packet.*;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
/**
* Interface for a transport session.
*
* This outlines all of the functionality that is required for a transport
* to implement. These are functions that the XMPP side of things are going
* interact with. The legacy transport itself is expected to handle messages
* going to the Jabber user.
*
* @author Daniel Henninger
*/
public abstract class TransportSession<B extends TransportBuddy> {
static Logger Log = Logger.getLogger(TransportSession.class);
/**
* Convenience constructor that includes priority.
*
* @param registration Registration this session is associated with.
* @param jid JID of user associated with this session.
* @param transport Transport this session is associated with.
* @param priority Priority associated with session.
*/
public TransportSession(Registration registration, JID jid, BaseTransport<B> transport, Integer priority) {
this.jid = new JID(jid.toBareJID());
this.registration = registration;
this.transportRef = new WeakReference<BaseTransport<B>>(transport);
mucSessionManager = new MUCTransportSessionManager<B>(this);
addResource(jid.getResource(), priority);
loadAvatar();
Log.debug("Created "+transport.getType()+" session for "+jid+" as '"+registration.getUsername()+"'");
// note: vcards are supported even if avatars are not!
setSupportedFeature(SupportedFeature.vcardtemp);
}
/**
* Registration that this session is associated with.
*/
public Registration registration;
/**
* Transport this session is associated with.
*/
public WeakReference<BaseTransport<B>> transportRef;
/**
* The bare JID the session is associated with.
*/
public JID jid;
/**
* All JIDs (including resources) that are associated with this session.
*/
public ConcurrentHashMap<String,Integer> resources = new ConcurrentHashMap<String,Integer>();
/**
* List of packets that are pending delivery while a session is detached.
*/
private ArrayList<Packet> pendingPackets = new ArrayList<Packet>();
/**
* Current highest resource.
*/
public String highestResource = null;
public IQ getRegistrationPacket() {
return registrationPacket;
}
public void setRegistrationPacket(IQ registrationPacket) {
this.registrationPacket = registrationPacket;
}
/**
* Registration packet that is awaiting a response.
*/
public IQ registrationPacket = null;
/**
* Is the roster locked for sync editing?
*/
public boolean rosterLocked = false;
/**
* Contains a list of specific roster items that are locked.
*/
public ArrayList<String> rosterItemsLocked = new ArrayList<String>();
/**
* The current login status on the legacy network.
*/
public TransportLoginStatus loginStatus = TransportLoginStatus.LOGGED_OUT;
/**
* The current reason behind a connection failure, if one has occurred.
*/
public ConnectionFailureReason failureStatus = ConnectionFailureReason.NO_ISSUE;
/**
* Supported features.
*/
public ArrayList<SupportedFeature> supportedFeatures = new ArrayList<SupportedFeature>();
/**
* Number of reconnection attempts made.
*/
public Integer reconnectionAttempts = 0;
/**
* If set, represents a unix timestamp when the session was detached. The expectation being it'll get cleaned
* up if it's been hanging around for too long.
*/
public long detachTimestamp = 0;
/**
* Current presence status.
*/
public PresenceType presence = PresenceType.unavailable;
/**
* Current verbose status.
*/
public String verboseStatus = "";
/**
* Pending presence status
*/
public PresenceType pendingPresence = null;
/**
* Pending verbose status.
*/
public String pendingVerboseStatus = null;
/**
* Transport buddy manager.
*/
public TransportBuddyManager<B> buddyManager = new TransportBuddyManager<B>(this);
/**
* This session's avatar.
*/
public Avatar avatar;
/**
* The MUC transport session manager.
*/
public MUCTransportSessionManager<B> mucSessionManager;
/**
* Retrieve the muc session manager.
*
* @return muc session manager instance.
*/
public MUCTransportSessionManager<B> getMUCSessionManager() {
return mucSessionManager;
}
/**
* Retrieve the buddy manager.
*
* @return buddy manager instance.
*/
public TransportBuddyManager<B> getBuddyManager() {
return buddyManager;
}
/**
* Associates a resource with the session, and tracks it's priority.
*
* @param resource Resource string
* @param priority Priority of resource
*/
public void addResource(String resource, Integer priority) {
resources.put(resource, priority);
if (highestResource == null || resources.get(highestResource) < priority) {
highestResource = resource;
}
}
/**
* Removes an association of a resource with the session.
*
* @param resource Resource string
*/
public void removeResource(String resource) {
resources.remove(resource);
JID retJID = new JID(getJID().getNode(),getJID().getDomain(),resource);
getBuddyManager().sendOfflineForAllAvailablePresences(retJID);
// Send unavailable message to resource that went offline
Presence p = new Presence();
p.setType(Presence.Type.unavailable);
p.setTo(retJID);
p.setFrom(getTransport().getJID());
getTransport().sendPacket(p);
// Recalculate the highest resource
if (resource.equals(highestResource)) {
Integer highestPriority = -255;
String tmpHighestResource = null;
for (String res : resources.keySet()) {
if (resources.get(res) > highestPriority) {
tmpHighestResource = res;
highestPriority = resources.get(res);
}
}
highestResource = tmpHighestResource;
}
}
/**
* Updates the priority of a resource.
*
* @param resource Resource string
* @param priority New priority
*/
public void updateResource(String resource, Integer priority) {
resources.put(resource, priority);
Integer highestPriority = -255;
String tmpHighestResource = null;
for (String res : resources.keySet()) {
if (resources.get(res) > highestPriority) {
tmpHighestResource = res;
highestPriority = resources.get(res);
}
}
highestResource = tmpHighestResource;
}
/**
* Removes all resources associated with a session.
*/
public void removeAllResources() {
for (String resource : resources.keySet()) {
removeResource(resource);
}
}
/**
* Returns the number of active resources.
*
* @return Number of active resources.
*/
public int getResourceCount() {
return resources.size();
}
/**
* Detaches the session, leaving it running in the background and "suspended".
*/
public void detachSession() {
detachTimestamp = new Date().getTime();
}
/**
* Attaches the session, indicating that it's actively used.
*/
public void attachSession() {
detachTimestamp = 0;
for (Packet p : pendingPackets) {
getTransport().sendPacket(p);
}
pendingPackets.clear();
}
/**
* Stores a pending packet for later delivery
*/
public void storePendingPacket(Packet p) {
pendingPackets.add(p);
}
/**
* Retrieves the detach timestamp for determining if the session is detached, and if so how long it's been
* detached.
*/
public long getDetachTimestamp() {
return detachTimestamp;
}
/**
* Returns if the roster is currently locked.
*
* @return true or false if the roster is locked.
*/
public boolean isRosterLocked() {
return rosterLocked;
}
/**
* Returns if a specific roster item is currently locked.
*
* Also checks global lock.
*
* @param jid JID to check whether it's locked.
* @return true or false if the roster item is locked.
*/
public boolean isRosterLocked(String jid) {
return rosterLocked || rosterItemsLocked.contains(jid);
}
/**
* Locks the roster (typically used for editing during syncing).
*/
public void lockRoster() {
rosterLocked = true;
}
/**
* Locks a specific roster item (typically used for direct roster item updates).
*
* @param jid JID to lock.
*/
public void lockRoster(String jid) {
if (!rosterItemsLocked.contains(jid)) {
rosterItemsLocked.add(jid);
}
}
/**
* Unlocks the roster after sync editing is complete.
*/
public void unlockRoster() {
rosterLocked = false;
}
/**
* Unlocks a specific roster item.
*
* @param jid JID to unlock.
*/
public void unlockRoster(String jid) {
if (rosterItemsLocked.contains(jid)) {
rosterItemsLocked.remove(jid);
}
}
/**
* Retrieves the registration information associated with the session.
*
* @return Registration information of the user associated with the session.
*/
public Registration getRegistration() {
return registration;
}
/**
* Retrieves the transport associated with the session.
*
* @return Transport associated with the session.
*/
public BaseTransport<B> getTransport() {
return transportRef.get();
}
/**
* Retrieves the roster associated with the session.
*
* @return Roster associated with the session, or null if none.
*/
public Roster getRoster() {
try {
return getTransport().getRosterManager().getRoster(getJID().getNode());
}
catch (UserNotFoundException e) {
return null;
}
}
/**
* Retrieves the bare jid associated with the session.
*
* @return JID of the user associated with this session.
*/
public JID getJID() {
return jid;
}
/**
* Retrieves the JID of the highest priority resource.
*
* @return Full JID including resource with highest priority.
*/
public JID getJIDWithHighestPriority() {
return new JID(jid.getNode(),jid.getDomain(),highestResource);
}
/**
* Given a resource, returns whether it's priority is the highest.
*
* @param resource Resource to be checked.
* @return True or false if the resource is the highest priority.
*/
public Boolean isHighestPriority(String resource) {
return (highestResource.equals(resource));
}
/**
* Change the priority of a given resource.
*
* @param resource Resource to be changed.
* @param priority New priority of resource
*/
public void updatePriority(String resource, Integer priority) {
boolean currentHighest = false;
if (isHighestPriority(resource)) {
currentHighest = true;
}
updateResource(resource, priority);
if (currentHighest && !isHighestPriority(resource)) {
Presence p = new Presence(Presence.Type.probe);
p.setTo(getJIDWithHighestPriority());
p.setFrom(getTransport().getJID());
getTransport().sendPacket(p);
}
}
/**
* Retrieves the priority of a given resource.
*
* @param resource Resource to be checked.
* @return Priority of the resource, or null if not found.
*/
public Integer getPriority(String resource) {
return resources.get(resource);
}
/**
* Given a resource, returns whether the resource is currently associated with this session.
*
* @param resource Resource to be checked.
* @return True of false if the resource is associated with this session.
*/
public boolean hasResource(String resource) {
return (resources.containsKey(resource));
}
/**
* Sets a feature that the client supports.
*
* @param feature Feature that the session supports.
*/
public void setSupportedFeature(SupportedFeature feature) {
if (!supportedFeatures.contains(feature)) {
supportedFeatures.add(feature);
}
}
/**
* Removes a feature that the client supports.
*
* @param feature Feature to be removed from the supported list.
*/
public void removeSupportedFeature(SupportedFeature feature) {
supportedFeatures.remove(feature);
}
/**
* Clears all of the supported features recorded.
*/
public void clearSupportedFeatures() {
supportedFeatures.clear();
}
/**
* Retrieves whether this session supports a specific feature.
*
* @param feature Feature to check for support of.
* @return True or false if the session supports the specified feature.
*/
public Boolean isFeatureSupported(SupportedFeature feature) {
return supportedFeatures.contains(feature);
}
/**
* Updates the login status.
*
* If there is a pending presence set, it will automatically commit the pending presence.
*
* @param status New login status.
*/
public void setLoginStatus(TransportLoginStatus status) {
loginStatus = status;
if (status.equals(TransportLoginStatus.LOGGED_IN)) {
reconnectionAttempts = 0;
setFailureStatus(ConnectionFailureReason.NO_ISSUE);
getRegistration().setLastLogin(new Date());
if (pendingPresence != null && pendingVerboseStatus != null) {
setPresenceAndStatus(pendingPresence, pendingVerboseStatus);
pendingPresence = null;
pendingVerboseStatus = null;
}
if (getRegistrationPacket() != null) {
new RegistrationHandler(getTransport()).completeRegistration(this);
}
}
}
/**
* Retrieves the current login status.
*
* @return Login status of session.
*/
public TransportLoginStatus getLoginStatus() {
return loginStatus;
}
public ConnectionFailureReason getFailureStatus() {
return failureStatus;
}
public void setFailureStatus(ConnectionFailureReason failureStatus) {
this.failureStatus = failureStatus;
}
/**
* Returns true only if we are completely logged in.
*
* @return True or false whether we are currently completely logged in.
*/
public Boolean isLoggedIn() {
return (loginStatus == TransportLoginStatus.LOGGED_IN);
}
/**
* Should be called when a session has been disconnected.
*
* This can be anything from a standard logout to a forced disconnect from the server.
*
* @param errorMessage Error message to send, or null if no message. (only sent on full disconnect)
*/
public void sessionDisconnected(String errorMessage) {
reconnectionAttempts++;
if (getRegistrationPacket() != null || !JiveGlobals.getBooleanProperty("plugin.gateway."+getTransport().getType()+"reconnect", true) || (reconnectionAttempts > JiveGlobals.getIntProperty("plugin.gateway."+getTransport().getType()+"reconnectattempts", 3))) {
sessionDisconnectedNoReconnect(errorMessage);
}
else {
cleanUp();
Log.debug("Session "+getJID()+" disconnected from "+getTransport().getJID()+". Reconnecting... (attempt "+reconnectionAttempts+")");
setLoginStatus(TransportLoginStatus.RECONNECTING);
ClientSession session = XMPPServer.getInstance().getSessionManager().getSession(getJIDWithHighestPriority());
if (session != null) {
logIn(getTransport().getPresenceType(session.getPresence()), null);
}
else {
sessionDisconnectedNoReconnect(errorMessage);
}
}
}
/**
* Should be called when a session has been disconnected but no reconnect attempt should be made.
*
* It is also called internally by sessionDisconnected to handle total failed attempt.
*
* @param errorMessage Error message to send, or null if no message.
*/
public void sessionDisconnectedNoReconnect(String errorMessage) {
Log.debug("Disconnecting session "+getJID()+" from "+getTransport().getJID());
try {
cleanUp();
}
catch (Exception e) {
Log.info("sessionDisconnectedNoReconnect: Error="+ e);
}
setLoginStatus(TransportLoginStatus.LOGGED_OUT);
if (getRegistrationPacket() != null) {
new RegistrationHandler(getTransport()).completeRegistration(this);
}
else {
Presence p = new Presence(Presence.Type.unavailable);
p.setTo(getJID());
p.setFrom(getTransport().getJID());
getTransport().sendPacket(p);
if (errorMessage != null) {
getTransport().sendMessage(
getJIDWithHighestPriority(),
getTransport().getJID(),
errorMessage,
Message.Type.error
);
}
getBuddyManager().sendOfflineForAllAvailablePresences(getJID());
}
buddyManager.resetBuddies();
getTransport().getSessionManager().removeSession(getJID());
}
/**
* 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)) {
Presence p = new Presence();
p.setTo(getJID());
p.setFrom(getTransport().getJID());
getTransport().setUpPresencePacket(p, newpresence);
if (!verboseStatus.equals("")) {
p.setStatus(verboseStatus);
}
getTransport().sendPacket(p);
}
presence = newpresence;
}
/**
* 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(getJID());
p.setFrom(getTransport().getJID());
getTransport().setUpPresencePacket(p, presence);
if (!newstatus.equals("")) {
p.setStatus(newstatus);
}
getTransport().sendPacket(p);
}
verboseStatus = newstatus;
}
/**
* 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) || !verboseStatus.equals(newstatus)) {
Presence p = new Presence();
p.setTo(getJID());
p.setFrom(getTransport().getJID());
getTransport().setUpPresencePacket(p, newpresence);
if (!newstatus.equals("")) {
p.setStatus(newstatus);
}
getTransport().sendPacket(p);
}
presence = newpresence;
verboseStatus = newstatus;
}
/**
* Sets a pending presence and verbose status that will trigger when login status is LOGGED_IN.
*
* @param newpresence New presence to set to.
* @param newstatus New verbose status.
*/
public void setPendingPresenceAndStatus(PresenceType newpresence, String newstatus) {
if (newpresence == null) {
newpresence = PresenceType.unknown;
}
if (newstatus == null) {
newstatus = "";
}
if (newpresence.equals(PresenceType.unavailable)) {
newstatus = "";
}
pendingPresence = newpresence;
pendingVerboseStatus = newstatus;
}
/**
* Sends the current presence to the session user.
*
* @param to JID to send presence updates to.
*/
public void sendPresence(JID to) {
Presence p = new Presence();
p.setTo(to);
p.setFrom(getTransport().getJID());
getTransport().setUpPresencePacket(p, presence);
p.setStatus(verboseStatus);
getTransport().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);
}
}
/**
* Retrieves the avatar for this session.
*
* @return Avatar instance associated with the JID of this session.
*/
public Avatar getAvatar() {
return avatar;
}
/**
* Sets the avatar associated with this session.
*
* @param avatar instance to associate with this session.
*/
public void setAvatar(Avatar avatar) {
this.avatar = avatar;
}
/**
* Loads an avatar if one is available.
*
* Pulls from cache. If nothing is in cache, we attempt to check their vcard info.
*/
private void loadAvatar() {
if (JiveGlobals.getBooleanProperty("plugin.gateway."+getTransport().getType()+".avatars", true)) {
try {
this.avatar = new Avatar(jid);
}
catch (NotFoundException e) {
Element vcardElem = VCardManager.getInstance().getVCard(jid.getNode());
if (vcardElem != null) {
Element photoElem = vcardElem.element("PHOTO");
if (photoElem != null) {
Element typeElem = photoElem.element("TYPE");
Element binElem = photoElem.element("BINVAL");
if (typeElem != null && binElem != null) {
byte[] imageData = Base64.decode(binElem.getText());
this.avatar = new Avatar(jid, imageData);
}
}
}
}
}
}
/**
* Provides a "neat" string representation of the session.
*/
@Override
public String toString() {
return "TransportSession["+getJID()+"]";
}
/**
* Updates status on legacy service.
*
* @param presenceType Type of presence.
* @param verboseStatus Longer status description.
*/
public abstract void updateStatus(PresenceType presenceType, String verboseStatus);
/**
* Adds a legacy contact to the legacy service.
*
* @param jid JID associated with the legacy contact.
* @param nickname Nickname associated with the legacy contact.
* @param groups Groups associated with the legacy contact.
*/
public abstract void addContact(JID jid, String nickname, ArrayList<String> groups);
/**
* Removes a legacy contact from the legacy service.
*
* @param contact Transport buddy item associated with the legacy contact.
*/
public abstract void removeContact(B contact);
/**
* Updates a legacy contact on the legacy service.
*
* @param contact Transport buddy item associated with the legacy contact.
*/
public abstract void updateContact(B contact);
/**
* Accept a legacy contact's add friend request.
*
* @param jid JID associated with the target contact.
*/
public abstract void acceptAddContact(JID jid);
/**
* Sends an outgoing message through the legacy service.
*
* @param jid JID associated with the target contact.
* @param message Message to be sent.
*/
public abstract void sendMessage(JID jid, String message);
/**
* Sends a chat state message through the legacy service.
*
* Not all chat states have to be handled. Note that composing message event
* is sent through this as well. (XEP-0022) Primarily this is used with XEP-0085.
*
* @param jid JID associated with the target contact.
* @param chatState Chat state to be reflected in the legacy service.
*/
public abstract void sendChatState(JID jid, ChatStateType chatState);
/**
* Sends a buzz notification through the legacy service.
*
* If the legacy service does not support this, ignore it. Though sometimes a message
* might be included and you may want to handle that in some sort of special way.
*
* @param jid JID associated with the target contact.
* @param message Message tied to the buzz.
*/
public abstract void sendBuzzNotification(JID jid, String message);
/**
* Updates the session's avatar on the legacy service.
*
* If the legacy service does not support this, ignore it.
*
* @param type Mime type of image.
* @param data Binary data (byte array) of image.
*/
public abstract void updateLegacyAvatar(String type, byte[] data);
/**
* Should be called when the service is to be logged into.
*
* This is expected to check for current logged in status and log in if appropriate.
*
* @param presenceType Initial status (away, available, etc) to be set upon logging in.
* @param verboseStatus Descriptive status to be set upon logging in.
*/
public abstract void logIn(PresenceType presenceType, String verboseStatus);
/**
* Should be called when the service is to be disconnected from.
*
* This is expected to check for current logged in status and log out if appropriate.
*/
public abstract void logOut();
/**
* Clean up session pieces for either a log out or in preparation for a reconnection.
*/
public abstract void cleanUp();
/**
* Retrieves a list of rooms (MUCTransportRoom) that are on the server the session is attached to.
*
* Because of the nature of the query, the legacy service is expected to send a response itself,
* instead of returning a list of information. The sendRooms command in BaseMUCTransport exists to
* facilitate this easily.
*
* This will never get called unless MUC support is implemented and is optional for non-MUC transports.
*/
public void getRooms() {
getTransport().getMUCTransport().cancelPendingRequest(getJID(), getTransport().getMUCTransport().getJID(), NameSpace.DISCO_ITEMS);
}
/**
* Retrieves information about a specific room (MUCTransportRoom).
*
* Because of the nature of the query, the legacy service is expected to send a response itself,
* instead of returning a list of information. The sendRoomInfo command in BaseMUCTransport exists to
* facilitate this easily.
*
* Override this if you support it.
*
* @param room Room to get information about.
*/
public void getRoomInfo(String room) {
getTransport().getMUCTransport().cancelPendingRequest(getJID(), getTransport().getMUCTransport().convertIDToJID(room, null), NameSpace.DISCO_INFO);
}
/**
* Retrieves members of a specific room (MUCTransportRoom).
*
* Because of the nature of the query, the legacy service is expected to send a response itself,
* instead of returning a list of information. The sendRoomMembers command in BaseMUCTransport exists to
* facilitate this easily.
*
* Override this if you support it.
*
* @param room Room to get members of.
*/
public void getRoomMembers(String room) {
getTransport().getMUCTransport().cancelPendingRequest(getJID(), getTransport().getMUCTransport().convertIDToJID(room, null), NameSpace.DISCO_ITEMS);
}
}