MultiUserChatManager.java

  1. /**
  2.  *
  3.  * Copyright © 2014 Florian Schmaus
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.muc;

  18. import java.lang.ref.WeakReference;
  19. import java.util.ArrayList;
  20. import java.util.Collections;
  21. import java.util.HashMap;
  22. import java.util.HashSet;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.Set;
  26. import java.util.WeakHashMap;
  27. import java.util.concurrent.CopyOnWriteArraySet;
  28. import java.util.logging.Logger;

  29. import org.jivesoftware.smack.ConnectionCreationListener;
  30. import org.jivesoftware.smack.Manager;
  31. import org.jivesoftware.smack.StanzaListener;
  32. import org.jivesoftware.smack.XMPPConnection;
  33. import org.jivesoftware.smack.XMPPConnectionRegistry;
  34. import org.jivesoftware.smack.SmackException.NoResponseException;
  35. import org.jivesoftware.smack.SmackException.NotConnectedException;
  36. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  37. import org.jivesoftware.smack.filter.AndFilter;
  38. import org.jivesoftware.smack.filter.MessageTypeFilter;
  39. import org.jivesoftware.smack.filter.StanzaExtensionFilter;
  40. import org.jivesoftware.smack.filter.StanzaFilter;
  41. import org.jivesoftware.smack.filter.NotFilter;
  42. import org.jivesoftware.smack.filter.StanzaTypeFilter;
  43. import org.jivesoftware.smack.packet.Message;
  44. import org.jivesoftware.smack.packet.Stanza;
  45. import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider;
  46. import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
  47. import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
  48. import org.jivesoftware.smackx.disco.packet.DiscoverItems;
  49. import org.jivesoftware.smackx.muc.packet.MUCInitialPresence;
  50. import org.jivesoftware.smackx.muc.packet.MUCUser;
  51. import org.jxmpp.jid.BareJid;
  52. import org.jxmpp.jid.DomainBareJid;
  53. import org.jxmpp.jid.Jid;
  54. import org.jxmpp.jid.JidWithLocalpart;

  55. public class MultiUserChatManager extends Manager {
  56.     private final static String DISCO_NODE = MUCInitialPresence.NAMESPACE + "#rooms";

  57.     private static final Logger LOGGER = Logger.getLogger(MultiUserChatManager.class.getName());

  58.     static {
  59.         XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
  60.             public void connectionCreated(final XMPPConnection connection) {
  61.                 // Set on every established connection that this client supports the Multi-User
  62.                 // Chat protocol. This information will be used when another client tries to
  63.                 // discover whether this client supports MUC or not.
  64.                 ServiceDiscoveryManager.getInstanceFor(connection).addFeature(MUCInitialPresence.NAMESPACE);

  65.                 // Set the NodeInformationProvider that will provide information about the
  66.                 // joined rooms whenever a disco request is received
  67.                 final WeakReference<XMPPConnection> weakRefConnection = new WeakReference<XMPPConnection>(connection);
  68.                 ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(DISCO_NODE,
  69.                                 new AbstractNodeInformationProvider() {
  70.                                     @Override
  71.                                     public List<DiscoverItems.Item> getNodeItems() {
  72.                                         XMPPConnection connection = weakRefConnection.get();
  73.                                         if (connection == null)
  74.                                             return Collections.emptyList();
  75.                                         Set<BareJid> joinedRooms = MultiUserChatManager.getInstanceFor(connection).getJoinedRooms();
  76.                                         List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>();
  77.                                         for (BareJid room : joinedRooms) {
  78.                                             answer.add(new DiscoverItems.Item(room));
  79.                                         }
  80.                                         return answer;
  81.                                     }
  82.                                 });
  83.             }
  84.         });
  85.     }

  86.     private static final Map<XMPPConnection, MultiUserChatManager> INSTANCES = new WeakHashMap<XMPPConnection, MultiUserChatManager>();

  87.     /**
  88.      * Get a instance of a multi user chat manager for the given connection.
  89.      *
  90.      * @param connection
  91.      * @return a multi user chat manager.
  92.      */
  93.     public static synchronized MultiUserChatManager getInstanceFor(XMPPConnection connection) {
  94.         MultiUserChatManager multiUserChatManager = INSTANCES.get(connection);
  95.         if (multiUserChatManager == null) {
  96.             multiUserChatManager = new MultiUserChatManager(connection);
  97.             INSTANCES.put(connection, multiUserChatManager);
  98.         }
  99.         return multiUserChatManager;
  100.     }

  101.     private static final StanzaFilter INVITATION_FILTER = new AndFilter(StanzaTypeFilter.MESSAGE, new StanzaExtensionFilter(new MUCUser()),
  102.                     new NotFilter(MessageTypeFilter.ERROR));

  103.     private final Set<InvitationListener> invitationsListeners = new CopyOnWriteArraySet<InvitationListener>();
  104.     private final Set<BareJid> joinedRooms = new HashSet<>();

  105.     /**
  106.      * A Map of MUC JIDs to {@link MultiUserChat} instances. We use weak references for the values in order to allow
  107.      * those instances to get garbage collected. Note that MultiUserChat instances can not get garbage collected while
  108.      * the user is joined, because then the MUC will have PacketListeners added to the XMPPConnection.
  109.      */
  110.     private final Map<BareJid, WeakReference<MultiUserChat>> multiUserChats = new HashMap<>();

  111.     private MultiUserChatManager(XMPPConnection connection) {
  112.         super(connection);
  113.         // Listens for all messages that include a MUCUser extension and fire the invitation
  114.         // listeners if the message includes an invitation.
  115.         StanzaListener invitationPacketListener = new StanzaListener() {
  116.             public void processPacket(Stanza packet) {
  117.                 final Message message = (Message) packet;
  118.                 // Get the MUCUser extension
  119.                 final MUCUser mucUser = MUCUser.from(message);
  120.                 // Check if the MUCUser extension includes an invitation
  121.                 if (mucUser.getInvite() != null) {
  122.                     BareJid mucJid = message.getFrom().asBareJidIfPossible();
  123.                     if (mucJid == null) {
  124.                         LOGGER.warning("Invite to non bare JID: '" + message.toXML() + "'");
  125.                         return;
  126.                     }
  127.                     // Fire event for invitation listeners
  128.                     final MultiUserChat muc = getMultiUserChat(mucJid);
  129.                     for (final InvitationListener listener : invitationsListeners) {
  130.                         listener.invitationReceived(connection(), muc, mucUser.getInvite().getFrom(),
  131.                                         mucUser.getInvite().getReason(), mucUser.getPassword(), message);
  132.                     }
  133.                 }
  134.             }
  135.         };
  136.         connection.addAsyncStanzaListener(invitationPacketListener, INVITATION_FILTER);
  137.     }

  138.     /**
  139.      * Creates a multi user chat. Note: no information is sent to or received from the server until you attempt to
  140.      * {@link MultiUserChat#join(org.jxmpp.jid.parts.Resourcepart) join} the chat room. On some server implementations, the room will not be
  141.      * created until the first person joins it.
  142.      * <p>
  143.      * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com for the XMPP server example.com).
  144.      * You must ensure that the room address you're trying to connect to includes the proper chat sub-domain.
  145.      * </p>
  146.      *
  147.      * @param jid the name of the room in the form "roomName@service", where "service" is the hostname at which the
  148.      *        multi-user chat service is running. Make sure to provide a valid JID.
  149.      */
  150.     public synchronized MultiUserChat getMultiUserChat(BareJid jid) {
  151.         WeakReference<MultiUserChat> weakRefMultiUserChat = multiUserChats.get(jid);
  152.         if (weakRefMultiUserChat == null) {
  153.             return createNewMucAndAddToMap(jid);
  154.         }
  155.         MultiUserChat multiUserChat = weakRefMultiUserChat.get();
  156.         if (multiUserChat == null) {
  157.             return createNewMucAndAddToMap(jid);
  158.         }
  159.         return multiUserChat;
  160.     }

  161.     private MultiUserChat createNewMucAndAddToMap(BareJid jid) {
  162.         MultiUserChat multiUserChat = new MultiUserChat(connection(), jid, this);
  163.         multiUserChats.put(jid, new WeakReference<MultiUserChat>(multiUserChat));
  164.         return multiUserChat;
  165.     }

  166.     /**
  167.      * Returns true if the specified user supports the Multi-User Chat protocol.
  168.      *
  169.      * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
  170.      * @return a boolean indicating whether the specified user supports the MUC protocol.
  171.      * @throws XMPPErrorException
  172.      * @throws NoResponseException
  173.      * @throws NotConnectedException
  174.      * @throws InterruptedException
  175.      */
  176.     public boolean isServiceEnabled(Jid user) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  177.         return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(user, MUCInitialPresence.NAMESPACE);
  178.     }

  179.     /**
  180.      * Returns a Set of the rooms where the user has joined. The Iterator will contain Strings where each String
  181.      * represents a room (e.g. room@muc.jabber.org).
  182.      *
  183.      * @return a List of the rooms where the user has joined using a given connection.
  184.      */
  185.     public Set<BareJid> getJoinedRooms() {
  186.         return Collections.unmodifiableSet(joinedRooms);
  187.     }

  188.     /**
  189.      * Returns a List of the rooms where the requested user has joined. The Iterator will contain Strings where each
  190.      * String represents a room (e.g. room@muc.jabber.org).
  191.      *
  192.      * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
  193.      * @return a List of the rooms where the requested user has joined.
  194.      * @throws XMPPErrorException
  195.      * @throws NoResponseException
  196.      * @throws NotConnectedException
  197.      * @throws InterruptedException
  198.      */
  199.     public List<BareJid> getJoinedRooms(JidWithLocalpart user) throws NoResponseException, XMPPErrorException,
  200.                     NotConnectedException, InterruptedException {
  201.         // Send the disco packet to the user
  202.         DiscoverItems result = ServiceDiscoveryManager.getInstanceFor(connection()).discoverItems(user, DISCO_NODE);
  203.         List<DiscoverItems.Item> items = result.getItems();
  204.         List<BareJid> answer = new ArrayList<>(items.size());
  205.         // Collect the entityID for each returned item
  206.         for (DiscoverItems.Item item : items) {
  207.             BareJid muc = item.getEntityID().asBareJidIfPossible();
  208.             if (muc == null) {
  209.                 LOGGER.warning("Not a bare JID: " + item.getEntityID());
  210.                 continue;
  211.             }
  212.             answer.add(muc);
  213.         }
  214.         return answer;
  215.     }

  216.     /**
  217.      * Returns the discovered information of a given room without actually having to join the room. The server will
  218.      * provide information only for rooms that are public.
  219.      *
  220.      * @param room the name of the room in the form "roomName@service" of which we want to discover its information.
  221.      * @return the discovered information of a given room without actually having to join the room.
  222.      * @throws XMPPErrorException
  223.      * @throws NoResponseException
  224.      * @throws NotConnectedException
  225.      * @throws InterruptedException
  226.      */
  227.     public RoomInfo getRoomInfo(BareJid room) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  228.         DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection()).discoverInfo(room);
  229.         return new RoomInfo(info);
  230.     }

  231.     /**
  232.      * Returns a collection with the XMPP addresses of the Multi-User Chat services.
  233.      *
  234.      * @return a collection with the XMPP addresses of the Multi-User Chat services.
  235.      * @throws XMPPErrorException
  236.      * @throws NoResponseException
  237.      * @throws NotConnectedException
  238.      * @throws InterruptedException
  239.      */
  240.     public List<DomainBareJid> getServiceNames() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  241.         ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection());
  242.         return sdm.findServices(MUCInitialPresence.NAMESPACE, false, false);
  243.     }

  244.     /**
  245.      * Returns a List of HostedRooms where each HostedRoom has the XMPP address of the room and the room's name.
  246.      * Once discovered the rooms hosted by a chat service it is possible to discover more detailed room information or
  247.      * join the room.
  248.      *
  249.      * @param serviceName the service that is hosting the rooms to discover.
  250.      * @return a collection of HostedRooms.
  251.      * @throws XMPPErrorException
  252.      * @throws NoResponseException
  253.      * @throws NotConnectedException
  254.      * @throws InterruptedException
  255.      */
  256.     public List<HostedRoom> getHostedRooms(DomainBareJid serviceName) throws NoResponseException, XMPPErrorException,
  257.                     NotConnectedException, InterruptedException {
  258.         ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection());
  259.         DiscoverItems discoverItems = discoManager.discoverItems(serviceName);
  260.         List<DiscoverItems.Item> items = discoverItems.getItems();
  261.         List<HostedRoom> answer = new ArrayList<HostedRoom>(items.size());
  262.         for (DiscoverItems.Item item : items) {
  263.             answer.add(new HostedRoom(item));
  264.         }
  265.         return answer;
  266.     }

  267.     /**
  268.      * Informs the sender of an invitation that the invitee declines the invitation. The rejection will be sent to the
  269.      * room which in turn will forward the rejection to the inviter.
  270.      *
  271.      * @param room the room that sent the original invitation.
  272.      * @param inviter the inviter of the declined invitation.
  273.      * @param reason the reason why the invitee is declining the invitation.
  274.      * @throws NotConnectedException
  275.      * @throws InterruptedException
  276.      */
  277.     public void decline(BareJid room, String inviter, String reason) throws NotConnectedException, InterruptedException {
  278.         Message message = new Message(room);

  279.         // Create the MUCUser packet that will include the rejection
  280.         MUCUser mucUser = new MUCUser();
  281.         MUCUser.Decline decline = new MUCUser.Decline();
  282.         decline.setTo(inviter);
  283.         decline.setReason(reason);
  284.         mucUser.setDecline(decline);
  285.         // Add the MUCUser packet that includes the rejection
  286.         message.addExtension(mucUser);

  287.         connection().sendStanza(message);
  288.     }

  289.     /**
  290.      * Adds a listener to invitation notifications. The listener will be fired anytime an invitation is received.
  291.      *
  292.      * @param listener an invitation listener.
  293.      */
  294.     public void addInvitationListener(InvitationListener listener) {
  295.         invitationsListeners.add(listener);
  296.     }

  297.     /**
  298.      * Removes a listener to invitation notifications. The listener will be fired anytime an invitation is received.
  299.      *
  300.      * @param listener an invitation listener.
  301.      */
  302.     public void removeInvitationListener(InvitationListener listener) {
  303.         invitationsListeners.remove(listener);
  304.     }

  305.     void addJoinedRoom(BareJid room) {
  306.         joinedRooms.add(room);
  307.     }

  308.     void removeJoinedRoom(BareJid room) {
  309.         joinedRooms.remove(room);
  310.     }
  311. }