001/**
002 *
003 * Copyright © 2014 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.muc;
018
019import java.lang.ref.WeakReference;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.WeakHashMap;
028import java.util.concurrent.CopyOnWriteArraySet;
029
030import org.jivesoftware.smack.ConnectionCreationListener;
031import org.jivesoftware.smack.Manager;
032import org.jivesoftware.smack.StanzaListener;
033import org.jivesoftware.smack.XMPPConnection;
034import org.jivesoftware.smack.XMPPConnectionRegistry;
035import org.jivesoftware.smack.SmackException.NoResponseException;
036import org.jivesoftware.smack.SmackException.NotConnectedException;
037import org.jivesoftware.smack.XMPPException.XMPPErrorException;
038import org.jivesoftware.smack.filter.AndFilter;
039import org.jivesoftware.smack.filter.MessageTypeFilter;
040import org.jivesoftware.smack.filter.StanzaExtensionFilter;
041import org.jivesoftware.smack.filter.StanzaFilter;
042import org.jivesoftware.smack.filter.NotFilter;
043import org.jivesoftware.smack.filter.StanzaTypeFilter;
044import org.jivesoftware.smack.packet.Message;
045import org.jivesoftware.smack.packet.Stanza;
046import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider;
047import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
048import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
049import org.jivesoftware.smackx.disco.packet.DiscoverItems;
050import org.jivesoftware.smackx.muc.packet.MUCInitialPresence;
051import org.jivesoftware.smackx.muc.packet.MUCUser;
052
053public class MultiUserChatManager extends Manager {
054    private final static String DISCO_NODE = MUCInitialPresence.NAMESPACE + "#rooms";
055
056    static {
057        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
058            public void connectionCreated(final XMPPConnection connection) {
059                // Set on every established connection that this client supports the Multi-User
060                // Chat protocol. This information will be used when another client tries to
061                // discover whether this client supports MUC or not.
062                ServiceDiscoveryManager.getInstanceFor(connection).addFeature(MUCInitialPresence.NAMESPACE);
063
064                // Set the NodeInformationProvider that will provide information about the
065                // joined rooms whenever a disco request is received
066                final WeakReference<XMPPConnection> weakRefConnection = new WeakReference<XMPPConnection>(connection);
067                ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(DISCO_NODE,
068                                new AbstractNodeInformationProvider() {
069                                    @Override
070                                    public List<DiscoverItems.Item> getNodeItems() {
071                                        XMPPConnection connection = weakRefConnection.get();
072                                        if (connection == null)
073                                            return Collections.emptyList();
074                                        Set<String> joinedRooms = MultiUserChatManager.getInstanceFor(connection).getJoinedRooms();
075                                        List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>();
076                                        for (String room : joinedRooms) {
077                                            answer.add(new DiscoverItems.Item(room));
078                                        }
079                                        return answer;
080                                    }
081                                });
082            }
083        });
084    }
085
086    private static final Map<XMPPConnection, MultiUserChatManager> INSTANCES = new WeakHashMap<XMPPConnection, MultiUserChatManager>();
087
088    /**
089     * Get a instance of a multi user chat manager for the given connection.
090     * 
091     * @param connection
092     * @return a multi user chat manager.
093     */
094    public static synchronized MultiUserChatManager getInstanceFor(XMPPConnection connection) {
095        MultiUserChatManager multiUserChatManager = INSTANCES.get(connection);
096        if (multiUserChatManager == null) {
097            multiUserChatManager = new MultiUserChatManager(connection);
098            INSTANCES.put(connection, multiUserChatManager);
099        }
100        return multiUserChatManager;
101    }
102
103    private static final StanzaFilter INVITATION_FILTER = new AndFilter(StanzaTypeFilter.MESSAGE, new StanzaExtensionFilter(new MUCUser()),
104                    new NotFilter(MessageTypeFilter.ERROR));
105
106    private final Set<InvitationListener> invitationsListeners = new CopyOnWriteArraySet<InvitationListener>();
107    private final Set<String> joinedRooms = new HashSet<String>();
108
109    /**
110     * A Map of MUC JIDs to {@link MultiUserChat} instances. We use weak references for the values in order to allow
111     * those instances to get garbage collected. Note that MultiUserChat instances can not get garbage collected while
112     * the user is joined, because then the MUC will have PacketListeners added to the XMPPConnection.
113     */
114    private final Map<String, WeakReference<MultiUserChat>> multiUserChats = new HashMap<String, WeakReference<MultiUserChat>>();
115
116    private MultiUserChatManager(XMPPConnection connection) {
117        super(connection);
118        // Listens for all messages that include a MUCUser extension and fire the invitation
119        // listeners if the message includes an invitation.
120        StanzaListener invitationPacketListener = new StanzaListener() {
121            public void processPacket(Stanza packet) {
122                final Message message = (Message) packet;
123                // Get the MUCUser extension
124                final MUCUser mucUser = MUCUser.from(message);
125                // Check if the MUCUser extension includes an invitation
126                if (mucUser.getInvite() != null) {
127                    // Fire event for invitation listeners
128                    final MultiUserChat muc = getMultiUserChat(packet.getFrom());
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    /**
140     * Creates a multi user chat. Note: no information is sent to or received from the server until you attempt to
141     * {@link MultiUserChat#join(String) join} the chat room. On some server implementations, the room will not be
142     * created until the first person joins it.
143     * <p>
144     * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com for the XMPP server example.com).
145     * You must ensure that the room address you're trying to connect to includes the proper chat sub-domain.
146     * </p>
147     *
148     * @param jid the name of the room in the form "roomName@service", where "service" is the hostname at which the
149     *        multi-user chat service is running. Make sure to provide a valid JID.
150     */
151    public synchronized MultiUserChat getMultiUserChat(String jid) {
152        WeakReference<MultiUserChat> weakRefMultiUserChat = multiUserChats.get(jid);
153        if (weakRefMultiUserChat == null) {
154            return createNewMucAndAddToMap(jid);
155        }
156        MultiUserChat multiUserChat = weakRefMultiUserChat.get();
157        if (multiUserChat == null) {
158            return createNewMucAndAddToMap(jid);
159        }
160        return multiUserChat;
161    }
162
163    private MultiUserChat createNewMucAndAddToMap(String jid) {
164        MultiUserChat multiUserChat = new MultiUserChat(connection(), jid, this);
165        multiUserChats.put(jid, new WeakReference<MultiUserChat>(multiUserChat));
166        return multiUserChat;
167    }
168
169    /**
170     * Returns true if the specified user supports the Multi-User Chat protocol.
171     *
172     * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
173     * @return a boolean indicating whether the specified user supports the MUC protocol.
174     * @throws XMPPErrorException
175     * @throws NoResponseException
176     * @throws NotConnectedException
177     */
178    public boolean isServiceEnabled(String user) throws NoResponseException, XMPPErrorException, NotConnectedException {
179        return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(user, MUCInitialPresence.NAMESPACE);
180    }
181
182    /**
183     * Returns a Set of the rooms where the user has joined. The Iterator will contain Strings where each String
184     * represents a room (e.g. room@muc.jabber.org).
185     *
186     * @return a List of the rooms where the user has joined using a given connection.
187     */
188    public Set<String> getJoinedRooms() {
189        return Collections.unmodifiableSet(joinedRooms);
190    }
191
192    /**
193     * Returns a List of the rooms where the requested user has joined. The Iterator will contain Strings where each
194     * String represents a room (e.g. room@muc.jabber.org).
195     *
196     * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
197     * @return a List of the rooms where the requested user has joined.
198     * @throws XMPPErrorException
199     * @throws NoResponseException
200     * @throws NotConnectedException
201     */
202    public List<String> getJoinedRooms(String user) throws NoResponseException, XMPPErrorException,
203                    NotConnectedException {
204        // Send the disco packet to the user
205        DiscoverItems result = ServiceDiscoveryManager.getInstanceFor(connection()).discoverItems(user, DISCO_NODE);
206        List<DiscoverItems.Item> items = result.getItems();
207        List<String> answer = new ArrayList<String>(items.size());
208        // Collect the entityID for each returned item
209        for (DiscoverItems.Item item : items) {
210            answer.add(item.getEntityID());
211        }
212        return answer;
213    }
214
215    /**
216     * Returns the discovered information of a given room without actually having to join the room. The server will
217     * provide information only for rooms that are public.
218     *
219     * @param room the name of the room in the form "roomName@service" of which we want to discover its information.
220     * @return the discovered information of a given room without actually having to join the room.
221     * @throws XMPPErrorException
222     * @throws NoResponseException
223     * @throws NotConnectedException
224     */
225    public RoomInfo getRoomInfo(String room) throws NoResponseException, XMPPErrorException, NotConnectedException {
226        DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection()).discoverInfo(room);
227        return new RoomInfo(info);
228    }
229
230    /**
231     * Returns a collection with the XMPP addresses of the Multi-User Chat services.
232     *
233     * @return a collection with the XMPP addresses of the Multi-User Chat services.
234     * @throws XMPPErrorException
235     * @throws NoResponseException
236     * @throws NotConnectedException
237     */
238    public List<String> getServiceNames() throws NoResponseException, XMPPErrorException, NotConnectedException {
239        ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection());
240        return sdm.findServices(MUCInitialPresence.NAMESPACE, false, false);
241    }
242
243    /**
244     * Returns a List of HostedRooms where each HostedRoom has the XMPP address of the room and the room's name.
245     * Once discovered the rooms hosted by a chat service it is possible to discover more detailed room information or
246     * join the room.
247     *
248     * @param serviceName the service that is hosting the rooms to discover.
249     * @return a collection of HostedRooms.
250     * @throws XMPPErrorException
251     * @throws NoResponseException
252     * @throws NotConnectedException
253     */
254    public List<HostedRoom> getHostedRooms(String serviceName) throws NoResponseException, XMPPErrorException,
255                    NotConnectedException {
256        ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection());
257        DiscoverItems discoverItems = discoManager.discoverItems(serviceName);
258        List<DiscoverItems.Item> items = discoverItems.getItems();
259        List<HostedRoom> answer = new ArrayList<HostedRoom>(items.size());
260        for (DiscoverItems.Item item : items) {
261            answer.add(new HostedRoom(item));
262        }
263        return answer;
264    }
265
266    /**
267     * Informs the sender of an invitation that the invitee declines the invitation. The rejection will be sent to the
268     * room which in turn will forward the rejection to the inviter.
269     *
270     * @param room the room that sent the original invitation.
271     * @param inviter the inviter of the declined invitation.
272     * @param reason the reason why the invitee is declining the invitation.
273     * @throws NotConnectedException
274     */
275    public void decline(String room, String inviter, String reason) throws NotConnectedException {
276        Message message = new Message(room);
277
278        // Create the MUCUser packet that will include the rejection
279        MUCUser mucUser = new MUCUser();
280        MUCUser.Decline decline = new MUCUser.Decline();
281        decline.setTo(inviter);
282        decline.setReason(reason);
283        mucUser.setDecline(decline);
284        // Add the MUCUser packet that includes the rejection
285        message.addExtension(mucUser);
286
287        connection().sendStanza(message);
288    }
289
290    /**
291     * Adds a listener to invitation notifications. The listener will be fired anytime an invitation is received.
292     *
293     * @param listener an invitation listener.
294     */
295    public void addInvitationListener(InvitationListener listener) {
296        invitationsListeners.add(listener);
297    }
298
299    /**
300     * Removes a listener to invitation notifications. The listener will be fired anytime an invitation is received.
301     *
302     * @param listener an invitation listener.
303     */
304    public void removeInvitationListener(InvitationListener listener) {
305        invitationsListeners.remove(listener);
306    }
307
308    void addJoinedRoom(String room) {
309        joinedRooms.add(room);
310    }
311
312    void removeJoinedRoom(String room) {
313        joinedRooms.remove(room);
314    }
315}