/** * OLAT - Online Learning and Training
* http://www.olat.org *

* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License. *

* Copyright (c) 1999-2006 at Multimedia- & E-Learning Services (MELS),
* University of Zurich, Switzerland. *

*/ package org.olat.instantMessaging; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; 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.XMPPException; import org.jivesoftware.smack.Roster.SubscriptionMode; import org.jivesoftware.smack.filter.PacketFilter; import org.jivesoftware.smack.filter.PacketTypeFilter; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.RosterPacket; import org.olat.core.logging.Tracing; /** * Description:
Instant Messaging Client class based on the open source * library SMACK (www.jivesoftware.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 java source code.

* * @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 = " "; // messages are stored if more the one arrives protected List messages = new ArrayList(); // 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 flashClientIsRunning = false; private boolean showOfflineBuddies = false; private boolean showGroupsInRoster = true; private String recentStatusMode = Presence.Mode.available.toString(); /** * @param username * @param password */ protected InstantMessagingClient(String username, String password) { this.username = username; this.password = password; jabberServer = InstantMessagingModule.getServername(); 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() { long initTime = System.currentTimeMillis(); try { // 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()); connection.login(username, password); addMessageListener(); addSubscriptionListener(); addPresenceListener(); isConnected = connection.isConnected(); if (isConnected) { connectionTimestamp = System.currentTimeMillis(); connection.addConnectionListener(new XMPPConnListener(this)); roster = connection.getRoster(); // subscription accept all = 0 roster.setSubscriptionMode(SubscriptionMode.accept_all); String defaultStatus = InstantMessagingModule.getDefaultRosterStatus(username); if (defaultStatus.equals(Presence.Type.unavailable.toString())) sendPresence(Presence.Type.unavailable, null, 0, null); else sendPresence(Presence.Type.available, null, 0, Presence.Mode.valueOf(defaultStatus)); } else { Tracing.logWarn("Error while trying to connect to Instant Messaging server (username, server): " + username + ", " + jabberServer + " After login connection.isConnected() returned false ",null, InstantMessagingClient.class); } } catch (XMPPException e) { isConnected = false; Tracing.logWarn("Error while trying to connect to Instant Messaging server (username, server): " + username + ", " + jabberServer,e,InstantMessagingClient.class); // reconnect(false); } catch (Exception e) { // also catch java.lang.IllegalStateException: Not // connected to server. -> at // org.jivesoftware.smack.XMPPConnection.addPacketListener(XMPPConnection.java:581) // and so on isConnected = false; Tracing.logWarn("Error while trying to connect to Instant Messaging server (username, server): " + username + ", " + jabberServer,e,InstantMessagingClient.class); } if (Tracing.isDebugEnabled(InstantMessagingClient.class)){ Tracing.logDebug("Connecting to IM server and fetching roster took (milliseconds): "+ (System.currentTimeMillis() - initTime),InstantMessagingClient.class); } } /** * change jabber status. Example: sendPresencePacket(Presence.Type.AVAILABLE, * "at meeting...", 1, Presence.Mode.AWAY); * * @param type * @param status * @param priority * @param mode */ protected void sendPresence(Presence.Type type, String status, int priority, Presence.Mode mode) { // get rif 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()); 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); } } /** * while listening to presence packets we are able to determine wether the * flash client in the second browser window is running or not. Like this we * get a clear event even when the user closes the window. */ private void addPresenceListener() { PacketFilter filter = new PacketTypeFilter(Presence.class); connection.createPacketCollector(filter); PacketListener myListener = new PacketListener() { public void processPacket(Packet packet) { Presence presence = (Presence) packet; // xiff is the resource string that is hardcoded in the xiff // library. // if we receive a packet with type "unavailable" and resource // string // "xiff" the user closed // the flash client. if (presence.getType() == Presence.Type.unavailable && presence.getFrom().startsWith(getUsername()) && presence.getFrom().endsWith("xiff")) setFlashClientIsRunning(false); if (presence.getType() == Presence.Type.available && presence.getFrom().startsWith(getUsername()) && presence.getFrom().endsWith("xiff")) { setFlashClientIsRunning(true); // TODO:gs presence vom flash client wird nur übermittelt // beim start und beim stop des // clients, die presence während dem laufen werden nicht // übermittelt, Frage??? standardconform??? // setStatus(presence.getMode()); } } }; connection.addPacketListener(myListener, filter); } /** * Each client listens to it's own messages */ private void addMessageListener() { PacketFilter filter = new PacketTypeFilter(Message.class); connection.createPacketCollector(filter); PacketListener myListener = new PacketListener() { public void processPacket(Packet packet) { Message jabbmessage = (Message) packet; jabbmessage.setProperty("receiveTime", new Long(new Date().getTime())); if ((jabbmessage.getType() == Message.Type.CHAT || jabbmessage.getType() == Message.Type.NORMAL) && jabbmessage.getBody() != null) { synchronized (messages) { messages.add(jabbmessage); } } } }; connection.addPacketListener(myListener, filter); } /** * By adding this methode (right now added to the contructor) we do have auto * subscription. All subscribe packets get automatically answered by a * subscribed packet. */ private 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: * * @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 message */ protected Message getMessage() { if (!collaborationDisabled) { synchronized (messages) { if (this.messages.size() > 0) { return (Message) this.messages.get(0); } } } return null; } protected List getMessagesByUsername(String usernameFromMsg) { List messagesByUsername = new ArrayList(); synchronized (messages) { for (Iterator i = messages.iterator(); i.hasNext();) { Message message = (Message) i.next(); if (message.getFrom().equals(usernameFromMsg)) messagesByUsername.add(message); } } return messagesByUsername; } /** * reset the message object users do this after reading a message in olat */ protected void resetMessage() { synchronized (messages) { if (this.messages.size() > 0) this.messages.remove(0); } } protected void resetMessagesByUsername(String usernameFromMsg) { List newMessages = new ArrayList(); synchronized (messages) { for (Iterator i = messages.iterator(); i.hasNext();) { Message message = (Message) i.next(); if (!message.getFrom().equals(usernameFromMsg)) newMessages.add(message); } } messages = newMessages; } /** * @return Returns the jid. */ protected String getJid() { String jid = this.username + "@" + jabberServer; return jid; } /** * @return Returns the status. */ protected String getStatus() { if (presenceMode != null) { return presenceMode.toString(); } return Presence.Mode.available.toString(); } /** * The status to set. * * @param status */ protected void setStatus(Presence.Mode status) { this.presenceMode = status; } /** * Used for communication with the flash jabber client. * * @param chatpartner * @param firstMessage */ protected void sendChatParamsToFlashClient(String chatpartner, String firstMessage) { StringBuffer out = new StringBuffer(); out.append("chatpartner:"); out.append(chatpartner); out.append(","); out.append("message:" + firstMessage); sendCustomMessagePacket(out.toString()); } /** * Used for communication with the flash jabber client. * * @param roomName */ protected void sendConferenceParamsToFlashClient(String roomName) { // String roomname = "chatroom:"+roomName; // sendCustomMessagePacket(roomname); StringBuffer out = new StringBuffer(); out.append("chatroom:"); out.append(roomName); out.append(","); out.append("commonRoomName:"); out.append(ConferenceRoomManager.getInstance().getRoomName(roomName)); sendCustomMessagePacket(out.toString()); } /** * Sends a message type="headline" to the flash client * * @param msgBody of the custom message */ protected void sendCustomMessagePacket(String msgBody) { // Chat chat = // connection.createChat(olatUsername+"@"+JABBERHOST+"/xiff"); // Message customMsg = chat.createMessage(); Message customMsg = new Message(); // custom messages are sent only to our flash client with ressource // /xiff customMsg.setTo(username + "@" + jabberServer + "/xiff"); customMsg.setType(Message.Type.HEADLINE); msgBody = msgBody.replaceAll("&", ""); msgBody = msgBody.replaceAll("&;", ""); customMsg.setBody(msgBody); try { connection.sendPacket(customMsg); } catch (RuntimeException e) { Tracing.logWarn("Error while trying to send Instant Messaging packet.", e, InstantMessagingClient.class); } } /** * @return Returns the roster. */ protected Roster getRoster() { return roster; } /** * Close the connection to the server */ protected void closeConnection() { // 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; try { connection.close(); connection = null; } catch (RuntimeException e) { Tracing.logWarn("Error while trying to send Instant Messaging packet.",e,InstantMessagingClient.class); } } /** * 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.available, reason, 10, Presence.Mode.dnd); collaborationDisabled = true; } /** * enable collaboration */ protected void enableCollaboration() { collaborationDisabled = false; // Set back the priority to normal level. Used for disabling // collabration if(recentStatusMode.equals(Presence.Type.unavailable.toString())) //TODO:gs:b send back to unavailable if it had this status befor sendPresence(Presence.Type.unavailable, null, 0, Presence.Mode.dnd); else sendPresence(Presence.Type.available, null, 0, Presence.Mode.valueOf(recentStatusMode)); } /** * Used by Velocity renderer indicates wheater the user has a new Message or * not * * @return true if user has a message */ protected boolean hasMessage() { if (collaborationDisabled) return false; if (getMessage() != null) return true; return false; } /** * @return true if user has more messages in the queue */ protected boolean hasMessages() { if (collaborationDisabled) return false; synchronized (messages) { if (messages.size() > 1) return true; } return false; } /** * @param usernameFromMsg * @return true if user has received more then one message from the same user */ protected boolean hasSeveralMessagesOfSameUser(String usernameFromMsg) { synchronized (messages) { if (messages.size() > 0) { Message message; List fromAdresses = new ArrayList(); for (Iterator i = messages.iterator(); i.hasNext();) { message = (Message) i.next(); fromAdresses.add(message.getFrom()); } if (fromAdresses.indexOf(usernameFromMsg) != fromAdresses.lastIndexOf(usernameFromMsg)) { return true; } } return false; } } /** * Used by Velocity renderer * * @return a String representing the online buddies out of the number of total * buddies */ protected 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 == null) onlineBuddyEntries--; } // final string looks like e.g. "(3/5)" StringBuffer sb = new StringBuffer(); 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 == null) buddyEntries--; } // final string looks like e.g. "(3/5)" StringBuffer sb = new StringBuffer(); 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"; if (presence != null) imageName = presence.getMode().toString(); return imageName; } /** * Used by Velocity renderer Inside the layers we can only use html * linebreaks. * * @param msgBody A message that gets prepared for viewing in a HTML layer * @return the message body without newline and quotes */ protected String getMessageFormatted(String msgBody) { String clean = ""; if (msgBody != null) { String patternNewline = "\n"; String patternCrNewline = "\r\n"; String patternQuote = "'"; String patternDQuote = "\""; Pattern r = Pattern.compile(patternNewline); Matcher m = r.matcher(msgBody); clean = m.replaceAll("
"); r = Pattern.compile(patternCrNewline); m = r.matcher(clean); clean = m.replaceAll("
"); r = Pattern.compile(patternQuote); m = r.matcher(clean); clean = m.replaceAll(" "); r = Pattern.compile(patternDQuote); m = r.matcher(clean); clean = m.replaceAll(""); } return clean; } /** * @return Returns the password. */ protected String getPassword() { return this.password; } /** * @return Returns the username. */ protected String getUsername() { return this.username; } /** * @return Returns true when user is connected to server */ protected boolean isConnected() { return isConnected; } protected void setIsConnected(boolean isConnected) { this.isConnected = isConnected; } /** * @return true if flash client is running otherwise false */ protected boolean getFlashClientIsRunning() { return this.flashClientIsRunning; } /** * When the user opens a second window with the flash client we set this true * * @param isRunning Used by Velocity renderer */ protected void setFlashClientIsRunning(boolean isRunning) { this.flashClientIsRunning = isRunning; } /** * 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; } }