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