1/** 2 * $RCSfile$ 3 * $Revision$ 4 * $Date$ 5 * 6 * Copyright 2003-2007 Jive Software. 7 * 8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); 9 * you may not use this file except in compliance with the License. 10 * You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, software 15 * distributed under the License is distributed on an "AS IS" BASIS, 16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 * See the License for the specific language governing permissions and 18 * limitations under the License. 19 */ 20 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} 2744