/**
* OLAT - Online Learning and Training<br />
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br />
* you may not use this file except in compliance with the License.<br />
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br />
* software distributed under the License is distributed on an "AS IS" BASIS,
* <br />
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br />
* See the License for the specific language governing permissions and <br />
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br />
* University of Zurich, Switzerland.
* <p>
*/
package org.olat.instantMessaging;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.RosterGroup;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.RosterPacket;
import org.olat.basesecurity.ManagerFactory;
import org.olat.core.commons.persistence.async.TaskExecutorThread;
import org.olat.core.commons.taskExecutor.TaskExecutorManager;
import org.olat.core.id.Identity;
import org.olat.core.id.UserConstants;
import org.olat.core.logging.Tracing;
import org.olat.instantMessaging.groupchat.GroupChatManagerController;
import org.olat.instantMessaging.rosterandchat.ConnectToServerTask;
/**
* Description: <br />
* Instant Messaging Client class based on the open source library SMACK
* (www.igniterealtime.org). It provides connection to the IM-server, sends
* presence packets, listens to messages and subscription requests. Methods that
* mention the velocity rendering stuff are uses by template files that render
* the html-response that for they dont have references in the java source code.
* <P>
*
* @version Initial Date: 14.10.2004
* @author Guido Schnider
*/
public class InstantMessagingClient {
// username is olat global unique username
private final String username;
// password is auto generated and only used for instant messaging
private final String password;
protected XMPPConnection connection = null;
private long connectionTimestamp = 0;
private Roster roster = null;
// null means 'not connected'
private Presence.Mode presenceMode = null;
private String statusMsg = " ";
// the jabber id like username@olat.ch
private final String jabberServer;
protected List subscribedUsers = new ArrayList();
private static final String OLATBUDDIES = "OLAT-Buddies";
protected boolean collaborationDisabled = false;
protected boolean isConnected;
private boolean showOfflineBuddies = false;
private boolean showGroupsInRoster = false;
private String recentStatusMode = Presence.Mode.available.toString();
private GroupChatManagerController groupChatManagerCtrl;
private String defaultRosterStatus;
/**
* @param username
* @param password
*/
protected InstantMessagingClient(String username, String password) {
this.username = username;
this.password = password;
jabberServer = InstantMessagingModule.getAdapter().getConfig().getServername();
Identity ident = ManagerFactory.getManager().findIdentityByName(username);
ImPreferences prefs = ImPrefsManager.getInstance().loadOrCreatePropertiesFor(ident);
this.defaultRosterStatus = prefs.getRosterDefaultStatus();
reconnect(true);
}
/**
* if connections fails upon server crash or other incidents, a new Thread
* gets created and tries to reconnect after a waiting period
*
* @param tryImmediately
*/
protected void reconnect(boolean tryImmediately) {
if (tryImmediately) {
doConnect();
}
}
/**
* connect to the JabberServer
*/
protected void doConnect() {
// Debug enables a java window with all traffic between client
// and server.
XMPPConnection.DEBUG_ENABLED = false;
// check if currently used im client is already connected
// (reusing clients from previous session)
connection = new XMPPConnection(InstantMessagingModule.getConnectionConfiguration());
// connecting to server is done by thread pool
if (Tracing.isDebugEnabled(this.getClass())) {
Tracing.logDebug("Connecting to server by thread pool with user: "+username, this.getClass());
}
TaskExecutorManager.getInstance().runTask(new ConnectToServerTask(this));
connectionTimestamp = System.currentTimeMillis();
}
/**
* change jabber status. Example: sendPresencePacket(Presence.Type.AVAILABLE,
* "at meeting...", 1, Presence.Mode.AWAY);
*
* @param type
* @param status
* @param priority
* @param mode
*/
public void sendPresence(Presence.Type type, String status, int priority, Presence.Mode mode) {
// get rid of "&" because they break xml packages!
if (status != null) status = status.replaceAll("&", "&");
if (connection == null || !connection.isConnected()) return;
if (collaborationDisabled) return;
setStatus(mode);
Presence presence = new Presence(type);
if (status == null) {
if (mode == Presence.Mode.available) {
status = InstantMessagingConstants.PRESENCE_MODE_AVAILABLE;
} else if (mode == Presence.Mode.away) {
status = InstantMessagingConstants.PRESENCE_MODE_AWAY;
} else if (mode == Presence.Mode.chat) {
status = InstantMessagingConstants.PRESENCE_MODE_CHAT;
} else if (mode == Presence.Mode.dnd) {
status = InstantMessagingConstants.PRESENCE_MODE_DND;
} else if (mode == Presence.Mode.xa) {
status = InstantMessagingConstants.PRESENCE_MODE_XAWAY;
}
presence.setStatus(status);
} else {
presence.setStatus(status);
}
setStatusMsg(presence.getStatus());
//setting prio when type == unavailable causes error on IM server
if (presence.getType() == Presence.Type.available) presence.setPriority(priority);
if (mode != null) presence.setMode(mode);
try {
connection.sendPacket(presence);
} catch (RuntimeException ex) {
Tracing.logWarn("Error while trying to send Instant Messaging packet for user: " + username + " .Errormessage: ", ex,
InstantMessagingClient.class);
}
}
/**
* send a presence packet "available" with a certain mode e.g. "away" to all
* buddies
*
* @param mode
*/
public void sendPresenceAvailable(Presence.Mode mode) {
sendPresence(Presence.Type.available, null, 0, mode);
}
/**
* send a presence packet "unavailable" to all buddies
*/
public void sendPresenceUnavailable() {
sendPresence(Presence.Type.unavailable, null, 0, null);
}
/**
* By adding this method (right now added to the constructor) we do have auto
* subscription. All subscribe packets get automatically answered by a
* subscribed packet.
*/
public void addSubscriptionListener() {
PacketFilter filter = new PacketTypeFilter(Presence.class);
connection.createPacketCollector(filter);
PacketListener myListener = new PacketListener() {
public void processPacket(Packet packet) {
Presence presence = (Presence) packet;
if (presence.getType() == Presence.Type.subscribe) {
Presence response = new Presence(Presence.Type.subscribe);
response.setTo(presence.getFrom());
// System.out.println("subscribed to: "+presence.getFrom());
connection.sendPacket(response);
// ask also for subscription
if (!subscribedUsers.contains(presence.getFrom())) {
response = null;
response = new Presence(Presence.Type.subscribe);
response.setTo(presence.getFrom());
connection.sendPacket(response);
// update the roster with the new user
RosterPacket rosterPacket = new RosterPacket();
rosterPacket.setType(IQ.Type.SET);
RosterPacket.Item item = new RosterPacket.Item(presence.getFrom(), parseName(presence.getFrom()));
item.addGroupName(OLATBUDDIES);
item.setItemType(RosterPacket.ItemType.both);
// item.setItemStatus(RosterPacket.ItemStatus.fromString());
rosterPacket.addRosterItem(item);
connection.sendPacket(rosterPacket);
}
}
if (presence.getType() == Presence.Type.subscribe) {
subscribedUsers.add(presence.getFrom());
}
}
};
connection.addPacketListener(myListener, filter);
}
/**
* For unsubscription we have to create a packet like: <iq type="set"
* id="ab7ba" > <query xmlns="jabber:iq:roster"> <item subscription="remove"
* jid="guido@localhost" /> </query> </iq>
*
* @param uname a valid username
*/
protected void removeSubscription(String uname) {
RosterPacket rosterPacket = new RosterPacket();
rosterPacket.setType(IQ.Type.SET);
RosterPacket.Item item = new RosterPacket.Item(uname + "@" + jabberServer, uname);
item.setItemType(RosterPacket.ItemType.remove);
rosterPacket.addRosterItem(item);
try {
connection.sendPacket(rosterPacket);
} catch (RuntimeException e) {
Tracing.logWarn("Error while trying to send Instant Messaging packet.", e, InstantMessagingClient.class);
}
}
/**
* @return Returns the jid.
*/
public String getJid() {
String jid = getChatUsername() + "@" + jabberServer;
return jid;
}
/**
* @return Returns the status.
*/
protected String getStatus() {
if (presenceMode != null) { return presenceMode.toString(); }
return Presence.Type.unavailable.toString();
}
public Presence.Mode getPresenceMode() {
return presenceMode;
}
/**
* The status to set.
*
* @param status
*/
protected void setStatus(Presence.Mode status) {
this.presenceMode = status;
}
/**
* @return Returns the roster.
*/
public Roster getRoster() {
return roster;
}
/**
* @param roster
*/
public void setRoster(Roster roster) {
this.roster = roster;
}
/**
* Close the connection to the server
*/
public void closeConnection(boolean closeSynchronously) {
// Set isConnected to false first since connection.close triggers an
// XMPPConnListener.connectionClosed() event which would result in
// in a cyclic call of this close method.
isConnected = false;
final XMPPConnection connectionToClose = connection;
Runnable connectionCloseRunnable = new Runnable() {
public void run() {
try {
connectionToClose.disconnect();
} catch (RuntimeException e) {
Tracing.logWarn("Error while trying to close instant messaging connection", e, InstantMessagingClient.class);
}
}
};
if (closeSynchronously) {
connectionCloseRunnable.run();
connection = null;
} else {
connection = null;
TaskExecutorManager.getInstance().runTask(connectionCloseRunnable);
}
}
/**
* Ask an other online user to subscribe to their roster
*
* @param uname
*/
protected void subscribeToUser(String uname) {
Presence presence = new Presence(Presence.Type.subscribe);
presence.setTo(uname + "@" + jabberServer);
try {
connection.sendPacket(presence);
} catch (RuntimeException e) {
Tracing.logWarn("Error while trying to send Instant Messaging packet.", e, InstantMessagingClient.class);
}
}
/**
* Sends a subscription request to the username answers are handled by the
* method
*
* @see org.olat.instantMessaging.InstantMessagingClient#addSubscriptionListener()
* @param uname
* @param groupname
*/
protected void subscribeToUser(String uname, String groupname) {
Presence presence = new Presence(Presence.Type.subscribe);
presence.setTo(uname + "@" + jabberServer);
try {
connection.sendPacket(presence);
RosterPacket rosterPacket = new RosterPacket();
rosterPacket.setType(IQ.Type.SET);
RosterPacket.Item item = new RosterPacket.Item(uname + "@" + jabberServer, uname);
item.addGroupName(groupname);
item.setItemType(RosterPacket.ItemType.both);
rosterPacket.addRosterItem(item);
connection.sendPacket(rosterPacket);
} catch (RuntimeException e) {
Tracing.logWarn("Error while trying to send Instant Messaging packet.", e, InstantMessagingClient.class);
}
}
/**
* @param xmppAddress jabber jid like guido@swissjabber.org
* @return returns just the name "guido" without the rest
*/
protected String parseName(String xmppAddress) {
if (xmppAddress == null) return null;
int atIndex = xmppAddress.indexOf("@");
if (atIndex <= 0) return "";
return xmppAddress.substring(0, atIndex);
}
/**
* @param xmppAddressWithRessource like guido@swissjabber.org/office
* @return treurns the jid without the ressource like guido@swissjabber.org
*/
protected String parseJid(String xmppAddressWithRessource) {
if (xmppAddressWithRessource == null) return null;
int atIndex = xmppAddressWithRessource.indexOf("/");
if (atIndex <= 0) {
// if no "/" is found we pass back the full adress
return xmppAddressWithRessource;
}
return xmppAddressWithRessource.substring(0, atIndex);
}
/**
* When doing a test, collaboration by IM should be disabled
*
* @param reason The reason why this user is not allowd to chat e.g. doing
* test By setting the third param to 10 all messages should be send
* to this client even if other clients are up. So we have full
* control over them.
*/
protected void disableCollaboration(String reason) {
recentStatusMode = this.getStatus();
sendPresence(Presence.Type.unavailable, reason, 10, Presence.Mode.dnd);
//trigger an event on the im controller, to update the status icon
InstantMessagingModule.getAdapter().getClientManager().sendPresenceEvent(Presence.Type.available, username);
collaborationDisabled = true;
}
/**
* enable collaboration
*/
protected void enableCollaboration() {
collaborationDisabled = false;
if (recentStatusMode.equals(Presence.Type.unavailable.toString())) {
sendPresence(Presence.Type.unavailable, null, 0, null);
InstantMessagingModule.getAdapter().getClientManager().sendPresenceEvent(Presence.Type.unavailable, username);
}
else {
// Set back the priority to normal level.
sendPresence(Presence.Type.available, null, 0, Presence.Mode.valueOf(recentStatusMode));
//trigger an event on the im controller, to update the status icon
InstantMessagingModule.getAdapter().getClientManager().sendPresenceEvent(Presence.Type.available, username);
}
}
public boolean isChatDisabled() {
return collaborationDisabled;
}
public String getRecentPresenceStatusMode(){
return recentStatusMode;
}
/**
* Used by Velocity renderer
*
* @return a String representing the online buddies out of the number of total
* buddies
*/
public String buddyCountOnline() {
int onlineBuddyEntries = connection.getRoster().getEntryCount();
int allBuddies = onlineBuddyEntries;
for (Iterator l = connection.getRoster().getEntries().iterator(); l.hasNext();) {
RosterEntry entry = (RosterEntry) l.next();
Presence presence = connection.getRoster().getPresence(entry.getUser());
if (presence.getType() == Presence.Type.unavailable) onlineBuddyEntries--;
}
// final string looks like e.g. "(3/5)"
StringBuilder sb = new StringBuilder(10);
sb.append("(");
sb.append(onlineBuddyEntries);
sb.append("/");
sb.append(allBuddies);
sb.append(")");
return sb.toString();
}
/**
* Used by Velocity renderer
*
* @param groupname
* @return a String representing the online buddies out of the number of total
* buddies for a single group like (3/5)
*/
protected String buddyCountOnlineForGroup(String groupname) {
RosterGroup rosterGroup = connection.getRoster().getGroup(groupname);
int buddyEntries = rosterGroup.getEntryCount();
int allBuddies = buddyEntries;
for (Iterator I = rosterGroup.getEntries().iterator(); I.hasNext();) {
RosterEntry entry = (RosterEntry) I.next();
Presence presence = connection.getRoster().getPresence(entry.getUser());
if (presence.getType() == Presence.Type.unavailable) buddyEntries--;
}
// final string looks like e.g. "(3/5)"
StringBuilder sb = new StringBuilder(10);
sb.append("(");
sb.append(buddyEntries);
sb.append("/");
sb.append(allBuddies);
sb.append(")");
return sb.toString();
}
/**
* Used by Velocity renderer
*
* @param jid
* @return get a presence for a specific user
*/
protected String getUserPresence(String jid) {
Presence presence = connection.getRoster().getPresence(jid);
String imageName = "offline"; // default
// mode == null is equals available!!
if (presence.getMode() == null && presence.getType() == Presence.Type.available) imageName = Presence.Mode.available
.toString();
if (presence.getMode() != null) imageName = presence.getMode().toString();
return imageName;
}
/**
* @return Returns the password.
*/
public String getPassword() {
return this.password;
}
/**
* @return Returns the username.
*/
public String getUsername() {
return this.username;
}
/**
*
* @return
*/
public String getFullName() {
Identity identity = ManagerFactory.getManager().findIdentityByName(username);
return identity != null ? identity.getUser().getProperty(UserConstants.FIRSTNAME, null) + " " + identity.getUser().getProperty(UserConstants.LASTNAME, null) : "" ;
}
/**
*
* @return
*/
public String getEmail() {
Identity identity = ManagerFactory.getManager().findIdentityByName(username);
return identity != null ? identity.getUser().getProperty(UserConstants.EMAIL, null): "" ;
}
/**
* in case multiple olat instances use one single jabber server, username needs to be distinguished by instance id
* @return returns the chat-username used on jabber server
*/
public String getChatUsername() {
return InstantMessagingModule.getAdapter().getIMUsername(username);
}
/**
* @return Returns true when user is connected to server
*/
public boolean isConnected() {
return isConnected;
}
public void setIsConnected(boolean isConnected) {
this.isConnected = isConnected;
}
/**
* TODO:gs used by velocity, change connected client list to client helper
* object
*
* @return Returns the statusMsg.
*/
protected String getStatusMsg() {
return statusMsg;
}
/**
* Free text to add more details to a specific status like status "away" and
* msg "eating lunch until 2pm..."
*
* @param statusMsg The statusMsg to set.
*/
protected void setStatusMsg(String statusMsg) {
if (statusMsg == null) statusMsg = "";
this.statusMsg = statusMsg;
}
/**
* @return Returns the showOfflineBuddies.
*/
protected boolean getShowOfflineBuddies() {
return showOfflineBuddies;
}
/**
* @param showOfflineBuddies The showOfflineBuddies to set.
*/
protected void setShowOfflineBuddies(boolean showOfflineBuddies) {
this.showOfflineBuddies = showOfflineBuddies;
}
/**
* TODO:gs used by velocity, change connected client list to client helper
* object
*
* @return minutes the user is online
*/
protected String getOnlineTime() {
long diff = System.currentTimeMillis() - connectionTimestamp;
return new Integer((int) diff / 1000 / 60).toString();
}
/**
* @return Returns the showGroupsInRoster.
*/
protected boolean isShowGroupsInRoster() {
return showGroupsInRoster;
}
/**
* @param showGroupsInRoster The showGroupsInRoster to set.
*/
protected void setShowGroupsInRoster(boolean showGroupsInRoster) {
this.showGroupsInRoster = showGroupsInRoster;
}
public XMPPConnection getConnection() {
return connection;
}
public String getServerName() {
return jabberServer;
}
public void sendPresenceAutoStatusIdle() {
recentStatusMode = getPresenceMode().toString();
//TODO:gs:a translate
sendPresence(Presence.Type.available, "auto away", 0, Presence.Mode.away);
}
/**
* set the per user instance of the group chat manager
* @param groupChatManagerController
*/
public void setGroupChatManager(GroupChatManagerController groupChatManagerCtrl) {
this.groupChatManagerCtrl = groupChatManagerCtrl;
}
/**
* access the per user instance of the group chat manager
* @return
*/
public GroupChatManagerController getGroupChatManagerController() {
return this.groupChatManagerCtrl;
}
/**
*
* @return status from IM preferences or default wich is "available"
*/
public String getDefaultRosterStatus() {
return defaultRosterStatus;
}
}