KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > jivesoftware > smackx > muc > MultiUserChat


1 /**
2  * $RCSfile$
3  * $Revision: 2731 $
4  * $Date: 2005-08-26 23:24:38 -0300 (Fri, 26 Aug 2005) $
5  *
6  * Copyright 2003-2004 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 JavaDoc;
24 import java.lang.reflect.*;
25 import java.util.*;
26
27 import org.jivesoftware.smack.*;
28 import org.jivesoftware.smack.filter.*;
29 import org.jivesoftware.smack.packet.*;
30 import org.jivesoftware.smackx.Form;
31 import org.jivesoftware.smackx.NodeInformationProvider;
32 import org.jivesoftware.smackx.ServiceDiscoveryManager;
33 import org.jivesoftware.smackx.packet.*;
34
35 /**
36  * A MultiUserChat is a conversation that takes place among many users in a virtual
37  * room. A room could have many occupants with different affiliation and roles.
38  * Possible affiliatons are "owner", "admin", "member", and "outcast". Possible roles
39  * are "moderator", "participant", and "visitor". Each role and affiliation guarantees
40  * different privileges (e.g. Send messages to all occupants, Kick participants and visitors,
41  * Grant voice, Edit member list, etc.).
42  *
43  * @author Gaston Dombiak
44  */

