aboutsummaryrefslogtreecommitdiff
path: root/src/org/jivesoftware/smackx/muc/MultiUserChat.java
blob: 9a464447df26c609f1c82212e178aaa04e050a8e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
 * Copyright 2003-2007 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.smackx.muc;

import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.MessageListener;
import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.PacketInterceptor;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.FromMatchesFilter;
import org.jivesoftware.smack.filter.MessageTypeFilter;
import org.jivesoftware.smack.filter.PacketExtensionFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Registration;
import org.jivesoftware.smackx.Form;
import org.jivesoftware.smackx.NodeInformationProvider;
import org.jivesoftware.smackx.ServiceDiscoveryManager;
import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.DiscoverItems;
import org.jivesoftware.smackx.packet.MUCAdmin;
import org.jivesoftware.smackx.packet.MUCInitialPresence;
import org.jivesoftware.smackx.packet.MUCOwner;
import org.jivesoftware.smackx.packet.MUCUser;

/**
 * A MultiUserChat is a conversation that takes place among many users in a virtual
 * room. A room could have many occupants with different affiliation and roles.
 * Possible affiliatons are "owner", "admin", "member", and "outcast". Possible roles
 * are "moderator", "participant", and "visitor". Each role and affiliation guarantees
 * different privileges (e.g. Send messages to all occupants, Kick participants and visitors,
 * Grant voice, Edit member list, etc.).
 *
 * @author Gaston Dombiak, Larry Kirschner
 */
public class MultiUserChat {

    private final static String discoNamespace = "http://jabber.org/protocol/muc";
    private final static String discoNode = "http://jabber.org/protocol/muc#rooms";

    private static Map<Connection, List<String>> joinedRooms =
            new WeakHashMap<Connection, List<String>>();

    private Connection connection;
    private String room;
    private String subject;
    private String nickname = null;
    private boolean joined = false;
    private Map<String, Presence> occupantsMap = new ConcurrentHashMap<String, Presence>();

    private final List<InvitationRejectionListener> invitationRejectionListeners =
            new ArrayList<InvitationRejectionListener>();
    private final List<SubjectUpdatedListener> subjectUpdatedListeners =
            new ArrayList<SubjectUpdatedListener>();
    private final List<UserStatusListener> userStatusListeners =
            new ArrayList<UserStatusListener>();
    private final List<ParticipantStatusListener> participantStatusListeners =
            new ArrayList<ParticipantStatusListener>();

    private PacketFilter presenceFilter;
    private List<PacketInterceptor> presenceInterceptors = new ArrayList<PacketInterceptor>();
    private PacketFilter messageFilter;
    private RoomListenerMultiplexor roomListenerMultiplexor;
    private ConnectionDetachedPacketCollector messageCollector;
    private List<PacketListener> connectionListeners = new ArrayList<PacketListener>();

    static {
        Connection.addConnectionCreationListener(new ConnectionCreationListener() {
            public void connectionCreated(final Connection connection) {
                // Set on every established connection that this client supports the Multi-User
                // Chat protocol. This information will be used when another client tries to
                // discover whether this client supports MUC or not.
                ServiceDiscoveryManager.getInstanceFor(connection).addFeature(discoNamespace);
                // Set the NodeInformationProvider that will provide information about the
                // joined rooms whenever a disco request is received
                ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(
                    discoNode,
                    new NodeInformationProvider() {
                        public List<DiscoverItems.Item> getNodeItems() {
                            List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>();
                            Iterator<String> rooms=MultiUserChat.getJoinedRooms(connection);
                            while (rooms.hasNext()) {
                                answer.add(new DiscoverItems.Item(rooms.next()));
                            }
                            return answer;
                        }

                        public List<String> getNodeFeatures() {
                            return null;
                        }

                        public List<DiscoverInfo.Identity> getNodeIdentities() {
                            return null;
                        }

                        @Override
                        public List<PacketExtension> getNodePacketExtensions() {
                            return null;
                        }
                    });
            }
        });
    }

    /**
     * Creates a new multi user chat with the specified connection and room name. Note: no
     * information is sent to or received from the server until you attempt to
     * {@link #join(String) join} the chat room. On some server implementations,
     * the room will not be created until the first person joins it.<p>
     *
     * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com
     * for the XMPP server example.com). You must ensure that the room address you're
     * trying to connect to includes the proper chat sub-domain.
     *
     * @param connection the XMPP connection.
     * @param room the name of the room in the form "roomName@service", where
     *      "service" is the hostname at which the multi-user chat
     *      service is running. Make sure to provide a valid JID.
     */
    public MultiUserChat(Connection connection, String room) {
        this.connection = connection;
        this.room = room.toLowerCase();
        init();
    }

    /**
     * Returns true if the specified user supports the Multi-User Chat protocol.
     *
     * @param connection the connection to use to perform the service discovery.
     * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
     * @return a boolean indicating whether the specified user supports the MUC protocol.
     */
    public static boolean isServiceEnabled(Connection connection, String user) {
        try {
            DiscoverInfo result =
                ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(user);
            return result.containsFeature(discoNamespace);
        }
        catch (XMPPException e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * Returns an Iterator on the rooms where the user has joined using a given connection.
     * The Iterator will contain Strings where each String represents a room
     * (e.g. room@muc.jabber.org).
     *
     * @param connection the connection used to join the rooms.
     * @return an Iterator on the rooms where the user has joined using a given connection.
     */
    private static Iterator<String> getJoinedRooms(Connection connection) {
        List<String> rooms = joinedRooms.get(connection);
        if (rooms != null) {
            return rooms.iterator();
        }
        // Return an iterator on an empty collection (i.e. the user never joined a room)
        return new ArrayList<String>().iterator();
    }

    /**
     * Returns an Iterator on the rooms where the requested user has joined. The Iterator will
     * contain Strings where each String represents a room (e.g. room@muc.jabber.org).
     *
     * @param connection the connection to use to perform the service discovery.
     * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
     * @return an Iterator on the rooms where the requested user has joined.
     */
    public static Iterator<String> getJoinedRooms(Connection connection, String user) {
        try {
            ArrayList<String> answer = new ArrayList<String>();
            // Send the disco packet to the user
            DiscoverItems result =
                ServiceDiscoveryManager.getInstanceFor(connection).discoverItems(user, discoNode);
            // Collect the entityID for each returned item
            for (Iterator<DiscoverItems.Item> items=result.getItems(); items.hasNext();) {
                answer.add(items.next().getEntityID());
            }
            return answer.iterator();
        }
        catch (XMPPException e) {
            e.printStackTrace();
            // Return an iterator on an empty collection
            return new ArrayList<String>().iterator();
        }
    }

    /**
     * Returns the discovered information of a given room without actually having to join the room.
     * The server will provide information only for rooms that are public.
     *
     * @param connection the XMPP connection to use for discovering information about the room.
     * @param room the name of the room in the form "roomName@service" of which we want to discover
     *        its information.
     * @return the discovered information of a given room without actually having to join the room.
     * @throws XMPPException if an error occured while trying to discover information of a room.
     */
    public static RoomInfo getRoomInfo(Connection connection, String room)
            throws XMPPException {
        DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(room);
        return new RoomInfo(info);
    }

    /**
     * Returns a collection with the XMPP addresses of the Multi-User Chat services.
     *
     * @param connection the XMPP connection to use for discovering Multi-User Chat services.
     * @return a collection with the XMPP addresses of the Multi-User Chat services.
     * @throws XMPPException if an error occured while trying to discover MUC services.
     */
    public static Collection<String> getServiceNames(Connection connection) throws XMPPException {
        final List<String> answer = new ArrayList<String>();
        ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
        DiscoverItems items = discoManager.discoverItems(connection.getServiceName());
        for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
            DiscoverItems.Item item = it.next();
            try {
                DiscoverInfo info = discoManager.discoverInfo(item.getEntityID());
                if (info.containsFeature("http://jabber.org/protocol/muc")) {
                    answer.add(item.getEntityID());
                }
            }
            catch (XMPPException e) {
                // Trouble finding info in some cases. This is a workaround for
                // discovering info on remote servers.
            }
        }
        return answer;
    }

    /**
     * Returns a collection of HostedRooms where each HostedRoom has the XMPP address of the room
     * and the room's name. Once discovered the rooms hosted by a chat service it is possible to
     * discover more detailed room information or join the room.
     *
     * @param connection the XMPP connection to use for discovering hosted rooms by the MUC service.
     * @param serviceName the service that is hosting the rooms to discover.
     * @return a collection of HostedRooms.
     * @throws XMPPException if an error occured while trying to discover the information.
     */
    public static Collection<HostedRoom> getHostedRooms(Connection connection, String serviceName)
            throws XMPPException {
        List<HostedRoom> answer = new ArrayList<HostedRoom>();
        ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
        DiscoverItems items = discoManager.discoverItems(serviceName);
        for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
            answer.add(new HostedRoom(it.next()));
        }
        return answer;
    }

    /**
     * Returns the name of the room this MultiUserChat object represents.
     *
     * @return the multi user chat room name.
     */
    public String getRoom() {
        return room;
    }

