/* * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package net.java.sip.communicator.impl.protocol.jabber; import java.util.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; import org.jivesoftware.smack.*; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smackx.*; import org.jivesoftware.smackx.muc.*; /** * A jabber implementation of the multi user chat operation set. * * @author Emil Ivov * @author Yana Stamcheva */ public class OperationSetMultiUserChatJabberImpl extends AbstractOperationSetMultiUserChat implements SubscriptionListener { private static final Logger logger = Logger.getLogger(OperationSetMultiUserChatJabberImpl.class); /** * The currently valid Jabber protocol provider service implementation. */ private final ProtocolProviderServiceJabberImpl jabberProvider; /** * A list of the rooms that are currently open by this account. Note that * we have not necessarily joined these rooms, we might have simply been * searching through them. */ private final Hashtable chatRoomCache = new Hashtable(); /** * The registration listener that would get notified when the underlying * Jabber provider gets registered. */ private final RegistrationStateListener providerRegListener = new RegistrationStateListener(); /** * Instantiates the user operation set with a currently valid instance of * the Jabber protocol provider. * @param jabberProvider a currently valid instance of * ProtocolProviderServiceJabberImpl. */ OperationSetMultiUserChatJabberImpl( ProtocolProviderServiceJabberImpl jabberProvider) { this.jabberProvider = jabberProvider; jabberProvider.addRegistrationStateChangeListener(providerRegListener); OperationSetPersistentPresence presenceOpSet = (OperationSetPersistentPresence) jabberProvider .getOperationSet(OperationSetPersistentPresence.class); presenceOpSet.addSubscriptionListener(this); } /** * Creates a room with the named roomName and according to the * specified roomProperties on the server that this protocol * provider is currently connected to. * * @param roomName the name of the ChatRoom to create. * @param roomProperties properties specifying how the room should be * created. * * @throws OperationFailedException if the room couldn't be created for * some reason (e.g. room already exists; user already joined to an * existent room or user has no permissions to create a chat room). * @throws OperationNotSupportedException if chat room creation is not * supported by this server * * @return ChatRoom the chat room that we've just created. */ public ChatRoom createChatRoom( String roomName, Map roomProperties) throws OperationFailedException, OperationNotSupportedException { //first make sure we are connected and the server supports multichat assertSupportedAndConnected(); ChatRoom room = findRoom(roomName); if (room == null) { MultiUserChat muc = new MultiUserChat( getXmppConnection(), getCanonicalRoomName(roomName)); try { muc.create(getXmppConnection().getUser()); muc.sendConfigurationForm(new Form(Form.TYPE_SUBMIT)); } catch (XMPPException ex) { logger.error("Failed to create chat room.", ex); throw new OperationFailedException("Failed to create chat room" , ex.getXMPPError().getCode() , ex.getCause()); } room = createLocalChatRoomInstance(muc); } return room; } /** * Creates a ChatRoom from the specified smack * MultiUserChat. * * @param muc the smack MultiUserChat instance that we're going to wrap our * chat room around. * * @return ChatRoom the chat room that we've just created. */ private ChatRoom createLocalChatRoomInstance(MultiUserChat muc) { synchronized(chatRoomCache) { ChatRoomJabberImpl chatRoom = new ChatRoomJabberImpl(muc, jabberProvider); cacheChatRoom(chatRoom); // Add the contained in this class SmackInvitationRejectionListener // which will dispatch all rejection events to the // ChatRoomInvitationRejectionListener. muc.addInvitationRejectionListener( new SmackInvitationRejectionListener(chatRoom)); return chatRoom; } } /** * Returns a reference to a chatRoom named roomName or null * if that room does not exist. * * @param roomName the name of the ChatRoom that we're looking * for. * @return the ChatRoom named roomName if it exists, null * otherwise. * @throws OperationFailedException if an error occurs while trying to * discover the room on the server. * @throws OperationNotSupportedException if the server does not support * multi user chat */ public synchronized ChatRoom findRoom(String roomName) throws OperationFailedException, OperationNotSupportedException { //make sure we are connected and multichat is supported. assertSupportedAndConnected(); String canonicalRoomName = getCanonicalRoomName(roomName); ChatRoom room = chatRoomCache.get(canonicalRoomName); if (room != null) return room; try { // throws Exception if room does not exist // do not use MultiUserChat.getRoomInfo as there is a bug which // throws NPE ServiceDiscoveryManager.getInstanceFor(getXmppConnection()). discoverInfo(canonicalRoomName); MultiUserChat muc = new MultiUserChat(getXmppConnection(), canonicalRoomName); room = new ChatRoomJabberImpl(muc, jabberProvider); chatRoomCache.put(canonicalRoomName, room); return room; } catch (XMPPException e) { // room not found return null; } } /** * Returns a list of the chat rooms that we have joined and are currently * active in. * * @return a List of the rooms where the user has joined using * a given connection. */ public List getCurrentlyJoinedChatRooms() { synchronized(chatRoomCache) { List joinedRooms = new LinkedList(this.chatRoomCache.values()); Iterator joinedRoomsIter = joinedRooms.iterator(); while (joinedRoomsIter.hasNext()) { if ( !( (ChatRoom) joinedRoomsIter.next()).isJoined()) joinedRoomsIter.remove(); } return joinedRooms; } } // **this method is not used** // // /** // * Returns a list of the names of all chat rooms that contact is // * currently a member of. // * // * @param contact the contact whose current ChatRooms we will be // * querying. // * @return a list of String indicating the names of the chat rooms // * that contact has joined and is currently active in. // * // * @throws OperationFailedException if an error occurs while trying to // * discover the room on the server. // * @throws OperationNotSupportedException if the server does not support // * multi user chat // */ // public List getCurrentlyJoinedChatRooms(Contact contact) // throws OperationFailedException, OperationNotSupportedException // { // assertSupportedAndConnected(); // // Iterator joinedRoomsIter // = MultiUserChat.getJoinedRooms( getXmppConnection() // , contact.getAddress()); // // List joinedRoomsForContact = new LinkedList(); // // while ( joinedRoomsIter.hasNext() ) // { // MultiUserChat muc = (MultiUserChat)joinedRoomsIter.next(); // joinedRoomsForContact.add(muc.getRoom()); // } // // return joinedRoomsForContact; // } /** * Returns the List of Strings indicating chat rooms * currently available on the server that this protocol provider is * connected to. * * @return a java.util.List of the name Strings for chat * rooms that are currently available on the server that this protocol * provider is connected to. * * @throws OperationFailedException if we faile retrieving this list from * the server. * @throws OperationNotSupportedException if the server does not support * multi user chat */ public List getExistingChatRooms() throws OperationFailedException, OperationNotSupportedException { assertSupportedAndConnected(); List list = new LinkedList(); //first retrieve all conference service names available on this server Iterator serviceNames = null; try { serviceNames = MultiUserChat .getServiceNames(getXmppConnection()).iterator(); } catch (XMPPException ex) { throw new OperationFailedException( "Failed to retrieve Jabber conference service names" , OperationFailedException.GENERAL_ERROR , ex); } //now retrieve all chat rooms currently available for every service name while(serviceNames.hasNext()) { String serviceName = serviceNames.next(); List roomsOnThisService = new LinkedList(); try { roomsOnThisService .addAll(MultiUserChat.getHostedRooms(getXmppConnection() , serviceName)); } catch (XMPPException ex) { logger.error("Failed to retrieve rooms for serviceName=" + serviceName, ex); //continue bravely with other service names continue; } //now go through all rooms available on this service Iterator serviceRoomsIter = roomsOnThisService.iterator(); //add the room name to the list of names we are returning while(serviceRoomsIter.hasNext()) list.add( serviceRoomsIter.next().getJid()); } /** @todo maybe we should add a check here and fail if retrieving chat * rooms failed for all service names*/ return list; } /** * Returns true if contact supports multi user chat sessions. * * @param contact reference to the contact whose support for chat rooms * we are currently querying. * @return a boolean indicating whether contact supports * chatrooms. * @todo Implement this * net.java.sip.communicator.service.protocol.OperationSetMultiUserChat * method */ public boolean isMultiChatSupportedByContact(Contact contact) { if(contact.getProtocolProvider() .getOperationSet(OperationSetMultiUserChat.class) != null) return true; return false; } /** * Informs the sender of an invitation that we decline their invitation. * * @param invitation the connection to use for sending the rejection. * @param rejectReason the reason to reject the given invitation */ public void rejectInvitation(ChatRoomInvitation invitation, String rejectReason) { MultiUserChat.decline(jabberProvider.getConnection(), invitation.getTargetChatRoom().getName(), invitation.getInviter(), rejectReason); } /** * Almost all MultiUserChat methods require an xmpp connection * param so I added this method only for the sake of utility. * * @return the XMPPConnection currently in use by the jabber provider or * null if jabber provider has yet to be initialized. */ private XMPPConnection getXmppConnection() { return (jabberProvider == null) ? null :jabberProvider.getConnection(); } /** * Makes sure that we are properly connected and that the server supports * multi user chats. * * @throws OperationFailedException if the provider is not registered or * the xmpp connection not connected. * @throws OperationNotSupportedException if the service is not supported * by the server. */ private void assertSupportedAndConnected() throws OperationFailedException, OperationNotSupportedException { //throw an exception if the provider is not registered or the xmpp //connection not connected. if( !jabberProvider.isRegistered() || !getXmppConnection().isConnected()) { throw new OperationFailedException( "Provider not connected to jabber server" , OperationFailedException.NETWORK_FAILURE); } //MultiUserChat.isServiceEnabled() *always* returns false, //altough the functionalty is implemented and advertised. Because of //that, we cant rely on it. //The problem has been reported to igniterealtime.org since 2006. // // if (!MultiUserChat.isServiceEnabled( // getXmppConnection() // , jabberProvider.getAccountID().getUserID())) // { // throw new OperationNotSupportedException( // "Chat rooms not supported on server " // + jabberProvider.getAccountID().getService() // + " for user " // + jabberProvider.getAccountID().getUserID()); // } } /** * In case roomName does not represent a complete room id, the * method returns a canonincal chat room name in the following form: * roomName@muc-servicename.jabserver.com. In case roomName is * already a canonical room name, the method simply returns it without * changing it. * * @param roomName the name of the room that we'd like to "canonize". * * @return the canonincal name of the room (which might be equal to * roomName in case it was already in a canonical format). * * @throws OperationFailedException if we fail retrieving the conference * service name */ private String getCanonicalRoomName(String roomName) throws OperationFailedException { if (roomName.indexOf('@') > 0) return roomName; Iterator serviceNamesIter = null; try { serviceNamesIter = MultiUserChat.getServiceNames(getXmppConnection()).iterator(); } catch (XMPPException ex) { logger.error("Failed to retrieve conference service name for user: " + jabberProvider.getAccountID().getUserID() + " on server: " + jabberProvider.getAccountID().getService() , ex); throw new OperationFailedException( "Failed to retrieve conference service name for user: " + jabberProvider.getAccountID().getUserID() + " on server: " + jabberProvider.getAccountID().getService() , OperationFailedException.GENERAL_ERROR , ex); } if (serviceNamesIter.hasNext()) return roomName + "@" + serviceNamesIter.next(); //hmmmm strange.. no service name returned. we should probably throw an //exception throw new OperationFailedException( "Failed to retrieve MultiUserChat service names." , OperationFailedException.GENERAL_ERROR); } /** * Adds chatRoom to the cache of chat rooms that this operation * set is handling. * * @param chatRoom the ChatRoom to cache. */ private void cacheChatRoom(ChatRoom chatRoom) { this.chatRoomCache.put(chatRoom.getName(), chatRoom); } /** * Returns a reference to the chat room named chatRoomName or * null if the room hasn't been cached yet. * * @param chatRoomName the name of the room we're looking for. * * @return the ChatRoomJabberImpl instance that has been cached * for chatRoomName or null if no such room has been cached so far. */ public ChatRoomJabberImpl getChatRoom(String chatRoomName) { return (ChatRoomJabberImpl)this.chatRoomCache.get(chatRoomName); } /** * Returns the list of currently joined chat rooms. */ public List getCurrentlyJoinedChatRooms(ChatRoomMember chatRoomMember) throws OperationFailedException, OperationNotSupportedException { assertSupportedAndConnected(); Iterator joinedRoomsIter = MultiUserChat.getJoinedRooms( getXmppConnection(), chatRoomMember.getContactAddress()); return (List) joinedRoomsIter; } /** * Delivers a ChatRoomInvitationReceivedEvent to all * registered ChatRoomInvitationListeners. * * @param targetChatRoom the room that invitation refers to * @param inviter the inviter that sent the invitation * @param reason the reason why the inviter sent the invitation * @param password the password to use when joining the room */ public void fireInvitationEvent( ChatRoom targetChatRoom, String inviter, String reason, byte[] password) { ChatRoomInvitationJabberImpl invitation = new ChatRoomInvitationJabberImpl( targetChatRoom, inviter, reason, password); fireInvitationReceived(invitation); } /** * A listener that is fired anytime an invitation to join a MUC room is * received. */ private class SmackInvitationListener implements InvitationListener { /** * Called when the an invitation to join a MUC room is received.

* * If the room is password-protected, the invitee will receive a * password to use to join the room. If the room is members-only, the * the invitee may be added to the member list. * * @param conn the XMPPConnection that received the invitation. * @param room the room that invitation refers to. * @param inviter the inviter that sent the invitation. * (e.g. crone1@shakespeare.lit). * @param reason the reason why the inviter sent the invitation. * @param password the password to use when joining the room. * @param message the message used by the inviter to send the invitation. */ public void invitationReceived(XMPPConnection conn, String room, String inviter, String reason, String password, Message message) { ChatRoomJabberImpl chatRoom; try { chatRoom = (ChatRoomJabberImpl) findRoom(room); if (chatRoom == null) { MultiUserChat muc = new MultiUserChat(conn, room); chatRoom = new ChatRoomJabberImpl(muc, jabberProvider); } if (password != null) fireInvitationEvent( chatRoom, inviter, reason, password.getBytes()); else fireInvitationEvent( chatRoom, inviter, reason, null); } catch (OperationFailedException e) { logger.error("Failed to find room with name: " + room, e); } catch (OperationNotSupportedException e) { logger.error("Failed to find room with name: " + room, e); } } } /** * A listener that is fired anytime an invitee declines or rejects an * invitation. */ private class SmackInvitationRejectionListener implements InvitationRejectionListener { private ChatRoom chatRoom; /** * Creates an instance of SmackInvitationRejectionListener and * passes to it the chat room for which it will listen for rejection * events. * * @param chatRoom */ public SmackInvitationRejectionListener(ChatRoom chatRoom) { this.chatRoom = chatRoom; } /** * Called when the invitee declines the invitation. * * @param invitee the invitee that declined the invitation. * (e.g. hecate@shakespeare.lit). * @param reason the reason why the invitee declined the invitation. */ public void invitationDeclined(String invitee, String reason) { fireInvitationRejectedEvent(chatRoom, invitee, reason); } } /** * Our listener that will tell us when we're registered to jabber and the * smack MultiUserChat is ready to accept us as a listener. */ private class RegistrationStateListener implements RegistrationStateChangeListener { /** * The method is called by a ProtocolProvider implementation whenver * a change in the registration state of the corresponding provider had * occurred. * @param evt ProviderStatusChangeEvent the event describing the status * change. */ public void registrationStateChanged(RegistrationStateChangeEvent evt) { if (evt.getNewState() == RegistrationState.REGISTERED) { logger.debug("adding an Invitation listener to the smack muc"); MultiUserChat.addInvitationListener( jabberProvider.getConnection(), new SmackInvitationListener()); } } } /** * Updates corresponding chat room members when a contact has been modified * in our contact list. */ public void contactModified(ContactPropertyChangeEvent evt) { Contact modifiedContact = evt.getSourceContact(); this.updateChatRoomMembers(modifiedContact); } /** * Updates corresponding chat room members when a contact has been created * in our contact list. */ public void subscriptionCreated(SubscriptionEvent evt) { Contact createdContact = evt.getSourceContact(); this.updateChatRoomMembers(createdContact); } /** * Not interested in this event for our member update purposes. */ public void subscriptionFailed(SubscriptionEvent evt) {} /** * Not interested in this event for our member update purposes. */ public void subscriptionMoved(SubscriptionMovedEvent evt) {} /** * Updates corresponding chat room members when a contact has been removed * from our contact list. */ public void subscriptionRemoved(SubscriptionEvent evt) { // Set to null the contact reference in all corresponding chat room // members. this.updateChatRoomMembers(null); } /** * Not interested in this event for our member update purposes. */ public void subscriptionResolved(SubscriptionEvent evt) {} /** * Finds all chat room members, which name corresponds to the name of the * given contact and updates their contact references. * * @param contact the contact we're looking correspondences for. */ private void updateChatRoomMembers(Contact contact) { Enumeration chatRooms = chatRoomCache.elements(); while (chatRooms.hasMoreElements()) { ChatRoomJabberImpl chatRoom = (ChatRoomJabberImpl) chatRooms.nextElement(); ChatRoomMemberJabberImpl member = chatRoom.findMemberForNickName(contact.getAddress()); if (member != null) { member.setContact(contact); member.setAvatar(contact.getImage()); } } } }