45 public class MultiUserChat {
46
47     private final static String JavaDoc discoNamespace = "http://jabber.org/protocol/muc";
48     private final static String JavaDoc discoNode = "http://jabber.org/protocol/muc#rooms";
49
50     private static Map joinedRooms = new WeakHashMap();
51
52     private XMPPConnection connection;
53     private String JavaDoc room;
54     private String JavaDoc subject;
55     private String JavaDoc nickname = null;
56     private boolean joined = false;
57     private Map occupantsMap = new HashMap();
58
59     private List invitationRejectionListeners = new ArrayList();
60     private List subjectUpdatedListeners = new ArrayList();
61     private List userStatusListeners = new ArrayList();
62     private List participantStatusListeners = new ArrayList();
63
64     private PacketFilter presenceFilter;
65     private PacketListener presenceListener;
66     private PacketFilter subjectFilter;
67     private PacketListener subjectListener;
68     private PacketFilter messageFilter;
69     private PacketFilter declinesFilter;
70     private PacketListener declinesListener;
71     private PacketCollector messageCollector;
72     private List connectionListeners = new ArrayList();
73
74     static {
75         XMPPConnection.addConnectionListener(new ConnectionEstablishedListener() {
76             public void connectionEstablished(final XMPPConnection connection) {
77                 // Set on every established connection that this client supports the Multi-User
78
// Chat protocol. This information will be used when another client tries to
79
// discover whether this client supports MUC or not.
80
ServiceDiscoveryManager.getInstanceFor(connection).addFeature(discoNamespace);
81                 // Set the NodeInformationProvider that will provide information about the
82
// joined rooms whenever a disco request is received
83
ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(
84                     discoNode,
85                     new NodeInformationProvider() {
86                         public Iterator getNodeItems() {
87                             ArrayList answer = new ArrayList();
88                             Iterator rooms=MultiUserChat.getJoinedRooms(connection);
89                             while (rooms.hasNext()) {
90                                 answer.add(new DiscoverItems.Item((String JavaDoc)rooms.next()));
91                             }
92                             return answer.iterator();
93                         }
94                     });
95             }
96         });
97     }
98
99     /**
100      * Creates a new multi user chat with the specified connection and room name. Note: no
101      * information is sent to or received from the server until you attempt to
102      * {@link #join(String) join} the chat room. On some server implementations,
103      * the room will not be created until the first person joins it.<p>
104      *
105      * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com
106      * for the XMPP server example.com). You must ensure that the room address you're
107      * trying to connect to includes the proper chat sub-domain.
108      *
109      * @param connection the XMPP connection.
110      * @param room the name of the room in the form "roomName@service", where
111      * "service" is the hostname at which the multi-user chat
112      * service is running. Make sure to provide a valid JID.
113      */

114     public MultiUserChat(XMPPConnection connection, String JavaDoc room) {
115         this.connection = connection;
116         this.room = room.toLowerCase();
117         init();
118     }
119
120     /**
121      * Returns true if the specified user supports the Multi-User Chat protocol.
122      *
123      * @param connection the connection to use to perform the service discovery.
124      * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
125      * @return a boolean indicating whether the specified user supports the MUC protocol.
126      */

127     public static boolean isServiceEnabled(XMPPConnection connection, String JavaDoc user) {
128         try {
129             DiscoverInfo result =
130                 ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(user);
131             return result.containsFeature(discoNamespace);
132         }
133         catch (XMPPException e) {
134             e.printStackTrace();
135             return false;
136         }
137     }
138
139     /**
140      * Returns an Iterator on the rooms where the user has joined using a given connection.
141      * The Iterator will contain Strings where each String represents a room
142      * (e.g. room@muc.jabber.org).
143      *
144      * @param connection the connection used to join the rooms.
145      * @return an Iterator on the rooms where the user has joined using a given connection.
146      */

147     private static Iterator getJoinedRooms(XMPPConnection connection) {
148         ArrayList rooms = (ArrayList)joinedRooms.get(connection);
149         if (rooms != null) {
150             return rooms.iterator();
151         }
152         // Return an iterator on an empty collection (i.e. the user never joined a room)
153
return new ArrayList().iterator();
154     }
155     
156     /**
157      * Returns an Iterator on the rooms where the requested user has joined. The Iterator will
158      * contain Strings where each String represents a room (e.g. room@muc.jabber.org).
159      *
160      * @param connection the connection to use to perform the service discovery.
161      * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
162      * @return an Iterator on the rooms where the requested user has joined.
163      */

164     public static Iterator getJoinedRooms(XMPPConnection connection, String JavaDoc user) {
165         try {
166             ArrayList answer = new ArrayList();
167             // Send the disco packet to the user
168
DiscoverItems result =
169                 ServiceDiscoveryManager.getInstanceFor(connection).discoverItems(user, discoNode);
170             // Collect the entityID for each returned item
171
for (Iterator items=result.getItems(); items.hasNext();) {
172                 answer.add(((DiscoverItems.Item)items.next()).getEntityID());
173             }
174             return answer.iterator();
175         }
176         catch (XMPPException e) {
177             e.printStackTrace();
178             // Return an iterator on an empty collection
179
return new ArrayList().iterator();
180         }
181     }
182
183     /**
184      * Returns the discovered information of a given room whithout actually having to join the room.
185      * The server will provide information only for rooms that are public.
186      *
187      * @param connection the XMPP connection to use for discovering information about the room.
188      * @param room the name of the room in the form "roomName@service" of which we want to discover
189      * its information.
190      * @return the discovered information of a given room whithout actually having to join the room.
191      * @throws XMPPException if an error occured while trying to discover information of a room.
192      */

193     public static RoomInfo getRoomInfo(XMPPConnection connection, String JavaDoc room)
194             throws XMPPException {
195         DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(room);
196         return new RoomInfo(info);
197     }
198
199     /**
200      * Returns a collection with the XMPP addresses of the Multi-User Chat services.
201      *
202      * @param connection the XMPP connection to use for discovering Multi-User Chat services.
203      * @return a collection with the XMPP addresses of the Multi-User Chat services.
204      * @throws XMPPException if an error occured while trying to discover MUC services.
205      */

206     public static Collection getServiceNames(XMPPConnection connection) throws XMPPException {
207         List answer = new ArrayList();
208         ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
209         DiscoverItems items = discoManager.discoverItems(connection.getServiceName());
210         for (Iterator it = items.getItems(); it.hasNext();) {
211             DiscoverItems.Item item = (DiscoverItems.Item) it.next();
212             DiscoverInfo info = discoManager.discoverInfo(item.getEntityID());
213             if (info.containsFeature("http://jabber.org/protocol/muc")) {
214                 answer.add(item.getEntityID());
215             }
216         }
217         return answer;
218     }
219
220     /**
221      * Returns a collection of HostedRooms where each HostedRoom has the XMPP address of the room
222      * and the room's name. Once discovered the rooms hosted by a chat service it is possible to
223      * discover more detailed room information or join the room.
224      *
225      * @param connection the XMPP connection to use for discovering hosted rooms by the MUC service.
226      * @param serviceName the service that is hosting the rooms to discover.
227      * @return a collection of HostedRooms.
228      * @throws XMPPException if an error occured while trying to discover the information.
229      */

230     public static Collection getHostedRooms(XMPPConnection connection, String JavaDoc serviceName)
231             throws XMPPException {
232         List answer = new ArrayList();
233         ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
234         DiscoverItems items = discoManager.discoverItems(serviceName);
235         for (Iterator it = items.getItems(); it.hasNext();) {
236             DiscoverItems.Item item = (DiscoverItems.Item) it.next();
237             answer.add(new HostedRoom(item));
238         }
239         return answer;
240     }
241
242     /**
243      * Returns the name of the room this MultiUserChat object represents.
244      *
245      * @return the multi user chat room name.
246      */

247     public String JavaDoc getRoom() {
248         return room;
249     }
250
251     /**
252      * Creates the room according to some default configuration, assign the requesting user
253      * as the room owner, and add the owner to the room but not allow anyone else to enter
254      * the room (effectively "locking" the room). The requesting user will join the room
255      * under the specified nickname as soon as the room has been created.<p>
256      *
257      * To create an "Instant Room", that means a room with some default configuration that is
258      * available for immediate access, the room's owner should send an empty form after creating
259      * the room. {@link #sendConfigurationForm(Form)}<p>
260      *
261      * To create a "Reserved Room", that means a room manually configured by the room creator
262      * before anyone is allowed to enter, the room's owner should complete and send a form after
263      * creating the room. Once the completed configutation form is sent to the server, the server
264      * will unlock the room. {@link #sendConfigurationForm(Form)}
265      *
266      * @param nickname the nickname to use.
267      * @throws XMPPException if the room couldn't be created for some reason
268      * (e.g. room already exists; user already joined to an existant room or
269      * 405 error if the user is not allowed to create the room)
270      */

271     public synchronized void create(String JavaDoc nickname) throws XMPPException {
272         if (nickname == null || nickname.equals("")) {
273             throw new IllegalArgumentException JavaDoc("Nickname must not be null or blank.");
274         }
275         // If we've already joined the room, leave it before joining under a new
276
// nickname.
277
if (joined) {
278             throw new IllegalStateException JavaDoc("Creation failed - User already joined the room.");
279         }
280         // We create a room by sending a presence packet to room@service/nick
281
// and signal support for MUC. The owner will be automatically logged into the room.
282
Presence joinPresence = new Presence(Presence.Type.AVAILABLE);
283         joinPresence.setTo(room + "/" + nickname);
284         // Indicate the the client supports MUC
285
joinPresence.addExtension(new MUCInitialPresence());
286
287         // Wait for a presence packet back from the server.
288
PacketFilter responseFilter =
289             new AndFilter(
290                 new FromMatchesFilter(room + "/" + nickname),
291                 new PacketTypeFilter(Presence.class));
292         PacketCollector response = connection.createPacketCollector(responseFilter);
293         // Send create & join packet.
294
connection.sendPacket(joinPresence);
295         // Wait up to a certain number of seconds for a reply.
296
Presence presence =
297             (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
298         // Stop queuing results
299
response.cancel();
300
301         if (presence == null) {
302             throw new XMPPException("No response from server.");
303         }
304         else if (presence.getError() != null) {
305             throw new XMPPException(presence.getError());
306         }
307         // Whether the room existed before or was created, the user has joined the room
308
this.nickname = nickname;
309         joined = true;
310         userHasJoined();
311
312         // Look for confirmation of room creation from the server
313
MUCUser mucUser = getMUCUserExtension(presence);
314         if (mucUser != null && mucUser.getStatus() != null) {
315             if ("201".equals(mucUser.getStatus().getCode())) {
316                 // Room was created and the user has joined the room
317
return;
318             }
319         }
320         // We need to leave the room since it seems that the room already existed
321
leave();
322         throw new XMPPException("Creation failed - Missing acknowledge of room creation.");
323     }
324
325     /**
326      * Joins the chat room using the specified nickname. If already joined
327      * using another nickname, this method will first leave the room and then
328      * re-join using the new nickname. The default timeout of Smack for a reply
329      * from the group chat server that the join succeeded will be used. After
330      * joining the room, the room will decide the amount of history to send.
331      *
332      * @param nickname the nickname to use.
333      * @throws XMPPException if an error occurs joining the room. In particular, a
334      * 401 error can occur if no password was provided and one is required; or a
335      * 403 error can occur if the user is banned; or a
336      * 404 error can occur if the room does not exist or is locked; or a
337      * 407 error can occur if user is not on the member list; or a
338      * 409 error can occur if someone is already in the group chat with the same nickname.
339      */

340     public void join(String JavaDoc nickname) throws XMPPException {
341         join(nickname, null, null, SmackConfiguration.getPacketReplyTimeout());
342     }
343
344     /**
345      * Joins the chat room using the specified nickname and password. If already joined
346      * using another nickname, this method will first leave the room and then
347      * re-join using the new nickname. The default timeout of Smack for a reply
348      * from the group chat server that the join succeeded will be used. After
349      * joining the room, the room will decide the amount of history to send.<p>
350      *
351      * A password is required when joining password protected rooms. If the room does
352      * not require a password there is no need to provide one.
353      *
354      * @param nickname the nickname to use.
355      * @param password the password to use.
356      * @throws XMPPException if an error occurs joining the room. In particular, a
357      * 401 error can occur if no password was provided and one is required; or a
358      * 403 error can occur if the user is banned; or a
359      * 404 error can occur if the room does not exist or is locked; or a
360      * 407 error can occur if user is not on the member list; or a
361      * 409 error can occur if someone is already in the group chat with the same nickname.
362      */

363     public void join(String JavaDoc nickname, String JavaDoc password) throws XMPPException {
364         join(nickname, password, null, SmackConfiguration.getPacketReplyTimeout());
365     }
366
367     /**
368      * Joins the chat room using the specified nickname and password. If already joined
369      * using another nickname, this method will first leave the room and then
370      * re-join using the new nickname.<p>
371      *
372      * To control the amount of history to receive while joining a room you will need to provide
373      * a configured DiscussionHistory object.<p>
374      *
375      * A password is required when joining password protected rooms. If the room does
376      * not require a password there is no need to provide one.<p>
377      *
378      * If the room does not already exist when the user seeks to enter it, the server will
379      * decide to create a new room or not.
380      *
381      * @param nickname the nickname to use.
382      * @param password the password to use.
383      * @param history the amount of discussion history to receive while joining a room.
384      * @param timeout the amount of time to wait for a reply from the MUC service(in milleseconds).
385      * @throws XMPPException if an error occurs joining the room. In particular, a
386      * 401 error can occur if no password was provided and one is required; or a
387      * 403 error can occur if the user is banned; or a
388      * 404 error can occur if the room does not exist or is locked; or a
389      * 407 error can occur if user is not on the member list; or a
390      * 409 error can occur if someone is already in the group chat with the same nickname.
391      */

392     public synchronized void join(
393         String JavaDoc nickname,
394         String JavaDoc password,
395         DiscussionHistory history,
396         long timeout)
397         throws XMPPException {
398         if (nickname == null || nickname.equals("")) {
399             throw new IllegalArgumentException JavaDoc("Nickname must not be null or blank.");
400         }
401         // If we've already joined the room, leave it before joining under a new
402
// nickname.
403
if (joined) {
404             leave();
405         }
406         // We join a room by sending a presence packet where the "to"
407
// field is in the form "roomName@service/nickname"
408
Presence joinPresence = new Presence(Presence.Type.AVAILABLE);
409         joinPresence.setTo(room + "/" + nickname);
410
411         // Indicate the the client supports MUC
412
MUCInitialPresence mucInitialPresence = new MUCInitialPresence();
413         if (password != null) {
414             mucInitialPresence.setPassword(password);
415         }
416         if (history != null) {
417             mucInitialPresence.setHistory(history.getMUCHistory());
418         }
419         joinPresence.addExtension(mucInitialPresence);
420
421         // Wait for a presence packet back from the server.
422
PacketFilter responseFilter =
423             new AndFilter(
424                 new FromMatchesFilter(room + "/" + nickname),
425                 new PacketTypeFilter(Presence.class));
426         PacketCollector response = connection.createPacketCollector(responseFilter);
427         // Send join packet.
428
connection.sendPacket(joinPresence);
429         // Wait up to a certain number of seconds for a reply.
430
Presence presence = (Presence) response.nextResult(timeout);
431         // Stop queuing results
432
response.cancel();
433
434         if (presence == null) {
435             throw new XMPPException("No response from server.");
436         }
437         else if (presence.getError() != null) {
438             throw new XMPPException(presence.getError());
439         }
440         this.nickname = nickname;
441         joined = true;
442         userHasJoined();
443     }
444
445     /**
446      * Returns true if currently in the multi user chat (after calling the {@link
447      * #join(String)} method).
448      *
449      * @return true if currently in the multi user chat room.
450      */

451     public boolean isJoined() {
452         return joined;
453     }
454
455     /**
456      * Leave the chat room.
457      */

458     public synchronized void leave() {
459         // If not joined already, do nothing.
460
if (!joined) {
461             return;
462         }
463         // We leave a room by sending a presence packet where the "to"
464
// field is in the form "roomName@service/nickname"
465
Presence leavePresence = new Presence(Presence.Type.UNAVAILABLE);
466         leavePresence.setTo(room + "/" + nickname);
467         connection.sendPacket(leavePresence);
468         // Reset occupant information.
469
occupantsMap = new HashMap();
470         nickname = null;
471         joined = false;
472         userHasLeft();
473     }
474
475     /**
476      * Returns the room's configuration form that the room's owner can use or <tt>null</tt> if
477      * no configuration is possible. The configuration form allows to set the room's language,
478      * enable logging, specify room's type, etc..
479      *
480      * @return the Form that contains the fields to complete together with the instrucions or
481      * <tt>null</tt> if no configuration is possible.
482      * @throws XMPPException if an error occurs asking the configuration form for the room.
483      */

484     public Form getConfigurationForm() throws XMPPException {
485         MUCOwner iq = new MUCOwner();
486         iq.setTo(room);
487         iq.setType(IQ.Type.GET);
488
489         // Filter packets looking for an answer from the server.
490
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
491         PacketCollector response = connection.createPacketCollector(responseFilter);
492         // Request the configuration form to the server.
493
connection.sendPacket(iq);
494         // Wait up to a certain number of seconds for a reply.
495
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
496         // Stop queuing results
497
response.cancel();
498
499         if (answer == null) {
500             throw new XMPPException("No response from server.");
501         }
502         else if (answer.getError() != null) {
503             throw new XMPPException(answer.getError());
504         }
505         return Form.getFormFrom(answer);
506     }
507
508     /**
509      * Sends the completed configuration form to the server. The room will be configured
510      * with the new settings defined in the form. If the form is empty then the server
511      * will create an instant room (will use default configuration).
512      *
513      * @param form the form with the new settings.
514      * @throws XMPPException if an error occurs setting the new rooms' configuration.
515      */

516     public void sendConfigurationForm(Form form) throws XMPPException {
517         MUCOwner iq = new MUCOwner();
518         iq.setTo(room);
519         iq.setType(IQ.Type.SET);
520         iq.addExtension(form.getDataFormToSend());
521
522         // Filter packets looking for an answer from the server.
523
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
524         PacketCollector response = connection.createPacketCollector(responseFilter);
525         // Send the completed configuration form to the server.
526
connection.sendPacket(iq);
527         // Wait up to a certain number of seconds for a reply.
528
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
529         // Stop queuing results
530
response.cancel();
531
532         if (answer == null) {
533             throw new XMPPException("No response from server.");
534         }
535         else if (answer.getError() != null) {
536             throw new XMPPException(answer.getError());
537         }
538     }
539
540     /**
541      * Returns the room's registration form that an unaffiliated user, can use to become a member
542      * of the room or <tt>null</tt> if no registration is possible. Some rooms may restrict the
543      * privilege to register members and allow only room admins to add new members.<p>
544      *
545      * If the user requesting registration requirements is not allowed to register with the room
546      * (e.g. because that privilege has been restricted), the room will return a "Not Allowed"
547      * error to the user (error code 405).
548      *
549      * @return the registration Form that contains the fields to complete together with the
550      * instrucions or <tt>null</tt> if no registration is possible.
551      * @throws XMPPException if an error occurs asking the registration form for the room or a
552      * 405 error if the user is not allowed to register with the room.
553      */

554     public Form getRegistrationForm() throws XMPPException {
555         Registration reg = new Registration();
556         reg.setType(IQ.Type.GET);
557         reg.setTo(room);
558
559         PacketFilter filter =
560             new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
561         PacketCollector collector = connection.createPacketCollector(filter);
562         connection.sendPacket(reg);
563         IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
564         collector.cancel();
565         if (result == null) {
566             throw new XMPPException("No response from server.");
567         }
568         else if (result.getType() == IQ.Type.ERROR) {
569             throw new XMPPException(result.getError());
570         }
571         return Form.getFormFrom(result);
572     }
573
574     /**
575      * Sends the completed registration form to the server. After the user successfully submits
576      * the form, the room may queue the request for review by the room admins or may immediately
577      * add the user to the member list by changing the user's affiliation from "none" to "member.<p>
578      *
579      * If the desired room nickname is already reserved for that room, the room will return a
580      * "Conflict" error to the user (error code 409). If the room does not support registration,
581      * it will return a "Service Unavailable" error to the user (error code 503).
582      *
583      * @param form the completed registration form.
584      * @throws XMPPException if an error occurs submitting the registration form. In particular, a
585      * 409 error can occur if the desired room nickname is already reserved for that room;
586      * or a 503 error can occur if the room does not support registration.
587      */

588     public void sendRegistrationForm(Form form) throws XMPPException {
589         Registration reg = new Registration();
590         reg.setType(IQ.Type.SET);
591         reg.setTo(room);
592         reg.addExtension(form.getDataFormToSend());
593
594         PacketFilter filter =
595             new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
596         PacketCollector collector = connection.createPacketCollector(filter);
597         connection.sendPacket(reg);
598         IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
599         collector.cancel();
600         if (result == null) {
601             throw new XMPPException("No response from server.");
602         }
603         else if (result.getType() == IQ.Type.ERROR) {
604             throw new XMPPException(result.getError());
605         }
606     }
607
608     /**
609      * Sends a request to the server to destroy the room. The sender of the request
610      * should be the room's owner. If the sender of the destroy request is not the room's owner
611      * then the server will answer a "Forbidden" error (403).
612      *
613      * @param reason the reason for the room destruction.
614      * @param alternateJID the JID of an alternate location.
615      * @throws XMPPException if an error occurs while trying to destroy the room.
616      * An error can occur which will be wrapped by an XMPPException --
617      * XMPP error code 403. The error code can be used to present more
618      * appropiate error messages to end-users.
619      */

620     public void destroy(String JavaDoc reason, String JavaDoc alternateJID) throws XMPPException {
621         MUCOwner iq = new MUCOwner();
622         iq.setTo(room);
623         iq.setType(IQ.Type.SET);
624
625         // Create the reason for the room destruction
626
MUCOwner.Destroy destroy = new MUCOwner.Destroy();
627         destroy.setReason(reason);
628         destroy.setJid(alternateJID);
629         iq.setDestroy(destroy);
630
631         // Wait for a presence packet back from the server.
632
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
633         PacketCollector response = connection.createPacketCollector(responseFilter);
634         // Send the room destruction request.
635
connection.sendPacket(iq);
636         // Wait up to a certain number of seconds for a reply.
637
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
638         // Stop queuing results
639
response.cancel();
640
641         if (answer == null) {
642             throw new XMPPException("No response from server.");
643         }
644         else if (answer.getError() != null) {
645             throw new XMPPException(answer.getError());
646         }
647         // Reset occupant information.
648
occupantsMap = new HashMap();
649         nickname = null;
650         joined = false;
651         userHasLeft();
652     }
653
654     /**
655      * Invites another user to the room in which one is an occupant. The invitation
656      * will be sent to the room which in turn will forward the invitation to the invitee.<p>
657      *
658      * If the room is password-protected, the invitee will receive a password to use to join
659      * the room. If the room is members-only, the the invitee may be added to the member list.
660      *
661      * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
662      * @param reason the reason why the user is being invited.
663      */

664     public void invite(String JavaDoc user, String JavaDoc reason) {
665         invite(new Message(), user, reason);
666     }
667
668     /**
669      * Invites another user to the room in which one is an occupant using a given Message. The invitation
670      * will be sent to the room which in turn will forward the invitation to the invitee.<p>
671      *
672      * If the room is password-protected, the invitee will receive a password to use to join
673      * the room. If the room is members-only, the the invitee may be added to the member list.
674      *
675      * @param message the message to use for sending the invitation.
676      * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
677      * @param reason the reason why the user is being invited.
678      */

679     public void invite(Message message, String JavaDoc user, String JavaDoc reason) {
680         // TODO listen for 404 error code when inviter supplies a non-existent JID
681
message.setTo(room);
682
683         // Create the MUCUser packet that will include the invitation
684
MUCUser mucUser = new MUCUser();
685         MUCUser.Invite invite = new MUCUser.Invite();
686         invite.setTo(user);
687         invite.setReason(reason);
688         mucUser.setInvite(invite);
689         // Add the MUCUser packet that includes the invitation to the message
690
message.addExtension(mucUser);
691
692         connection.sendPacket(message);
693     }
694
695     /**
696      * Informs the sender of an invitation that the invitee declines the invitation. The rejection
697      * will be sent to the room which in turn will forward the rejection to the inviter.
698      *
699      * @param conn the connection to use for sending the rejection.
700      * @param room the room that sent the original invitation.
701      * @param inviter the inviter of the declined invitation.
702      * @param reason the reason why the invitee is declining the invitation.
703      */

704     public static void decline(XMPPConnection conn, String JavaDoc room, String JavaDoc inviter, String JavaDoc reason) {
705         Message message = new Message(room);
706
707         // Create the MUCUser packet that will include the rejection
708
MUCUser mucUser = new MUCUser();
709         MUCUser.Decline decline = new MUCUser.Decline();
710         decline.setTo(inviter);
711         decline.setReason(reason);
712         mucUser.setDecline(decline);
713         // Add the MUCUser packet that includes the rejection
714
message.addExtension(mucUser);
715
716         conn.sendPacket(message);
717     }
718
719     /**
720      * Adds a listener to invitation notifications. The listener will be fired anytime
721      * an invitation is received.
722      *
723      * @param conn the connection where the listener will be applied.
724      * @param listener an invitation listener.
725      */

726     public static void addInvitationListener(XMPPConnection conn, InvitationListener listener) {
727         InvitationsMonitor.getInvitationsMonitor(conn).addInvitationListener(listener);
728     }
729
730     /**
731      * Removes a listener to invitation notifications. The listener will be fired anytime
732      * an invitation is received.
733      *
734      * @param conn the connection where the listener was applied.
735      * @param listener an invitation listener.
736      */

737     public static void removeInvitationListener(XMPPConnection conn, InvitationListener listener) {
738         InvitationsMonitor.getInvitationsMonitor(conn).removeInvitationListener(listener);
739     }
740
741     /**
742      * Adds a listener to invitation rejections notifications. The listener will be fired anytime
743      * an invitation is declined.
744      *
745      * @param listener an invitation rejection listener.
746      */

747     public void addInvitationRejectionListener(InvitationRejectionListener listener) {
748         synchronized (invitationRejectionListeners) {
749             if (!invitationRejectionListeners.contains(listener)) {
750                 invitationRejectionListeners.add(listener);
751             }
752         }
753     }
754
755     /**
756      * Removes a listener from invitation rejections notifications. The listener will be fired
757      * anytime an invitation is declined.
758      *
759      * @param listener an invitation rejection listener.
760      */

761     public void removeInvitationRejectionListener(InvitationRejectionListener listener) {
762         synchronized (invitationRejectionListeners) {
763             invitationRejectionListeners.remove(listener);
764         }
765     }
766
767     /**
768      * Fires invitation rejection listeners.
769      */

770     private void fireInvitationRejectionListeners(String JavaDoc invitee, String JavaDoc reason) {
771         InvitationRejectionListener[] listeners = null;
772         synchronized (invitationRejectionListeners) {
773             listeners = new InvitationRejectionListener[invitationRejectionListeners.size()];
774             invitationRejectionListeners.toArray(listeners);
775         }
776         for (int i = 0; i < listeners.length; i++) {
777             listeners[i].invitationDeclined(invitee, reason);
778         }
779     }
780     
781     /**
782      * Adds a listener to subject change notifications. The listener will be fired anytime
783      * the room's subject changes.
784      *
785      * @param listener a subject updated listener.
786      */

787     public void addSubjectUpdatedListener(SubjectUpdatedListener listener) {
788         synchronized (subjectUpdatedListeners) {
789             if (!subjectUpdatedListeners.contains(listener)) {
790                 subjectUpdatedListeners.add(listener);
791             }
792         }
793     }
794
795     /**
796      * Removes a listener from subject change notifications. The listener will be fired
797      * anytime the room's subject changes.
798      *
799      * @param listener a subject updated listener.
800      */

801     public void removeSubjectUpdatedListener(SubjectUpdatedListener listener) {
802         synchronized (subjectUpdatedListeners) {
803             subjectUpdatedListeners.remove(listener);
804         }
805     }
806
807     /**
808      * Fires subject updated listeners.
809      */

810     private void fireSubjectUpdatedListeners(String JavaDoc subject, String JavaDoc from) {
811         SubjectUpdatedListener[] listeners = null;
812         synchronized (subjectUpdatedListeners) {
813             listeners = new SubjectUpdatedListener[subjectUpdatedListeners.size()];
814             subjectUpdatedListeners.toArray(listeners);
815         }
816         for (int i = 0; i < listeners.length; i++) {
817             listeners[i].subjectUpdated(subject, from);
818         }
819     }
820
821     /**
822      * Returns the last known room's subject or <tt>null</tt> if the user hasn't joined the room
823      * or the room does not have a subject yet. In case the room has a subject, as soon as the
824      * user joins the room a message with the current room's subject will be received.<p>
825      *
826      * To be notified every time the room's subject change you should add a listener
827      * to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p>
828      *
829      * To change the room's subject use {@link #changeSubject(String)}.
830      *
831      * @return the room's subject or <tt>null</tt> if the user hasn't joined the room or the
832      * room does not have a subject yet.
833      */

834     public String JavaDoc getSubject() {
835         return subject;
836     }
837
838     /**
839      * Returns the reserved room nickname for the user in the room. A user may have a reserved
840      * nickname, for example through explicit room registration or database integration. In such
841      * cases it may be desirable for the user to discover the reserved nickname before attempting
842      * to enter the room.
843      *
844      * @return the reserved room nickname or <tt>null</tt> if none.
845      */

846     public String JavaDoc getReservedNickname() {
847         try {
848             DiscoverInfo result =
849                 ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(
850                     room,
851                     "x-roomuser-item");
852             // Look for an Identity that holds the reserved nickname and return its name
853
for (Iterator identities = result.getIdentities(); identities.hasNext();) {
854                 DiscoverInfo.Identity identity = (DiscoverInfo.Identity) identities.next();
855                 return identity.getName();
856             }
857             // If no Identity was found then the user does not have a reserved room nickname
858
return null;
859         }
860         catch (XMPPException e) {
861             e.printStackTrace();
862             return null;
863         }
864     }
865
866     /**
867      * Returns the nickname that was used to join the room, or <tt>null</tt> if not
868      * currently joined.
869      *
870      * @return the nickname currently being used.
871      */

872     public String JavaDoc getNickname() {
873         return nickname;
874     }
875
876     /**
877      * Changes the occupant's nickname to a new nickname within the room. Each room occupant
878      * will receive two presence packets. One of type "unavailable" for the old nickname and one
879      * indicating availability for the new nickname. The unavailable presence will contain the new
880      * nickname and an appropriate status code (namely 303) as extended presence information. The
881      * status code 303 indicates that the occupant is changing his/her nickname.
882      *
883      * @param nickname the new nickname within the room.
884      * @throws XMPPException if the new nickname is already in use by another occupant.
885      */

886     public void changeNickname(String JavaDoc nickname) throws XMPPException {
887         if (nickname == null || nickname.equals("")) {
888             throw new IllegalArgumentException JavaDoc("Nickname must not be null or blank.");
889         }
890         // Check that we already have joined the room before attempting to change the
891
// nickname.
892
if (!joined) {
893             throw new IllegalStateException JavaDoc("Must be logged into the room to change nickname.");
894         }
895         // We change the nickname by sending a presence packet where the "to"
896
// field is in the form "roomName@service/nickname"
897
// We don't have to signal the MUC support again
898
Presence joinPresence = new Presence(Presence.Type.AVAILABLE);
899         joinPresence.setTo(room + "/" + nickname);
900
901         // Wait for a presence packet back from the server.
902
PacketFilter responseFilter =
903             new AndFilter(
904                 new FromMatchesFilter(room + "/" + nickname),
905                 new PacketTypeFilter(Presence.class));
906         PacketCollector response = connection.createPacketCollector(responseFilter);
907         // Send join packet.
908
connection.sendPacket(joinPresence);
909         // Wait up to a certain number of seconds for a reply.
910
Presence presence =
911             (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
912         // Stop queuing results
913
response.cancel();
914
915         if (presence == null) {
916             throw new XMPPException("No response from server.");
917         }
918         else if (presence.getError() != null) {
919             throw new XMPPException(presence.getError());
920         }
921         this.nickname = nickname;
922     }
923
924     /**
925      * Changes the occupant's availability status within the room. The presence type
926      * will remain available but with a new status that describes the presence update and
927      * a new presence mode (e.g. Extended away).
928      *
929      * @param status a text message describing the presence update.
930      * @param mode the mode type for the presence update.
931      */

932     public void changeAvailabilityStatus(String JavaDoc status, Presence.Mode mode) {
933         if (nickname == null || nickname.equals("")) {
934             throw new IllegalArgumentException JavaDoc("Nickname must not be null or blank.");
935         }
936         // Check that we already have joined the room before attempting to change the
937
// availability status.
938
if (!joined) {
939             throw new IllegalStateException JavaDoc(
940                 "Must be logged into the room to change the " + "availability status.");
941         }
942         // We change the availability status by sending a presence packet to the room with the
943
// new presence status and mode
944
Presence joinPresence = new Presence(Presence.Type.AVAILABLE);
945         joinPresence.setStatus(status);
946         joinPresence.setMode(mode);
947         joinPresence.setTo(room + "/" + nickname);
948
949         // Send join packet.
950
connection.sendPacket(joinPresence);
951     }
952
953     /**
954      * Kicks a visitor or participant from the room. The kicked occupant will receive a presence
955      * of type "unavailable" including a status code 307 and optionally along with the reason
956      * (if provided) and the bare JID of the user who initiated the kick. After the occupant
957      * was kicked from the room, the rest of the occupants will receive a presence of type
958      * "unavailable". The presence will include a status code 307 which means that the occupant
959      * was kicked from the room.
960      *
961      * @param nickname the nickname of the participant or visitor to kick from the room
962      * (e.g. "john").
963      * @param reason the reason why the participant or visitor is being kicked from the room.
964      * @throws XMPPException if an error occurs kicking the occupant. In particular, a
965      * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
966      * was intended to be kicked (i.e. Not Allowed error); or a
967      * 403 error can occur if the occupant that intended to kick another occupant does
968      * not have kicking privileges (i.e. Forbidden error); or a
969      * 400 error can occur if the provided nickname is not present in the room.
970      */

971     public void kickParticipant(String JavaDoc nickname, String JavaDoc reason) throws XMPPException {
972         changeRole(nickname, "none", reason);
973     }
974
975     /**
976      * Grants voice to visitors in the room. In a moderated room, a moderator may want to manage
977      * who does and does not have "voice" in the room. To have voice means that a room occupant
978      * is able to send messages to the room occupants.
979      *
980      * @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john").
981      * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
982      * 403 error can occur if the occupant that intended to grant voice is not
983      * a moderator in this room (i.e. Forbidden error); or a
984      * 400 error can occur if the provided nickname is not present in the room.
985      */

986     public void grantVoice(Collection nicknames) throws XMPPException {
987         changeRole(nicknames, "participant");
988     }
989
990     /**
991      * Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage
992      * who does and does not have "voice" in the room. To have voice means that a room occupant
993      * is able to send messages to the room occupants.
994      *
995      * @param nickname the nickname of the visitor to grant voice in the room (e.g. "john").
996      * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
997      * 403 error can occur if the occupant that intended to grant voice is not
998      * a moderator in this room (i.e. Forbidden error); or a
999      * 400 error can occur if the provided nickname is not present in the room.
1000     */

1001    public void grantVoice(String JavaDoc nickname) throws XMPPException {
1002        changeRole(nickname, "participant", null);
1003    }
1004
1005    /**
1006     * Revokes voice from participants in the room. In a moderated room, a moderator may want to
1007     * revoke an occupant's privileges to speak. To have voice means that a room occupant
1008     * is able to send messages to the room occupants.
1009     *
1010     * @param nicknames the nicknames of the participants to revoke voice (e.g. "john").
1011     * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a
1012     * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1013     * was tried to revoke his voice (i.e. Not Allowed error); or a
1014     * 400 error can occur if the provided nickname is not present in the room.
1015     */

1016    public void revokeVoice(Collection nicknames) throws XMPPException {
1017        changeRole(nicknames, "visitor");
1018    }
1019
1020    /**
1021     * Revokes voice from a participant in the room. In a moderated room, a moderator may want to
1022     * revoke an occupant's privileges to speak. To have voice means that a room occupant
1023     * is able to send messages to the room occupants.
1024     *
1025     * @param nickname the nickname of the participant to revoke voice (e.g. "john").
1026     * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a
1027     * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1028     * was tried to revoke his voice (i.e. Not Allowed error); or a
1029     * 400 error can occur if the provided nickname is not present in the room.
1030     */

1031    public void revokeVoice(String JavaDoc nickname) throws XMPPException {
1032        changeRole(nickname, "visitor", null);
1033    }
1034
1035    /**
1036     * Bans users from the room. An admin or owner of the room can ban users from a room. This
1037     * means that the banned user will no longer be able to join the room unless the ban has been
1038     * removed. If the banned user was present in the room then he/she will be removed from the
1039     * room and notified that he/she was banned along with the reason (if provided) and the bare
1040     * XMPP user ID of the user who initiated the ban.
1041     *
1042     * @param jids the bare XMPP user IDs of the users to ban.
1043     * @throws XMPPException if an error occurs banning a user. In particular, a
1044     * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1045     * was tried to be banned (i.e. Not Allowed error).
1046     */

1047    public void banUsers(Collection jids) throws XMPPException {
1048        changeAffiliationByAdmin(jids, "outcast");
1049    }
1050
1051    /**
1052     * Bans a user from the room. An admin or owner of the room can ban users from a room. This
1053     * means that the banned user will no longer be able to join the room unless the ban has been
1054     * removed. If the banned user was present in the room then he/she will be removed from the
1055     * room and notified that he/she was banned along with the reason (if provided) and the bare
1056     * XMPP user ID of the user who initiated the ban.
1057     *
1058     * @param jid the bare XMPP user ID of the user to ban (e.g. "user@host.org").
1059     * @param reason the reason why the user was banned.
1060     * @throws XMPPException if an error occurs banning a user. In particular, a
1061     * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1062     * was tried to be banned (i.e. Not Allowed error).
1063     */

1064    public void banUser(String JavaDoc jid, String JavaDoc reason) throws XMPPException {
1065        changeAffiliationByAdmin(jid, "outcast", reason);
1066    }
1067
1068    /**
1069     * Grants membership to other users. Only administrators are able to grant membership. A user
1070     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1071     * that a user cannot enter without being on the member list).
1072     *
1073     * @param jids the XMPP user IDs of the users to grant membership.
1074     * @throws XMPPException if an error occurs granting membership to a user.
1075     */

1076    public void grantMembership(Collection jids) throws XMPPException {
1077        changeAffiliationByAdmin(jids, "member");
1078    }
1079
1080    /**
1081     * Grants membership to a user. Only administrators are able to grant membership. A user
1082     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1083     * that a user cannot enter without being on the member list).
1084     *
1085     * @param jid the XMPP user ID of the user to grant membership (e.g. "user@host.org").
1086     * @throws XMPPException if an error occurs granting membership to a user.
1087     */

1088    public void grantMembership(String JavaDoc jid) throws XMPPException {
1089        changeAffiliationByAdmin(jid, "member", null);
1090    }
1091
1092    /**
1093     * Revokes users' membership. Only administrators are able to revoke membership. A user
1094     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1095     * that a user cannot enter without being on the member list). If the user is in the room and
1096     * the room is of type members-only then the user will be removed from the room.
1097     *
1098     * @param jids the bare XMPP user IDs of the users to revoke membership.
1099     * @throws XMPPException if an error occurs revoking membership to a user.
1100     */

1101    public void revokeMembership(Collection jids) throws XMPPException {
1102        changeAffiliationByAdmin(jids, "none");
1103    }
1104
1105    /**
1106     * Revokes a user's membership. Only administrators are able to revoke membership. A user
1107     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1108     * that a user cannot enter without being on the member list). If the user is in the room and
1109     * the room is of type members-only then the user will be removed from the room.
1110     *
1111     * @param jid the bare XMPP user ID of the user to revoke membership (e.g. "user@host.org").
1112     * @throws XMPPException if an error occurs revoking membership to a user.
1113     */

1114    public void revokeMembership(String JavaDoc jid) throws XMPPException {
1115        changeAffiliationByAdmin(jid, "none", null);
1116    }
1117
1118    /**
1119     * Grants moderator privileges to participants or visitors. Room administrators may grant
1120     * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
1121     * other users, modify room's subject plus all the partcipants privileges.
1122     *
1123     * @param nicknames the nicknames of the occupants to grant moderator privileges.
1124     * @throws XMPPException if an error occurs granting moderator privileges to a user.
1125     */

1126    public void grantModerator(Collection nicknames) throws XMPPException {
1127        changeRole(nicknames, "moderator");
1128    }
1129
1130    /**
1131     * Grants moderator privileges to a participant or visitor. Room administrators may grant
1132     * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
1133     * other users, modify room's subject plus all the partcipants privileges.
1134     *
1135     * @param nickname the nickname of the occupant to grant moderator privileges.
1136     * @throws XMPPException if an error occurs granting moderator privileges to a user.
1137     */

1138    public void grantModerator(String JavaDoc nickname) throws XMPPException {
1139        changeRole(nickname, "moderator", null);
1140    }
1141
1142    /**
1143     * Revokes moderator privileges from other users. The occupant that loses moderator
1144     * privileges will become a participant. Room administrators may revoke moderator privileges
1145     * only to occupants whose affiliation is member or none. This means that an administrator is
1146     * not allowed to revoke moderator privileges from other room administrators or owners.
1147     *
1148     * @param nicknames the nicknames of the occupants to revoke moderator privileges.
1149     * @throws XMPPException if an error occurs revoking moderator privileges from a user.
1150     */

1151    public void revokeModerator(Collection nicknames) throws XMPPException {
1152        changeRole(nicknames, "participant");
1153    }
1154
1155    /**
1156     * Revokes moderator privileges from another user. The occupant that loses moderator
1157     * privileges will become a participant. Room administrators may revoke moderator privileges
1158     * only to occupants whose affiliation is member or none. This means that an administrator is
1159     * not allowed to revoke moderator privileges from other room administrators or owners.
1160     *
1161     * @param nickname the nickname of the occupant to revoke moderator privileges.
1162     * @throws XMPPException if an error occurs revoking moderator privileges from a user.
1163     */

1164    public void revokeModerator(String JavaDoc nickname) throws XMPPException {
1165        changeRole(nickname, "participant", null);
1166    }
1167
1168    /**
1169     * Grants ownership privileges to other users. Room owners may grant ownership privileges.
1170     * Some room implementations will not allow to grant ownership privileges to other users.
1171     * An owner is allowed to change defining room features as well as perform all administrative
1172     * functions.
1173     *
1174     * @param jids the collection of bare XMPP user IDs of the users to grant ownership.
1175     * @throws XMPPException if an error occurs granting ownership privileges to a user.
1176     */

1177    public void grantOwnership(Collection jids) throws XMPPException {
1178        changeAffiliationByOwner(jids, "owner");
1179    }
1180
1181    /**
1182     * Grants ownership privileges to another user. Room owners may grant ownership privileges.
1183     * Some room implementations will not allow to grant ownership privileges to other users.
1184     * An owner is allowed to change defining room features as well as perform all administrative
1185     * functions.
1186     *
1187     * @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user@host.org").
1188     * @throws XMPPException if an error occurs granting ownership privileges to a user.
1189     */

1190    public void grantOwnership(String JavaDoc jid) throws XMPPException {
1191        changeAffiliationByOwner(jid, "owner");
1192    }
1193
1194    /**
1195     * Revokes ownership privileges from other users. The occupant that loses ownership
1196     * privileges will become an administrator. Room owners may revoke ownership privileges.
1197     * Some room implementations will not allow to grant ownership privileges to other users.
1198     *
1199     * @param jids the bare XMPP user IDs of the users to revoke ownership.
1200     * @throws XMPPException if an error occurs revoking ownership privileges from a user.
1201     */

1202    public void revokeOwnership(Collection jids) throws XMPPException {
1203        changeAffiliationByOwner(jids, "admin");
1204    }
1205
1206    /**
1207     * Revokes ownership privileges from another user. The occupant that loses ownership
1208     * privileges will become an administrator. Room owners may revoke ownership privileges.
1209     * Some room implementations will not allow to grant ownership privileges to other users.
1210     *
1211     * @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user@host.org").
1212     * @throws XMPPException if an error occurs revoking ownership privileges from a user.
1213     */

1214    public void revokeOwnership(String JavaDoc jid) throws XMPPException {
1215        changeAffiliationByOwner(jid, "admin");
1216    }
1217
1218    /**
1219     * Grants administrator privileges to other users. Room owners may grant administrator
1220     * privileges to a member or unaffiliated user. An administrator is allowed to perform
1221     * administrative functions such as banning users and edit moderator list.
1222     *
1223     * @param jids the bare XMPP user IDs of the users to grant administrator privileges.
1224     * @throws XMPPException if an error occurs granting administrator privileges to a user.
1225     */

1226    public void grantAdmin(Collection jids) throws XMPPException {
1227        changeAffiliationByOwner(jids, "admin");
1228    }
1229
1230    /**
1231     * Grants administrator privileges to another user. Room owners may grant administrator
1232     * privileges to a member or unaffiliated user. An administrator is allowed to perform
1233     * administrative functions such as banning users and edit moderator list.
1234     *
1235     * @param jid the bare XMPP user ID of the user to grant administrator privileges
1236     * (e.g. "user@host.org").
1237     * @throws XMPPException if an error occurs granting administrator privileges to a user.
1238     */

1239    public void grantAdmin(String JavaDoc jid) throws XMPPException {
1240        changeAffiliationByOwner(jid, "admin");
1241    }
1242
1243    /**
1244     * Revokes administrator privileges from users. The occupant that loses administrator
1245     * privileges will become a member. Room owners may revoke administrator privileges from
1246     * a member or unaffiliated user.
1247     *
1248     * @param jids the bare XMPP user IDs of the user to revoke administrator privileges.
1249     * @throws XMPPException if an error occurs revoking administrator privileges from a user.
1250     */

1251    public void revokeAdmin(Collection jids) throws XMPPException {
1252        changeAffiliationByOwner(jids, "member");
1253    }
1254
1255    /**
1256     * Revokes administrator privileges from a user. The occupant that loses administrator
1257     * privileges will become a member. Room owners may revoke administrator privileges from
1258     * a member or unaffiliated user.
1259     *
1260     * @param jid the bare XMPP user ID of the user to revoke administrator privileges
1261     * (e.g. "user@host.org").
1262     * @throws XMPPException if an error occurs revoking administrator privileges from a user.
1263     */

1264    public void revokeAdmin(String JavaDoc jid) throws XMPPException {
1265        changeAffiliationByOwner(jid, "member");
1266    }
1267
1268    private void changeAffiliationByOwner(String JavaDoc jid, String JavaDoc affiliation) throws XMPPException {
1269        MUCOwner iq = new MUCOwner();
1270        iq.setTo(room);
1271        iq.setType(IQ.Type.SET);
1272        // Set the new affiliation.
1273
MUCOwner.Item item = new MUCOwner.Item(affiliation);
1274        item.setJid(jid);
1275        iq.addItem(item);
1276
1277        // Wait for a response packet back from the server.
1278
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1279        PacketCollector response = connection.createPacketCollector(responseFilter);
1280        // Send the change request to the server.
1281
connection.sendPacket(iq);
1282        // Wait up to a certain number of seconds for a reply.
1283
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1284        // Stop queuing results
1285
response.cancel();
1286
1287        if (answer == null) {
1288            throw new XMPPException("No response from server.");
1289        }
1290        else if (answer.getError() != null) {
1291            throw new XMPPException(answer.getError());
1292        }
1293    }
1294
1295    private void changeAffiliationByOwner(Collection jids, String JavaDoc affiliation)
1296            throws XMPPException {
1297        MUCOwner iq = new MUCOwner();
1298        iq.setTo(room);
1299        iq.setType(IQ.Type.SET);
1300        for (Iterator it=jids.iterator(); it.hasNext();) {
1301            // Set the new affiliation.
1302
MUCOwner.Item item = new MUCOwner.Item(affiliation);
1303            item.setJid((String JavaDoc) it.next());
1304            iq.addItem(item);
1305        }
1306
1307        // Wait for a response packet back from the server.
1308
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1309        PacketCollector response = connection.createPacketCollector(responseFilter);
1310        // Send the change request to the server.
1311
connection.sendPacket(iq);
1312        // Wait up to a certain number of seconds for a reply.
1313
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1314        // Stop queuing results
1315
response.cancel();
1316
1317        if (answer == null) {
1318            throw new XMPPException("No response from server.");
1319        }
1320        else if (answer.getError() != null) {
1321            throw new XMPPException(answer.getError());
1322        }
1323    }
1324
1325    private void changeAffiliationByAdmin(String JavaDoc jid, String JavaDoc affiliation, String JavaDoc reason)
1326            throws XMPPException {
1327        MUCAdmin iq = new MUCAdmin();
1328        iq.setTo(room);
1329        iq.setType(IQ.Type.SET);
1330        // Set the new affiliation.
1331
MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
1332        item.setJid(jid);
1333        item.setReason(reason);
1334        iq.addItem(item);
1335
1336        // Wait for a response packet back from the server.
1337
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1338        PacketCollector response = connection.createPacketCollector(responseFilter);
1339        // Send the change request to the server.
1340
connection.sendPacket(iq);
1341        // Wait up to a certain number of seconds for a reply.
1342
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1343        // Stop queuing results
1344
response.cancel();
1345
1346        if (answer == null) {
1347            throw new XMPPException("No response from server.");
1348        }
1349        else if (answer.getError() != null) {
1350            throw new XMPPException(answer.getError());
1351        }
1352    }
1353
1354    private void changeAffiliationByAdmin(Collection jids, String JavaDoc affiliation)
1355            throws XMPPException {
1356        MUCAdmin iq = new MUCAdmin();
1357        iq.setTo(room);
1358        iq.setType(IQ.Type.SET);
1359        for (Iterator it=jids.iterator(); it.hasNext();) {
1360            // Set the new affiliation.
1361
MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
1362            item.setJid((String JavaDoc) it.next());
1363            iq.addItem(item);
1364        }
1365
1366        // Wait for a response packet back from the server.
1367
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1368        PacketCollector response = connection.createPacketCollector(responseFilter);
1369        // Send the change request to the server.
1370
connection.sendPacket(iq);
1371        // Wait up to a certain number of seconds for a reply.
1372
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1373        // Stop queuing results
1374
response.cancel();
1375
1376        if (answer == null) {
1377            throw new XMPPException("No response from server.");
1378        }
1379        else if (answer.getError() != null) {
1380            throw new XMPPException(answer.getError());
1381        }
1382    }
1383
1384    private void changeRole(String JavaDoc nickname, String JavaDoc role, String JavaDoc reason) throws XMPPException {
1385        MUCAdmin iq = new MUCAdmin();
1386        iq.setTo(room);
1387        iq.setType(IQ.Type.SET);
1388        // Set the new role.
1389
MUCAdmin.Item item = new MUCAdmin.Item(null, role);
1390        item.setNick(nickname);
1391        item.setReason(reason);
1392        iq.addItem(item);
1393
1394        // Wait for a response packet back from the server.
1395
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1396        PacketCollector response = connection.createPacketCollector(responseFilter);
1397        // Send the change request to the server.
1398
connection.sendPacket(iq);
1399        // Wait up to a certain number of seconds for a reply.
1400
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1401        // Stop queuing results
1402
response.cancel();
1403
1404        if (answer == null) {
1405            throw new XMPPException("No response from server.");
1406        }
1407        else if (answer.getError() != null) {
1408            throw new XMPPException(answer.getError());
1409        }
1410    }
1411
1412    private void changeRole(Collection nicknames, String JavaDoc role) throws XMPPException {
1413        MUCAdmin iq = new MUCAdmin();
1414        iq.setTo(room);
1415        iq.setType(IQ.Type.SET);
1416        for (Iterator it=nicknames.iterator(); it.hasNext();) {
1417            // Set the new role.
1418
MUCAdmin.Item item = new MUCAdmin.Item(null, role);
1419            item.setNick((String JavaDoc) it.next());
1420            iq.addItem(item);
1421        }
1422
1423        // Wait for a response packet back from the server.
1424
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1425        PacketCollector response = connection.createPacketCollector(responseFilter);
1426        // Send the change request to the server.
1427
connection.sendPacket(iq);
1428        // Wait up to a certain number of seconds for a reply.
1429
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1430        // Stop queuing results
1431
response.cancel();
1432
1433        if (answer == null) {
1434            throw new XMPPException("No response from server.");
1435        }
1436        else if (answer.getError() != null) {
1437            throw new XMPPException(answer.getError());
1438        }
1439    }
1440
1441    /**
1442     * Returns the number of occupants in the group chat.<p>
1443     *
1444     * Note: this value will only be accurate after joining the group chat, and
1445     * may fluctuate over time. If you query this value directly after joining the
1446     * group chat it may not be accurate, as it takes a certain amount of time for
1447     * the server to send all presence packets to this client.
1448     *
1449     * @return the number of occupants in the group chat.
1450     */

1451    public int getOccupantsCount() {
1452        synchronized (occupantsMap) {
1453            return occupantsMap.size();
1454        }
1455    }
1456
1457    /**
1458     * Returns an Iterator (of Strings) for the list of fully qualified occupants
1459     * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser".
1460     * Typically, a client would only display the nickname of the occupant. To
1461     * get the nickname from the fully qualified name, use the
1462     * {@link org.jivesoftware.smack.util.StringUtils#parseResource(String)} method.
1463     * Note: this value will only be accurate after joining the group chat, and may
1464     * fluctuate over time.
1465     *
1466     * @return an Iterator for the occupants in the group chat.
1467     */

1468    public Iterator getOccupants() {
1469        synchronized (occupantsMap) {
1470            return Collections.unmodifiableList(new ArrayList(occupantsMap.keySet())).iterator();
1471        }
1472    }
1473
1474    /**
1475     * Returns the presence info for a particular user, or <tt>null</tt> if the user
1476     * is not in the room.<p>
1477     *
1478     * @param user the room occupant to search for his presence. The format of user must
1479     * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
1480     * @return the occupant's current presence, or <tt>null</tt> if the user is unavailable
1481     * or if no presence information is available.
1482     */

1483    public Presence getOccupantPresence(String JavaDoc user) {
1484        return (Presence) occupantsMap.get(user);
1485    }
1486
1487    /**
1488     * Returns the Occupant information for a particular occupant, or <tt>null</tt> if the
1489     * user is not in the room. The Occupant object may include information such as full
1490     * JID of the user as well as the role and affiliation of the user in the room.<p>
1491     *
1492     * @param user the room occupant to search for his presence. The format of user must
1493     * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
1494     * @return the Occupant or <tt>null</tt> if the user is unavailable (i.e. not in the room).
1495     */

1496    public Occupant getOccupant(String JavaDoc user) {
1497        Presence presence = (Presence) occupantsMap.get(user);
1498        if (presence != null) {
1499            return new Occupant(presence);
1500        }
1501        return null;
1502    }
1503
1504    /**
1505     * Adds a packet listener that will be notified of any new Presence packets
1506     * sent to the group chat. Using a listener is a suitable way to know when the list
1507     * of occupants should be re-loaded due to any changes.
1508     *
1509     * @param listener a packet listener that will be notified of any presence packets
1510     * sent to the group chat.
1511     */

1512    public void addParticipantListener(PacketListener listener) {
1513        connection.addPacketListener(listener, presenceFilter);
1514        connectionListeners.add(listener);
1515    }
1516
1517    /**
1518     * Remoces a packet listener that was being notified of any new Presence packets
1519     * sent to the group chat.
1520     *
1521     * @param listener a packet listener that was being notified of any presence packets
1522     * sent to the group chat.
1523     */

1524    public void removeParticipantListener(PacketListener listener) {
1525        connection.removePacketListener(listener);
1526        connectionListeners.remove(listener);
1527    }
1528
1529    /**
1530     * Returns a collection of <code>Affiliate</code> with the room owners.
1531     *
1532     * @return a collection of <code>Affiliate</code> with the room owners.
1533     * @throws XMPPException if an error occured while performing the request to the server or you
1534     * don't have enough privileges to get this information.
1535     */

1536    public Collection getOwners() throws XMPPException {
1537        return getAffiliatesByOwner("owner");
1538    }
1539
1540    /**
1541     * Returns a collection of <code>Affiliate</code> with the room administrators.
1542     *
1543     * @return a collection of <code>Affiliate</code> with the room administrators.
1544     * @throws XMPPException if an error occured while performing the request to the server or you
1545     * don't have enough privileges to get this information.
1546     */

1547    public Collection getAdmins() throws XMPPException {
1548        return getAffiliatesByOwner("admin");
1549    }
1550
1551    /**
1552     * Returns a collection of <code>Affiliate</code> with the room members.
1553     *
1554     * @return a collection of <code>Affiliate</code> with the room members.
1555     * @throws XMPPException if an error occured while performing the request to the server or you
1556     * don't have enough privileges to get this information.
1557     */

1558    public Collection getMembers() throws XMPPException {
1559        return getAffiliatesByAdmin("member");
1560    }
1561
1562    /**
1563     * Returns a collection of <code>Affiliate</code> with the room outcasts.
1564     *
1565     * @return a collection of <code>Affiliate</code> with the room outcasts.
1566     * @throws XMPPException if an error occured while performing the request to the server or you
1567     * don't have enough privileges to get this information.
1568     */

1569    public Collection getOutcasts() throws XMPPException {
1570        return getAffiliatesByAdmin("outcast");
1571    }
1572
1573    /**
1574     * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
1575     * sending a request in the owner namespace.
1576     *
1577     * @param affiliation the affiliation of the users in the room.
1578     * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
1579     * @throws XMPPException if an error occured while performing the request to the server or you
1580     * don't have enough privileges to get this information.
1581     */

1582    private Collection getAffiliatesByOwner(String JavaDoc affiliation) throws XMPPException {
1583        MUCOwner iq = new MUCOwner();
1584        iq.setTo(room);
1585        iq.setType(IQ.Type.GET);
1586        // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
1587
MUCOwner.Item item = new MUCOwner.Item(affiliation);
1588        iq.addItem(item);
1589
1590        // Wait for a response packet back from the server.
1591
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1592        PacketCollector response = connection.createPacketCollector(responseFilter);
1593        // Send the request to the server.
1594
connection.sendPacket(iq);
1595        // Wait up to a certain number of seconds for a reply.
1596
MUCOwner answer = (MUCOwner) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1597        // Stop queuing results
1598
response.cancel();
1599
1600        if (answer == null) {
1601            throw new XMPPException("No response from server.");
1602        }
1603        else if (answer.getError() != null) {
1604            throw new XMPPException(answer.getError());
1605        }
1606        // Get the list of affiliates from the server's answer
1607
List affiliates = new ArrayList();
1608        for (Iterator it = answer.getItems(); it.hasNext();) {
1609            affiliates.add(new Affiliate((MUCOwner.Item) it.next()));
1610        }
1611        return affiliates;
1612    }
1613
1614    /**
1615     * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
1616     * sending a request in the admin namespace.
1617     *
1618     * @param affiliation the affiliation of the users in the room.
1619     * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
1620     * @throws XMPPException if an error occured while performing the request to the server or you
1621     * don't have enough privileges to get this information.
1622     */

1623    private Collection getAffiliatesByAdmin(String JavaDoc affiliation) throws XMPPException {
1624        MUCAdmin iq = new MUCAdmin();
1625        iq.setTo(room);
1626        iq.setType(IQ.Type.GET);
1627        // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
1628
MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
1629        iq.addItem(item);
1630
1631        // Wait for a response packet back from the server.
1632
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1633        PacketCollector response = connection.createPacketCollector(responseFilter);
1634        // Send the request to the server.
1635
connection.sendPacket(iq);
1636        // Wait up to a certain number of seconds for a reply.
1637
MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1638        // Stop queuing results
1639
response.cancel();
1640
1641        if (answer == null) {
1642            throw new XMPPException("No response from server.");
1643        }
1644        else if (answer.getError() != null) {
1645            throw new XMPPException(answer.getError());
1646        }
1647        // Get the list of affiliates from the server's answer
1648
List affiliates = new ArrayList();
1649        for (Iterator it = answer.getItems(); it.hasNext();) {
1650            affiliates.add(new Affiliate((MUCAdmin.Item) it.next()));
1651        }
1652        return affiliates;
1653    }
1654
1655    /**
1656     * Returns a collection of <code>Occupant</code> with the room moderators.
1657     *
1658     * @return a collection of <code>Occupant</code> with the room moderators.
1659     * @throws XMPPException if an error occured while performing the request to the server or you
1660     * don't have enough privileges to get this information.
1661     */

1662    public Collection getModerators() throws XMPPException {
1663        return getOccupants("moderator");
1664    }
1665
1666    /**
1667     * Returns a collection of <code>Occupant</code> with the room participants.
1668     *
1669     * @return a collection of <code>Occupant</code> with the room participants.
1670     * @throws XMPPException if an error occured while performing the request to the server or you
1671     * don't have enough privileges to get this information.
1672     */

1673    public Collection getParticipants() throws XMPPException {
1674        return getOccupants("participant");
1675    }
1676
1677    /**
1678     * Returns a collection of <code>Occupant</code> that have the specified room role.
1679     *
1680     * @param role the role of the occupant in the room.
1681     * @return a collection of <code>Occupant</code> that have the specified room role.
1682     * @throws XMPPException if an error occured while performing the request to the server or you
1683     * don't have enough privileges to get this information.
1684     */

1685    private Collection getOccupants(String JavaDoc role) throws XMPPException {
1686        MUCAdmin iq = new MUCAdmin();
1687        iq.setTo(room);
1688        iq.setType(IQ.Type.GET);
1689        // Set the specified role. This may request the list of moderators/participants.
1690
MUCAdmin.Item item = new MUCAdmin.Item(null, role);
1691        iq.addItem(item);
1692
1693        // Wait for a response packet back from the server.
1694
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
1695        PacketCollector response = connection.createPacketCollector(responseFilter);
1696        // Send the request to the server.
1697
connection.sendPacket(iq);
1698        // Wait up to a certain number of seconds for a reply.
1699
MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1700        // Stop queuing results
1701
response.cancel();
1702
1703        if (answer == null) {
1704            throw new XMPPException("No response from server.");
1705        }
1706        else if (answer.getError() != null) {
1707            throw new XMPPException(answer.getError());
1708        }
1709        // Get the list of participants from the server's answer
1710
List participants = new ArrayList();
1711        for (Iterator it = answer.getItems(); it.hasNext();) {
1712            participants.add(new Occupant((MUCAdmin.Item) it.next()));
1713        }
1714        return participants;
1715    }
1716
1717    /**
1718     * Sends a message to the chat room.
1719     *
1720     * @param text the text of the message to send.
1721     * @throws XMPPException if sending the message fails.
1722     */

1723    public void sendMessage(String JavaDoc text) throws XMPPException {
1724        Message message = new Message(room, Message.Type.GROUP_CHAT);
1725        message.setBody(text);
1726        connection.sendPacket(message);
1727    }
1728
1729    /**
1730     * Returns a new Chat for sending private messages to a given room occupant.
1731     * The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server
1732     * service will change the 'from' address to the sender's room JID and delivering the message
1733     * to the intended recipient's full JID.
1734     *
1735     * @param occupant occupant unique room JID (e.g. 'darkcave@macbeth.shakespeare.lit/Paul').
1736     * @return new Chat for sending private messages to a given room occupant.
1737     */

1738    public Chat createPrivateChat(String JavaDoc occupant) {
1739        return new Chat(connection, occupant);
1740    }
1741
1742    /**
1743     * Creates a new Message to send to the chat room.
1744     *
1745     * @return a new Message addressed to the chat room.
1746     */

1747    public Message createMessage() {
1748        return new Message(room, Message.Type.GROUP_CHAT);
1749    }
1750
1751    /**
1752     * Sends a Message to the chat room.
1753     *
1754     * @param message the message.
1755     * @throws XMPPException if sending the message fails.
1756     */

1757    public void sendMessage(Message message) throws XMPPException {
1758        connection.sendPacket(message);
1759    }
1760
1761    /**
1762    * Polls for and returns the next message, or <tt>null</tt> if there isn't
1763    * a message immediately available. This method provides significantly different
1764    * functionalty than the {@link #nextMessage()} method since it's non-blocking.
1765    * In other words, the method call will always return immediately, whereas the
1766    * nextMessage method will return only when a message is available (or after
1767    * a specific timeout).
1768    *
1769    * @return the next message if one is immediately available and
1770    * <tt>null</tt> otherwise.
1771    */

1772    public Message pollMessage() {
1773        return (Message) messageCollector.pollResult();
1774    }
1775
1776    /**
1777     * Returns the next available message in the chat. The method call will block
1778     * (not return) until a message is available.
1779     *
1780     * @return the next message.
1781     */

1782    public Message nextMessage() {
1783        return (Message) messageCollector.nextResult();
1784    }
1785
1786    /**
1787     * Returns the next available message in the chat. The method call will block
1788     * (not return) until a packet is available or the <tt>timeout</tt> has elapased.
1789     * If the timeout elapses without a result, <tt>null</tt> will be returned.
1790     *
1791     * @param timeout the maximum amount of time to wait for the next message.
1792     * @return the next message, or <tt>null</tt> if the timeout elapses without a
1793     * message becoming available.
1794     */

1795    public Message nextMessage(long timeout) {
1796        return (Message) messageCollector.nextResult(timeout);
1797    }
1798
1799    /**
1800     * Adds a packet listener that will be notified of any new messages in the
1801     * group chat. Only "group chat" messages addressed to this group chat will
1802     * be delivered to the listener. If you wish to listen for other packets
1803     * that may be associated with this group chat, you should register a
1804     * PacketListener directly with the XMPPConnection with the appropriate
1805     * PacketListener.
1806     *
1807     * @param listener a packet listener.
1808     */

1809    public void addMessageListener(PacketListener listener) {
1810        connection.addPacketListener(listener, messageFilter);
1811        connectionListeners.add(listener);
1812    }
1813
1814    /**
1815     * Removes a packet listener that was being notified of any new messages in the
1816     * multi user chat. Only "group chat" messages addressed to this multi user chat were
1817     * being delivered to the listener.
1818     *
1819     * @param listener a packet listener.
1820     */

1821    public void removeMessageListener(PacketListener listener) {
1822        connection.removePacketListener(listener);
1823        connectionListeners.remove(listener);
1824    }
1825
1826    /**
1827     * Changes the subject within the room. As a default, only users with a role of "moderator"
1828     * are allowed to change the subject in a room. Although some rooms may be configured to
1829     * allow a mere participant or even a visitor to change the subject.
1830     *
1831     * @param subject the new room's subject to set.
1832     * @throws XMPPException if someone without appropriate privileges attempts to change the
1833     * room subject will throw an error with code 403 (i.e. Forbidden)
1834     */

1835    public void changeSubject(final String JavaDoc subject) throws XMPPException {
1836        Message message = new Message(room, Message.Type.GROUP_CHAT);
1837        message.setSubject(subject);
1838        // Wait for an error or confirmation message back from the server.
1839
PacketFilter responseFilter =
1840            new AndFilter(
1841                new FromMatchesFilter(room),
1842                new PacketTypeFilter(Message.class));
1843        responseFilter = new AndFilter(responseFilter, new PacketFilter() {
1844            public boolean accept(Packet packet) {
1845                Message msg = (Message) packet;
1846                return subject.equals(msg.getSubject());
1847            }
1848        });
1849        PacketCollector response = connection.createPacketCollector(responseFilter);
1850        // Send change subject packet.
1851
connection.sendPacket(message);
1852        // Wait up to a certain number of seconds for a reply.
1853
Message answer =
1854            (Message) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
1855        // Stop queuing results
1856
response.cancel();
1857
1858        if (answer == null) {
1859            throw new XMPPException("No response from server.");
1860        }
1861        else if (answer.getError() != null) {
1862            throw new XMPPException(answer.getError());
1863        }
1864    }
1865
1866    /**
1867     * Notification message that the user has joined the room.
1868     */

1869    private synchronized void userHasJoined() {
1870        // Update the list of joined rooms through this connection
1871
ArrayList rooms = (ArrayList)joinedRooms.get(connection);
1872        if (rooms == null) {
1873            rooms = new ArrayList();
1874            joinedRooms.put(connection, rooms);
1875        }
1876        rooms.add(room);
1877    }
1878    
1879    /**
1880     * Notification message that the user has left the room.
1881     */

1882    private synchronized void userHasLeft() {
1883        // Update the list of joined rooms through this connection
1884
ArrayList rooms = (ArrayList)joinedRooms.get(connection);
1885        if (rooms == null) {
1886            return;
1887        }
1888        rooms.remove(room);
1889    }
1890    
1891    /**
1892     * Returns the MUCUser packet extension included in the packet or <tt>null</tt> if none.
1893     *
1894     * @param packet the packet that may include the MUCUser extension.
1895     * @return the MUCUser found in the packet.
1896     */

1897    private MUCUser getMUCUserExtension(Packet packet) {
1898        if (packet != null) {
1899            // Get the MUC User extension
1900
return (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
1901        }
1902        return null;
1903    }
1904
1905    /**
1906     * Adds a listener that will be notified of changes in your status in the room
1907     * such as the user being kicked, banned, or granted admin permissions.
1908     *
1909     * @param listener a user status listener.
1910     */

1911    public void addUserStatusListener(UserStatusListener listener) {
1912        synchronized (userStatusListeners) {
1913            if (!userStatusListeners.contains(listener)) {
1914                userStatusListeners.add(listener);
1915            }
1916        }
1917    }
1918
1919    /**
1920     * Removes a listener that was being notified of changes in your status in the room
1921     * such as the user being kicked, banned, or granted admin permissions.
1922     *
1923     * @param listener a user status listener.
1924     */

1925    public void removeUserStatusListener(UserStatusListener listener) {
1926        synchronized (userStatusListeners) {
1927            userStatusListeners.remove(listener);
1928        }
1929    }
1930
1931    private void fireUserStatusListeners(String JavaDoc methodName, Object JavaDoc[] params) {
1932        UserStatusListener[] listeners = null;
1933        synchronized (userStatusListeners) {
1934            listeners = new UserStatusListener[userStatusListeners.size()];
1935            userStatusListeners.toArray(listeners);
1936        }
1937        // Get the classes of the method parameters
1938
Class JavaDoc[] paramClasses = new Class JavaDoc[params.length];
1939        for (int i = 0; i < params.length; i++) {
1940            paramClasses[i] = params[i].getClass();
1941        }
1942        try {
1943            // Get the method to execute based on the requested methodName and parameters classes
1944
Method method = UserStatusListener.class.getDeclaredMethod(methodName, paramClasses);
1945            for (int i = 0; i < listeners.length; i++) {
1946                method.invoke(listeners[i], params);
1947            }
1948        } catch (NoSuchMethodException JavaDoc e) {
1949            e.printStackTrace();
1950        } catch (InvocationTargetException e) {
1951            e.printStackTrace();
1952        } catch (IllegalAccessException JavaDoc e) {
1953            e.printStackTrace();
1954        }
1955    }
1956
1957    /**
1958     * Adds a listener that will be notified of changes in occupants status in the room
1959     * such as the user being kicked, banned, or granted admin permissions.
1960     *
1961     * @param listener a participant status listener.
1962     */

1963    public void addParticipantStatusListener(ParticipantStatusListener listener) {
1964        synchronized (participantStatusListeners) {
1965            if (!participantStatusListeners.contains(listener)) {
1966                participantStatusListeners.add(listener);
1967            }
1968        }
1969    }
1970
1971    /**
1972     * Removes a listener that was being notified of changes in occupants status in the room
1973     * such as the user being kicked, banned, or granted admin permissions.
1974     *
1975     * @param listener a participant status listener.
1976     */

1977    public void removeParticipantStatusListener(ParticipantStatusListener listener) {
1978        synchronized (participantStatusListeners) {
1979            participantStatusListeners.remove(listener);
1980        }
1981    }
1982
1983    private void fireParticipantStatusListeners(String JavaDoc methodName, String JavaDoc param) {
1984        ParticipantStatusListener[] listeners = null;
1985        synchronized (participantStatusListeners) {
1986            listeners = new ParticipantStatusListener[participantStatusListeners.size()];
1987            participantStatusListeners.toArray(listeners);
1988        }
1989        try {
1990            // Get the method to execute based on the requested methodName and parameter
1991
Method method =
1992                ParticipantStatusListener.class.getDeclaredMethod(
1993                    methodName,
1994                    new Class JavaDoc[] { String JavaDoc.class });
1995            for (int i = 0; i < listeners.length; i++) {
1996                method.invoke(listeners[i], new Object JavaDoc[] {param});
1997            }
1998        } catch (NoSuchMethodException JavaDoc e) {
1999            e.printStackTrace();
2000        } catch (InvocationTargetException e) {
2001            e.printStackTrace();
2002        } catch (IllegalAccessException JavaDoc e) {
2003            e.printStackTrace();
2004        }
2005    }
2006    
2007    private void init() {
2008        // Create a collector for incoming messages.
2009
messageFilter =
2010            new AndFilter(
2011                new FromMatchesFilter(room),
2012                new MessageTypeFilter(Message.Type.GROUP_CHAT));
2013        messageFilter = new AndFilter(messageFilter, new PacketFilter() {
2014            public boolean accept(Packet packet) {
2015                Message msg = (Message) packet;
2016                return msg.getBody() != null;
2017            }
2018        });
2019        messageCollector = connection.createPacketCollector(messageFilter);
2020
2021        // Create a listener for subject updates.
2022
subjectFilter =
2023            new AndFilter(
2024                new FromMatchesFilter(room),
2025                new MessageTypeFilter(Message.Type.GROUP_CHAT));
2026        subjectFilter = new AndFilter(subjectFilter, new PacketFilter() {
2027            public boolean accept(Packet packet) {
2028                Message msg = (Message) packet;
2029                return msg.getSubject() != null;
2030            }
2031        });
2032        subjectListener = new PacketListener() {
2033            public void processPacket(Packet packet) {
2034                Message msg = (Message) packet;
2035                // Update the room subject
2036
subject = msg.getSubject();
2037                // Fire event for subject updated listeners
2038
fireSubjectUpdatedListeners(
2039                    msg.getSubject(),
2040                    msg.getFrom());
2041                
2042            }
2043        };
2044        connection.addPacketListener(subjectListener, subjectFilter);
2045
2046        // Create a listener for all presence updates.
2047
presenceFilter =
2048            new AndFilter(new FromMatchesFilter(room), new PacketTypeFilter(Presence.class));
2049        presenceListener = new PacketListener() {
2050            public void processPacket(Packet packet) {
2051                Presence presence = (Presence) packet;
2052                String JavaDoc from = presence.getFrom();
2053                String JavaDoc myRoomJID = room + "/" + nickname;
2054                boolean isUserStatusModification = presence.getFrom().equals(myRoomJID);
2055                if (presence.getType() == Presence.Type.AVAILABLE) {
2056                    Presence oldPresence;
2057                    synchronized (occupantsMap) {
2058                        oldPresence = (Presence)occupantsMap.get(from);
2059                        occupantsMap.put(from, presence);
2060                    }
2061                    if (oldPresence != null) {
2062                        // Get the previous occupant's affiliation & role
2063
MUCUser mucExtension = getMUCUserExtension(oldPresence);
2064                        String JavaDoc oldAffiliation = mucExtension.getItem().getAffiliation();
2065                        String JavaDoc oldRole = mucExtension.getItem().getRole();
2066                        // Get the new occupant's affiliation & role
2067
mucExtension = getMUCUserExtension(presence);
2068                        String JavaDoc newAffiliation = mucExtension.getItem().getAffiliation();
2069                        String JavaDoc newRole = mucExtension.getItem().getRole();
2070                        // Fire role modification events
2071
checkRoleModifications(oldRole, newRole, isUserStatusModification, from);
2072                        // Fire affiliation modification events
2073
checkAffiliationModifications(
2074                            oldAffiliation,
2075                            newAffiliation,
2076                            isUserStatusModification,
2077                            from);
2078                    }
2079                    else {
2080                        // A new occupant has joined the room
2081
if (!isUserStatusModification) {
2082                            fireParticipantStatusListeners("joined", from);
2083                        }
2084                    }
2085                }
2086                else if (presence.getType() == Presence.Type.UNAVAILABLE) {
2087                    synchronized (occupantsMap) {
2088                        occupantsMap.remove(from);
2089                    }
2090                    MUCUser mucUser = getMUCUserExtension(presence);
2091                    if (mucUser != null && mucUser.getStatus() != null) {
2092                        // Fire events according to the received presence code
2093
checkPresenceCode(
2094                            mucUser.getStatus().getCode(),
2095                            presence.getFrom().equals(myRoomJID),
2096                            mucUser,
2097                            from);
2098                    } else {
2099                        // An occupant has left the room
2100
if (!isUserStatusModification) {
2101                            fireParticipantStatusListeners("left", from);
2102                        }
2103                    }
2104                }
2105            }
2106        };
2107        connection.addPacketListener(presenceListener, presenceFilter);
2108
2109        // Listens for all messages that include a MUCUser extension and fire the invitation
2110
// rejection listeners if the message includes an invitation rejection.
2111
declinesFilter = new PacketExtensionFilter("x", "http://jabber.org/protocol/muc#user");
2112        declinesListener = new PacketListener() {
2113            public void processPacket(Packet packet) {
2114                // Get the MUC User extension
2115
MUCUser mucUser = getMUCUserExtension(packet);
2116                // Check if the MUCUser informs that the invitee has declined the invitation
2117
if (mucUser.getDecline() != null) {
2118                    // Fire event for invitation rejection listeners
2119
fireInvitationRejectionListeners(
2120                        mucUser.getDecline().getFrom(),
2121                        mucUser.getDecline().getReason());
2122                }
2123            };
2124        };
2125        connection.addPacketListener(declinesListener, declinesFilter);
2126    }
2127
2128    /**
2129     * Fires notification events if the role of a room occupant has changed. If the occupant that
2130     * changed his role is your occupant then the <code>UserStatusListeners</code> added to this
2131     * <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed
2132     * his role is not yours then the <code>ParticipantStatusListeners</code> added to this
2133     * <code>MultiUserChat</code> will be fired. The following table shows the events that will
2134     * be fired depending on the previous and new role of the occupant.
2135     *
2136     * <pre>
2137     * <table border="1">
2138     * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
2139     *
2140     * <tr><td>None</td><td>Visitor</td><td>--</td></tr>
2141     * <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr>
2142     * <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr>
2143     *
2144     * <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr>
2145     * <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
2146     * <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
2147     *
2148     * <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr>
2149     * <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr>
2150     * <tr><td>Visitor</td><td>None</td><td>kicked</td></tr>
2151     *
2152     * <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr>
2153     * <tr><td>Moderator</td><td>None</td><td>kicked</td></tr>
2154     * <tr><td>Participant</td><td>None</td><td>kicked</td></tr>
2155     * </table>
2156     * </pre>
2157     *
2158     * @param oldRole the previous role of the user in the room before receiving the new presence
2159     * @param newRole the new role of the user in the room after receiving the new presence
2160     * @param isUserModification whether the received presence is about your user in the room or not
2161     * @param from the occupant whose role in the room has changed
2162     * (e.g. room@conference.jabber.org/nick).
2163     */

2164    private void checkRoleModifications(
2165        String JavaDoc oldRole,
2166        String JavaDoc newRole,
2167        boolean isUserModification,
2168        String JavaDoc from) {
2169        // Voice was granted to a visitor
2170
if (("visitor".equals(oldRole) || "none".equals(oldRole))
2171            && "participant".equals(newRole)) {
2172            if (isUserModification) {
2173                fireUserStatusListeners("voiceGranted", new Object JavaDoc[] {});
2174            }
2175            else {
2176                fireParticipantStatusListeners("voiceGranted", from);
2177            }
2178        }
2179        // The participant's voice was revoked from the room
2180
else if (
2181            "participant".equals(oldRole)
2182                && ("visitor".equals(newRole) || "none".equals(newRole))) {
2183            if (isUserModification) {
2184                fireUserStatusListeners("voiceRevoked", new Object JavaDoc[] {});
2185            }
2186            else {
2187                fireParticipantStatusListeners("voiceRevoked", from);
2188            }
2189        }
2190        // Moderator privileges were granted to a participant
2191
if (!"moderator".equals(oldRole) && "moderator".equals(newRole)) {
2192            if ("visitor".equals(oldRole) || "none".equals(oldRole)) {
2193                if (isUserModification) {
2194                    fireUserStatusListeners("voiceGranted", new Object JavaDoc[] {});
2195                }
2196                else {
2197                    fireParticipantStatusListeners("voiceGranted", from);
2198                }
2199            }
2200            if (isUserModification) {
2201                fireUserStatusListeners("moderatorGranted", new Object JavaDoc[] {});
2202            }
2203            else {
2204                fireParticipantStatusListeners("moderatorGranted", from);
2205            }
2206        }
2207        // Moderator privileges were revoked from a participant
2208
else if ("moderator".equals(oldRole) && !"moderator".equals(newRole)) {
2209            if ("visitor".equals(newRole) || "none".equals(newRole)) {
2210                if (isUserModification) {
2211                    fireUserStatusListeners("voiceRevoked", new Object JavaDoc[] {});
2212                }
2213                else {
2214                    fireParticipantStatusListeners("voiceRevoked", from);
2215                }
2216            }
2217            if (isUserModification) {
2218                fireUserStatusListeners("moderatorRevoked", new Object JavaDoc[] {});
2219            }
2220            else {
2221                fireParticipantStatusListeners("moderatorRevoked", from);
2222            }
2223        }
2224    }
2225
2226    /**
2227     * Fires notification events if the affiliation of a room occupant has changed. If the
2228     * occupant that changed his affiliation is your occupant then the
2229     * <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired.
2230     * On the other hand, if the occupant that changed his affiliation is not yours then the
2231     * <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be
2232     * fired. The following table shows the events that will be fired depending on the previous
2233     * and new affiliation of the occupant.
2234     *
2235     * <pre>
2236     * <table border="1">
2237     * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
2238     *
2239     * <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr>
2240     * <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr>
2241     * <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr>
2242     *
2243     * <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr>
2244     * <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr>
2245     * <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr>
2246     *
2247     * <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr>
2248     * <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr>
2249     * <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr>
2250     *
2251     * <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr>
2252     * <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr>
2253     * <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr>
2254     * <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr>
2255     * </table>
2256     * </pre>
2257     *
2258     * @param oldAffiliation the previous affiliation of the user in the room before receiving the
2259     * new presence
2260     * @param newAffiliation the new affiliation of the user in the room after receiving the new
2261     * presence
2262     * @param isUserModification whether the received presence is about your user in the room or not
2263     * @param from the occupant whose role in the room has changed
2264     * (e.g. room@conference.jabber.org/nick).
2265     */

2266    private void checkAffiliationModifications(
2267        String JavaDoc oldAffiliation,
2268        String JavaDoc newAffiliation,
2269        boolean isUserModification,
2270        String JavaDoc from) {
2271        // First check for revoked affiliation and then for granted affiliations. The idea is to
2272
// first fire the "revoke" events and then fire the "grant" events.
2273

2274        // The user's ownership to the room was revoked
2275
if ("owner".equals(oldAffiliation) && !"owner".equals(newAffiliation)) {
2276            if (isUserModification) {
2277                fireUserStatusListeners("ownershipRevoked", new Object JavaDoc[] {});
2278            }
2279            else {
2280                fireParticipantStatusListeners("ownershipRevoked", from);
2281            }
2282        }
2283        // The user's administrative privileges to the room were revoked
2284
else if ("admin".equals(oldAffiliation) && !"admin".equals(newAffiliation)) {
2285            if (isUserModification) {
2286                fireUserStatusListeners("adminRevoked", new Object JavaDoc[] {});
2287            }
2288            else {
2289                fireParticipantStatusListeners("adminRevoked", from);
2290            }
2291        }
2292        // The user's membership to the room was revoked
2293
else if ("member".equals(oldAffiliation) && !"member".equals(newAffiliation)) {
2294            if (isUserModification) {
2295                fireUserStatusListeners("membershipRevoked", new Object JavaDoc[] {});
2296            }
2297            else {
2298                fireParticipantStatusListeners("membershipRevoked", from);
2299            }
2300        }
2301
2302        // The user was granted ownership to the room
2303
if (!"owner".equals(oldAffiliation) && "owner".equals(newAffiliation)) {
2304            if (isUserModification) {
2305                fireUserStatusListeners("ownershipGranted", new Object JavaDoc[] {});
2306            }
2307            else {
2308                fireParticipantStatusListeners("ownershipGranted", from);
2309            }
2310        }
2311        // The user was granted administrative privileges to the room
2312
else if (!"admin".equals(oldAffiliation) && "admin".equals(newAffiliation)) {
2313            if (isUserModification) {
2314                fireUserStatusListeners("adminGranted", new Object JavaDoc[] {});
2315            }
2316            else {
2317                fireParticipantStatusListeners("adminGranted", from);
2318            }
2319        }
2320        // The user was granted membership to the room
2321
else if (!"member".equals(oldAffiliation) && "member".equals(newAffiliation)) {
2322            if (isUserModification) {
2323                fireUserStatusListeners("membershipGranted", new Object JavaDoc[] {});
2324            }
2325            else {
2326                fireParticipantStatusListeners("membershipGranted", from);
2327            }
2328        }
2329    }
2330
2331    /**
2332     * Fires events according to the received presence code.
2333     *
2334     * @param code
2335     * @param isUserModification
2336     * @param mucUser
2337     * @param from
2338     */

2339    private void checkPresenceCode(
2340        String JavaDoc code,
2341        boolean isUserModification,
2342        MUCUser mucUser,
2343        String JavaDoc from) {
2344        // Check if an occupant was kicked from the room
2345
if ("307".equals(code)) {
2346            // Check if this occupant was kicked
2347
if (isUserModification) {
2348                joined = false;
2349
2350                fireUserStatusListeners(
2351                    "kicked",
2352                    new Object JavaDoc[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});
2353
2354                // Reset occupant information.
2355
occupantsMap = new HashMap();
2356                nickname = null;
2357                userHasLeft();
2358            }
2359            else {
2360                fireParticipantStatusListeners("kicked", from);
2361            }
2362        }
2363        // A user was banned from the room
2364
else if ("301".equals(code)) {
2365            // Check if this occupant was banned
2366
if (isUserModification) {
2367                joined = false;
2368
2369                fireUserStatusListeners(
2370                    "banned",
2371                    new Object JavaDoc[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});
2372
2373                // Reset occupant information.
2374
occupantsMap = new HashMap();
2375                nickname = null;
2376                userHasLeft();
2377            }
2378            else {
2379                // TODO Check if we have to send the JID of the banned user
2380
fireParticipantStatusListeners("banned", from);
2381            }
2382        }
2383        // A user's membership was revoked from the room
2384
else if ("321".equals(code)) {
2385            // Check if this occupant's membership was revoked
2386
if (isUserModification) {
2387                joined = false;
2388
2389                fireUserStatusListeners("membershipRevoked", new Object JavaDoc[] {});
2390
2391                // Reset occupant information.
2392
occupantsMap = new HashMap();
2393                nickname = null;
2394                userHasLeft();
2395            }
2396        }
2397        // A occupant has changed his nickname in the room
2398
else if ("303".equals(code)) {
2399            fireParticipantStatusListeners("nicknameChanged", mucUser.getItem().getNick());
2400        }
2401    }
2402
2403    public void finalize() throws Throwable JavaDoc {
2404        super.finalize();
2405        try {
2406            if (connection != null) {
2407                messageCollector.cancel();
2408                connection.removePacketListener(subjectListener);
2409                connection.removePacketListener(presenceListener);
2410                connection.removePacketListener(declinesListener);
2411                // Remove all the PacketListeners added to the connection by this chat
2412
for (Iterator it=connectionListeners.iterator(); it.hasNext();) {
2413                    connection.removePacketListener((PacketListener) it.next());
2414                }
2415            }
2416        }
2417        catch (Exception JavaDoc e) {}
2418    }
2419
2420    /**
2421     * An InvitationsMonitor monitors a given connection to detect room invitations. Every
2422     * time the InvitationsMonitor detects a new invitation it will fire the invitation listeners.
2423     *
2424     * @author Gaston Dombiak
2425     */

2426    private static class InvitationsMonitor implements ConnectionListener {
2427        // We use a WeakHashMap so that the GC can collect the monitor when the
2428
// connection is no longer referenced by any object.
2429
private static Map monitors = new WeakHashMap();
2430
2431        private List invitationsListeners = new ArrayList();
2432        private XMPPConnection connection;
2433        private PacketFilter invitationFilter;
2434        private PacketListener invitationPacketListener;
2435
2436        /**
2437         * Returns a new or existing InvitationsMonitor for a given connection.
2438         *
2439         * @param conn the connection to monitor for room invitations.
2440         * @return a new or existing InvitationsMonitor for a given connection.
2441         */

2442        public static InvitationsMonitor getInvitationsMonitor(XMPPConnection conn) {
2443            synchronized (monitors) {
2444                if (!monitors.containsKey(conn)) {
2445                    // We need to use a WeakReference because the monitor references the
2446
// connection and this could prevent the GC from collecting the monitor
2447
// when no other object references the monitor
2448
monitors.put(conn, new WeakReference JavaDoc(new InvitationsMonitor(conn)));
2449                }
2450                // Return the InvitationsMonitor that monitors the connection
2451
return (InvitationsMonitor) ((WeakReference JavaDoc) monitors.get(conn)).get();
2452            }
2453        }
2454
2455        /**
2456         * Creates a new InvitationsMonitor that will monitor invitations received
2457         * on a given connection.
2458         *
2459         * @param connection the connection to monitor for possible room invitations
2460         */

2461        private InvitationsMonitor(XMPPConnection connection) {
2462            this.connection = connection;
2463        }
2464
2465        /**
2466         * Adds a listener to invitation notifications. The listener will be fired anytime
2467         * an invitation is received.<p>
2468         *
2469         * If this is the first monitor's listener then the monitor will be initialized in
2470         * order to start listening to room invitations.
2471         *
2472         * @param listener an invitation listener.
2473         */

2474        public void addInvitationListener(InvitationListener listener) {
2475            synchronized (invitationsListeners) {
2476                // If this is the first monitor's listener then initialize the listeners
2477
// on the connection to detect room invitations
2478
if (invitationsListeners.size() == 0) {
2479                    init();
2480                }
2481                if (!invitationsListeners.contains(listener)) {
2482                    invitationsListeners.add(listener);
2483                }
2484            }
2485        }
2486
2487        /**
2488         * Removes a listener to invitation notifications. The listener will be fired anytime
2489         * an invitation is received.<p>
2490         *
2491         * If there are no more listeners to notifiy for room invitations then the monitor will
2492         * be stopped. As soon as a new listener is added to the monitor, the monitor will resume
2493         * monitoring the connection for new room invitations.
2494         *
2495         * @param listener an invitation listener.
2496         */

2497        public void removeInvitationListener(InvitationListener listener) {
2498            synchronized (invitationsListeners) {
2499                if (invitationsListeners.contains(listener)) {
2500                    invitationsListeners.remove(listener);
2501                }
2502                // If there are no more listeners to notifiy for room invitations
2503
// then proceed to cancel/release this monitor
2504
if (invitationsListeners.size() == 0) {
2505                    cancel();
2506                }
2507            }
2508        }
2509
2510        /**
2511         * Fires invitation listeners.
2512         */

2513        private void fireInvitationListeners(String JavaDoc room, String JavaDoc inviter, String JavaDoc reason, String JavaDoc password,
2514                                             Message message) {
2515            InvitationListener[] listeners = null;
2516            synchronized (invitationsListeners) {
2517                listeners = new InvitationListener[invitationsListeners.size()];
2518                invitationsListeners.toArray(listeners);
2519            }
2520            for (int i = 0; i < listeners.length; i++) {
2521                listeners[i].invitationReceived(connection, room, inviter, reason, password, message);
2522            }
2523        }
2524
2525        public void connectionClosed() {
2526            cancel();
2527        }
2528
2529        public void connectionClosedOnError(Exception JavaDoc e) {
2530            cancel();
2531        }
2532
2533        /**
2534         * Initializes the listeners to detect received room invitations and to detect when the
2535         * connection gets closed. As soon as a room invitation is received the invitations
2536         * listeners will be fired. When the connection gets closed the monitor will remove
2537         * his listeners on the connection.
2538         */

2539        private void init() {
2540            // Listens for all messages that include a MUCUser extension and fire the invitation
2541
// listeners if the message includes an invitation.
2542
invitationFilter =
2543                new PacketExtensionFilter("x", "http://jabber.org/protocol/muc#user");
2544            invitationPacketListener = new PacketListener() {
2545                public void processPacket(Packet packet) {
2546                    // Get the MUCUser extension
2547
MUCUser mucUser =
2548                        (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
2549                    // Check if the MUCUser extension includes an invitation
2550
if (mucUser.getInvite() != null) {
2551                        // Fire event for invitation listeners
2552
fireInvitationListeners(packet.getFrom(), mucUser.getInvite().getFrom(),
2553                                mucUser.getInvite().getReason(), mucUser.getPassword(), (Message) packet);
2554                    }
2555                };
2556            };
2557            connection.addPacketListener(invitationPacketListener, invitationFilter);
2558            // Add a listener to detect when the connection gets closed in order to
2559
// cancel/release this monitor
2560
connection.addConnectionListener(this);
2561        }
2562
2563        /**
2564         * Cancels all the listeners that this InvitationsMonitor has added to the connection.
2565         */

2566        private void cancel() {
2567            connection.removePacketListener(invitationPacketListener);
2568            connection.removeConnectionListener(this);
2569        }
2570
2571    }
2572}
2573
Popular Tags