    /**
     * Creates the room according to some default configuration, assign the requesting user
     * as the room owner, and add the owner to the room but not allow anyone else to enter
     * the room (effectively "locking" the room). The requesting user will join the room
     * under the specified nickname as soon as the room has been created.<p>
     *
     * To create an "Instant Room", that means a room with some default configuration that is
     * available for immediate access, the room's owner should send an empty form after creating
     * the room. {@link #sendConfigurationForm(Form)}<p>
     *
     * To create a "Reserved Room", that means a room manually configured by the room creator
     * before anyone is allowed to enter, the room's owner should complete and send a form after
     * creating the room. Once the completed configutation form is sent to the server, the server
     * will unlock the room. {@link #sendConfigurationForm(Form)}
     *
     * @param nickname the nickname to use.
     * @throws XMPPException if the room couldn't be created for some reason
     *          (e.g. room already exists; user already joined to an existant room or
     *          405 error if the user is not allowed to create the room)
     */
    public synchronized void create(String nickname) throws XMPPException {
        if (nickname == null || nickname.equals("")) {
            throw new IllegalArgumentException("Nickname must not be null or blank.");
        }
        // If we've already joined the room, leave it before joining under a new
        // nickname.
        if (joined) {
            throw new IllegalStateException("Creation failed - User already joined the room.");
        }
        // We create a room by sending a presence packet to room@service/nick
        // and signal support for MUC. The owner will be automatically logged into the room.
        Presence joinPresence = new Presence(Presence.Type.available);
        joinPresence.setTo(room + "/" + nickname);
        // Indicate the the client supports MUC
        joinPresence.addExtension(new MUCInitialPresence());
        // Invoke presence interceptors so that extra information can be dynamically added
        for (PacketInterceptor packetInterceptor : presenceInterceptors) {
            packetInterceptor.interceptPacket(joinPresence);
        }

        // Wait for a presence packet back from the server.
        PacketFilter responseFilter =
            new AndFilter(
                new FromMatchesFilter(room + "/" + nickname),
                new PacketTypeFilter(Presence.class));
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send create & join packet.
        connection.sendPacket(joinPresence);
        // Wait up to a certain number of seconds for a reply.
        Presence presence =
            (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (presence == null) {
            throw new XMPPException("No response from server.");
        }
        else if (presence.getError() != null) {
            throw new XMPPException(presence.getError());
        }
        // Whether the room existed before or was created, the user has joined the room
        this.nickname = nickname;
        joined = true;
        userHasJoined();

        // Look for confirmation of room creation from the server
        MUCUser mucUser = getMUCUserExtension(presence);
        if (mucUser != null && mucUser.getStatus() != null) {
            if ("201".equals(mucUser.getStatus().getCode())) {
                // Room was created and the user has joined the room
                return;
            }
        }
        // We need to leave the room since it seems that the room already existed
        leave();
        throw new XMPPException("Creation failed - Missing acknowledge of room creation.");
    }

    /**
     * Joins the chat room using the specified nickname. If already joined
     * using another nickname, this method will first leave the room and then
     * re-join using the new nickname. The default timeout of Smack for a reply
     * from the group chat server that the join succeeded will be used. After
     * joining the room, the room will decide the amount of history to send.
     *
     * @param nickname the nickname to use.
     * @throws XMPPException if an error occurs joining the room. In particular, a
     *      401 error can occur if no password was provided and one is required; or a
     *      403 error can occur if the user is banned; or a
     *      404 error can occur if the room does not exist or is locked; or a
     *      407 error can occur if user is not on the member list; or a
     *      409 error can occur if someone is already in the group chat with the same nickname.
     */
    public void join(String nickname) throws XMPPException {
        join(nickname, null, null, SmackConfiguration.getPacketReplyTimeout());
    }

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

    /**
     * Joins the chat room using the specified nickname and password. If already joined
     * using another nickname, this method will first leave the room and then
     * re-join using the new nickname.<p>
     *
     * To control the amount of history to receive while joining a room you will need to provide
     * a configured DiscussionHistory object.<p>
     *
     * A password is required when joining password protected rooms. If the room does
     * not require a password there is no need to provide one.<p>
     *
     * If the room does not already exist when the user seeks to enter it, the server will
     * decide to create a new room or not.
     *
     * @param nickname the nickname to use.
     * @param password the password to use.
     * @param history the amount of discussion history to receive while joining a room.
     * @param timeout the amount of time to wait for a reply from the MUC service(in milleseconds).
     * @throws XMPPException if an error occurs joining the room. In particular, a
     *      401 error can occur if no password was provided and one is required; or a
     *      403 error can occur if the user is banned; or a
     *      404 error can occur if the room does not exist or is locked; or a
     *      407 error can occur if user is not on the member list; or a
     *      409 error can occur if someone is already in the group chat with the same nickname.
     */
    public synchronized void join(
        String nickname,
        String password,
        DiscussionHistory history,
        long timeout)
        throws XMPPException {
        if (nickname == null || nickname.equals("")) {
            throw new IllegalArgumentException("Nickname must not be null or blank.");
        }
        // If we've already joined the room, leave it before joining under a new
        // nickname.
        if (joined) {
            leave();
        }
        // We join a room by sending a presence packet where the "to"
        // field is in the form "roomName@service/nickname"
        Presence joinPresence = new Presence(Presence.Type.available);
        joinPresence.setTo(room + "/" + nickname);

        // Indicate the the client supports MUC
        MUCInitialPresence mucInitialPresence = new MUCInitialPresence();
        if (password != null) {
            mucInitialPresence.setPassword(password);
        }
        if (history != null) {
            mucInitialPresence.setHistory(history.getMUCHistory());
        }
        joinPresence.addExtension(mucInitialPresence);
        // Invoke presence interceptors so that extra information can be dynamically added
        for (PacketInterceptor packetInterceptor : presenceInterceptors) {
            packetInterceptor.interceptPacket(joinPresence);
        }

        // Wait for a presence packet back from the server.
        PacketFilter responseFilter =
                new AndFilter(
                        new FromMatchesFilter(room + "/" + nickname),
                        new PacketTypeFilter(Presence.class));
        PacketCollector response = null;
        Presence presence;
        try {
            response = connection.createPacketCollector(responseFilter);
            // Send join packet.
            connection.sendPacket(joinPresence);
            // Wait up to a certain number of seconds for a reply.
            presence = (Presence) response.nextResult(timeout);
        }
        finally {
            // Stop queuing results
            if (response != null) {
                response.cancel();
            }
        }

        if (presence == null) {
            throw new XMPPException("No response from server.");
        }
        else if (presence.getError() != null) {
            throw new XMPPException(presence.getError());
        }
        this.nickname = nickname;
        joined = true;
        userHasJoined();
    }

    /**
     * Returns true if currently in the multi user chat (after calling the {@link
     * #join(String)} method).
     *
     * @return true if currently in the multi user chat room.
     */
    public boolean isJoined() {
        return joined;
    }

    /**
     * Leave the chat room.
     */
    public synchronized void leave() {
        // If not joined already, do nothing.
        if (!joined) {
            return;
        }
        // We leave a room by sending a presence packet where the "to"
        // field is in the form "roomName@service/nickname"
        Presence leavePresence = new Presence(Presence.Type.unavailable);
        leavePresence.setTo(room + "/" + nickname);
        // Invoke presence interceptors so that extra information can be dynamically added
        for (PacketInterceptor packetInterceptor : presenceInterceptors) {
            packetInterceptor.interceptPacket(leavePresence);
        }
        connection.sendPacket(leavePresence);
        // Reset occupant information.
        occupantsMap.clear();
        nickname = null;
        joined = false;
        userHasLeft();
    }

    /**
     * Returns the room's configuration form that the room's owner can use or <tt>null</tt> if
     * no configuration is possible. The configuration form allows to set the room's language,
     * enable logging, specify room's type, etc..
     *
     * @return the Form that contains the fields to complete together with the instrucions or
     * <tt>null</tt> if no configuration is possible.
     * @throws XMPPException if an error occurs asking the configuration form for the room.
     */
    public Form getConfigurationForm() throws XMPPException {
        MUCOwner iq = new MUCOwner();
        iq.setTo(room);
        iq.setType(IQ.Type.GET);

        // Filter packets looking for an answer from the server.
        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Request the configuration form to the server.
        connection.sendPacket(iq);
        // Wait up to a certain number of seconds for a reply.
        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
        return Form.getFormFrom(answer);
    }

    /**
     * Sends the completed configuration form to the server. The room will be configured
     * with the new settings defined in the form. If the form is empty then the server
     * will create an instant room (will use default configuration).
     *
     * @param form the form with the new settings.
     * @throws XMPPException if an error occurs setting the new rooms' configuration.
     */
    public void sendConfigurationForm(Form form) throws XMPPException {
        MUCOwner iq = new MUCOwner();
        iq.setTo(room);
        iq.setType(IQ.Type.SET);
        iq.addExtension(form.getDataFormToSend());

        // Filter packets looking for an answer from the server.
        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send the completed configuration form to the server.
        connection.sendPacket(iq);
        // Wait up to a certain number of seconds for a reply.
        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
    }

    /**
     * Returns the room's registration form that an unaffiliated user, can use to become a member
     * of the room or <tt>null</tt> if no registration is possible. Some rooms may restrict the
     * privilege to register members and allow only room admins to add new members.<p>
     *
     * If the user requesting registration requirements is not allowed to register with the room
     * (e.g. because that privilege has been restricted), the room will return a "Not Allowed"
     * error to the user (error code 405).
     *
     * @return the registration Form that contains the fields to complete together with the
     * instrucions or <tt>null</tt> if no registration is possible.
     * @throws XMPPException if an error occurs asking the registration form for the room or a
     * 405 error if the user is not allowed to register with the room.
     */
    public Form getRegistrationForm() throws XMPPException {
        Registration reg = new Registration();
        reg.setType(IQ.Type.GET);
        reg.setTo(room);

        PacketFilter filter =
            new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
        PacketCollector collector = connection.createPacketCollector(filter);
        connection.sendPacket(reg);
        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
        collector.cancel();
        if (result == null) {
            throw new XMPPException("No response from server.");
        }
        else if (result.getType() == IQ.Type.ERROR) {
            throw new XMPPException(result.getError());
        }
        return Form.getFormFrom(result);
    }

    /**
     * Sends the completed registration form to the server. After the user successfully submits
     * the form, the room may queue the request for review by the room admins or may immediately
     * add the user to the member list by changing the user's affiliation from "none" to "member.<p>
     *
     * If the desired room nickname is already reserved for that room, the room will return a
     * "Conflict" error to the user (error code 409). If the room does not support registration,
     * it will return a "Service Unavailable" error to the user (error code 503).
     *
     * @param form the completed registration form.
     * @throws XMPPException if an error occurs submitting the registration form. In particular, a
     *      409 error can occur if the desired room nickname is already reserved for that room;
     *      or a 503 error can occur if the room does not support registration.
     */
    public void sendRegistrationForm(Form form) throws XMPPException {
        Registration reg = new Registration();
        reg.setType(IQ.Type.SET);
        reg.setTo(room);
        reg.addExtension(form.getDataFormToSend());

        PacketFilter filter =
            new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
        PacketCollector collector = connection.createPacketCollector(filter);
        connection.sendPacket(reg);
        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
        collector.cancel();
        if (result == null) {
            throw new XMPPException("No response from server.");
        }
        else if (result.getType() == IQ.Type.ERROR) {
            throw new XMPPException(result.getError());
        }
    }

    /**
     * Sends a request to the server to destroy the room. The sender of the request
     * should be the room's owner. If the sender of the destroy request is not the room's owner
     * then the server will answer a "Forbidden" error (403).
     *
     * @param reason the reason for the room destruction.
     * @param alternateJID the JID of an alternate location.
     * @throws XMPPException if an error occurs while trying to destroy the room.
     *      An error can occur which will be wrapped by an XMPPException --
     *      XMPP error code 403. The error code can be used to present more
     *      appropiate error messages to end-users.
     */
    public void destroy(String reason, String alternateJID) throws XMPPException {
        MUCOwner iq = new MUCOwner();
        iq.setTo(room);
        iq.setType(IQ.Type.SET);

        // Create the reason for the room destruction
        MUCOwner.Destroy destroy = new MUCOwner.Destroy();
        destroy.setReason(reason);
        destroy.setJid(alternateJID);
        iq.setDestroy(destroy);

        // Wait for a presence packet back from the server.
        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send the room destruction request.
        connection.sendPacket(iq);
        // Wait up to a certain number of seconds for a reply.
        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
        // Reset occupant information.
        occupantsMap.clear();
        nickname = null;
        joined = false;
        userHasLeft();
    }

    /**
     * Invites another user to the room in which one is an occupant. The invitation
     * will be sent to the room which in turn will forward the invitation to the invitee.<p>
     *
     * If the room is password-protected, the invitee will receive a password to use to join
     * the room. If the room is members-only, the the invitee may be added to the member list.
     *
     * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
     * @param reason the reason why the user is being invited.
     */
    public void invite(String user, String reason) {
        invite(new Message(), user, reason);
    }

    /**
     * Invites another user to the room in which one is an occupant using a given Message. The invitation
     * will be sent to the room which in turn will forward the invitation to the invitee.<p>
     *
     * If the room is password-protected, the invitee will receive a password to use to join
     * the room. If the room is members-only, the the invitee may be added to the member list.
     *
     * @param message the message to use for sending the invitation.
     * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
     * @param reason the reason why the user is being invited.
     */
    public void invite(Message message, String user, String reason) {
        // TODO listen for 404 error code when inviter supplies a non-existent JID
        message.setTo(room);

        // Create the MUCUser packet that will include the invitation
        MUCUser mucUser = new MUCUser();
        MUCUser.Invite invite = new MUCUser.Invite();
        invite.setTo(user);
        invite.setReason(reason);
        mucUser.setInvite(invite);
        // Add the MUCUser packet that includes the invitation to the message
        message.addExtension(mucUser);

        connection.sendPacket(message);
    }

    /**
     * Informs the sender of an invitation that the invitee declines the invitation. The rejection
     * will be sent to the room which in turn will forward the rejection to the inviter.
     *
     * @param conn the connection to use for sending the rejection.
     * @param room the room that sent the original invitation.
     * @param inviter the inviter of the declined invitation.
     * @param reason the reason why the invitee is declining the invitation.
     */
    public static void decline(Connection conn, String room, String inviter, String reason) {
        Message message = new Message(room);

        // Create the MUCUser packet that will include the rejection
        MUCUser mucUser = new MUCUser();
        MUCUser.Decline decline = new MUCUser.Decline();
        decline.setTo(inviter);
        decline.setReason(reason);
        mucUser.setDecline(decline);
        // Add the MUCUser packet that includes the rejection
        message.addExtension(mucUser);

        conn.sendPacket(message);
    }

    /**
     * Adds a listener to invitation notifications. The listener will be fired anytime
     * an invitation is received.
     *
     * @param conn the connection where the listener will be applied.
     * @param listener an invitation listener.
     */
    public static void addInvitationListener(Connection conn, InvitationListener listener) {
        InvitationsMonitor.getInvitationsMonitor(conn).addInvitationListener(listener);
    }

    /**
     * Removes a listener to invitation notifications. The listener will be fired anytime
     * an invitation is received.
     *
     * @param conn the connection where the listener was applied.
     * @param listener an invitation listener.
     */
    public static void removeInvitationListener(Connection conn, InvitationListener listener) {
        InvitationsMonitor.getInvitationsMonitor(conn).removeInvitationListener(listener);
    }

    /**
     * Adds a listener to invitation rejections notifications. The listener will be fired anytime
     * an invitation is declined.
     *
     * @param listener an invitation rejection listener.
     */
    public void addInvitationRejectionListener(InvitationRejectionListener listener) {
        synchronized (invitationRejectionListeners) {
            if (!invitationRejectionListeners.contains(listener)) {
                invitationRejectionListeners.add(listener);
            }
        }
    }

    /**
     * Removes a listener from invitation rejections notifications. The listener will be fired
     * anytime an invitation is declined.
     *
     * @param listener an invitation rejection listener.
     */
    public void removeInvitationRejectionListener(InvitationRejectionListener listener) {
        synchronized (invitationRejectionListeners) {
            invitationRejectionListeners.remove(listener);
        }
    }

    /**
     * Fires invitation rejection listeners.
     *
     * @param invitee the user being invited.
     * @param reason the reason for the rejection
     */
    private void fireInvitationRejectionListeners(String invitee, String reason) {
        InvitationRejectionListener[] listeners;
        synchronized (invitationRejectionListeners) {
            listeners = new InvitationRejectionListener[invitationRejectionListeners.size()];
            invitationRejectionListeners.toArray(listeners);
        }
        for (InvitationRejectionListener listener : listeners) {
            listener.invitationDeclined(invitee, reason);
        }
    }

    /**
     * Adds a listener to subject change notifications. The listener will be fired anytime
     * the room's subject changes.
     *
     * @param listener a subject updated listener.
     */
    public void addSubjectUpdatedListener(SubjectUpdatedListener listener) {
        synchronized (subjectUpdatedListeners) {
            if (!subjectUpdatedListeners.contains(listener)) {
                subjectUpdatedListeners.add(listener);
            }
        }
    }

    /**
     * Removes a listener from subject change notifications. The listener will be fired
     * anytime the room's subject changes.
     *
     * @param listener a subject updated listener.
     */
    public void removeSubjectUpdatedListener(SubjectUpdatedListener listener) {
        synchronized (subjectUpdatedListeners) {
            subjectUpdatedListeners.remove(listener);
        }
    }

    /**
     * Fires subject updated listeners.
     */
    private void fireSubjectUpdatedListeners(String subject, String from) {
        SubjectUpdatedListener[] listeners;
        synchronized (subjectUpdatedListeners) {
            listeners = new SubjectUpdatedListener[subjectUpdatedListeners.size()];
            subjectUpdatedListeners.toArray(listeners);
        }
        for (SubjectUpdatedListener listener : listeners) {
            listener.subjectUpdated(subject, from);
        }
    }

    /**
     * Adds a new {@link PacketInterceptor} that will be invoked every time a new presence
     * is going to be sent by this MultiUserChat to the server. Packet interceptors may
     * add new extensions to the presence that is going to be sent to the MUC service.
     *
     * @param presenceInterceptor the new packet interceptor that will intercept presence packets.
     */
    public void addPresenceInterceptor(PacketInterceptor presenceInterceptor) {
        presenceInterceptors.add(presenceInterceptor);
    }

    /**
     * Removes a {@link PacketInterceptor} that was being invoked every time a new presence
     * was being sent by this MultiUserChat to the server. Packet interceptors may
     * add new extensions to the presence that is going to be sent to the MUC service.
     *
     * @param presenceInterceptor the packet interceptor to remove.
     */
    public void removePresenceInterceptor(PacketInterceptor presenceInterceptor) {
        presenceInterceptors.remove(presenceInterceptor);
    }

    /**
     * Returns the last known room's subject or <tt>null</tt> if the user hasn't joined the room
     * or the room does not have a subject yet. In case the room has a subject, as soon as the
     * user joins the room a message with the current room's subject will be received.<p>
     *
     * To be notified every time the room's subject change you should add a listener
     * to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p>
     *
     * To change the room's subject use {@link #changeSubject(String)}.
     *
     * @return the room's subject or <tt>null</tt> if the user hasn't joined the room or the
     * room does not have a subject yet.
     */
    public String getSubject() {
        return subject;
    }

    /**
     * Returns the reserved room nickname for the user in the room. A user may have a reserved
     * nickname, for example through explicit room registration or database integration. In such
     * cases it may be desirable for the user to discover the reserved nickname before attempting
     * to enter the room.
     *
     * @return the reserved room nickname or <tt>null</tt> if none.
     */
    public String getReservedNickname() {
        try {
            DiscoverInfo result =
                ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(
                    room,
                    "x-roomuser-item");
            // Look for an Identity that holds the reserved nickname and return its name
            for (Iterator<DiscoverInfo.Identity> identities = result.getIdentities();
                 identities.hasNext();) {
                DiscoverInfo.Identity identity = identities.next();
                return identity.getName();
            }
            // If no Identity was found then the user does not have a reserved room nickname
            return null;
        }
        catch (XMPPException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Returns the nickname that was used to join the room, or <tt>null</tt> if not
     * currently joined.
     *
     * @return the nickname currently being used.
     */
    public String getNickname() {
        return nickname;
    }

    /**
     * Changes the occupant's nickname to a new nickname within the room. Each room occupant
     * will receive two presence packets. One of type "unavailable" for the old nickname and one
     * indicating availability for the new nickname. The unavailable presence will contain the new
     * nickname and an appropriate status code (namely 303) as extended presence information. The
     * status code 303 indicates that the occupant is changing his/her nickname.
     *
     * @param nickname the new nickname within the room.
     * @throws XMPPException if the new nickname is already in use by another occupant.
     */
    public void changeNickname(String nickname) throws XMPPException {
        if (nickname == null || nickname.equals("")) {
            throw new IllegalArgumentException("Nickname must not be null or blank.");
        }
        // Check that we already have joined the room before attempting to change the
        // nickname.
        if (!joined) {
            throw new IllegalStateException("Must be logged into the room to change nickname.");
        }
        // We change the nickname by sending a presence packet where the "to"
        // field is in the form "roomName@service/nickname"
        // We don't have to signal the MUC support again
        Presence joinPresence = new Presence(Presence.Type.available);
        joinPresence.setTo(room + "/" + nickname);
        // Invoke presence interceptors so that extra information can be dynamically added
        for (PacketInterceptor packetInterceptor : presenceInterceptors) {
            packetInterceptor.interceptPacket(joinPresence);
        }

        // Wait for a presence packet back from the server.
        PacketFilter responseFilter =
            new AndFilter(
                new FromMatchesFilter(room + "/" + nickname),
                new PacketTypeFilter(Presence.class));
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send join packet.
        connection.sendPacket(joinPresence);
        // Wait up to a certain number of seconds for a reply.
        Presence presence =
            (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (presence == null) {
            throw new XMPPException("No response from server.");
        }
        else if (presence.getError() != null) {
            throw new XMPPException(presence.getError());
        }
        this.nickname = nickname;
    }

    /**
     * Changes the occupant's availability status within the room. The presence type
     * will remain available but with a new status that describes the presence update and
     * a new presence mode (e.g. Extended away).
     *
     * @param status a text message describing the presence update.
     * @param mode the mode type for the presence update.
     */
    public void changeAvailabilityStatus(String status, Presence.Mode mode) {
        if (nickname == null || nickname.equals("")) {
            throw new IllegalArgumentException("Nickname must not be null or blank.");
        }
        // Check that we already have joined the room before attempting to change the
        // availability status.
        if (!joined) {
            throw new IllegalStateException(
                "Must be logged into the room to change the " + "availability status.");
        }
        // We change the availability status by sending a presence packet to the room with the
        // new presence status and mode
        Presence joinPresence = new Presence(Presence.Type.available);
        joinPresence.setStatus(status);
        joinPresence.setMode(mode);
        joinPresence.setTo(room + "/" + nickname);
        // Invoke presence interceptors so that extra information can be dynamically added
        for (PacketInterceptor packetInterceptor : presenceInterceptors) {
            packetInterceptor.interceptPacket(joinPresence);
        }

        // Send join packet.
        connection.sendPacket(joinPresence);
    }

    /**
     * Kicks a visitor or participant from the room. The kicked occupant will receive a presence
     * of type "unavailable" including a status code 307 and optionally along with the reason
     * (if provided) and the bare JID of the user who initiated the kick. After the occupant
     * was kicked from the room, the rest of the occupants will receive a presence of type
     * "unavailable". The presence will include a status code 307 which means that the occupant
     * was kicked from the room.
     *
     * @param nickname the nickname of the participant or visitor to kick from the room
     * (e.g. "john").
     * @param reason the reason why the participant or visitor is being kicked from the room.
     * @throws XMPPException if an error occurs kicking the occupant. In particular, a
     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
     *      was intended to be kicked (i.e. Not Allowed error); or a
     *      403 error can occur if the occupant that intended to kick another occupant does
     *      not have kicking privileges (i.e. Forbidden error); or a
     *      400 error can occur if the provided nickname is not present in the room.
     */
    public void kickParticipant(String nickname, String reason) throws XMPPException {
        changeRole(nickname, "none", reason);
    }

    /**
     * Grants voice to visitors in the room. In a moderated room, a moderator may want to manage
     * who does and does not have "voice" in the room. To have voice means that a room occupant
     * is able to send messages to the room occupants.
     *
     * @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john").
     * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
     *      403 error can occur if the occupant that intended to grant voice is not
     *      a moderator in this room (i.e. Forbidden error); or a
     *      400 error can occur if the provided nickname is not present in the room.
     */
    public void grantVoice(Collection<String> nicknames) throws XMPPException {
        changeRole(nicknames, "participant");
    }

    /**
     * Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage
     * who does and does not have "voice" in the room. To have voice means that a room occupant
     * is able to send messages to the room occupants.
     *
     * @param nickname the nickname of the visitor to grant voice in the room (e.g. "john").
     * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
     *      403 error can occur if the occupant that intended to grant voice is not
     *      a moderator in this room (i.e. Forbidden error); or a
     *      400 error can occur if the provided nickname is not present in the room.
     */
    public void grantVoice(String nickname) throws XMPPException {
        changeRole(nickname, "participant", null);
    }

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

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

    /**
     * Bans users from the room. An admin or owner of the room can ban users from a room. This
     * means that the banned user will no longer be able to join the room unless the ban has been
     * removed. If the banned user was present in the room then he/she will be removed from the
     * room and notified that he/she was banned along with the reason (if provided) and the bare
     * XMPP user ID of the user who initiated the ban.
     *
     * @param jids the bare XMPP user IDs of the users to ban.
     * @throws XMPPException if an error occurs banning a user. In particular, a
     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
     *      was tried to be banned (i.e. Not Allowed error).
     */
    public void banUsers(Collection<String> jids) throws XMPPException {
        changeAffiliationByAdmin(jids, "outcast");
    }

    /**
     * Bans a user from the room. An admin or owner of the room can ban users from a room. This
     * means that the banned user will no longer be able to join the room unless the ban has been
     * removed. If the banned user was present in the room then he/she will be removed from the
     * room and notified that he/she was banned along with the reason (if provided) and the bare
     * XMPP user ID of the user who initiated the ban.
     *
     * @param jid the bare XMPP user ID of the user to ban (e.g. "user@host.org").
     * @param reason the optional reason why the user was banned.
     * @throws XMPPException if an error occurs banning a user. In particular, a
     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
     *      was tried to be banned (i.e. Not Allowed error).
     */
    public void banUser(String jid, String reason) throws XMPPException {
        changeAffiliationByAdmin(jid, "outcast", reason);
    }

    /**
     * Grants membership to other users. Only administrators are able to grant membership. A user
     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
     * that a user cannot enter without being on the member list).
     *
     * @param jids the XMPP user IDs of the users to grant membership.
     * @throws XMPPException if an error occurs granting membership to a user.
     */
    public void grantMembership(Collection<String> jids) throws XMPPException {
        changeAffiliationByAdmin(jids, "member");
    }

    /**
     * Grants membership to a user. Only administrators are able to grant membership. A user
     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
     * that a user cannot enter without being on the member list).
     *
     * @param jid the XMPP user ID of the user to grant membership (e.g. "user@host.org").
     * @throws XMPPException if an error occurs granting membership to a user.
     */
    public void grantMembership(String jid) throws XMPPException {
        changeAffiliationByAdmin(jid, "member", null);
    }

    /**
     * Revokes users' membership. Only administrators are able to revoke membership. A user
     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
     * that a user cannot enter without being on the member list). If the user is in the room and
     * the room is of type members-only then the user will be removed from the room.
     *
     * @param jids the bare XMPP user IDs of the users to revoke membership.
     * @throws XMPPException if an error occurs revoking membership to a user.
     */
    public void revokeMembership(Collection<String> jids) throws XMPPException {
        changeAffiliationByAdmin(jids, "none");
    }

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

    /**
     * Grants moderator privileges to participants or visitors. Room administrators may grant
     * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
     * other users, modify room's subject plus all the partcipants privileges.
     *
     * @param nicknames the nicknames of the occupants to grant moderator privileges.
     * @throws XMPPException if an error occurs granting moderator privileges to a user.
     */
    public void grantModerator(Collection<String> nicknames) throws XMPPException {
        changeRole(nicknames, "moderator");
    }

    /**
     * Grants moderator privileges to a participant or visitor. Room administrators may grant
     * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
     * other users, modify room's subject plus all the partcipants privileges.
     *
     * @param nickname the nickname of the occupant to grant moderator privileges.
     * @throws XMPPException if an error occurs granting moderator privileges to a user.
     */
    public void grantModerator(String nickname) throws XMPPException {
        changeRole(nickname, "moderator", null);
    }

    /**
     * Revokes moderator privileges from other users. The occupant that loses moderator
     * privileges will become a participant. Room administrators may revoke moderator privileges
     * only to occupants whose affiliation is member or none. This means that an administrator is
     * not allowed to revoke moderator privileges from other room administrators or owners.
     *
     * @param nicknames the nicknames of the occupants to revoke moderator privileges.
     * @throws XMPPException if an error occurs revoking moderator privileges from a user.
     */
    public void revokeModerator(Collection<String> nicknames) throws XMPPException {
        changeRole(nicknames, "participant");
    }

    /**
     * Revokes moderator privileges from another user. The occupant that loses moderator
     * privileges will become a participant. Room administrators may revoke moderator privileges
     * only to occupants whose affiliation is member or none. This means that an administrator is
     * not allowed to revoke moderator privileges from other room administrators or owners.
     *
     * @param nickname the nickname of the occupant to revoke moderator privileges.
     * @throws XMPPException if an error occurs revoking moderator privileges from a user.
     */
    public void revokeModerator(String nickname) throws XMPPException {
        changeRole(nickname, "participant", null);
    }

    /**
     * Grants ownership privileges to other users. Room owners may grant ownership privileges.
     * Some room implementations will not allow to grant ownership privileges to other users.
     * An owner is allowed to change defining room features as well as perform all administrative
     * functions.
     *
     * @param jids the collection of bare XMPP user IDs of the users to grant ownership.
     * @throws XMPPException if an error occurs granting ownership privileges to a user.
     */
    public void grantOwnership(Collection<String> jids) throws XMPPException {
        changeAffiliationByAdmin(jids, "owner");
    }

    /**
     * Grants ownership privileges to another user. Room owners may grant ownership privileges.
     * Some room implementations will not allow to grant ownership privileges to other users.
     * An owner is allowed to change defining room features as well as perform all administrative
     * functions.
     *
     * @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user@host.org").
     * @throws XMPPException if an error occurs granting ownership privileges to a user.
     */
    public void grantOwnership(String jid) throws XMPPException {
        changeAffiliationByAdmin(jid, "owner", null);
    }

    /**
     * Revokes ownership privileges from other users. The occupant that loses ownership
     * privileges will become an administrator. Room owners may revoke ownership privileges.
     * Some room implementations will not allow to grant ownership privileges to other users.
     *
     * @param jids the bare XMPP user IDs of the users to revoke ownership.
     * @throws XMPPException if an error occurs revoking ownership privileges from a user.
     */
    public void revokeOwnership(Collection<String> jids) throws XMPPException {
        changeAffiliationByAdmin(jids, "admin");
    }

    /**
     * Revokes ownership privileges from another user. The occupant that loses ownership
     * privileges will become an administrator. Room owners may revoke ownership privileges.
     * Some room implementations will not allow to grant ownership privileges to other users.
     *
     * @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user@host.org").
     * @throws XMPPException if an error occurs revoking ownership privileges from a user.
     */
    public void revokeOwnership(String jid) throws XMPPException {
        changeAffiliationByAdmin(jid, "admin", null);
    }

    /**
     * Grants administrator privileges to other users. Room owners may grant administrator
     * privileges to a member or unaffiliated user. An administrator is allowed to perform
     * administrative functions such as banning users and edit moderator list.
     *
     * @param jids the bare XMPP user IDs of the users to grant administrator privileges.
     * @throws XMPPException if an error occurs granting administrator privileges to a user.
     */
    public void grantAdmin(Collection<String> jids) throws XMPPException {
        changeAffiliationByOwner(jids, "admin");
    }

    /**
     * Grants administrator privileges to another user. Room owners may grant administrator
     * privileges to a member or unaffiliated user. An administrator is allowed to perform
     * administrative functions such as banning users and edit moderator list.
     *
     * @param jid the bare XMPP user ID of the user to grant administrator privileges
     * (e.g. "user@host.org").
     * @throws XMPPException if an error occurs granting administrator privileges to a user.
     */
    public void grantAdmin(String jid) throws XMPPException {
        changeAffiliationByOwner(jid, "admin");
    }

    /**
     * Revokes administrator privileges from users. The occupant that loses administrator
     * privileges will become a member. Room owners may revoke administrator privileges from
     * a member or unaffiliated user.
     *
     * @param jids the bare XMPP user IDs of the user to revoke administrator privileges.
     * @throws XMPPException if an error occurs revoking administrator privileges from a user.
     */
    public void revokeAdmin(Collection<String> jids) throws XMPPException {
        changeAffiliationByOwner(jids, "member");
    }

    /**
     * Revokes administrator privileges from a user. The occupant that loses administrator
     * privileges will become a member. Room owners may revoke administrator privileges from
     * a member or unaffiliated user.
     *
     * @param jid the bare XMPP user ID of the user to revoke administrator privileges
     * (e.g. "user@host.org").
     * @throws XMPPException if an error occurs revoking administrator privileges from a user.
     */
    public void revokeAdmin(String jid) throws XMPPException {
        changeAffiliationByOwner(jid, "member");
    }

    private void changeAffiliationByOwner(String jid, String affiliation) throws XMPPException {
        MUCOwner iq = new MUCOwner();
        iq.setTo(room);
        iq.setType(IQ.Type.SET);
        // Set the new affiliation.
        MUCOwner.Item item = new MUCOwner.Item(affiliation);
        item.setJid(jid);
        iq.addItem(item);

        // Wait for a response packet back from the server.
        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send the change request to the server.
        connection.sendPacket(iq);
        // Wait up to a certain number of seconds for a reply.
        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
    }

    private void changeAffiliationByOwner(Collection<String> jids, String affiliation)
            throws XMPPException {
        MUCOwner iq = new MUCOwner();
        iq.setTo(room);
        iq.setType(IQ.Type.SET);
        for (String jid : jids) {
            // Set the new affiliation.
            MUCOwner.Item item = new MUCOwner.Item(affiliation);
            item.setJid(jid);
            iq.addItem(item);
        }

        // Wait for a response packet back from the server.
        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send the change request to the server.
        connection.sendPacket(iq);
        // Wait up to a certain number of seconds for a reply.
        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
    }

    /**
     * Tries to change the affiliation with an 'muc#admin' namespace
     *
     * @param jid
     * @param affiliation
     * @param reason the reason for the affiliation change (optional)
     * @throws XMPPException
     */
    private void changeAffiliationByAdmin(String jid, String affiliation, String reason)
            throws XMPPException {
        MUCAdmin iq = new MUCAdmin();
        iq.setTo(room);
        iq.setType(IQ.Type.SET);
        // Set the new affiliation.
        MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
        item.setJid(jid);
        if(reason != null)
            item.setReason(reason);
        iq.addItem(item);

        // Wait for a response packet back from the server.
        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send the change request to the server.
        connection.sendPacket(iq);
        // Wait up to a certain number of seconds for a reply.
        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
    }

    private void changeAffiliationByAdmin(Collection<String> jids, String affiliation)
            throws XMPPException {
        MUCAdmin iq = new MUCAdmin();
        iq.setTo(room);
        iq.setType(IQ.Type.SET);
        for (String jid : jids) {
            // Set the new affiliation.
            MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
            item.setJid(jid);
            iq.addItem(item);
        }

        // Wait for a response packet back from the server.
        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send the change request to the server.
        connection.sendPacket(iq);
        // Wait up to a certain number of seconds for a reply.
        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
    }

    private void changeRole(String nickname, String role, String reason) throws XMPPException {
        MUCAdmin iq = new MUCAdmin();
        iq.setTo(room);
        iq.setType(IQ.Type.SET);
        // Set the new role.
        MUCAdmin.Item item = new MUCAdmin.Item(null, role);
        item.setNick(nickname);
        item.setReason(reason);
        iq.addItem(item);

        // Wait for a response packet back from the server.
        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send the change request to the server.
        connection.sendPacket(iq);
        // Wait up to a certain number of seconds for a reply.
        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
    }

    private void changeRole(Collection<String> nicknames, String role) throws XMPPException {
        MUCAdmin iq = new MUCAdmin();
        iq.setTo(room);
        iq.setType(IQ.Type.SET);
        for (String nickname : nicknames) {
            // Set the new role.
            MUCAdmin.Item item = new MUCAdmin.Item(null, role);
            item.setNick(nickname);
            iq.addItem(item);
        }

        // Wait for a response packet back from the server.
        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send the change request to the server.
        connection.sendPacket(iq);
        // Wait up to a certain number of seconds for a reply.
        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
    }

    /**
     * Returns the number of occupants in the group chat.<p>
     *
     * Note: this value will only be accurate after joining the group chat, and
     * may fluctuate over time. If you query this value directly after joining the
     * group chat it may not be accurate, as it takes a certain amount of time for
     * the server to send all presence packets to this client.
     *
     * @return the number of occupants in the group chat.
     */
    public int getOccupantsCount() {
        return occupantsMap.size();
    }

    /**
     * Returns an Iterator (of Strings) for the list of fully qualified occupants
     * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser".
     * Typically, a client would only display the nickname of the occupant. To
     * get the nickname from the fully qualified name, use the
     * {@link org.jivesoftware.smack.util.StringUtils#parseResource(String)} method.
     * Note: this value will only be accurate after joining the group chat, and may
     * fluctuate over time.
     *
     * @return an Iterator for the occupants in the group chat.
     */
    public Iterator<String> getOccupants() {
        return Collections.unmodifiableList(new ArrayList<String>(occupantsMap.keySet()))
                .iterator();
    }

    /**
     * Returns the presence info for a particular user, or <tt>null</tt> if the user
     * is not in the room.<p>
     *
     * @param user the room occupant to search for his presence. The format of user must
     * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
     * @return the occupant's current presence, or <tt>null</tt> if the user is unavailable
     *      or if no presence information is available.
     */
    public Presence getOccupantPresence(String user) {
        return occupantsMap.get(user);
    }

    /**
     * Returns the Occupant information for a particular occupant, or <tt>null</tt> if the
     * user is not in the room. The Occupant object may include information such as full
     * JID of the user as well as the role and affiliation of the user in the room.<p>
     *
     * @param user the room occupant to search for his presence. The format of user must
     * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
     * @return the Occupant or <tt>null</tt> if the user is unavailable (i.e. not in the room).
     */
    public Occupant getOccupant(String user) {
        Presence presence = occupantsMap.get(user);
        if (presence != null) {
            return new Occupant(presence);
        }
        return null;
    }

    /**
     * Adds a packet listener that will be notified of any new Presence packets
     * sent to the group chat. Using a listener is a suitable way to know when the list
     * of occupants should be re-loaded due to any changes.
     *
     * @param listener a packet listener that will be notified of any presence packets
     *      sent to the group chat.
     */
    public void addParticipantListener(PacketListener listener) {
        connection.addPacketListener(listener, presenceFilter);
        connectionListeners.add(listener);
    }

    /**
     * Remoces a packet listener that was being notified of any new Presence packets
     * sent to the group chat.
     *
     * @param listener a packet listener that was being notified of any presence packets
     *      sent to the group chat.
     */
    public void removeParticipantListener(PacketListener listener) {
        connection.removePacketListener(listener);
        connectionListeners.remove(listener);
    }

    /**
     * Returns a collection of <code>Affiliate</code> with the room owners.
     *
     * @return a collection of <code>Affiliate</code> with the room owners.
     * @throws XMPPException if an error occured while performing the request to the server or you
     *         don't have enough privileges to get this information.
     */
    public Collection<Affiliate> getOwners() throws XMPPException {
        return getAffiliatesByAdmin("owner");
    }

    /**
     * Returns a collection of <code>Affiliate</code> with the room administrators.
     *
     * @return a collection of <code>Affiliate</code> with the room administrators.
     * @throws XMPPException if an error occured while performing the request to the server or you
     *         don't have enough privileges to get this information.
     */
    public Collection<Affiliate> getAdmins() throws XMPPException {
        return getAffiliatesByOwner("admin");
    }

    /**
     * Returns a collection of <code>Affiliate</code> with the room members.
     *
     * @return a collection of <code>Affiliate</code> with the room members.
     * @throws XMPPException if an error occured while performing the request to the server or you
     *         don't have enough privileges to get this information.
     */
    public Collection<Affiliate> getMembers() throws XMPPException {
        return getAffiliatesByAdmin("member");
    }

    /**
     * Returns a collection of <code>Affiliate</code> with the room outcasts.
     *
     * @return a collection of <code>Affiliate</code> with the room outcasts.
     * @throws XMPPException if an error occured while performing the request to the server or you
     *         don't have enough privileges to get this information.
     */
    public Collection<Affiliate> getOutcasts() throws XMPPException {
        return getAffiliatesByAdmin("outcast");
    }

    /**
     * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
     * sending a request in the owner namespace.
     *
     * @param affiliation the affiliation of the users in the room.
     * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
     * @throws XMPPException if an error occured while performing the request to the server or you
     *         don't have enough privileges to get this information.
     */
    private Collection<Affiliate> getAffiliatesByOwner(String affiliation) throws XMPPException {
        MUCOwner iq = new MUCOwner();
        iq.setTo(room);
        iq.setType(IQ.Type.GET);
        // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
        MUCOwner.Item item = new MUCOwner.Item(affiliation);
        iq.addItem(item);

        // Wait for a response packet back from the server.
        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send the request to the server.
        connection.sendPacket(iq);
        // Wait up to a certain number of seconds for a reply.
        MUCOwner answer = (MUCOwner) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
        // Get the list of affiliates from the server's answer
        List<Affiliate> affiliates = new ArrayList<Affiliate>();
        for (Iterator<MUCOwner.Item> it = answer.getItems(); it.hasNext();) {
            affiliates.add(new Affiliate(it.next()));
        }
        return affiliates;
    }

    /**
     * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
     * sending a request in the admin namespace.
     *
     * @param affiliation the affiliation of the users in the room.
     * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
     * @throws XMPPException if an error occured while performing the request to the server or you
     *         don't have enough privileges to get this information.
     */
    private Collection<Affiliate> getAffiliatesByAdmin(String affiliation) throws XMPPException {
        MUCAdmin iq = new MUCAdmin();
        iq.setTo(room);
        iq.setType(IQ.Type.GET);
        // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
        MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
        iq.addItem(item);

        // Wait for a response packet back from the server.
        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send the request to the server.
        connection.sendPacket(iq);
        // Wait up to a certain number of seconds for a reply.
        MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
        // Get the list of affiliates from the server's answer
        List<Affiliate> affiliates = new ArrayList<Affiliate>();
        for (Iterator<MUCAdmin.Item> it = answer.getItems(); it.hasNext();) {
            affiliates.add(new Affiliate(it.next()));
        }
        return affiliates;
    }

    /**
     * Returns a collection of <code>Occupant</code> with the room moderators.
     *
     * @return a collection of <code>Occupant</code> with the room moderators.
     * @throws XMPPException if an error occured while performing the request to the server or you
     *         don't have enough privileges to get this information.
     */
    public Collection<Occupant> getModerators() throws XMPPException {
        return getOccupants("moderator");
    }

    /**
     * Returns a collection of <code>Occupant</code> with the room participants.
     *
     * @return a collection of <code>Occupant</code> with the room participants.
     * @throws XMPPException if an error occured while performing the request to the server or you
     *         don't have enough privileges to get this information.
     */
    public Collection<Occupant> getParticipants() throws XMPPException {
        return getOccupants("participant");
    }

    /**
     * Returns a collection of <code>Occupant</code> that have the specified room role.
     *
     * @param role the role of the occupant in the room.
     * @return a collection of <code>Occupant</code> that have the specified room role.
     * @throws XMPPException if an error occured while performing the request to the server or you
     *         don't have enough privileges to get this information.
     */
    private Collection<Occupant> getOccupants(String role) throws XMPPException {
        MUCAdmin iq = new MUCAdmin();
        iq.setTo(room);
        iq.setType(IQ.Type.GET);
        // Set the specified role. This may request the list of moderators/participants.
        MUCAdmin.Item item = new MUCAdmin.Item(null, role);
        iq.addItem(item);

        // Wait for a response packet back from the server.
        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send the request to the server.
        connection.sendPacket(iq);
        // Wait up to a certain number of seconds for a reply.
        MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
        // Get the list of participants from the server's answer
        List<Occupant> participants = new ArrayList<Occupant>();
        for (Iterator<MUCAdmin.Item> it = answer.getItems(); it.hasNext();) {
            participants.add(new Occupant(it.next()));
        }
        return participants;
    }

    /**
     * Sends a message to the chat room.
     *
     * @param text the text of the message to send.
     * @throws XMPPException if sending the message fails.
     */
    public void sendMessage(String text) throws XMPPException {
        Message message = new Message(room, Message.Type.groupchat);
        message.setBody(text);
        connection.sendPacket(message);
    }

    /**
     * Returns a new Chat for sending private messages to a given room occupant.
     * The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server
     * service will change the 'from' address to the sender's room JID and delivering the message
     * to the intended recipient's full JID.
     *
     * @param occupant occupant unique room JID (e.g. 'darkcave@macbeth.shakespeare.lit/Paul').
     * @param listener the listener is a message listener that will handle messages for the newly
     * created chat.
     * @return new Chat for sending private messages to a given room occupant.
     */
    public Chat createPrivateChat(String occupant, MessageListener listener) {
        return connection.getChatManager().createChat(occupant, listener);
    }

    /**
     * Creates a new Message to send to the chat room.
     *
     * @return a new Message addressed to the chat room.
     */
    public Message createMessage() {
        return new Message(room, Message.Type.groupchat);
    }

    /**
     * Sends a Message to the chat room.
     *
     * @param message the message.
     * @throws XMPPException if sending the message fails.
     */
    public void sendMessage(Message message) throws XMPPException {
        connection.sendPacket(message);
    }

    /**
    * Polls for and returns the next message, or <tt>null</tt> if there isn't
    * a message immediately available. This method provides significantly different
    * functionalty than the {@link #nextMessage()} method since it's non-blocking.
    * In other words, the method call will always return immediately, whereas the
    * nextMessage method will return only when a message is available (or after
    * a specific timeout).
    *
    * @return the next message if one is immediately available and
    *      <tt>null</tt> otherwise.
    */
    public Message pollMessage() {
        return (Message) messageCollector.pollResult();
    }

    /**
     * Returns the next available message in the chat. The method call will block
     * (not return) until a message is available.
     *
     * @return the next message.
     */
    public Message nextMessage() {
        return (Message) messageCollector.nextResult();
    }

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

    /**
     * Adds a packet listener that will be notified of any new messages in the
     * group chat. Only "group chat" messages addressed to this group chat will
     * be delivered to the listener. If you wish to listen for other packets
     * that may be associated with this group chat, you should register a
     * PacketListener directly with the Connection with the appropriate
     * PacketListener.
     *
     * @param listener a packet listener.
     */
    public void addMessageListener(PacketListener listener) {
        connection.addPacketListener(listener, messageFilter);
        connectionListeners.add(listener);
    }

    /**
     * Removes a packet listener that was being notified of any new messages in the
     * multi user chat. Only "group chat" messages addressed to this multi user chat were
     * being delivered to the listener.
     *
     * @param listener a packet listener.
     */
    public void removeMessageListener(PacketListener listener) {
        connection.removePacketListener(listener);
        connectionListeners.remove(listener);
    }

    /**
     * Changes the subject within the room. As a default, only users with a role of "moderator"
     * are allowed to change the subject in a room. Although some rooms may be configured to
     * allow a mere participant or even a visitor to change the subject.
     *
     * @param subject the new room's subject to set.
     * @throws XMPPException if someone without appropriate privileges attempts to change the
     *          room subject will throw an error with code 403 (i.e. Forbidden)
     */
    public void changeSubject(final String subject) throws XMPPException {
        Message message = new Message(room, Message.Type.groupchat);
        message.setSubject(subject);
        // Wait for an error or confirmation message back from the server.
        PacketFilter responseFilter =
            new AndFilter(
                new FromMatchesFilter(room),
                new PacketTypeFilter(Message.class));
        responseFilter = new AndFilter(responseFilter, new PacketFilter() {
            public boolean accept(Packet packet) {
                Message msg = (Message) packet;
                return subject.equals(msg.getSubject());
            }
        });
        PacketCollector response = connection.createPacketCollector(responseFilter);
        // Send change subject packet.
        connection.sendPacket(message);
        // Wait up to a certain number of seconds for a reply.
        Message answer =
            (Message) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        response.cancel();

        if (answer == null) {
            throw new XMPPException("No response from server.");
        }
        else if (answer.getError() != null) {
            throw new XMPPException(answer.getError());
        }
    }

    /**
     * Notification message that the user has joined the room.
     */
    private synchronized void userHasJoined() {
        // Update the list of joined rooms through this connection
        List<String> rooms = joinedRooms.get(connection);
        if (rooms == null) {
            rooms = new ArrayList<String>();
            joinedRooms.put(connection, rooms);
        }
        rooms.add(room);
    }

    /**
     * Notification message that the user has left the room.
     */
    private synchronized void userHasLeft() {
        // Update the list of joined rooms through this connection
        List<String> rooms = joinedRooms.get(connection);
        if (rooms == null) {
            return;
        }
        rooms.remove(room);
        cleanup();
    }

    /**
     * Returns the MUCUser packet extension included in the packet or <tt>null</tt> if none.
     *
     * @param packet the packet that may include the MUCUser extension.
     * @return the MUCUser found in the packet.
     */
    private MUCUser getMUCUserExtension(Packet packet) {
        if (packet != null) {
            // Get the MUC User extension
            return (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
        }
        return null;
    }

    /**
     * Adds a listener that will be notified of changes in your status in the room
     * such as the user being kicked, banned, or granted admin permissions.
     *
     * @param listener a user status listener.
     */
    public void addUserStatusListener(UserStatusListener listener) {
        synchronized (userStatusListeners) {
            if (!userStatusListeners.contains(listener)) {
                userStatusListeners.add(listener);
            }
        }
    }

    /**
     * Removes a listener that was being notified of changes in your status in the room
     * such as the user being kicked, banned, or granted admin permissions.
     *
     * @param listener a user status listener.
     */
    public void removeUserStatusListener(UserStatusListener listener) {
        synchronized (userStatusListeners) {
            userStatusListeners.remove(listener);
        }
    }

    private void fireUserStatusListeners(String methodName, Object[] params) {
        UserStatusListener[] listeners;
        synchronized (userStatusListeners) {
            listeners = new UserStatusListener[userStatusListeners.size()];
            userStatusListeners.toArray(listeners);
        }
        // Get the classes of the method parameters
        Class<?>[] paramClasses = new Class[params.length];
        for (int i = 0; i < params.length; i++) {
            paramClasses[i] = params[i].getClass();
        }
        try {
            // Get the method to execute based on the requested methodName and parameters classes
            Method method = UserStatusListener.class.getDeclaredMethod(methodName, paramClasses);
            for (UserStatusListener listener : listeners) {
                method.invoke(listener, params);
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * Adds a listener that will be notified of changes in occupants status in the room
     * such as the user being kicked, banned, or granted admin permissions.
     *
     * @param listener a participant status listener.
     */
    public void addParticipantStatusListener(ParticipantStatusListener listener) {
        synchronized (participantStatusListeners) {
            if (!participantStatusListeners.contains(listener)) {
                participantStatusListeners.add(listener);
            }
        }
    }

    /**
     * Removes a listener that was being notified of changes in occupants status in the room
     * such as the user being kicked, banned, or granted admin permissions.
     *
     * @param listener a participant status listener.
     */
    public void removeParticipantStatusListener(ParticipantStatusListener listener) {
        synchronized (participantStatusListeners) {
            participantStatusListeners.remove(listener);
        }
    }

    private void fireParticipantStatusListeners(String methodName, List<String> params) {
        ParticipantStatusListener[] listeners;
        synchronized (participantStatusListeners) {
            listeners = new ParticipantStatusListener[participantStatusListeners.size()];
            participantStatusListeners.toArray(listeners);
        }
        try {
            // Get the method to execute based on the requested methodName and parameter
            Class<?>[] classes = new Class[params.size()];
            for (int i=0;i<params.size(); i++) {
                classes[i] = String.class;
            }
            Method method = ParticipantStatusListener.class.getDeclaredMethod(methodName, classes);
            for (ParticipantStatusListener listener : listeners) {
                method.invoke(listener, params.toArray());
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private void init() {
        // Create filters
        messageFilter =
            new AndFilter(
                new FromMatchesFilter(room),
                new MessageTypeFilter(Message.Type.groupchat));
        messageFilter = new AndFilter(messageFilter, new PacketFilter() {
            public boolean accept(Packet packet) {
                Message msg = (Message) packet;
                return msg.getBody() != null;
            }
        });
        presenceFilter =
            new AndFilter(new FromMatchesFilter(room), new PacketTypeFilter(Presence.class));

        // Create a collector for incoming messages.
        messageCollector = new ConnectionDetachedPacketCollector();

        // Create a listener for subject updates.
        PacketListener subjectListener = new PacketListener() {
            public void processPacket(Packet packet) {
                Message msg = (Message) packet;
                // Update the room subject
                subject = msg.getSubject();
                // Fire event for subject updated listeners
                fireSubjectUpdatedListeners(
                    msg.getSubject(),
                    msg.getFrom());

            }
        };

        // Create a listener for all presence updates.
        PacketListener presenceListener = new PacketListener() {
            public void processPacket(Packet packet) {
                Presence presence = (Presence) packet;
                String from = presence.getFrom();
                String myRoomJID = room + "/" + nickname;
                boolean isUserStatusModification = presence.getFrom().equals(myRoomJID);
                if (presence.getType() == Presence.Type.available) {
                    Presence oldPresence = occupantsMap.put(from, presence);
                    if (oldPresence != null) {
                        // Get the previous occupant's affiliation & role
                        MUCUser mucExtension = getMUCUserExtension(oldPresence);
                        String oldAffiliation = mucExtension.getItem().getAffiliation();
                        String oldRole = mucExtension.getItem().getRole();
                        // Get the new occupant's affiliation & role
                        mucExtension = getMUCUserExtension(presence);
                        String newAffiliation = mucExtension.getItem().getAffiliation();
                        String newRole = mucExtension.getItem().getRole();
                        // Fire role modification events
                        checkRoleModifications(oldRole, newRole, isUserStatusModification, from);
                        // Fire affiliation modification events
                        checkAffiliationModifications(
                            oldAffiliation,
                            newAffiliation,
                            isUserStatusModification,
                            from);
                    }
                    else {
                        // A new occupant has joined the room
                        if (!isUserStatusModification) {
                            List<String> params = new ArrayList<String>();
                            params.add(from);
                            fireParticipantStatusListeners("joined", params);
                        }
                    }
                }
                else if (presence.getType() == Presence.Type.unavailable) {
                    occupantsMap.remove(from);
                    MUCUser mucUser = getMUCUserExtension(presence);
                    if (mucUser != null && mucUser.getStatus() != null) {
                        // Fire events according to the received presence code
                        checkPresenceCode(
                            mucUser.getStatus().getCode(),
                            presence.getFrom().equals(myRoomJID),
                            mucUser,
                            from);
                    } else {
                        // An occupant has left the room
                        if (!isUserStatusModification) {
                            List<String> params = new ArrayList<String>();
                            params.add(from);
                            fireParticipantStatusListeners("left", params);
                        }
                    }
                }
            }
        };

        // Listens for all messages that include a MUCUser extension and fire the invitation
        // rejection listeners if the message includes an invitation rejection.
        PacketListener declinesListener = new PacketListener() {
            public void processPacket(Packet packet) {
                // Get the MUC User extension
                MUCUser mucUser = getMUCUserExtension(packet);
                // Check if the MUCUser informs that the invitee has declined the invitation
                if (mucUser.getDecline() != null &&
                        ((Message) packet).getType() != Message.Type.error) {
                    // Fire event for invitation rejection listeners
                    fireInvitationRejectionListeners(
                        mucUser.getDecline().getFrom(),
                        mucUser.getDecline().getReason());
                }
            }
        };

        PacketMultiplexListener packetMultiplexor = new PacketMultiplexListener(
                messageCollector, presenceListener, subjectListener,
                declinesListener);

        roomListenerMultiplexor = RoomListenerMultiplexor.getRoomMultiplexor(connection);

        roomListenerMultiplexor.addRoom(room, packetMultiplexor);
    }

    /**
     * Fires notification events if the role of a room occupant has changed. If the occupant that
     * changed his role is your occupant then the <code>UserStatusListeners</code> added to this
     * <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed
     * his role is not yours then the <code>ParticipantStatusListeners</code> added to this
     * <code>MultiUserChat</code> will be fired. The following table shows the events that will
     * be fired depending on the previous and new role of the occupant.
     *
     * <pre>
     * <table border="1">
     * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
     *
     * <tr><td>None</td><td>Visitor</td><td>--</td></tr>
     * <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr>
     * <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr>
     *
     * <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr>
     * <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
     * <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
     *
     * <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr>
     * <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr>
     * <tr><td>Visitor</td><td>None</td><td>kicked</td></tr>
     *
     * <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr>
     * <tr><td>Moderator</td><td>None</td><td>kicked</td></tr>
     * <tr><td>Participant</td><td>None</td><td>kicked</td></tr>
     * </table>
     * </pre>
     *
     * @param oldRole the previous role of the user in the room before receiving the new presence
     * @param newRole the new role of the user in the room after receiving the new presence
     * @param isUserModification whether the received presence is about your user in the room or not
     * @param from the occupant whose role in the room has changed
     * (e.g. room@conference.jabber.org/nick).
     */
    private void checkRoleModifications(
        String oldRole,
        String newRole,
        boolean isUserModification,
        String from) {
        // Voice was granted to a visitor
        if (("visitor".equals(oldRole) || "none".equals(oldRole))
            && "participant".equals(newRole)) {
            if (isUserModification) {
                fireUserStatusListeners("voiceGranted", new Object[] {});
            }
            else {
                List<String> params = new ArrayList<String>();
                params.add(from);
                fireParticipantStatusListeners("voiceGranted", params);
            }
        }
        // The participant's voice was revoked from the room
        else if (
            "participant".equals(oldRole)
                && ("visitor".equals(newRole) || "none".equals(newRole))) {
            if (isUserModification) {
                fireUserStatusListeners("voiceRevoked", new Object[] {});
            }
            else {
                List<String> params = new ArrayList<String>();
                params.add(from);
                fireParticipantStatusListeners("voiceRevoked", params);
            }
        }
        // Moderator privileges were granted to a participant
        if (!"moderator".equals(oldRole) && "moderator".equals(newRole)) {
            if ("visitor".equals(oldRole) || "none".equals(oldRole)) {
                if (isUserModification) {
                    fireUserStatusListeners("voiceGranted", new Object[] {});
                }
                else {
                    List<String> params = new ArrayList<String>();
                    params.add(from);
                    fireParticipantStatusListeners("voiceGranted", params);
                }
            }
            if (isUserModification) {
                fireUserStatusListeners("moderatorGranted", new Object[] {});
            }
            else {
                List<String> params = new ArrayList<String>();
                params.add(from);
                fireParticipantStatusListeners("moderatorGranted", params);
            }
        }
        // Moderator privileges were revoked from a participant
        else if ("moderator".equals(oldRole) && !"moderator".equals(newRole)) {
            if ("visitor".equals(newRole) || "none".equals(newRole)) {
                if (isUserModification) {
                    fireUserStatusListeners("voiceRevoked", new Object[] {});
                }
                else {
                    List<String> params = new ArrayList<String>();
                    params.add(from);
                    fireParticipantStatusListeners("voiceRevoked", params);
                }
            }
            if (isUserModification) {
                fireUserStatusListeners("moderatorRevoked", new Object[] {});
            }
            else {
                List<String> params = new ArrayList<String>();
                params.add(from);
                fireParticipantStatusListeners("moderatorRevoked", params);
            }
        }
    }

    /**
     * Fires notification events if the affiliation of a room occupant has changed. If the
     * occupant that changed his affiliation is your occupant then the
     * <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired.
     * On the other hand, if the occupant that changed his affiliation is not yours then the
     * <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be
     * fired. The following table shows the events that will be fired depending on the previous
     * and new affiliation of the occupant.
     *
     * <pre>
     * <table border="1">
     * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
     *
     * <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr>
     * <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr>
     * <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr>
     *
     * <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr>
     * <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr>
     * <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr>
     *
     * <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr>
     * <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr>
     * <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr>
     *
     * <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr>
     * <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr>
     * <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr>
     * <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr>
     * </table>
     * </pre>
     *
     * @param oldAffiliation the previous affiliation of the user in the room before receiving the
     * new presence
     * @param newAffiliation the new affiliation of the user in the room after receiving the new
     * presence
     * @param isUserModification whether the received presence is about your user in the room or not
     * @param from the occupant whose role in the room has changed
     * (e.g. room@conference.jabber.org/nick).
     */
    private void checkAffiliationModifications(
        String oldAffiliation,
        String newAffiliation,
        boolean isUserModification,
        String from) {
        // First check for revoked affiliation and then for granted affiliations. The idea is to
        // first fire the "revoke" events and then fire the "grant" events.

        // The user's ownership to the room was revoked
        if ("owner".equals(oldAffiliation) && !"owner".equals(newAffiliation)) {
            if (isUserModification) {
                fireUserStatusListeners("ownershipRevoked", new Object[] {});
            }
            else {
                List<String> params = new ArrayList<String>();
                params.add(from);
                fireParticipantStatusListeners("ownershipRevoked", params);
            }
        }
        // The user's administrative privileges to the room were revoked
        else if ("admin".equals(oldAffiliation) && !"admin".equals(newAffiliation)) {
            if (isUserModification) {
                fireUserStatusListeners("adminRevoked", new Object[] {});
            }
            else {
                List<String> params = new ArrayList<String>();
                params.add(from);
                fireParticipantStatusListeners("adminRevoked", params);
            }
        }
        // The user's membership to the room was revoked
        else if ("member".equals(oldAffiliation) && !"member".equals(newAffiliation)) {
            if (isUserModification) {
                fireUserStatusListeners("membershipRevoked", new Object[] {});
            }
            else {
                List<String> params = new ArrayList<String>();
                params.add(from);
                fireParticipantStatusListeners("membershipRevoked", params);
            }
        }

        // The user was granted ownership to the room
        if (!"owner".equals(oldAffiliation) && "owner".equals(newAffiliation)) {
            if (isUserModification) {
                fireUserStatusListeners("ownershipGranted", new Object[] {});
            }
            else {
                List<String> params = new ArrayList<String>();
                params.add(from);
                fireParticipantStatusListeners("ownershipGranted", params);
            }
        }
        // The user was granted administrative privileges to the room
        else if (!"admin".equals(oldAffiliation) && "admin".equals(newAffiliation)) {
            if (isUserModification) {
                fireUserStatusListeners("adminGranted", new Object[] {});
            }
            else {
                List<String> params = new ArrayList<String>();
                params.add(from);
                fireParticipantStatusListeners("adminGranted", params);
            }
        }
        // The user was granted membership to the room
        else if (!"member".equals(oldAffiliation) && "member".equals(newAffiliation)) {
            if (isUserModification) {
                fireUserStatusListeners("membershipGranted", new Object[] {});
            }
            else {
                List<String> params = new ArrayList<String>();
                params.add(from);
                fireParticipantStatusListeners("membershipGranted", params);
            }
        }
    }

    /**
     * Fires events according to the received presence code.
     *
     * @param code
     * @param isUserModification
     * @param mucUser
     * @param from
     */
    private void checkPresenceCode(
        String code,
        boolean isUserModification,
        MUCUser mucUser,
        String from) {
        // Check if an occupant was kicked from the room
        if ("307".equals(code)) {
            // Check if this occupant was kicked
            if (isUserModification) {
                joined = false;

                fireUserStatusListeners(
                    "kicked",
                    new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});

                // Reset occupant information.
                occupantsMap.clear();
                nickname = null;
                userHasLeft();
            }
            else {
                List<String> params = new ArrayList<String>();
                params.add(from);
                params.add(mucUser.getItem().getActor());
                params.add(mucUser.getItem().getReason());
                fireParticipantStatusListeners("kicked", params);
            }
        }
        // A user was banned from the room
        else if ("301".equals(code)) {
            // Check if this occupant was banned
            if (isUserModification) {
                joined = false;

                fireUserStatusListeners(
                    "banned",
                    new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});

                // Reset occupant information.
                occupantsMap.clear();
                nickname = null;
                userHasLeft();
            }
            else {
                List<String> params = new ArrayList<String>();
                params.add(from);
                params.add(mucUser.getItem().getActor());
                params.add(mucUser.getItem().getReason());
                fireParticipantStatusListeners("banned", params);
            }
        }
        // A user's membership was revoked from the room
        else if ("321".equals(code)) {
            // Check if this occupant's membership was revoked
            if (isUserModification) {
                joined = false;

                fireUserStatusListeners("membershipRevoked", new Object[] {});

                // Reset occupant information.
                occupantsMap.clear();
                nickname = null;
                userHasLeft();
            }
        }
        // A occupant has changed his nickname in the room
        else if ("303".equals(code)) {
            List<String> params = new ArrayList<String>();
            params.add(from);
            params.add(mucUser.getItem().getNick());
            fireParticipantStatusListeners("nicknameChanged", params);
        }
    }

    private void cleanup() {
        try {
            if (connection != null) {
                roomListenerMultiplexor.removeRoom(room);
                // Remove all the PacketListeners added to the connection by this chat
                for (PacketListener connectionListener : connectionListeners) {
                    connection.removePacketListener(connectionListener);
                }
            }
        } catch (Exception e) {
            // Do nothing
        }
    }

    protected void finalize() throws Throwable {
        cleanup();
        super.finalize();
    }

    /**
     * An InvitationsMonitor monitors a given connection to detect room invitations. Every
     * time the InvitationsMonitor detects a new invitation it will fire the invitation listeners.
     *
     * @author Gaston Dombiak
     */
    private static class InvitationsMonitor implements ConnectionListener {
        // We use a WeakHashMap so that the GC can collect the monitor when the
        // connection is no longer referenced by any object.
        // Note that when the InvitationsMonitor is used, i.e. when there are InvitationListeners, it will add a
        // PacketListener to the Connection and therefore a strong reference from the Connection to the
        // InvitationsMonior will exists, preventing it from beeing gc'ed. After the last InvitationListener is gone,
        // the PacketListener will get removed (cancel()) allowing the garbage collection of the InvitationsMonitor
        // instance.
        private final static Map<Connection, WeakReference<InvitationsMonitor>> monitors =
                new WeakHashMap<Connection, WeakReference<InvitationsMonitor>>();

        // We don't use a synchronized List here because it would break the semantic of (add|remove)InvitationListener
        private final List<InvitationListener> invitationsListeners =
                new ArrayList<InvitationListener>();
        private Connection connection;
        private PacketFilter invitationFilter;
        private PacketListener invitationPacketListener;

        /**
         * Returns a new or existing InvitationsMonitor for a given connection.
         *
         * @param conn the connection to monitor for room invitations.
         * @return a new or existing InvitationsMonitor for a given connection.
         */
        public static InvitationsMonitor getInvitationsMonitor(Connection conn) {
            synchronized (monitors) {
                if (!monitors.containsKey(conn) || monitors.get(conn).get() == null) {
                    // We need to use a WeakReference because the monitor references the
                    // connection and this could prevent the GC from collecting the monitor
                    // when no other object references the monitor
                    InvitationsMonitor ivm = new InvitationsMonitor(conn);
                    monitors.put(conn, new WeakReference<InvitationsMonitor>(ivm));
                    return ivm;
                }
                // Return the InvitationsMonitor that monitors the connection
                return monitors.get(conn).get();
            }
        }

        /**
         * Creates a new InvitationsMonitor that will monitor invitations received
         * on a given connection.
         * 
         * @param connection the connection to monitor for possible room invitations
         */
        private InvitationsMonitor(Connection connection) {
            this.connection = connection;
        }

        /**
         * Adds a listener to invitation notifications. The listener will be fired anytime
         * an invitation is received.<p>
         *
         * If this is the first monitor's listener then the monitor will be initialized in
         * order to start listening to room invitations.
         *
         * @param listener an invitation listener.
         */
        public void addInvitationListener(InvitationListener listener) {
            synchronized (invitationsListeners) {
                // If this is the first monitor's listener then initialize the listeners
                // on the connection to detect room invitations
                if (invitationsListeners.size() == 0) {
                    init();
                }
                if (!invitationsListeners.contains(listener)) {
                    invitationsListeners.add(listener);
                }
            }
        }

        /**
         * Removes a listener to invitation notifications. The listener will be fired anytime
         * an invitation is received.<p>
         *
         * If there are no more listeners to notifiy for room invitations then the monitor will
         * be stopped. As soon as a new listener is added to the monitor, the monitor will resume
         * monitoring the connection for new room invitations.
         *
         * @param listener an invitation listener.
         */
        public void removeInvitationListener(InvitationListener listener) {
            synchronized (invitationsListeners) {
                if (invitationsListeners.contains(listener)) {
                    invitationsListeners.remove(listener);
                }
                // If there are no more listeners to notifiy for room invitations
                // then proceed to cancel/release this monitor
                if (invitationsListeners.size() == 0) {
                    cancel();
                }
            }
        }

        /**
         * Fires invitation listeners.
         */
        private void fireInvitationListeners(String room, String inviter, String reason, String password,
                                             Message message) {
            InvitationListener[] listeners;
            synchronized (invitationsListeners) {
                listeners = new InvitationListener[invitationsListeners.size()];
                invitationsListeners.toArray(listeners);
            }
            for (InvitationListener listener : listeners) {
                listener.invitationReceived(connection, room, inviter, reason, password, message);
            }
        }

        public void connectionClosed() {
            cancel();
        }

        public void connectionClosedOnError(Exception e) {
            // ignore              
        }

        public void reconnectingIn(int seconds) {
            // ignore
        }

        public void reconnectionSuccessful() {
            // ignore
        }

        public void reconnectionFailed(Exception e) {
            // ignore
        }

        /**
         * Initializes the listeners to detect received room invitations and to detect when the
         * connection gets closed. As soon as a room invitation is received the invitations
         * listeners will be fired. When the connection gets closed the monitor will remove
         * his listeners on the connection.
         */
        private void init() {
            // Listens for all messages that include a MUCUser extension and fire the invitation
            // listeners if the message includes an invitation.
            invitationFilter =
                new PacketExtensionFilter("x", "http://jabber.org/protocol/muc#user");
            invitationPacketListener = new PacketListener() {
                public void processPacket(Packet packet) {
                    // Get the MUCUser extension
                    MUCUser mucUser =
                        (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
                    // Check if the MUCUser extension includes an invitation
                    if (mucUser.getInvite() != null &&
                            ((Message) packet).getType() != Message.Type.error) {
                        // Fire event for invitation listeners
                        fireInvitationListeners(packet.getFrom(), mucUser.getInvite().getFrom(),
                                mucUser.getInvite().getReason(), mucUser.getPassword(), (Message) packet);
                    }
                }
            };
            connection.addPacketListener(invitationPacketListener, invitationFilter);
            // Add a listener to detect when the connection gets closed in order to
            // cancel/release this monitor 
            connection.addConnectionListener(this);
        }

        /**
         * Cancels all the listeners that this InvitationsMonitor has added to the connection.
         */
        private void cancel() {
            connection.removePacketListener(invitationPacketListener);
            connection.removeConnectionListener(this);
        }

    }
}