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