• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * $RCSfile$
3  * $Revision$
4  * $Date$
5  *
6  * Copyright 2003-2007 Jive Software.
7  *
8  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *     http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  */
20 
21 package org.jivesoftware.smackx.muc;
22 
23 import java.lang.ref.WeakReference;
24 import java.lang.reflect.InvocationTargetException;
25 import java.lang.reflect.Method;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.WeakHashMap;
33 import java.util.concurrent.ConcurrentHashMap;
34 
35 import org.jivesoftware.smack.Chat;
36 import org.jivesoftware.smack.ConnectionCreationListener;
37 import org.jivesoftware.smack.ConnectionListener;
38 import org.jivesoftware.smack.MessageListener;
39 import org.jivesoftware.smack.PacketCollector;
40 import org.jivesoftware.smack.PacketInterceptor;
41 import org.jivesoftware.smack.PacketListener;
42 import org.jivesoftware.smack.SmackConfiguration;
43 import org.jivesoftware.smack.Connection;
44 import org.jivesoftware.smack.XMPPException;
45 import org.jivesoftware.smack.filter.AndFilter;
46 import org.jivesoftware.smack.filter.FromMatchesFilter;
47 import org.jivesoftware.smack.filter.MessageTypeFilter;
48 import org.jivesoftware.smack.filter.PacketExtensionFilter;
49 import org.jivesoftware.smack.filter.PacketFilter;
50 import org.jivesoftware.smack.filter.PacketIDFilter;
51 import org.jivesoftware.smack.filter.PacketTypeFilter;
52 import org.jivesoftware.smack.packet.IQ;
53 import org.jivesoftware.smack.packet.Message;
54 import org.jivesoftware.smack.packet.Packet;
55 import org.jivesoftware.smack.packet.PacketExtension;
56 import org.jivesoftware.smack.packet.Presence;
57 import org.jivesoftware.smack.packet.Registration;
58 import org.jivesoftware.smackx.Form;
59 import org.jivesoftware.smackx.NodeInformationProvider;
60 import org.jivesoftware.smackx.ServiceDiscoveryManager;
61 import org.jivesoftware.smackx.packet.DiscoverInfo;
62 import org.jivesoftware.smackx.packet.DiscoverItems;
63 import org.jivesoftware.smackx.packet.MUCAdmin;
64 import org.jivesoftware.smackx.packet.MUCInitialPresence;
65 import org.jivesoftware.smackx.packet.MUCOwner;
66 import org.jivesoftware.smackx.packet.MUCUser;
67 
68 /**
69  * A MultiUserChat is a conversation that takes place among many users in a virtual
70  * room. A room could have many occupants with different affiliation and roles.
71  * Possible affiliatons are "owner", "admin", "member", and "outcast". Possible roles
72  * are "moderator", "participant", and "visitor". Each role and affiliation guarantees
73  * different privileges (e.g. Send messages to all occupants, Kick participants and visitors,
74  * Grant voice, Edit member list, etc.).
75  *
76  * @author Gaston Dombiak, Larry Kirschner
77  */
78 public class MultiUserChat {
79 
80     private final static String discoNamespace = "http://jabber.org/protocol/muc";
81     private final static String discoNode = "http://jabber.org/protocol/muc#rooms";
82 
83     private static Map<Connection, List<String>> joinedRooms =
84             new WeakHashMap<Connection, List<String>>();
85 
86     private Connection connection;
87     private String room;
88     private String subject;
89     private String nickname = null;
90     private boolean joined = false;
91     private Map<String, Presence> occupantsMap = new ConcurrentHashMap<String, Presence>();
92 
93     private final List<InvitationRejectionListener> invitationRejectionListeners =
94             new ArrayList<InvitationRejectionListener>();
95     private final List<SubjectUpdatedListener> subjectUpdatedListeners =
96             new ArrayList<SubjectUpdatedListener>();
97     private final List<UserStatusListener> userStatusListeners =
98             new ArrayList<UserStatusListener>();
99     private final List<ParticipantStatusListener> participantStatusListeners =
100             new ArrayList<ParticipantStatusListener>();
101 
102     private PacketFilter presenceFilter;
103     private List<PacketInterceptor> presenceInterceptors = new ArrayList<PacketInterceptor>();
104     private PacketFilter messageFilter;
105     private RoomListenerMultiplexor roomListenerMultiplexor;
106     private ConnectionDetachedPacketCollector messageCollector;
107     private List<PacketListener> connectionListeners = new ArrayList<PacketListener>();
108 
109     static {
Connection.addConnectionCreationListener(new ConnectionCreationListener() { public void connectionCreated(final Connection connection) { ServiceDiscoveryManager.getInstanceFor(connection).addFeature(discoNamespace); ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider( discoNode, new NodeInformationProvider() { public List<DiscoverItems.Item> getNodeItems() { List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>(); Iterator<String> rooms=MultiUserChat.getJoinedRooms(connection); while (rooms.hasNext()) { answer.add(new DiscoverItems.Item(rooms.next())); } return answer; } public List<String> getNodeFeatures() { return null; } public List<DiscoverInfo.Identity> getNodeIdentities() { return null; } @Override public List<PacketExtension> getNodePacketExtensions() { return null; } }); } })110         Connection.addConnectionCreationListener(new ConnectionCreationListener() {
111             public void connectionCreated(final Connection connection) {
112                 // Set on every established connection that this client supports the Multi-User
113                 // Chat protocol. This information will be used when another client tries to
114                 // discover whether this client supports MUC or not.
115                 ServiceDiscoveryManager.getInstanceFor(connection).addFeature(discoNamespace);
116                 // Set the NodeInformationProvider that will provide information about the
117                 // joined rooms whenever a disco request is received
118                 ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(
119                     discoNode,
120                     new NodeInformationProvider() {
121                         public List<DiscoverItems.Item> getNodeItems() {
122                             List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>();
123                             Iterator<String> rooms=MultiUserChat.getJoinedRooms(connection);
124                             while (rooms.hasNext()) {
125                                 answer.add(new DiscoverItems.Item(rooms.next()));
126                             }
127                             return answer;
128                         }
129 
130                         public List<String> getNodeFeatures() {
131                             return null;
132                         }
133 
134                         public List<DiscoverInfo.Identity> getNodeIdentities() {
135                             return null;
136                         }
137 
138                         @Override
139                         public List<PacketExtension> getNodePacketExtensions() {
140                             return null;
141                         }
142                     });
143             }
144         });
145     }
146 
147     /**
148      * Creates a new multi user chat with the specified connection and room name. Note: no
149      * information is sent to or received from the server until you attempt to
150      * {@link #join(String) join} the chat room. On some server implementations,
151      * the room will not be created until the first person joins it.<p>
152      *
153      * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com
154      * for the XMPP server example.com). You must ensure that the room address you're
155      * trying to connect to includes the proper chat sub-domain.
156      *
157      * @param connection the XMPP connection.
158      * @param room the name of the room in the form "roomName@service", where
159      *      "service" is the hostname at which the multi-user chat
160      *      service is running. Make sure to provide a valid JID.
161      */
MultiUserChat(Connection connection, String room)162     public MultiUserChat(Connection connection, String room) {
163         this.connection = connection;
164         this.room = room.toLowerCase();
165         init();
166     }
167 
168     /**
169      * Returns true if the specified user supports the Multi-User Chat protocol.
170      *
171      * @param connection the connection to use to perform the service discovery.
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      */
isServiceEnabled(Connection connection, String user)175     public static boolean isServiceEnabled(Connection connection, String user) {
176         try {
177             DiscoverInfo result =
178                 ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(user);
179             return result.containsFeature(discoNamespace);
180         }
181         catch (XMPPException e) {
182             e.printStackTrace();
183             return false;
184         }
185     }
186 
187     /**
188      * Returns an Iterator on the rooms where the user has joined using a given connection.
189      * The Iterator will contain Strings where each String represents a room
190      * (e.g. room@muc.jabber.org).
191      *
192      * @param connection the connection used to join the rooms.
193      * @return an Iterator on the rooms where the user has joined using a given connection.
194      */
getJoinedRooms(Connection connection)195     private static Iterator<String> getJoinedRooms(Connection connection) {
196         List<String> rooms = joinedRooms.get(connection);
197         if (rooms != null) {
198             return rooms.iterator();
199         }
200         // Return an iterator on an empty collection (i.e. the user never joined a room)
201         return new ArrayList<String>().iterator();
202     }
203 
204     /**
205      * Returns an Iterator on the rooms where the requested user has joined. The Iterator will
206      * contain Strings where each String represents a room (e.g. room@muc.jabber.org).
207      *
208      * @param connection the connection to use to perform the service discovery.
209      * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
210      * @return an Iterator on the rooms where the requested user has joined.
211      */
getJoinedRooms(Connection connection, String user)212     public static Iterator<String> getJoinedRooms(Connection connection, String user) {
213         try {
214             ArrayList<String> answer = new ArrayList<String>();
215             // Send the disco packet to the user
216             DiscoverItems result =
217                 ServiceDiscoveryManager.getInstanceFor(connection).discoverItems(user, discoNode);
218             // Collect the entityID for each returned item
219             for (Iterator<DiscoverItems.Item> items=result.getItems(); items.hasNext();) {
220                 answer.add(items.next().getEntityID());
221             }
222             return answer.iterator();
223         }
224         catch (XMPPException e) {
225             e.printStackTrace();
226             // Return an iterator on an empty collection
227             return new ArrayList<String>().iterator();
228         }
229     }
230 
231     /**
232      * Returns the discovered information of a given room without actually having to join the room.
233      * The server will provide information only for rooms that are public.
234      *
235      * @param connection the XMPP connection to use for discovering information about the room.
236      * @param room the name of the room in the form "roomName@service" of which we want to discover
237      *        its information.
238      * @return the discovered information of a given room without actually having to join the room.
239      * @throws XMPPException if an error occured while trying to discover information of a room.
240      */
getRoomInfo(Connection connection, String room)241     public static RoomInfo getRoomInfo(Connection connection, String room)
242             throws XMPPException {
243         DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(room);
244         return new RoomInfo(info);
245     }
246 
247     /**
248      * Returns a collection with the XMPP addresses of the Multi-User Chat services.
249      *
250      * @param connection the XMPP connection to use for discovering Multi-User Chat services.
251      * @return a collection with the XMPP addresses of the Multi-User Chat services.
252      * @throws XMPPException if an error occured while trying to discover MUC services.
253      */
getServiceNames(Connection connection)254     public static Collection<String> getServiceNames(Connection connection) throws XMPPException {
255         final List<String> answer = new ArrayList<String>();
256         ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
257         DiscoverItems items = discoManager.discoverItems(connection.getServiceName());
258         for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
259             DiscoverItems.Item item = it.next();
260             try {
261                 DiscoverInfo info = discoManager.discoverInfo(item.getEntityID());
262                 if (info.containsFeature("http://jabber.org/protocol/muc")) {
263                     answer.add(item.getEntityID());
264                 }
265             }
266             catch (XMPPException e) {
267                 // Trouble finding info in some cases. This is a workaround for
268                 // discovering info on remote servers.
269             }
270         }
271         return answer;
272     }
273 
274     /**
275      * Returns a collection of HostedRooms where each HostedRoom has the XMPP address of the room
276      * and the room's name. Once discovered the rooms hosted by a chat service it is possible to
277      * discover more detailed room information or join the room.
278      *
279      * @param connection the XMPP connection to use for discovering hosted rooms by the MUC service.
280      * @param serviceName the service that is hosting the rooms to discover.
281      * @return a collection of HostedRooms.
282      * @throws XMPPException if an error occured while trying to discover the information.
283      */
getHostedRooms(Connection connection, String serviceName)284     public static Collection<HostedRoom> getHostedRooms(Connection connection, String serviceName)
285             throws XMPPException {
286         List<HostedRoom> answer = new ArrayList<HostedRoom>();
287         ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
288         DiscoverItems items = discoManager.discoverItems(serviceName);
289         for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
290             answer.add(new HostedRoom(it.next()));
291         }
292         return answer;
293     }
294 
295     /**
296      * Returns the name of the room this MultiUserChat object represents.
297      *
298      * @return the multi user chat room name.
299      */
getRoom()300     public String getRoom() {
301         return room;
302     }
303 
304     /**
305      * Creates the room according to some default configuration, assign the requesting user
306      * as the room owner, and add the owner to the room but not allow anyone else to enter
307      * the room (effectively "locking" the room). The requesting user will join the room
308      * under the specified nickname as soon as the room has been created.<p>
309      *
310      * To create an "Instant Room", that means a room with some default configuration that is
311      * available for immediate access, the room's owner should send an empty form after creating
312      * the room. {@link #sendConfigurationForm(Form)}<p>
313      *
314      * To create a "Reserved Room", that means a room manually configured by the room creator
315      * before anyone is allowed to enter, the room's owner should complete and send a form after
316      * creating the room. Once the completed configutation form is sent to the server, the server
317      * will unlock the room. {@link #sendConfigurationForm(Form)}
318      *
319      * @param nickname the nickname to use.
320      * @throws XMPPException if the room couldn't be created for some reason
321      *          (e.g. room already exists; user already joined to an existant room or
322      *          405 error if the user is not allowed to create the room)
323      */
create(String nickname)324     public synchronized void create(String nickname) throws XMPPException {
325         if (nickname == null || nickname.equals("")) {
326             throw new IllegalArgumentException("Nickname must not be null or blank.");
327         }
328         // If we've already joined the room, leave it before joining under a new
329         // nickname.
330         if (joined) {
331             throw new IllegalStateException("Creation failed - User already joined the room.");
332         }
333         // We create a room by sending a presence packet to room@service/nick
334         // and signal support for MUC. The owner will be automatically logged into the room.
335         Presence joinPresence = new Presence(Presence.Type.available);
336         joinPresence.setTo(room + "/" + nickname);
337         // Indicate the the client supports MUC
338         joinPresence.addExtension(new MUCInitialPresence());
339         // Invoke presence interceptors so that extra information can be dynamically added
340         for (PacketInterceptor packetInterceptor : presenceInterceptors) {
341             packetInterceptor.interceptPacket(joinPresence);
342         }
343 
344         // Wait for a presence packet back from the server.
345         PacketFilter responseFilter =
346             new AndFilter(
347                 new FromMatchesFilter(room + "/" + nickname),
348                 new PacketTypeFilter(Presence.class));
349         PacketCollector response = connection.createPacketCollector(responseFilter);
350         // Send create & join packet.
351         connection.sendPacket(joinPresence);
352         // Wait up to a certain number of seconds for a reply.
353         Presence presence =
354             (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
355         // Stop queuing results
356         response.cancel();
357 
358         if (presence == null) {
359             throw new XMPPException("No response from server.");
360         }
361         else if (presence.getError() != null) {
362             throw new XMPPException(presence.getError());
363         }
364         // Whether the room existed before or was created, the user has joined the room
365         this.nickname = nickname;
366         joined = true;
367         userHasJoined();
368 
369         // Look for confirmation of room creation from the server
370         MUCUser mucUser = getMUCUserExtension(presence);
371         if (mucUser != null && mucUser.getStatus() != null) {
372             if ("201".equals(mucUser.getStatus().getCode())) {
373                 // Room was created and the user has joined the room
374                 return;
375             }
376         }
377         // We need to leave the room since it seems that the room already existed
378         leave();
379         throw new XMPPException("Creation failed - Missing acknowledge of room creation.");
380     }
381 
382     /**
383      * Joins the chat room using the specified nickname. If already joined
384      * using another nickname, this method will first leave the room and then
385      * re-join using the new nickname. The default timeout of Smack for a reply
386      * from the group chat server that the join succeeded will be used. After
387      * joining the room, the room will decide the amount of history to send.
388      *
389      * @param nickname the nickname to use.
390      * @throws XMPPException if an error occurs joining the room. In particular, a
391      *      401 error can occur if no password was provided and one is required; or a
392      *      403 error can occur if the user is banned; or a
393      *      404 error can occur if the room does not exist or is locked; or a
394      *      407 error can occur if user is not on the member list; or a
395      *      409 error can occur if someone is already in the group chat with the same nickname.
396      */
join(String nickname)397     public void join(String nickname) throws XMPPException {
398         join(nickname, null, null, SmackConfiguration.getPacketReplyTimeout());
399     }
400 
401     /**
402      * Joins the chat room using the specified nickname and password. If already joined
403      * using another nickname, this method will first leave the room and then
404      * re-join using the new nickname. The default timeout of Smack for a reply
405      * from the group chat server that the join succeeded will be used. After
406      * joining the room, the room will decide the amount of history to send.<p>
407      *
408      * A password is required when joining password protected rooms. If the room does
409      * not require a password there is no need to provide one.
410      *
411      * @param nickname the nickname to use.
412      * @param password the password to use.
413      * @throws XMPPException if an error occurs joining the room. In particular, a
414      *      401 error can occur if no password was provided and one is required; or a
415      *      403 error can occur if the user is banned; or a
416      *      404 error can occur if the room does not exist or is locked; or a
417      *      407 error can occur if user is not on the member list; or a
418      *      409 error can occur if someone is already in the group chat with the same nickname.
419      */
join(String nickname, String password)420     public void join(String nickname, String password) throws XMPPException {
421         join(nickname, password, null, SmackConfiguration.getPacketReplyTimeout());
422     }
423 
424     /**
425      * Joins the chat room using the specified nickname and password. If already joined
426      * using another nickname, this method will first leave the room and then
427      * re-join using the new nickname.<p>
428      *
429      * To control the amount of history to receive while joining a room you will need to provide
430      * a configured DiscussionHistory object.<p>
431      *
432      * A password is required when joining password protected rooms. If the room does
433      * not require a password there is no need to provide one.<p>
434      *
435      * If the room does not already exist when the user seeks to enter it, the server will
436      * decide to create a new room or not.
437      *
438      * @param nickname the nickname to use.
439      * @param password the password to use.
440      * @param history the amount of discussion history to receive while joining a room.
441      * @param timeout the amount of time to wait for a reply from the MUC service(in milleseconds).
442      * @throws XMPPException if an error occurs joining the room. In particular, a
443      *      401 error can occur if no password was provided and one is required; or a
444      *      403 error can occur if the user is banned; or a
445      *      404 error can occur if the room does not exist or is locked; or a
446      *      407 error can occur if user is not on the member list; or a
447      *      409 error can occur if someone is already in the group chat with the same nickname.
448      */
join( String nickname, String password, DiscussionHistory history, long timeout)449     public synchronized void join(
450         String nickname,
451         String password,
452         DiscussionHistory history,
453         long timeout)
454         throws XMPPException {
455         if (nickname == null || nickname.equals("")) {
456             throw new IllegalArgumentException("Nickname must not be null or blank.");
457         }
458         // If we've already joined the room, leave it before joining under a new
459         // nickname.
460         if (joined) {
461             leave();
462         }
463         // We join a room by sending a presence packet where the "to"
464         // field is in the form "roomName@service/nickname"
465         Presence joinPresence = new Presence(Presence.Type.available);
466         joinPresence.setTo(room + "/" + nickname);
467 
468         // Indicate the the client supports MUC
469         MUCInitialPresence mucInitialPresence = new MUCInitialPresence();
470         if (password != null) {
471             mucInitialPresence.setPassword(password);
472         }
473         if (history != null) {
474             mucInitialPresence.setHistory(history.getMUCHistory());
475         }
476         joinPresence.addExtension(mucInitialPresence);
477         // Invoke presence interceptors so that extra information can be dynamically added
478         for (PacketInterceptor packetInterceptor : presenceInterceptors) {
479             packetInterceptor.interceptPacket(joinPresence);
480         }
481 
482         // Wait for a presence packet back from the server.
483         PacketFilter responseFilter =
484                 new AndFilter(
485                         new FromMatchesFilter(room + "/" + nickname),
486                         new PacketTypeFilter(Presence.class));
487         PacketCollector response = null;
488         Presence presence;
489         try {
490             response = connection.createPacketCollector(responseFilter);
491             // Send join packet.
492             connection.sendPacket(joinPresence);
493             // Wait up to a certain number of seconds for a reply.
494             presence = (Presence) response.nextResult(timeout);
495         }
496         finally {
497             // Stop queuing results
498             if (response != null) {
499                 response.cancel();
500             }
501         }
502 
503         if (presence == null) {
504             throw new XMPPException("No response from server.");
505         }
506         else if (presence.getError() != null) {
507             throw new XMPPException(presence.getError());
508         }
509         this.nickname = nickname;
510         joined = true;
511         userHasJoined();
512     }
513 
514     /**
515      * Returns true if currently in the multi user chat (after calling the {@link
516      * #join(String)} method).
517      *
518      * @return true if currently in the multi user chat room.
519      */
isJoined()520     public boolean isJoined() {
521         return joined;
522     }
523 
524     /**
525      * Leave the chat room.
526      */
leave()527     public synchronized void leave() {
528         // If not joined already, do nothing.
529         if (!joined) {
530             return;
531         }
532         // We leave a room by sending a presence packet where the "to"
533         // field is in the form "roomName@service/nickname"
534         Presence leavePresence = new Presence(Presence.Type.unavailable);
535         leavePresence.setTo(room + "/" + nickname);
536         // Invoke presence interceptors so that extra information can be dynamically added
537         for (PacketInterceptor packetInterceptor : presenceInterceptors) {
538             packetInterceptor.interceptPacket(leavePresence);
539         }
540         connection.sendPacket(leavePresence);
541         // Reset occupant information.
542         occupantsMap.clear();
543         nickname = null;
544         joined = false;
545         userHasLeft();
546     }
547 
548     /**
549      * Returns the room's configuration form that the room's owner can use or <tt>null</tt> if
550      * no configuration is possible. The configuration form allows to set the room's language,
551      * enable logging, specify room's type, etc..
552      *
553      * @return the Form that contains the fields to complete together with the instrucions or
554      * <tt>null</tt> if no configuration is possible.
555      * @throws XMPPException if an error occurs asking the configuration form for the room.
556      */
getConfigurationForm()557     public Form getConfigurationForm() throws XMPPException {
558         MUCOwner iq = new MUCOwner();
559         iq.setTo(room);
560         iq.setType(IQ.Type.GET);
561 
562         // Filter packets looking for an answer from the server.
563         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
564         PacketCollector response = connection.createPacketCollector(responseFilter);
565         // Request the configuration form to the server.
566         connection.sendPacket(iq);
567         // Wait up to a certain number of seconds for a reply.
568         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
569         // Stop queuing results
570         response.cancel();
571 
572         if (answer == null) {
573             throw new XMPPException("No response from server.");
574         }
575         else if (answer.getError() != null) {
576             throw new XMPPException(answer.getError());
577         }
578         return Form.getFormFrom(answer);
579     }
580 
581     /**
582      * Sends the completed configuration form to the server. The room will be configured
583      * with the new settings defined in the form. If the form is empty then the server
584      * will create an instant room (will use default configuration).
585      *
586      * @param form the form with the new settings.
587      * @throws XMPPException if an error occurs setting the new rooms' configuration.
588      */
sendConfigurationForm(Form form)589     public void sendConfigurationForm(Form form) throws XMPPException {
590         MUCOwner iq = new MUCOwner();
591         iq.setTo(room);
592         iq.setType(IQ.Type.SET);
593         iq.addExtension(form.getDataFormToSend());
594 
595         // Filter packets looking for an answer from the server.
596         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
597         PacketCollector response = connection.createPacketCollector(responseFilter);
598         // Send the completed configuration form to the server.
599         connection.sendPacket(iq);
600         // Wait up to a certain number of seconds for a reply.
601         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
602         // Stop queuing results
603         response.cancel();
604 
605         if (answer == null) {
606             throw new XMPPException("No response from server.");
607         }
608         else if (answer.getError() != null) {
609             throw new XMPPException(answer.getError());
610         }
611     }
612 
613     /**
614      * Returns the room's registration form that an unaffiliated user, can use to become a member
615      * of the room or <tt>null</tt> if no registration is possible. Some rooms may restrict the
616      * privilege to register members and allow only room admins to add new members.<p>
617      *
618      * If the user requesting registration requirements is not allowed to register with the room
619      * (e.g. because that privilege has been restricted), the room will return a "Not Allowed"
620      * error to the user (error code 405).
621      *
622      * @return the registration Form that contains the fields to complete together with the
623      * instrucions or <tt>null</tt> if no registration is possible.
624      * @throws XMPPException if an error occurs asking the registration form for the room or a
625      * 405 error if the user is not allowed to register with the room.
626      */
getRegistrationForm()627     public Form getRegistrationForm() throws XMPPException {
628         Registration reg = new Registration();
629         reg.setType(IQ.Type.GET);
630         reg.setTo(room);
631 
632         PacketFilter filter =
633             new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
634         PacketCollector collector = connection.createPacketCollector(filter);
635         connection.sendPacket(reg);
636         IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
637         collector.cancel();
638         if (result == null) {
639             throw new XMPPException("No response from server.");
640         }
641         else if (result.getType() == IQ.Type.ERROR) {
642             throw new XMPPException(result.getError());
643         }
644         return Form.getFormFrom(result);
645     }
646 
647     /**
648      * Sends the completed registration form to the server. After the user successfully submits
649      * the form, the room may queue the request for review by the room admins or may immediately
650      * add the user to the member list by changing the user's affiliation from "none" to "member.<p>
651      *
652      * If the desired room nickname is already reserved for that room, the room will return a
653      * "Conflict" error to the user (error code 409). If the room does not support registration,
654      * it will return a "Service Unavailable" error to the user (error code 503).
655      *
656      * @param form the completed registration form.
657      * @throws XMPPException if an error occurs submitting the registration form. In particular, a
658      *      409 error can occur if the desired room nickname is already reserved for that room;
659      *      or a 503 error can occur if the room does not support registration.
660      */
sendRegistrationForm(Form form)661     public void sendRegistrationForm(Form form) throws XMPPException {
662         Registration reg = new Registration();
663         reg.setType(IQ.Type.SET);
664         reg.setTo(room);
665         reg.addExtension(form.getDataFormToSend());
666 
667         PacketFilter filter =
668             new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
669         PacketCollector collector = connection.createPacketCollector(filter);
670         connection.sendPacket(reg);
671         IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
672         collector.cancel();
673         if (result == null) {
674             throw new XMPPException("No response from server.");
675         }
676         else if (result.getType() == IQ.Type.ERROR) {
677             throw new XMPPException(result.getError());
678         }
679     }
680 
681     /**
682      * Sends a request to the server to destroy the room. The sender of the request
683      * should be the room's owner. If the sender of the destroy request is not the room's owner
684      * then the server will answer a "Forbidden" error (403).
685      *
686      * @param reason the reason for the room destruction.
687      * @param alternateJID the JID of an alternate location.
688      * @throws XMPPException if an error occurs while trying to destroy the room.
689      *      An error can occur which will be wrapped by an XMPPException --
690      *      XMPP error code 403. The error code can be used to present more
691      *      appropiate error messages to end-users.
692      */
destroy(String reason, String alternateJID)693     public void destroy(String reason, String alternateJID) throws XMPPException {
694         MUCOwner iq = new MUCOwner();
695         iq.setTo(room);
696         iq.setType(IQ.Type.SET);
697 
698         // Create the reason for the room destruction
699         MUCOwner.Destroy destroy = new MUCOwner.Destroy();
700         destroy.setReason(reason);
701         destroy.setJid(alternateJID);
702         iq.setDestroy(destroy);
703 
704         // Wait for a presence packet back from the server.
705         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
706         PacketCollector response = connection.createPacketCollector(responseFilter);
707         // Send the room destruction request.
708         connection.sendPacket(iq);
709         // Wait up to a certain number of seconds for a reply.
710         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
711         // Stop queuing results
712         response.cancel();
713 
714         if (answer == null) {
715             throw new XMPPException("No response from server.");
716         }
717         else if (answer.getError() != null) {
718             throw new XMPPException(answer.getError());
719         }
720         // Reset occupant information.
721         occupantsMap.clear();
722         nickname = null;
723         joined = false;
724         userHasLeft();
725     }
726 
727     /**
728      * Invites another user to the room in which one is an occupant. The invitation
729      * will be sent to the room which in turn will forward the invitation to the invitee.<p>
730      *
731      * If the room is password-protected, the invitee will receive a password to use to join
732      * the room. If the room is members-only, the the invitee may be added to the member list.
733      *
734      * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
735      * @param reason the reason why the user is being invited.
736      */
invite(String user, String reason)737     public void invite(String user, String reason) {
738         invite(new Message(), user, reason);
739     }
740 
741     /**
742      * Invites another user to the room in which one is an occupant using a given Message. The invitation
743      * will be sent to the room which in turn will forward the invitation to the invitee.<p>
744      *
745      * If the room is password-protected, the invitee will receive a password to use to join
746      * the room. If the room is members-only, the the invitee may be added to the member list.
747      *
748      * @param message the message to use for sending the invitation.
749      * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
750      * @param reason the reason why the user is being invited.
751      */
invite(Message message, String user, String reason)752     public void invite(Message message, String user, String reason) {
753         // TODO listen for 404 error code when inviter supplies a non-existent JID
754         message.setTo(room);
755 
756         // Create the MUCUser packet that will include the invitation
757         MUCUser mucUser = new MUCUser();
758         MUCUser.Invite invite = new MUCUser.Invite();
759         invite.setTo(user);
760         invite.setReason(reason);
761         mucUser.setInvite(invite);
762         // Add the MUCUser packet that includes the invitation to the message
763         message.addExtension(mucUser);
764 
765         connection.sendPacket(message);
766     }
767 
768     /**
769      * Informs the sender of an invitation that the invitee declines the invitation. The rejection
770      * will be sent to the room which in turn will forward the rejection to the inviter.
771      *
772      * @param conn the connection to use for sending the rejection.
773      * @param room the room that sent the original invitation.
774      * @param inviter the inviter of the declined invitation.
775      * @param reason the reason why the invitee is declining the invitation.
776      */
decline(Connection conn, String room, String inviter, String reason)777     public static void decline(Connection conn, String room, String inviter, String reason) {
778         Message message = new Message(room);
779 
780         // Create the MUCUser packet that will include the rejection
781         MUCUser mucUser = new MUCUser();
782         MUCUser.Decline decline = new MUCUser.Decline();
783         decline.setTo(inviter);
784         decline.setReason(reason);
785         mucUser.setDecline(decline);
786         // Add the MUCUser packet that includes the rejection
787         message.addExtension(mucUser);
788 
789         conn.sendPacket(message);
790     }
791 
792     /**
793      * Adds a listener to invitation notifications. The listener will be fired anytime
794      * an invitation is received.
795      *
796      * @param conn the connection where the listener will be applied.
797      * @param listener an invitation listener.
798      */
addInvitationListener(Connection conn, InvitationListener listener)799     public static void addInvitationListener(Connection conn, InvitationListener listener) {
800         InvitationsMonitor.getInvitationsMonitor(conn).addInvitationListener(listener);
801     }
802 
803     /**
804      * Removes a listener to invitation notifications. The listener will be fired anytime
805      * an invitation is received.
806      *
807      * @param conn the connection where the listener was applied.
808      * @param listener an invitation listener.
809      */
removeInvitationListener(Connection conn, InvitationListener listener)810     public static void removeInvitationListener(Connection conn, InvitationListener listener) {
811         InvitationsMonitor.getInvitationsMonitor(conn).removeInvitationListener(listener);
812     }
813 
814     /**
815      * Adds a listener to invitation rejections notifications. The listener will be fired anytime
816      * an invitation is declined.
817      *
818      * @param listener an invitation rejection listener.
819      */
addInvitationRejectionListener(InvitationRejectionListener listener)820     public void addInvitationRejectionListener(InvitationRejectionListener listener) {
821         synchronized (invitationRejectionListeners) {
822             if (!invitationRejectionListeners.contains(listener)) {
823                 invitationRejectionListeners.add(listener);
824             }
825         }
826     }
827 
828     /**
829      * Removes a listener from invitation rejections notifications. The listener will be fired
830      * anytime an invitation is declined.
831      *
832      * @param listener an invitation rejection listener.
833      */
removeInvitationRejectionListener(InvitationRejectionListener listener)834     public void removeInvitationRejectionListener(InvitationRejectionListener listener) {
835         synchronized (invitationRejectionListeners) {
836             invitationRejectionListeners.remove(listener);
837         }
838     }
839 
840     /**
841      * Fires invitation rejection listeners.
842      *
843      * @param invitee the user being invited.
844      * @param reason the reason for the rejection
845      */
fireInvitationRejectionListeners(String invitee, String reason)846     private void fireInvitationRejectionListeners(String invitee, String reason) {
847         InvitationRejectionListener[] listeners;
848         synchronized (invitationRejectionListeners) {
849             listeners = new InvitationRejectionListener[invitationRejectionListeners.size()];
850             invitationRejectionListeners.toArray(listeners);
851         }
852         for (InvitationRejectionListener listener : listeners) {
853             listener.invitationDeclined(invitee, reason);
854         }
855     }
856 
857     /**
858      * Adds a listener to subject change notifications. The listener will be fired anytime
859      * the room's subject changes.
860      *
861      * @param listener a subject updated listener.
862      */
addSubjectUpdatedListener(SubjectUpdatedListener listener)863     public void addSubjectUpdatedListener(SubjectUpdatedListener listener) {
864         synchronized (subjectUpdatedListeners) {
865             if (!subjectUpdatedListeners.contains(listener)) {
866                 subjectUpdatedListeners.add(listener);
867             }
868         }
869     }
870 
871     /**
872      * Removes a listener from subject change notifications. The listener will be fired
873      * anytime the room's subject changes.
874      *
875      * @param listener a subject updated listener.
876      */
removeSubjectUpdatedListener(SubjectUpdatedListener listener)877     public void removeSubjectUpdatedListener(SubjectUpdatedListener listener) {
878         synchronized (subjectUpdatedListeners) {
879             subjectUpdatedListeners.remove(listener);
880         }
881     }
882 
883     /**
884      * Fires subject updated listeners.
885      */
fireSubjectUpdatedListeners(String subject, String from)886     private void fireSubjectUpdatedListeners(String subject, String from) {
887         SubjectUpdatedListener[] listeners;
888         synchronized (subjectUpdatedListeners) {
889             listeners = new SubjectUpdatedListener[subjectUpdatedListeners.size()];
890             subjectUpdatedListeners.toArray(listeners);
891         }
892         for (SubjectUpdatedListener listener : listeners) {
893             listener.subjectUpdated(subject, from);
894         }
895     }
896 
897     /**
898      * Adds a new {@link PacketInterceptor} that will be invoked every time a new presence
899      * is going to be sent by this MultiUserChat to the server. Packet interceptors may
900      * add new extensions to the presence that is going to be sent to the MUC service.
901      *
902      * @param presenceInterceptor the new packet interceptor that will intercept presence packets.
903      */
addPresenceInterceptor(PacketInterceptor presenceInterceptor)904     public void addPresenceInterceptor(PacketInterceptor presenceInterceptor) {
905         presenceInterceptors.add(presenceInterceptor);
906     }
907 
908     /**
909      * Removes a {@link PacketInterceptor} that was being invoked every time a new presence
910      * was being sent by this MultiUserChat to the server. Packet interceptors may
911      * add new extensions to the presence that is going to be sent to the MUC service.
912      *
913      * @param presenceInterceptor the packet interceptor to remove.
914      */
removePresenceInterceptor(PacketInterceptor presenceInterceptor)915     public void removePresenceInterceptor(PacketInterceptor presenceInterceptor) {
916         presenceInterceptors.remove(presenceInterceptor);
917     }
918 
919     /**
920      * Returns the last known room's subject or <tt>null</tt> if the user hasn't joined the room
921      * or the room does not have a subject yet. In case the room has a subject, as soon as the
922      * user joins the room a message with the current room's subject will be received.<p>
923      *
924      * To be notified every time the room's subject change you should add a listener
925      * to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p>
926      *
927      * To change the room's subject use {@link #changeSubject(String)}.
928      *
929      * @return the room's subject or <tt>null</tt> if the user hasn't joined the room or the
930      * room does not have a subject yet.
931      */
getSubject()932     public String getSubject() {
933         return subject;
934     }
935 
936     /**
937      * Returns the reserved room nickname for the user in the room. A user may have a reserved
938      * nickname, for example through explicit room registration or database integration. In such
939      * cases it may be desirable for the user to discover the reserved nickname before attempting
940      * to enter the room.
941      *
942      * @return the reserved room nickname or <tt>null</tt> if none.
943      */
getReservedNickname()944     public String getReservedNickname() {
945         try {
946             DiscoverInfo result =
947                 ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(
948                     room,
949                     "x-roomuser-item");
950             // Look for an Identity that holds the reserved nickname and return its name
951             for (Iterator<DiscoverInfo.Identity> identities = result.getIdentities();
952                  identities.hasNext();) {
953                 DiscoverInfo.Identity identity = identities.next();
954                 return identity.getName();
955             }
956             // If no Identity was found then the user does not have a reserved room nickname
957             return null;
958         }
959         catch (XMPPException e) {
960             e.printStackTrace();
961             return null;
962         }
963     }
964 
965     /**
966      * Returns the nickname that was used to join the room, or <tt>null</tt> if not
967      * currently joined.
968      *
969      * @return the nickname currently being used.
970      */
getNickname()971     public String getNickname() {
972         return nickname;
973     }
974 
975     /**
976      * Changes the occupant's nickname to a new nickname within the room. Each room occupant
977      * will receive two presence packets. One of type "unavailable" for the old nickname and one
978      * indicating availability for the new nickname. The unavailable presence will contain the new
979      * nickname and an appropriate status code (namely 303) as extended presence information. The
980      * status code 303 indicates that the occupant is changing his/her nickname.
981      *
982      * @param nickname the new nickname within the room.
983      * @throws XMPPException if the new nickname is already in use by another occupant.
984      */
changeNickname(String nickname)985     public void changeNickname(String nickname) throws XMPPException {
986         if (nickname == null || nickname.equals("")) {
987             throw new IllegalArgumentException("Nickname must not be null or blank.");
988         }
989         // Check that we already have joined the room before attempting to change the
990         // nickname.
991         if (!joined) {
992             throw new IllegalStateException("Must be logged into the room to change nickname.");
993         }
994         // We change the nickname by sending a presence packet where the "to"
995         // field is in the form "roomName@service/nickname"
996         // We don't have to signal the MUC support again
997         Presence joinPresence = new Presence(Presence.Type.available);
998         joinPresence.setTo(room + "/" + nickname);
999         // Invoke presence interceptors so that extra information can be dynamically added
1000         for (PacketInterceptor packetInterceptor : presenceInterceptors) {
1001             packetInterceptor.interceptPacket(joinPresence);
1002         }
1003 
1004         // Wait for a presence packet back from the server.
1005         PacketFilter responseFilter =
1006             new AndFilter(
1007                 new FromMatchesFilter(room + "/" + nickname),
1008                 new PacketTypeFilter(Presence.class));
1009         PacketCollector response = connection.createPacketCollector(responseFilter);
1010         // Send join packet.
1011         connection.sendPacket(joinPresence);
1012         // Wait up to a certain number of seconds for a reply.
1013         Presence presence =
1014             (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1015         // Stop queuing results
1016         response.cancel();
1017 
1018         if (presence == null) {
1019             throw new XMPPException("No response from server.");
1020         }
1021         else if (presence.getError() != null) {
1022             throw new XMPPException(presence.getError());
1023         }
1024         this.nickname = nickname;
1025     }
1026 
1027     /**
1028      * Changes the occupant's availability status within the room. The presence type
1029      * will remain available but with a new status that describes the presence update and
1030      * a new presence mode (e.g. Extended away).
1031      *
1032      * @param status a text message describing the presence update.
1033      * @param mode the mode type for the presence update.
1034      */
changeAvailabilityStatus(String status, Presence.Mode mode)1035     public void changeAvailabilityStatus(String status, Presence.Mode mode) {
1036         if (nickname == null || nickname.equals("")) {
1037             throw new IllegalArgumentException("Nickname must not be null or blank.");
1038         }
1039         // Check that we already have joined the room before attempting to change the
1040         // availability status.
1041         if (!joined) {
1042             throw new IllegalStateException(
1043                 "Must be logged into the room to change the " + "availability status.");
1044         }
1045         // We change the availability status by sending a presence packet to the room with the
1046         // new presence status and mode
1047         Presence joinPresence = new Presence(Presence.Type.available);
1048         joinPresence.setStatus(status);
1049         joinPresence.setMode(mode);
1050         joinPresence.setTo(room + "/" + nickname);
1051         // Invoke presence interceptors so that extra information can be dynamically added
1052         for (PacketInterceptor packetInterceptor : presenceInterceptors) {
1053             packetInterceptor.interceptPacket(joinPresence);
1054         }
1055 
1056         // Send join packet.
1057         connection.sendPacket(joinPresence);
1058     }
1059 
1060     /**
1061      * Kicks a visitor or participant from the room. The kicked occupant will receive a presence
1062      * of type "unavailable" including a status code 307 and optionally along with the reason
1063      * (if provided) and the bare JID of the user who initiated the kick. After the occupant
1064      * was kicked from the room, the rest of the occupants will receive a presence of type
1065      * "unavailable". The presence will include a status code 307 which means that the occupant
1066      * was kicked from the room.
1067      *
1068      * @param nickname the nickname of the participant or visitor to kick from the room
1069      * (e.g. "john").
1070      * @param reason the reason why the participant or visitor is being kicked from the room.
1071      * @throws XMPPException if an error occurs kicking the occupant. In particular, a
1072      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1073      *      was intended to be kicked (i.e. Not Allowed error); or a
1074      *      403 error can occur if the occupant that intended to kick another occupant does
1075      *      not have kicking privileges (i.e. Forbidden error); or a
1076      *      400 error can occur if the provided nickname is not present in the room.
1077      */
kickParticipant(String nickname, String reason)1078     public void kickParticipant(String nickname, String reason) throws XMPPException {
1079         changeRole(nickname, "none", reason);
1080     }
1081 
1082     /**
1083      * Grants voice to visitors in the room. In a moderated room, a moderator may want to manage
1084      * who does and does not have "voice" in the room. To have voice means that a room occupant
1085      * is able to send messages to the room occupants.
1086      *
1087      * @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john").
1088      * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
1089      *      403 error can occur if the occupant that intended to grant voice is not
1090      *      a moderator in this room (i.e. Forbidden error); or a
1091      *      400 error can occur if the provided nickname is not present in the room.
1092      */
grantVoice(Collection<String> nicknames)1093     public void grantVoice(Collection<String> nicknames) throws XMPPException {
1094         changeRole(nicknames, "participant");
1095     }
1096 
1097     /**
1098      * Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage
1099      * who does and does not have "voice" in the room. To have voice means that a room occupant
1100      * is able to send messages to the room occupants.
1101      *
1102      * @param nickname the nickname of the visitor to grant voice in the room (e.g. "john").
1103      * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
1104      *      403 error can occur if the occupant that intended to grant voice is not
1105      *      a moderator in this room (i.e. Forbidden error); or a
1106      *      400 error can occur if the provided nickname is not present in the room.
1107      */
grantVoice(String nickname)1108     public void grantVoice(String nickname) throws XMPPException {
1109         changeRole(nickname, "participant", null);
1110     }
1111 
1112     /**
1113      * Revokes voice from participants in the room. In a moderated room, a moderator may want to
1114      * revoke an occupant's privileges to speak. To have voice means that a room occupant
1115      * is able to send messages to the room occupants.
1116      *
1117      * @param nicknames the nicknames of the participants to revoke voice (e.g. "john").
1118      * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a
1119      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1120      *      was tried to revoke his voice (i.e. Not Allowed error); or a
1121      *      400 error can occur if the provided nickname is not present in the room.
1122      */
revokeVoice(Collection<String> nicknames)1123     public void revokeVoice(Collection<String> nicknames) throws XMPPException {
1124         changeRole(nicknames, "visitor");
1125     }
1126 
1127     /**
1128      * Revokes voice from a participant in the room. In a moderated room, a moderator may want to
1129      * revoke an occupant's privileges to speak. To have voice means that a room occupant
1130      * is able to send messages to the room occupants.
1131      *
1132      * @param nickname the nickname of the participant to revoke voice (e.g. "john").
1133      * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a
1134      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1135      *      was tried to revoke his voice (i.e. Not Allowed error); or a
1136      *      400 error can occur if the provided nickname is not present in the room.
1137      */
revokeVoice(String nickname)1138     public void revokeVoice(String nickname) throws XMPPException {
1139         changeRole(nickname, "visitor", null);
1140     }
1141 
1142     /**
1143      * Bans users from the room. An admin or owner of the room can ban users from a room. This
1144      * means that the banned user will no longer be able to join the room unless the ban has been
1145      * removed. If the banned user was present in the room then he/she will be removed from the
1146      * room and notified that he/she was banned along with the reason (if provided) and the bare
1147      * XMPP user ID of the user who initiated the ban.
1148      *
1149      * @param jids the bare XMPP user IDs of the users to ban.
1150      * @throws XMPPException if an error occurs banning a user. In particular, a
1151      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1152      *      was tried to be banned (i.e. Not Allowed error).
1153      */
banUsers(Collection<String> jids)1154     public void banUsers(Collection<String> jids) throws XMPPException {
1155         changeAffiliationByAdmin(jids, "outcast");
1156     }
1157 
1158     /**
1159      * Bans a user from the room. An admin or owner of the room can ban users from a room. This
1160      * means that the banned user will no longer be able to join the room unless the ban has been
1161      * removed. If the banned user was present in the room then he/she will be removed from the
1162      * room and notified that he/she was banned along with the reason (if provided) and the bare
1163      * XMPP user ID of the user who initiated the ban.
1164      *
1165      * @param jid the bare XMPP user ID of the user to ban (e.g. "user@host.org").
1166      * @param reason the optional reason why the user was banned.
1167      * @throws XMPPException if an error occurs banning a user. In particular, a
1168      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1169      *      was tried to be banned (i.e. Not Allowed error).
1170      */
banUser(String jid, String reason)1171     public void banUser(String jid, String reason) throws XMPPException {
1172         changeAffiliationByAdmin(jid, "outcast", reason);
1173     }
1174 
1175     /**
1176      * Grants membership to other users. Only administrators are able to grant membership. A user
1177      * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1178      * that a user cannot enter without being on the member list).
1179      *
1180      * @param jids the XMPP user IDs of the users to grant membership.
1181      * @throws XMPPException if an error occurs granting membership to a user.
1182      */
grantMembership(Collection<String> jids)1183     public void grantMembership(Collection<String> jids) throws XMPPException {
1184         changeAffiliationByAdmin(jids, "member");
1185     }
1186 
1187     /**
1188      * Grants membership to a user. Only administrators are able to grant membership. A user
1189      * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1190      * that a user cannot enter without being on the member list).
1191      *
1192      * @param jid the XMPP user ID of the user to grant membership (e.g. "user@host.org").
1193      * @throws XMPPException if an error occurs granting membership to a user.
1194      */
grantMembership(String jid)1195     public void grantMembership(String jid) throws XMPPException {
1196         changeAffiliationByAdmin(jid, "member", null);
1197     }
1198 
1199     /**
1200      * Revokes users' membership. Only administrators are able to revoke membership. A user
1201      * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1202      * that a user cannot enter without being on the member list). If the user is in the room and
1203      * the room is of type members-only then the user will be removed from the room.
1204      *
1205      * @param jids the bare XMPP user IDs of the users to revoke membership.
1206      * @throws XMPPException if an error occurs revoking membership to a user.
1207      */
revokeMembership(Collection<String> jids)1208     public void revokeMembership(Collection<String> jids) throws XMPPException {
1209         changeAffiliationByAdmin(jids, "none");
1210     }
1211 
1212     /**
1213      * Revokes a user's membership. Only administrators are able to revoke membership. A user
1214      * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1215      * that a user cannot enter without being on the member list). If the user is in the room and
1216      * the room is of type members-only then the user will be removed from the room.
1217      *
1218      * @param jid the bare XMPP user ID of the user to revoke membership (e.g. "user@host.org").
1219      * @throws XMPPException if an error occurs revoking membership to a user.
1220      */
revokeMembership(String jid)1221     public void revokeMembership(String jid) throws XMPPException {
1222         changeAffiliationByAdmin(jid, "none", null);
1223     }
1224 
1225     /**
1226      * Grants moderator privileges to participants or visitors. Room administrators may grant
1227      * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
1228      * other users, modify room's subject plus all the partcipants privileges.
1229      *
1230      * @param nicknames the nicknames of the occupants to grant moderator privileges.
1231      * @throws XMPPException if an error occurs granting moderator privileges to a user.
1232      */
grantModerator(Collection<String> nicknames)1233     public void grantModerator(Collection<String> nicknames) throws XMPPException {
1234         changeRole(nicknames, "moderator");
1235     }
1236 
1237     /**
1238      * Grants moderator privileges to a participant or visitor. Room administrators may grant
1239      * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
1240      * other users, modify room's subject plus all the partcipants privileges.
1241      *
1242      * @param nickname the nickname of the occupant to grant moderator privileges.
1243      * @throws XMPPException if an error occurs granting moderator privileges to a user.
1244      */
grantModerator(String nickname)1245     public void grantModerator(String nickname) throws XMPPException {
1246         changeRole(nickname, "moderator", null);
1247     }
1248 
1249     /**
1250      * Revokes moderator privileges from other users. The occupant that loses moderator
1251      * privileges will become a participant. Room administrators may revoke moderator privileges
1252      * only to occupants whose affiliation is member or none. This means that an administrator is
1253      * not allowed to revoke moderator privileges from other room administrators or owners.
1254      *
1255      * @param nicknames the nicknames of the occupants to revoke moderator privileges.
1256      * @throws XMPPException if an error occurs revoking moderator privileges from a user.
1257      */
revokeModerator(Collection<String> nicknames)1258     public void revokeModerator(Collection<String> nicknames) throws XMPPException {
1259         changeRole(nicknames, "participant");
1260     }
1261 
1262     /**
1263      * Revokes moderator privileges from another user. The occupant that loses moderator
1264      * privileges will become a participant. Room administrators may revoke moderator privileges
1265      * only to occupants whose affiliation is member or none. This means that an administrator is
1266      * not allowed to revoke moderator privileges from other room administrators or owners.
1267      *
1268      * @param nickname the nickname of the occupant to revoke moderator privileges.
1269      * @throws XMPPException if an error occurs revoking moderator privileges from a user.
1270      */
revokeModerator(String nickname)1271     public void revokeModerator(String nickname) throws XMPPException {
1272         changeRole(nickname, "participant", null);
1273     }
1274 
1275     /**
1276      * Grants ownership privileges to other users. Room owners may grant ownership privileges.
1277      * Some room implementations will not allow to grant ownership privileges to other users.
1278      * An owner is allowed to change defining room features as well as perform all administrative
1279      * functions.
1280      *
1281      * @param jids the collection of bare XMPP user IDs of the users to grant ownership.
1282      * @throws XMPPException if an error occurs granting ownership privileges to a user.
1283      */
grantOwnership(Collection<String> jids)1284     public void grantOwnership(Collection<String> jids) throws XMPPException {
1285         changeAffiliationByAdmin(jids, "owner");
1286     }
1287 
1288     /**
1289      * Grants ownership privileges to another user. Room owners may grant ownership privileges.
1290      * Some room implementations will not allow to grant ownership privileges to other users.
1291      * An owner is allowed to change defining room features as well as perform all administrative
1292      * functions.
1293      *
1294      * @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user@host.org").
1295      * @throws XMPPException if an error occurs granting ownership privileges to a user.
1296      */
grantOwnership(String jid)1297     public void grantOwnership(String jid) throws XMPPException {
1298         changeAffiliationByAdmin(jid, "owner", null);
1299     }
1300 
1301     /**
1302      * Revokes ownership privileges from other users. The occupant that loses ownership
1303      * privileges will become an administrator. Room owners may revoke ownership privileges.
1304      * Some room implementations will not allow to grant ownership privileges to other users.
1305      *
1306      * @param jids the bare XMPP user IDs of the users to revoke ownership.
1307      * @throws XMPPException if an error occurs revoking ownership privileges from a user.
1308      */
revokeOwnership(Collection<String> jids)1309     public void revokeOwnership(Collection<String> jids) throws XMPPException {
1310         changeAffiliationByAdmin(jids, "admin");
1311     }
1312 
1313     /**
1314      * Revokes ownership privileges from another user. The occupant that loses ownership
1315      * privileges will become an administrator. Room owners may revoke ownership privileges.
1316      * Some room implementations will not allow to grant ownership privileges to other users.
1317      *
1318      * @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user@host.org").
1319      * @throws XMPPException if an error occurs revoking ownership privileges from a user.
1320      */
revokeOwnership(String jid)1321     public void revokeOwnership(String jid) throws XMPPException {
1322         changeAffiliationByAdmin(jid, "admin", null);
1323     }
1324 
1325     /**
1326      * Grants administrator privileges to other users. Room owners may grant administrator
1327      * privileges to a member or unaffiliated user. An administrator is allowed to perform
1328      * administrative functions such as banning users and edit moderator list.
1329      *
1330      * @param jids the bare XMPP user IDs of the users to grant administrator privileges.
1331      * @throws XMPPException if an error occurs granting administrator privileges to a user.
1332      */
grantAdmin(Collection<String> jids)1333     public void grantAdmin(Collection<String> jids) throws XMPPException {
1334         changeAffiliationByOwner(jids, "admin");
1335     }
1336 
1337     /**
1338      * Grants administrator privileges to another user. Room owners may grant administrator
1339      * privileges to a member or unaffiliated user. An administrator is allowed to perform
1340      * administrative functions such as banning users and edit moderator list.
1341      *
1342      * @param jid the bare XMPP user ID of the user to grant administrator privileges
1343      * (e.g. "user@host.org").
1344      * @throws XMPPException if an error occurs granting administrator privileges to a user.
1345      */
grantAdmin(String jid)1346     public void grantAdmin(String jid) throws XMPPException {
1347         changeAffiliationByOwner(jid, "admin");
1348     }
1349 
1350     /**
1351      * Revokes administrator privileges from users. The occupant that loses administrator
1352      * privileges will become a member. Room owners may revoke administrator privileges from
1353      * a member or unaffiliated user.
1354      *
1355      * @param jids the bare XMPP user IDs of the user to revoke administrator privileges.
1356      * @throws XMPPException if an error occurs revoking administrator privileges from a user.
1357      */
revokeAdmin(Collection<String> jids)1358     public void revokeAdmin(Collection<String> jids) throws XMPPException {
1359         changeAffiliationByOwner(jids, "member");
1360     }
1361 
1362     /**
1363      * Revokes administrator privileges from a user. The occupant that loses administrator
1364      * privileges will become a member. Room owners may revoke administrator privileges from
1365      * a member or unaffiliated user.
1366      *
1367      * @param jid the bare XMPP user ID of the user to revoke administrator privileges
1368      * (e.g. "user@host.org").
1369      * @throws XMPPException if an error occurs revoking administrator privileges from a user.
1370      */
revokeAdmin(String jid)1371     public void revokeAdmin(String jid) throws XMPPException {
1372         changeAffiliationByOwner(jid, "member");
1373     }
1374 
changeAffiliationByOwner(String jid, String affiliation)1375     private void changeAffiliationByOwner(String jid, String affiliation) throws XMPPException {
1376         MUCOwner iq = new MUCOwner();
1377         iq.setTo(room);
1378         iq.setType(IQ.Type.SET);
1379         // Set the new affiliation.
1380         MUCOwner.Item item = new MUCOwner.Item(affiliation);
1381         item.setJid(jid);
1382         iq.addItem(item);
1383 
1384         // Wait for a response packet back from the server.
1385         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1386         PacketCollector response = connection.createPacketCollector(responseFilter);
1387         // Send the change request to the server.
1388         connection.sendPacket(iq);
1389         // Wait up to a certain number of seconds for a reply.
1390         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1391         // Stop queuing results
1392         response.cancel();
1393 
1394         if (answer == null) {
1395             throw new XMPPException("No response from server.");
1396         }
1397         else if (answer.getError() != null) {
1398             throw new XMPPException(answer.getError());
1399         }
1400     }
1401 
changeAffiliationByOwner(Collection<String> jids, String affiliation)1402     private void changeAffiliationByOwner(Collection<String> jids, String affiliation)
1403             throws XMPPException {
1404         MUCOwner iq = new MUCOwner();
1405         iq.setTo(room);
1406         iq.setType(IQ.Type.SET);
1407         for (String jid : jids) {
1408             // Set the new affiliation.
1409             MUCOwner.Item item = new MUCOwner.Item(affiliation);
1410             item.setJid(jid);
1411             iq.addItem(item);
1412         }
1413 
1414         // Wait for a response packet back from the server.
1415         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1416         PacketCollector response = connection.createPacketCollector(responseFilter);
1417         // Send the change request to the server.
1418         connection.sendPacket(iq);
1419         // Wait up to a certain number of seconds for a reply.
1420         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1421         // Stop queuing results
1422         response.cancel();
1423 
1424         if (answer == null) {
1425             throw new XMPPException("No response from server.");
1426         }
1427         else if (answer.getError() != null) {
1428             throw new XMPPException(answer.getError());
1429         }
1430     }
1431 
1432     /**
1433      * Tries to change the affiliation with an 'muc#admin' namespace
1434      *
1435      * @param jid
1436      * @param affiliation
1437      * @param reason the reason for the affiliation change (optional)
1438      * @throws XMPPException
1439      */
changeAffiliationByAdmin(String jid, String affiliation, String reason)1440     private void changeAffiliationByAdmin(String jid, String affiliation, String reason)
1441             throws XMPPException {
1442         MUCAdmin iq = new MUCAdmin();
1443         iq.setTo(room);
1444         iq.setType(IQ.Type.SET);
1445         // Set the new affiliation.
1446         MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
1447         item.setJid(jid);
1448         if(reason != null)
1449             item.setReason(reason);
1450         iq.addItem(item);
1451 
1452         // Wait for a response packet back from the server.
1453         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1454         PacketCollector response = connection.createPacketCollector(responseFilter);
1455         // Send the change request to the server.
1456         connection.sendPacket(iq);
1457         // Wait up to a certain number of seconds for a reply.
1458         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1459         // Stop queuing results
1460         response.cancel();
1461 
1462         if (answer == null) {
1463             throw new XMPPException("No response from server.");
1464         }
1465         else if (answer.getError() != null) {
1466             throw new XMPPException(answer.getError());
1467         }
1468     }
1469 
changeAffiliationByAdmin(Collection<String> jids, String affiliation)1470     private void changeAffiliationByAdmin(Collection<String> jids, String affiliation)
1471             throws XMPPException {
1472         MUCAdmin iq = new MUCAdmin();
1473         iq.setTo(room);
1474         iq.setType(IQ.Type.SET);
1475         for (String jid : jids) {
1476             // Set the new affiliation.
1477             MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
1478             item.setJid(jid);
1479             iq.addItem(item);
1480         }
1481 
1482         // Wait for a response packet back from the server.
1483         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1484         PacketCollector response = connection.createPacketCollector(responseFilter);
1485         // Send the change request to the server.
1486         connection.sendPacket(iq);
1487         // Wait up to a certain number of seconds for a reply.
1488         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1489         // Stop queuing results
1490         response.cancel();
1491 
1492         if (answer == null) {
1493             throw new XMPPException("No response from server.");
1494         }
1495         else if (answer.getError() != null) {
1496             throw new XMPPException(answer.getError());
1497         }
1498     }
1499 
changeRole(String nickname, String role, String reason)1500     private void changeRole(String nickname, String role, String reason) throws XMPPException {
1501         MUCAdmin iq = new MUCAdmin();
1502         iq.setTo(room);
1503         iq.setType(IQ.Type.SET);
1504         // Set the new role.
1505         MUCAdmin.Item item = new MUCAdmin.Item(null, role);
1506         item.setNick(nickname);
1507         item.setReason(reason);
1508         iq.addItem(item);
1509 
1510         // Wait for a response packet back from the server.
1511         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1512         PacketCollector response = connection.createPacketCollector(responseFilter);
1513         // Send the change request to the server.
1514         connection.sendPacket(iq);
1515         // Wait up to a certain number of seconds for a reply.
1516         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1517         // Stop queuing results
1518         response.cancel();
1519 
1520         if (answer == null) {
1521             throw new XMPPException("No response from server.");
1522         }
1523         else if (answer.getError() != null) {
1524             throw new XMPPException(answer.getError());
1525         }
1526     }
1527 
changeRole(Collection<String> nicknames, String role)1528     private void changeRole(Collection<String> nicknames, String role) throws XMPPException {
1529         MUCAdmin iq = new MUCAdmin();
1530         iq.setTo(room);
1531         iq.setType(IQ.Type.SET);
1532         for (String nickname : nicknames) {
1533             // Set the new role.
1534             MUCAdmin.Item item = new MUCAdmin.Item(null, role);
1535             item.setNick(nickname);
1536             iq.addItem(item);
1537         }
1538 
1539         // Wait for a response packet back from the server.
1540         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1541         PacketCollector response = connection.createPacketCollector(responseFilter);
1542         // Send the change request to the server.
1543         connection.sendPacket(iq);
1544         // Wait up to a certain number of seconds for a reply.
1545         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1546         // Stop queuing results
1547         response.cancel();
1548 
1549         if (answer == null) {
1550             throw new XMPPException("No response from server.");
1551         }
1552         else if (answer.getError() != null) {
1553             throw new XMPPException(answer.getError());
1554         }
1555     }
1556 
1557     /**
1558      * Returns the number of occupants in the group chat.<p>
1559      *
1560      * Note: this value will only be accurate after joining the group chat, and
1561      * may fluctuate over time. If you query this value directly after joining the
1562      * group chat it may not be accurate, as it takes a certain amount of time for
1563      * the server to send all presence packets to this client.
1564      *
1565      * @return the number of occupants in the group chat.
1566      */
getOccupantsCount()1567     public int getOccupantsCount() {
1568         return occupantsMap.size();
1569     }
1570 
1571     /**
1572      * Returns an Iterator (of Strings) for the list of fully qualified occupants
1573      * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser".
1574      * Typically, a client would only display the nickname of the occupant. To
1575      * get the nickname from the fully qualified name, use the
1576      * {@link org.jivesoftware.smack.util.StringUtils#parseResource(String)} method.
1577      * Note: this value will only be accurate after joining the group chat, and may
1578      * fluctuate over time.
1579      *
1580      * @return an Iterator for the occupants in the group chat.
1581      */
getOccupants()1582     public Iterator<String> getOccupants() {
1583         return Collections.unmodifiableList(new ArrayList<String>(occupantsMap.keySet()))
1584                 .iterator();
1585     }
1586 
1587     /**
1588      * Returns the presence info for a particular user, or <tt>null</tt> if the user
1589      * is not in the room.<p>
1590      *
1591      * @param user the room occupant to search for his presence. The format of user must
1592      * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
1593      * @return the occupant's current presence, or <tt>null</tt> if the user is unavailable
1594      *      or if no presence information is available.
1595      */
getOccupantPresence(String user)1596     public Presence getOccupantPresence(String user) {
1597         return occupantsMap.get(user);
1598     }
1599 
1600     /**
1601      * Returns the Occupant information for a particular occupant, or <tt>null</tt> if the
1602      * user is not in the room. The Occupant object may include information such as full
1603      * JID of the user as well as the role and affiliation of the user in the room.<p>
1604      *
1605      * @param user the room occupant to search for his presence. The format of user must
1606      * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
1607      * @return the Occupant or <tt>null</tt> if the user is unavailable (i.e. not in the room).
1608      */
getOccupant(String user)1609     public Occupant getOccupant(String user) {
1610         Presence presence = occupantsMap.get(user);
1611         if (presence != null) {
1612             return new Occupant(presence);
1613         }
1614         return null;
1615     }
1616 
1617     /**
1618      * Adds a packet listener that will be notified of any new Presence packets
1619      * sent to the group chat. Using a listener is a suitable way to know when the list
1620      * of occupants should be re-loaded due to any changes.
1621      *
1622      * @param listener a packet listener that will be notified of any presence packets
1623      *      sent to the group chat.
1624      */
addParticipantListener(PacketListener listener)1625     public void addParticipantListener(PacketListener listener) {
1626         connection.addPacketListener(listener, presenceFilter);
1627         connectionListeners.add(listener);
1628     }
1629 
1630     /**
1631      * Remoces a packet listener that was being notified of any new Presence packets
1632      * sent to the group chat.
1633      *
1634      * @param listener a packet listener that was being notified of any presence packets
1635      *      sent to the group chat.
1636      */
removeParticipantListener(PacketListener listener)1637     public void removeParticipantListener(PacketListener listener) {
1638         connection.removePacketListener(listener);
1639         connectionListeners.remove(listener);
1640     }
1641 
1642     /**
1643      * Returns a collection of <code>Affiliate</code> with the room owners.
1644      *
1645      * @return a collection of <code>Affiliate</code> with the room owners.
1646      * @throws XMPPException if an error occured while performing the request to the server or you
1647      *         don't have enough privileges to get this information.
1648      */
getOwners()1649     public Collection<Affiliate> getOwners() throws XMPPException {
1650         return getAffiliatesByAdmin("owner");
1651     }
1652 
1653     /**
1654      * Returns a collection of <code>Affiliate</code> with the room administrators.
1655      *
1656      * @return a collection of <code>Affiliate</code> with the room administrators.
1657      * @throws XMPPException if an error occured while performing the request to the server or you
1658      *         don't have enough privileges to get this information.
1659      */
getAdmins()1660     public Collection<Affiliate> getAdmins() throws XMPPException {
1661         return getAffiliatesByOwner("admin");
1662     }
1663 
1664     /**
1665      * Returns a collection of <code>Affiliate</code> with the room members.
1666      *
1667      * @return a collection of <code>Affiliate</code> with the room members.
1668      * @throws XMPPException if an error occured while performing the request to the server or you
1669      *         don't have enough privileges to get this information.
1670      */
getMembers()1671     public Collection<Affiliate> getMembers() throws XMPPException {
1672         return getAffiliatesByAdmin("member");
1673     }
1674 
1675     /**
1676      * Returns a collection of <code>Affiliate</code> with the room outcasts.
1677      *
1678      * @return a collection of <code>Affiliate</code> with the room outcasts.
1679      * @throws XMPPException if an error occured while performing the request to the server or you
1680      *         don't have enough privileges to get this information.
1681      */
getOutcasts()1682     public Collection<Affiliate> getOutcasts() throws XMPPException {
1683         return getAffiliatesByAdmin("outcast");
1684     }
1685 
1686     /**
1687      * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
1688      * sending a request in the owner namespace.
1689      *
1690      * @param affiliation the affiliation of the users in the room.
1691      * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
1692      * @throws XMPPException if an error occured while performing the request to the server or you
1693      *         don't have enough privileges to get this information.
1694      */
getAffiliatesByOwner(String affiliation)1695     private Collection<Affiliate> getAffiliatesByOwner(String affiliation) throws XMPPException {
1696         MUCOwner iq = new MUCOwner();
1697         iq.setTo(room);
1698         iq.setType(IQ.Type.GET);
1699         // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
1700         MUCOwner.Item item = new MUCOwner.Item(affiliation);
1701         iq.addItem(item);
1702 
1703         // Wait for a response packet back from the server.
1704         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1705         PacketCollector response = connection.createPacketCollector(responseFilter);
1706         // Send the request to the server.
1707         connection.sendPacket(iq);
1708         // Wait up to a certain number of seconds for a reply.
1709         MUCOwner answer = (MUCOwner) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1710         // Stop queuing results
1711         response.cancel();
1712 
1713         if (answer == null) {
1714             throw new XMPPException("No response from server.");
1715         }
1716         else if (answer.getError() != null) {
1717             throw new XMPPException(answer.getError());
1718         }
1719         // Get the list of affiliates from the server's answer
1720         List<Affiliate> affiliates = new ArrayList<Affiliate>();
1721         for (Iterator<MUCOwner.Item> it = answer.getItems(); it.hasNext();) {
1722             affiliates.add(new Affiliate(it.next()));
1723         }
1724         return affiliates;
1725     }
1726 
1727     /**
1728      * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
1729      * sending a request in the admin namespace.
1730      *
1731      * @param affiliation the affiliation of the users in the room.
1732      * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
1733      * @throws XMPPException if an error occured while performing the request to the server or you
1734      *         don't have enough privileges to get this information.
1735      */
getAffiliatesByAdmin(String affiliation)1736     private Collection<Affiliate> getAffiliatesByAdmin(String affiliation) throws XMPPException {
1737         MUCAdmin iq = new MUCAdmin();
1738         iq.setTo(room);
1739         iq.setType(IQ.Type.GET);
1740         // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
1741         MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
1742         iq.addItem(item);
1743 
1744         // Wait for a response packet back from the server.
1745         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1746         PacketCollector response = connection.createPacketCollector(responseFilter);
1747         // Send the request to the server.
1748         connection.sendPacket(iq);
1749         // Wait up to a certain number of seconds for a reply.
1750         MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1751         // Stop queuing results
1752         response.cancel();
1753 
1754         if (answer == null) {
1755             throw new XMPPException("No response from server.");
1756         }
1757         else if (answer.getError() != null) {
1758             throw new XMPPException(answer.getError());
1759         }
1760         // Get the list of affiliates from the server's answer
1761         List<Affiliate> affiliates = new ArrayList<Affiliate>();
1762         for (Iterator<MUCAdmin.Item> it = answer.getItems(); it.hasNext();) {
1763             affiliates.add(new Affiliate(it.next()));
1764         }
1765         return affiliates;
1766     }
1767 
1768     /**
1769      * Returns a collection of <code>Occupant</code> with the room moderators.
1770      *
1771      * @return a collection of <code>Occupant</code> with the room moderators.
1772      * @throws XMPPException if an error occured while performing the request to the server or you
1773      *         don't have enough privileges to get this information.
1774      */
getModerators()1775     public Collection<Occupant> getModerators() throws XMPPException {
1776         return getOccupants("moderator");
1777     }
1778 
1779     /**
1780      * Returns a collection of <code>Occupant</code> with the room participants.
1781      *
1782      * @return a collection of <code>Occupant</code> with the room participants.
1783      * @throws XMPPException if an error occured while performing the request to the server or you
1784      *         don't have enough privileges to get this information.
1785      */
getParticipants()1786     public Collection<Occupant> getParticipants() throws XMPPException {
1787         return getOccupants("participant");
1788     }
1789 
1790     /**
1791      * Returns a collection of <code>Occupant</code> that have the specified room role.
1792      *
1793      * @param role the role of the occupant in the room.
1794      * @return a collection of <code>Occupant</code> that have the specified room role.
1795      * @throws XMPPException if an error occured while performing the request to the server or you
1796      *         don't have enough privileges to get this information.
1797      */
getOccupants(String role)1798     private Collection<Occupant> getOccupants(String role) throws XMPPException {
1799         MUCAdmin iq = new MUCAdmin();
1800         iq.setTo(room);
1801         iq.setType(IQ.Type.GET);
1802         // Set the specified role. This may request the list of moderators/participants.
1803         MUCAdmin.Item item = new MUCAdmin.Item(null, role);
1804         iq.addItem(item);
1805 
1806         // Wait for a response packet back from the server.
1807         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1808         PacketCollector response = connection.createPacketCollector(responseFilter);
1809         // Send the request to the server.
1810         connection.sendPacket(iq);
1811         // Wait up to a certain number of seconds for a reply.
1812         MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1813         // Stop queuing results
1814         response.cancel();
1815 
1816         if (answer == null) {
1817             throw new XMPPException("No response from server.");
1818         }
1819         else if (answer.getError() != null) {
1820             throw new XMPPException(answer.getError());
1821         }
1822         // Get the list of participants from the server's answer
1823         List<Occupant> participants = new ArrayList<Occupant>();
1824         for (Iterator<MUCAdmin.Item> it = answer.getItems(); it.hasNext();) {
1825             participants.add(new Occupant(it.next()));
1826         }
1827         return participants;
1828     }
1829 
1830     /**
1831      * Sends a message to the chat room.
1832      *
1833      * @param text the text of the message to send.
1834      * @throws XMPPException if sending the message fails.
1835      */
sendMessage(String text)1836     public void sendMessage(String text) throws XMPPException {
1837         Message message = new Message(room, Message.Type.groupchat);
1838         message.setBody(text);
1839         connection.sendPacket(message);
1840     }
1841 
1842     /**
1843      * Returns a new Chat for sending private messages to a given room occupant.
1844      * The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server
1845      * service will change the 'from' address to the sender's room JID and delivering the message
1846      * to the intended recipient's full JID.
1847      *
1848      * @param occupant occupant unique room JID (e.g. 'darkcave@macbeth.shakespeare.lit/Paul').
1849      * @param listener the listener is a message listener that will handle messages for the newly
1850      * created chat.
1851      * @return new Chat for sending private messages to a given room occupant.
1852      */
createPrivateChat(String occupant, MessageListener listener)1853     public Chat createPrivateChat(String occupant, MessageListener listener) {
1854         return connection.getChatManager().createChat(occupant, listener);
1855     }
1856 
1857     /**
1858      * Creates a new Message to send to the chat room.
1859      *
1860      * @return a new Message addressed to the chat room.
1861      */
createMessage()1862     public Message createMessage() {
1863         return new Message(room, Message.Type.groupchat);
1864     }
1865 
1866     /**
1867      * Sends a Message to the chat room.
1868      *
1869      * @param message the message.
1870      * @throws XMPPException if sending the message fails.
1871      */
sendMessage(Message message)1872     public void sendMessage(Message message) throws XMPPException {
1873         connection.sendPacket(message);
1874     }
1875 
1876     /**
1877     * Polls for and returns the next message, or <tt>null</tt> if there isn't
1878     * a message immediately available. This method provides significantly different
1879     * functionalty than the {@link #nextMessage()} method since it's non-blocking.
1880     * In other words, the method call will always return immediately, whereas the
1881     * nextMessage method will return only when a message is available (or after
1882     * a specific timeout).
1883     *
1884     * @return the next message if one is immediately available and
1885     *      <tt>null</tt> otherwise.
1886     */
pollMessage()1887     public Message pollMessage() {
1888         return (Message) messageCollector.pollResult();
1889     }
1890 
1891     /**
1892      * Returns the next available message in the chat. The method call will block
1893      * (not return) until a message is available.
1894      *
1895      * @return the next message.
1896      */
nextMessage()1897     public Message nextMessage() {
1898         return (Message) messageCollector.nextResult();
1899     }
1900 
1901     /**
1902      * Returns the next available message in the chat. The method call will block
1903      * (not return) until a packet is available or the <tt>timeout</tt> has elapased.
1904      * If the timeout elapses without a result, <tt>null</tt> will be returned.
1905      *
1906      * @param timeout the maximum amount of time to wait for the next message.
1907      * @return the next message, or <tt>null</tt> if the timeout elapses without a
1908      *      message becoming available.
1909      */
nextMessage(long timeout)1910     public Message nextMessage(long timeout) {
1911         return (Message) messageCollector.nextResult(timeout);
1912     }
1913 
1914     /**
1915      * Adds a packet listener that will be notified of any new messages in the
1916      * group chat. Only "group chat" messages addressed to this group chat will
1917      * be delivered to the listener. If you wish to listen for other packets
1918      * that may be associated with this group chat, you should register a
1919      * PacketListener directly with the Connection with the appropriate
1920      * PacketListener.
1921      *
1922      * @param listener a packet listener.
1923      */
addMessageListener(PacketListener listener)1924     public void addMessageListener(PacketListener listener) {
1925         connection.addPacketListener(listener, messageFilter);
1926         connectionListeners.add(listener);
1927     }
1928 
1929     /**
1930      * Removes a packet listener that was being notified of any new messages in the
1931      * multi user chat. Only "group chat" messages addressed to this multi user chat were
1932      * being delivered to the listener.
1933      *
1934      * @param listener a packet listener.
1935      */
removeMessageListener(PacketListener listener)1936     public void removeMessageListener(PacketListener listener) {
1937         connection.removePacketListener(listener);
1938         connectionListeners.remove(listener);
1939     }
1940 
1941     /**
1942      * Changes the subject within the room. As a default, only users with a role of "moderator"
1943      * are allowed to change the subject in a room. Although some rooms may be configured to
1944      * allow a mere participant or even a visitor to change the subject.
1945      *
1946      * @param subject the new room's subject to set.
1947      * @throws XMPPException if someone without appropriate privileges attempts to change the
1948      *          room subject will throw an error with code 403 (i.e. Forbidden)
1949      */
changeSubject(final String subject)1950     public void changeSubject(final String subject) throws XMPPException {
1951         Message message = new Message(room, Message.Type.groupchat);
1952         message.setSubject(subject);
1953         // Wait for an error or confirmation message back from the server.
1954         PacketFilter responseFilter =
1955             new AndFilter(
1956                 new FromMatchesFilter(room),
1957                 new PacketTypeFilter(Message.class));
1958         responseFilter = new AndFilter(responseFilter, new PacketFilter() {
1959             public boolean accept(Packet packet) {
1960                 Message msg = (Message) packet;
1961                 return subject.equals(msg.getSubject());
1962             }
1963         });
1964         PacketCollector response = connection.createPacketCollector(responseFilter);
1965         // Send change subject packet.
1966         connection.sendPacket(message);
1967         // Wait up to a certain number of seconds for a reply.
1968         Message answer =
1969             (Message) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1970         // Stop queuing results
1971         response.cancel();
1972 
1973         if (answer == null) {
1974             throw new XMPPException("No response from server.");
1975         }
1976         else if (answer.getError() != null) {
1977             throw new XMPPException(answer.getError());
1978         }
1979     }
1980 
1981     /**
1982      * Notification message that the user has joined the room.
1983      */
userHasJoined()1984     private synchronized void userHasJoined() {
1985         // Update the list of joined rooms through this connection
1986         List<String> rooms = joinedRooms.get(connection);
1987         if (rooms == null) {
1988             rooms = new ArrayList<String>();
1989             joinedRooms.put(connection, rooms);
1990         }
1991         rooms.add(room);
1992     }
1993 
1994     /**
1995      * Notification message that the user has left the room.
1996      */
userHasLeft()1997     private synchronized void userHasLeft() {
1998         // Update the list of joined rooms through this connection
1999         List<String> rooms = joinedRooms.get(connection);
2000         if (rooms == null) {
2001             return;
2002         }
2003         rooms.remove(room);
2004         cleanup();
2005     }
2006 
2007     /**
2008      * Returns the MUCUser packet extension included in the packet or <tt>null</tt> if none.
2009      *
2010      * @param packet the packet that may include the MUCUser extension.
2011      * @return the MUCUser found in the packet.
2012      */
getMUCUserExtension(Packet packet)2013     private MUCUser getMUCUserExtension(Packet packet) {
2014         if (packet != null) {
2015             // Get the MUC User extension
2016             return (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
2017         }
2018         return null;
2019     }
2020 
2021     /**
2022      * Adds a listener that will be notified of changes in your status in the room
2023      * such as the user being kicked, banned, or granted admin permissions.
2024      *
2025      * @param listener a user status listener.
2026      */
addUserStatusListener(UserStatusListener listener)2027     public void addUserStatusListener(UserStatusListener listener) {
2028         synchronized (userStatusListeners) {
2029             if (!userStatusListeners.contains(listener)) {
2030                 userStatusListeners.add(listener);
2031             }
2032         }
2033     }
2034 
2035     /**
2036      * Removes a listener that was being notified of changes in your status in the room
2037      * such as the user being kicked, banned, or granted admin permissions.
2038      *
2039      * @param listener a user status listener.
2040      */
removeUserStatusListener(UserStatusListener listener)2041     public void removeUserStatusListener(UserStatusListener listener) {
2042         synchronized (userStatusListeners) {
2043             userStatusListeners.remove(listener);
2044         }
2045     }
2046 
fireUserStatusListeners(String methodName, Object[] params)2047     private void fireUserStatusListeners(String methodName, Object[] params) {
2048         UserStatusListener[] listeners;
2049         synchronized (userStatusListeners) {
2050             listeners = new UserStatusListener[userStatusListeners.size()];
2051             userStatusListeners.toArray(listeners);
2052         }
2053         // Get the classes of the method parameters
2054         Class<?>[] paramClasses = new Class[params.length];
2055         for (int i = 0; i < params.length; i++) {
2056             paramClasses[i] = params[i].getClass();
2057         }
2058         try {
2059             // Get the method to execute based on the requested methodName and parameters classes
2060             Method method = UserStatusListener.class.getDeclaredMethod(methodName, paramClasses);
2061             for (UserStatusListener listener : listeners) {
2062                 method.invoke(listener, params);
2063             }
2064         } catch (NoSuchMethodException e) {
2065             e.printStackTrace();
2066         } catch (InvocationTargetException e) {
2067             e.printStackTrace();
2068         } catch (IllegalAccessException e) {
2069             e.printStackTrace();
2070         }
2071     }
2072 
2073     /**
2074      * Adds a listener that will be notified of changes in occupants status in the room
2075      * such as the user being kicked, banned, or granted admin permissions.
2076      *
2077      * @param listener a participant status listener.
2078      */
addParticipantStatusListener(ParticipantStatusListener listener)2079     public void addParticipantStatusListener(ParticipantStatusListener listener) {
2080         synchronized (participantStatusListeners) {
2081             if (!participantStatusListeners.contains(listener)) {
2082                 participantStatusListeners.add(listener);
2083             }
2084         }
2085     }
2086 
2087     /**
2088      * Removes a listener that was being notified of changes in occupants status in the room
2089      * such as the user being kicked, banned, or granted admin permissions.
2090      *
2091      * @param listener a participant status listener.
2092      */
removeParticipantStatusListener(ParticipantStatusListener listener)2093     public void removeParticipantStatusListener(ParticipantStatusListener listener) {
2094         synchronized (participantStatusListeners) {
2095             participantStatusListeners.remove(listener);
2096         }
2097     }
2098 
fireParticipantStatusListeners(String methodName, List<String> params)2099     private void fireParticipantStatusListeners(String methodName, List<String> params) {
2100         ParticipantStatusListener[] listeners;
2101         synchronized (participantStatusListeners) {
2102             listeners = new ParticipantStatusListener[participantStatusListeners.size()];
2103             participantStatusListeners.toArray(listeners);
2104         }
2105         try {
2106             // Get the method to execute based on the requested methodName and parameter
2107             Class<?>[] classes = new Class[params.size()];
2108             for (int i=0;i<params.size(); i++) {
2109                 classes[i] = String.class;
2110             }
2111             Method method = ParticipantStatusListener.class.getDeclaredMethod(methodName, classes);
2112             for (ParticipantStatusListener listener : listeners) {
2113                 method.invoke(listener, params.toArray());
2114             }
2115         } catch (NoSuchMethodException e) {
2116             e.printStackTrace();
2117         } catch (InvocationTargetException e) {
2118             e.printStackTrace();
2119         } catch (IllegalAccessException e) {
2120             e.printStackTrace();
2121         }
2122     }
2123 
init()2124     private void init() {
2125         // Create filters
2126         messageFilter =
2127             new AndFilter(
2128                 new FromMatchesFilter(room),
2129                 new MessageTypeFilter(Message.Type.groupchat));
2130         messageFilter = new AndFilter(messageFilter, new PacketFilter() {
2131             public boolean accept(Packet packet) {
2132                 Message msg = (Message) packet;
2133                 return msg.getBody() != null;
2134             }
2135         });
2136         presenceFilter =
2137             new AndFilter(new FromMatchesFilter(room), new PacketTypeFilter(Presence.class));
2138 
2139         // Create a collector for incoming messages.
2140         messageCollector = new ConnectionDetachedPacketCollector();
2141 
2142         // Create a listener for subject updates.
2143         PacketListener subjectListener = new PacketListener() {
2144             public void processPacket(Packet packet) {
2145                 Message msg = (Message) packet;
2146                 // Update the room subject
2147                 subject = msg.getSubject();
2148                 // Fire event for subject updated listeners
2149                 fireSubjectUpdatedListeners(
2150                     msg.getSubject(),
2151                     msg.getFrom());
2152 
2153             }
2154         };
2155 
2156         // Create a listener for all presence updates.
2157         PacketListener presenceListener = new PacketListener() {
2158             public void processPacket(Packet packet) {
2159                 Presence presence = (Presence) packet;
2160                 String from = presence.getFrom();
2161                 String myRoomJID = room + "/" + nickname;
2162                 boolean isUserStatusModification = presence.getFrom().equals(myRoomJID);
2163                 if (presence.getType() == Presence.Type.available) {
2164                     Presence oldPresence = occupantsMap.put(from, presence);
2165                     if (oldPresence != null) {
2166                         // Get the previous occupant's affiliation & role
2167                         MUCUser mucExtension = getMUCUserExtension(oldPresence);
2168                         String oldAffiliation = mucExtension.getItem().getAffiliation();
2169                         String oldRole = mucExtension.getItem().getRole();
2170                         // Get the new occupant's affiliation & role
2171                         mucExtension = getMUCUserExtension(presence);
2172                         String newAffiliation = mucExtension.getItem().getAffiliation();
2173                         String newRole = mucExtension.getItem().getRole();
2174                         // Fire role modification events
2175                         checkRoleModifications(oldRole, newRole, isUserStatusModification, from);
2176                         // Fire affiliation modification events
2177                         checkAffiliationModifications(
2178                             oldAffiliation,
2179                             newAffiliation,
2180                             isUserStatusModification,
2181                             from);
2182                     }
2183                     else {
2184                         // A new occupant has joined the room
2185                         if (!isUserStatusModification) {
2186                             List<String> params = new ArrayList<String>();
2187                             params.add(from);
2188                             fireParticipantStatusListeners("joined", params);
2189                         }
2190                     }
2191                 }
2192                 else if (presence.getType() == Presence.Type.unavailable) {
2193                     occupantsMap.remove(from);
2194                     MUCUser mucUser = getMUCUserExtension(presence);
2195                     if (mucUser != null && mucUser.getStatus() != null) {
2196                         // Fire events according to the received presence code
2197                         checkPresenceCode(
2198                             mucUser.getStatus().getCode(),
2199                             presence.getFrom().equals(myRoomJID),
2200                             mucUser,
2201                             from);
2202                     } else {
2203                         // An occupant has left the room
2204                         if (!isUserStatusModification) {
2205                             List<String> params = new ArrayList<String>();
2206                             params.add(from);
2207                             fireParticipantStatusListeners("left", params);
2208                         }
2209                     }
2210                 }
2211             }
2212         };
2213 
2214         // Listens for all messages that include a MUCUser extension and fire the invitation
2215         // rejection listeners if the message includes an invitation rejection.
2216         PacketListener declinesListener = new PacketListener() {
2217             public void processPacket(Packet packet) {
2218                 // Get the MUC User extension
2219                 MUCUser mucUser = getMUCUserExtension(packet);
2220                 // Check if the MUCUser informs that the invitee has declined the invitation
2221                 if (mucUser.getDecline() != null &&
2222                         ((Message) packet).getType() != Message.Type.error) {
2223                     // Fire event for invitation rejection listeners
2224                     fireInvitationRejectionListeners(
2225                         mucUser.getDecline().getFrom(),
2226                         mucUser.getDecline().getReason());
2227                 }
2228             }
2229         };
2230 
2231         PacketMultiplexListener packetMultiplexor = new PacketMultiplexListener(
2232                 messageCollector, presenceListener, subjectListener,
2233                 declinesListener);
2234 
2235         roomListenerMultiplexor = RoomListenerMultiplexor.getRoomMultiplexor(connection);
2236 
2237         roomListenerMultiplexor.addRoom(room, packetMultiplexor);
2238     }
2239 
2240     /**
2241      * Fires notification events if the role of a room occupant has changed. If the occupant that
2242      * changed his role is your occupant then the <code>UserStatusListeners</code> added to this
2243      * <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed
2244      * his role is not yours then the <code>ParticipantStatusListeners</code> added to this
2245      * <code>MultiUserChat</code> will be fired. The following table shows the events that will
2246      * be fired depending on the previous and new role of the occupant.
2247      *
2248      * <pre>
2249      * <table border="1">
2250      * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
2251      *
2252      * <tr><td>None</td><td>Visitor</td><td>--</td></tr>
2253      * <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr>
2254      * <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr>
2255      *
2256      * <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr>
2257      * <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
2258      * <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
2259      *
2260      * <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr>
2261      * <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr>
2262      * <tr><td>Visitor</td><td>None</td><td>kicked</td></tr>
2263      *
2264      * <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr>
2265      * <tr><td>Moderator</td><td>None</td><td>kicked</td></tr>
2266      * <tr><td>Participant</td><td>None</td><td>kicked</td></tr>
2267      * </table>
2268      * </pre>
2269      *
2270      * @param oldRole the previous role of the user in the room before receiving the new presence
2271      * @param newRole the new role of the user in the room after receiving the new presence
2272      * @param isUserModification whether the received presence is about your user in the room or not
2273      * @param from the occupant whose role in the room has changed
2274      * (e.g. room@conference.jabber.org/nick).
2275      */
checkRoleModifications( String oldRole, String newRole, boolean isUserModification, String from)2276     private void checkRoleModifications(
2277         String oldRole,
2278         String newRole,
2279         boolean isUserModification,
2280         String from) {
2281         // Voice was granted to a visitor
2282         if (("visitor".equals(oldRole) || "none".equals(oldRole))
2283             && "participant".equals(newRole)) {
2284             if (isUserModification) {
2285                 fireUserStatusListeners("voiceGranted", new Object[] {});
2286             }
2287             else {
2288                 List<String> params = new ArrayList<String>();
2289                 params.add(from);
2290                 fireParticipantStatusListeners("voiceGranted", params);
2291             }
2292         }
2293         // The participant's voice was revoked from the room
2294         else if (
2295             "participant".equals(oldRole)
2296                 && ("visitor".equals(newRole) || "none".equals(newRole))) {
2297             if (isUserModification) {
2298                 fireUserStatusListeners("voiceRevoked", new Object[] {});
2299             }
2300             else {
2301                 List<String> params = new ArrayList<String>();
2302                 params.add(from);
2303                 fireParticipantStatusListeners("voiceRevoked", params);
2304             }
2305         }
2306         // Moderator privileges were granted to a participant
2307         if (!"moderator".equals(oldRole) && "moderator".equals(newRole)) {
2308             if ("visitor".equals(oldRole) || "none".equals(oldRole)) {
2309                 if (isUserModification) {
2310                     fireUserStatusListeners("voiceGranted", new Object[] {});
2311                 }
2312                 else {
2313                     List<String> params = new ArrayList<String>();
2314                     params.add(from);
2315                     fireParticipantStatusListeners("voiceGranted", params);
2316                 }
2317             }
2318             if (isUserModification) {
2319                 fireUserStatusListeners("moderatorGranted", new Object[] {});
2320             }
2321             else {
2322                 List<String> params = new ArrayList<String>();
2323                 params.add(from);
2324                 fireParticipantStatusListeners("moderatorGranted", params);
2325             }
2326         }
2327         // Moderator privileges were revoked from a participant
2328         else if ("moderator".equals(oldRole) && !"moderator".equals(newRole)) {
2329             if ("visitor".equals(newRole) || "none".equals(newRole)) {
2330                 if (isUserModification) {
2331                     fireUserStatusListeners("voiceRevoked", new Object[] {});
2332                 }
2333                 else {
2334                     List<String> params = new ArrayList<String>();
2335                     params.add(from);
2336                     fireParticipantStatusListeners("voiceRevoked", params);
2337                 }
2338             }
2339             if (isUserModification) {
2340                 fireUserStatusListeners("moderatorRevoked", new Object[] {});
2341             }
2342             else {
2343                 List<String> params = new ArrayList<String>();
2344                 params.add(from);
2345                 fireParticipantStatusListeners("moderatorRevoked", params);
2346             }
2347         }
2348     }
2349 
2350     /**
2351      * Fires notification events if the affiliation of a room occupant has changed. If the
2352      * occupant that changed his affiliation is your occupant then the
2353      * <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired.
2354      * On the other hand, if the occupant that changed his affiliation is not yours then the
2355      * <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be
2356      * fired. The following table shows the events that will be fired depending on the previous
2357      * and new affiliation of the occupant.
2358      *
2359      * <pre>
2360      * <table border="1">
2361      * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
2362      *
2363      * <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr>
2364      * <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr>
2365      * <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr>
2366      *
2367      * <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr>
2368      * <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr>
2369      * <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr>
2370      *
2371      * <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr>
2372      * <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr>
2373      * <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr>
2374      *
2375      * <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr>
2376      * <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr>
2377      * <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr>
2378      * <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr>
2379      * </table>
2380      * </pre>
2381      *
2382      * @param oldAffiliation the previous affiliation of the user in the room before receiving the
2383      * new presence
2384      * @param newAffiliation the new affiliation of the user in the room after receiving the new
2385      * presence
2386      * @param isUserModification whether the received presence is about your user in the room or not
2387      * @param from the occupant whose role in the room has changed
2388      * (e.g. room@conference.jabber.org/nick).
2389      */
checkAffiliationModifications( String oldAffiliation, String newAffiliation, boolean isUserModification, String from)2390     private void checkAffiliationModifications(
2391         String oldAffiliation,
2392         String newAffiliation,
2393         boolean isUserModification,
2394         String from) {
2395         // First check for revoked affiliation and then for granted affiliations. The idea is to
2396         // first fire the "revoke" events and then fire the "grant" events.
2397 
2398         // The user's ownership to the room was revoked
2399         if ("owner".equals(oldAffiliation) && !"owner".equals(newAffiliation)) {
2400             if (isUserModification) {
2401                 fireUserStatusListeners("ownershipRevoked", new Object[] {});
2402             }
2403             else {
2404                 List<String> params = new ArrayList<String>();
2405                 params.add(from);
2406                 fireParticipantStatusListeners("ownershipRevoked", params);
2407             }
2408         }
2409         // The user's administrative privileges to the room were revoked
2410         else if ("admin".equals(oldAffiliation) && !"admin".equals(newAffiliation)) {
2411             if (isUserModification) {
2412                 fireUserStatusListeners("adminRevoked", new Object[] {});
2413             }
2414             else {
2415                 List<String> params = new ArrayList<String>();
2416                 params.add(from);
2417                 fireParticipantStatusListeners("adminRevoked", params);
2418             }
2419         }
2420         // The user's membership to the room was revoked
2421         else if ("member".equals(oldAffiliation) && !"member".equals(newAffiliation)) {
2422             if (isUserModification) {
2423                 fireUserStatusListeners("membershipRevoked", new Object[] {});
2424             }
2425             else {
2426                 List<String> params = new ArrayList<String>();
2427                 params.add(from);
2428                 fireParticipantStatusListeners("membershipRevoked", params);
2429             }
2430         }
2431 
2432         // The user was granted ownership to the room
2433         if (!"owner".equals(oldAffiliation) && "owner".equals(newAffiliation)) {
2434             if (isUserModification) {
2435                 fireUserStatusListeners("ownershipGranted", new Object[] {});
2436             }
2437             else {
2438                 List<String> params = new ArrayList<String>();
2439                 params.add(from);
2440                 fireParticipantStatusListeners("ownershipGranted", params);
2441             }
2442         }
2443         // The user was granted administrative privileges to the room
2444         else if (!"admin".equals(oldAffiliation) && "admin".equals(newAffiliation)) {
2445             if (isUserModification) {
2446                 fireUserStatusListeners("adminGranted", new Object[] {});
2447             }
2448             else {
2449                 List<String> params = new ArrayList<String>();
2450                 params.add(from);
2451                 fireParticipantStatusListeners("adminGranted", params);
2452             }
2453         }
2454         // The user was granted membership to the room
2455         else if (!"member".equals(oldAffiliation) && "member".equals(newAffiliation)) {
2456             if (isUserModification) {
2457                 fireUserStatusListeners("membershipGranted", new Object[] {});
2458             }
2459             else {
2460                 List<String> params = new ArrayList<String>();
2461                 params.add(from);
2462                 fireParticipantStatusListeners("membershipGranted", params);
2463             }
2464         }
2465     }
2466 
2467     /**
2468      * Fires events according to the received presence code.
2469      *
2470      * @param code
2471      * @param isUserModification
2472      * @param mucUser
2473      * @param from
2474      */
checkPresenceCode( String code, boolean isUserModification, MUCUser mucUser, String from)2475     private void checkPresenceCode(
2476         String code,
2477         boolean isUserModification,
2478         MUCUser mucUser,
2479         String from) {
2480         // Check if an occupant was kicked from the room
2481         if ("307".equals(code)) {
2482             // Check if this occupant was kicked
2483             if (isUserModification) {
2484                 joined = false;
2485 
2486                 fireUserStatusListeners(
2487                     "kicked",
2488                     new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});
2489 
2490                 // Reset occupant information.
2491                 occupantsMap.clear();
2492                 nickname = null;
2493                 userHasLeft();
2494             }
2495             else {
2496                 List<String> params = new ArrayList<String>();
2497                 params.add(from);
2498                 params.add(mucUser.getItem().getActor());
2499                 params.add(mucUser.getItem().getReason());
2500                 fireParticipantStatusListeners("kicked", params);
2501             }
2502         }
2503         // A user was banned from the room
2504         else if ("301".equals(code)) {
2505             // Check if this occupant was banned
2506             if (isUserModification) {
2507                 joined = false;
2508 
2509                 fireUserStatusListeners(
2510                     "banned",
2511                     new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});
2512 
2513                 // Reset occupant information.
2514                 occupantsMap.clear();
2515                 nickname = null;
2516                 userHasLeft();
2517             }
2518             else {
2519                 List<String> params = new ArrayList<String>();
2520                 params.add(from);
2521                 params.add(mucUser.getItem().getActor());
2522                 params.add(mucUser.getItem().getReason());
2523                 fireParticipantStatusListeners("banned", params);
2524             }
2525         }
2526         // A user's membership was revoked from the room
2527         else if ("321".equals(code)) {
2528             // Check if this occupant's membership was revoked
2529             if (isUserModification) {
2530                 joined = false;
2531 
2532                 fireUserStatusListeners("membershipRevoked", new Object[] {});
2533 
2534                 // Reset occupant information.
2535                 occupantsMap.clear();
2536                 nickname = null;
2537                 userHasLeft();
2538             }
2539         }
2540         // A occupant has changed his nickname in the room
2541         else if ("303".equals(code)) {
2542             List<String> params = new ArrayList<String>();
2543             params.add(from);
2544             params.add(mucUser.getItem().getNick());
2545             fireParticipantStatusListeners("nicknameChanged", params);
2546         }
2547     }
2548 
cleanup()2549     private void cleanup() {
2550         try {
2551             if (connection != null) {
2552                 roomListenerMultiplexor.removeRoom(room);
2553                 // Remove all the PacketListeners added to the connection by this chat
2554                 for (PacketListener connectionListener : connectionListeners) {
2555                     connection.removePacketListener(connectionListener);
2556                 }
2557             }
2558         } catch (Exception e) {
2559             // Do nothing
2560         }
2561     }
2562 
finalize()2563     protected void finalize() throws Throwable {
2564         cleanup();
2565         super.finalize();
2566     }
2567 
2568     /**
2569      * An InvitationsMonitor monitors a given connection to detect room invitations. Every
2570      * time the InvitationsMonitor detects a new invitation it will fire the invitation listeners.
2571      *
2572      * @author Gaston Dombiak
2573      */
2574     private static class InvitationsMonitor implements ConnectionListener {
2575         // We use a WeakHashMap so that the GC can collect the monitor when the
2576         // connection is no longer referenced by any object.
2577         // Note that when the InvitationsMonitor is used, i.e. when there are InvitationListeners, it will add a
2578         // PacketListener to the Connection and therefore a strong reference from the Connection to the
2579         // InvitationsMonior will exists, preventing it from beeing gc'ed. After the last InvitationListener is gone,
2580         // the PacketListener will get removed (cancel()) allowing the garbage collection of the InvitationsMonitor
2581         // instance.
2582         private final static Map<Connection, WeakReference<InvitationsMonitor>> monitors =
2583                 new WeakHashMap<Connection, WeakReference<InvitationsMonitor>>();
2584 
2585         // We don't use a synchronized List here because it would break the semantic of (add|remove)InvitationListener
2586         private final List<InvitationListener> invitationsListeners =
2587                 new ArrayList<InvitationListener>();
2588         private Connection connection;
2589         private PacketFilter invitationFilter;
2590         private PacketListener invitationPacketListener;
2591 
2592         /**
2593          * Returns a new or existing InvitationsMonitor for a given connection.
2594          *
2595          * @param conn the connection to monitor for room invitations.
2596          * @return a new or existing InvitationsMonitor for a given connection.
2597          */
getInvitationsMonitor(Connection conn)2598         public static InvitationsMonitor getInvitationsMonitor(Connection conn) {
2599             synchronized (monitors) {
2600                 if (!monitors.containsKey(conn) || monitors.get(conn).get() == null) {
2601                     // We need to use a WeakReference because the monitor references the
2602                     // connection and this could prevent the GC from collecting the monitor
2603                     // when no other object references the monitor
2604                     InvitationsMonitor ivm = new InvitationsMonitor(conn);
2605                     monitors.put(conn, new WeakReference<InvitationsMonitor>(ivm));
2606                     return ivm;
2607                 }
2608                 // Return the InvitationsMonitor that monitors the connection
2609                 return monitors.get(conn).get();
2610             }
2611         }
2612 
2613         /**
2614          * Creates a new InvitationsMonitor that will monitor invitations received
2615          * on a given connection.
2616          *
2617          * @param connection the connection to monitor for possible room invitations
2618          */
InvitationsMonitor(Connection connection)2619         private InvitationsMonitor(Connection connection) {
2620             this.connection = connection;
2621         }
2622 
2623         /**
2624          * Adds a listener to invitation notifications. The listener will be fired anytime
2625          * an invitation is received.<p>
2626          *
2627          * If this is the first monitor's listener then the monitor will be initialized in
2628          * order to start listening to room invitations.
2629          *
2630          * @param listener an invitation listener.
2631          */
addInvitationListener(InvitationListener listener)2632         public void addInvitationListener(InvitationListener listener) {
2633             synchronized (invitationsListeners) {
2634                 // If this is the first monitor's listener then initialize the listeners
2635                 // on the connection to detect room invitations
2636                 if (invitationsListeners.size() == 0) {
2637                     init();
2638                 }
2639                 if (!invitationsListeners.contains(listener)) {
2640                     invitationsListeners.add(listener);
2641                 }
2642             }
2643         }
2644 
2645         /**
2646          * Removes a listener to invitation notifications. The listener will be fired anytime
2647          * an invitation is received.<p>
2648          *
2649          * If there are no more listeners to notifiy for room invitations then the monitor will
2650          * be stopped. As soon as a new listener is added to the monitor, the monitor will resume
2651          * monitoring the connection for new room invitations.
2652          *
2653          * @param listener an invitation listener.
2654          */
removeInvitationListener(InvitationListener listener)2655         public void removeInvitationListener(InvitationListener listener) {
2656             synchronized (invitationsListeners) {
2657                 if (invitationsListeners.contains(listener)) {
2658                     invitationsListeners.remove(listener);
2659                 }
2660                 // If there are no more listeners to notifiy for room invitations
2661                 // then proceed to cancel/release this monitor
2662                 if (invitationsListeners.size() == 0) {
2663                     cancel();
2664                 }
2665             }
2666         }
2667 
2668         /**
2669          * Fires invitation listeners.
2670          */
fireInvitationListeners(String room, String inviter, String reason, String password, Message message)2671         private void fireInvitationListeners(String room, String inviter, String reason, String password,
2672                                              Message message) {
2673             InvitationListener[] listeners;
2674             synchronized (invitationsListeners) {
2675                 listeners = new InvitationListener[invitationsListeners.size()];
2676                 invitationsListeners.toArray(listeners);
2677             }
2678             for (InvitationListener listener : listeners) {
2679                 listener.invitationReceived(connection, room, inviter, reason, password, message);
2680             }
2681         }
2682 
connectionClosed()2683         public void connectionClosed() {
2684             cancel();
2685         }
2686 
connectionClosedOnError(Exception e)2687         public void connectionClosedOnError(Exception e) {
2688             // ignore
2689         }
2690 
reconnectingIn(int seconds)2691         public void reconnectingIn(int seconds) {
2692             // ignore
2693         }
2694 
reconnectionSuccessful()2695         public void reconnectionSuccessful() {
2696             // ignore
2697         }
2698 
reconnectionFailed(Exception e)2699         public void reconnectionFailed(Exception e) {
2700             // ignore
2701         }
2702 
2703         /**
2704          * Initializes the listeners to detect received room invitations and to detect when the
2705          * connection gets closed. As soon as a room invitation is received the invitations
2706          * listeners will be fired. When the connection gets closed the monitor will remove
2707          * his listeners on the connection.
2708          */
init()2709         private void init() {
2710             // Listens for all messages that include a MUCUser extension and fire the invitation
2711             // listeners if the message includes an invitation.
2712             invitationFilter =
2713                 new PacketExtensionFilter("x", "http://jabber.org/protocol/muc#user");
2714             invitationPacketListener = new PacketListener() {
2715                 public void processPacket(Packet packet) {
2716                     // Get the MUCUser extension
2717                     MUCUser mucUser =
2718                         (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
2719                     // Check if the MUCUser extension includes an invitation
2720                     if (mucUser.getInvite() != null &&
2721                             ((Message) packet).getType() != Message.Type.error) {
2722                         // Fire event for invitation listeners
2723                         fireInvitationListeners(packet.getFrom(), mucUser.getInvite().getFrom(),
2724                                 mucUser.getInvite().getReason(), mucUser.getPassword(), (Message) packet);
2725                     }
2726                 }
2727             };
2728             connection.addPacketListener(invitationPacketListener, invitationFilter);
2729             // Add a listener to detect when the connection gets closed in order to
2730             // cancel/release this monitor
2731             connection.addConnectionListener(this);
2732         }
2733 
2734         /**
2735          * Cancels all the listeners that this InvitationsMonitor has added to the connection.
2736          */
cancel()2737         private void cancel() {
2738             connection.removePacketListener(invitationPacketListener);
2739             connection.removeConnectionListener(this);
2740         }
2741 
2742     }
2743 }
2744