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
/**
* $RCSfile$
* $Revision: 1701 $
* $Date: 2005-07-26 02:23:45 -0300 (Tue, 26 Jul 2005) $
*
* Copyright (C) 2005-2008 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.openfire.disco;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.dom4j.tree.DefaultElement;
import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.cluster.ClusterEventListener;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.cluster.NodeID;
import org.jivesoftware.openfire.handler.IQHandler;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
import org.jivesoftware.util.cache.ExternalizableUtil;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;
import org.xmpp.resultsetmanagement.ResultSet;
import org.xmpp.resultsetmanagement.ResultSetImpl;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
/**
* IQDiscoItemsHandler is responsible for handling disco#items requests. This class holds a map with
* the main entities and the associated DiscoItemsProvider. We are considering the host of the
* recipient JIDs as main entities. It's the DiscoItemsProvider responsibility to provide the items
* associated with the JID's name together with any possible requested node.<p>
* <p/>
* For example, let's have in the entities map the following entries: "localhost" and
* "conference.localhost". Associated with each entry we have different DiscoItemsProvider. Now we
* receive a disco#items request for the following JID: "room@conference.localhost" which is a disco
* request for a MUC room. So IQDiscoItemsHandler will look for the DiscoItemsProvider associated
* with the JID's host which in this case is "conference.localhost". Once we have located the
* provider we will delegate to the provider the responsibility to provide the items specific to
* the JID's name which in this case is "room". Depending on the implementation, the items could be
* the list of existing occupants if that information is publicly available. Finally, after we have
* collected all the items provided by the provider we will add them to the reply. On the other
* hand, if no provider was found or the provider has no information for the requested name/node
* then a not-found error will be returned.<p>
* <p/>
* Publishing of client items is still not supported.
*
* @author Gaston Dombiak
*/
public class IQDiscoItemsHandler extends IQHandler implements ServerFeaturesProvider, ClusterEventListener,
UserItemsProvider {
public static final String NAMESPACE_DISCO_ITEMS = "http://jabber.org/protocol/disco#items";
private Map<String,DiscoItemsProvider> entities = new HashMap<String,DiscoItemsProvider>();
private Map<String, Element> localServerItems = new HashMap<String, Element>();
private Cache<String, ClusteredServerItem> serverItems;
private Map<String, DiscoItemsProvider> serverNodeProviders = new ConcurrentHashMap<String, DiscoItemsProvider>();
private IQHandlerInfo info;
private IQDiscoInfoHandler infoHandler;
public IQDiscoItemsHandler() {
super("XMPP Disco Items Handler");
info = new IQHandlerInfo("query", NAMESPACE_DISCO_ITEMS);
}
@Override
public IQHandlerInfo getInfo() {
return info;
}
@Override
public IQ handleIQ(IQ packet) {
// Create a copy of the sent pack that will be used as the reply
// we only need to add the requested items to the reply if any otherwise add
// a not found error
IQ reply = IQ.createResultIQ(packet);
// TODO Implement publishing client items
if (IQ.Type.set == packet.getType()) {
reply.setChildElement(packet.getChildElement().createCopy());
reply.setError(PacketError.Condition.feature_not_implemented);
return reply;
}
// Look for a DiscoItemsProvider associated with the requested entity.
// We consider the host of the recipient JID of the packet as the entity. It's the
// DiscoItemsProvider responsibility to provide the items associated with the JID's name
// together with any possible requested node.
DiscoItemsProvider itemsProvider = getProvider(packet.getTo() == null ?
XMPPServer.getInstance().getServerInfo().getXMPPDomain() : packet.getTo().getDomain());
if (itemsProvider != null) {
// Get the JID's name
String name = packet.getTo() == null ? null : packet.getTo().getNode();
if (name == null || name.trim().length() == 0) {
name = null;
}
// Get the requested node
Element iq = packet.getChildElement();
String node = iq.attributeValue("node");
// Check if we have items associated with the requested name and node
Iterator<DiscoItem> itemsItr = itemsProvider.getItems(name, node, packet.getFrom());
if (itemsItr != null) {
reply.setChildElement(iq.createCopy());
Element queryElement = reply.getChildElement();
// See if the requesting entity would like to apply 'result set
// management'
final Element rsmElement = packet.getChildElement().element(
QName.get("set",
ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT));
// apply RSM only if the element exists, and the (total) results
// set is not empty.
final boolean applyRSM = rsmElement != null
&& itemsItr.hasNext();
if (applyRSM) {
if (!ResultSet.isValidRSMRequest(rsmElement))
{
reply.setError(PacketError.Condition.bad_request);
return reply;
}
// Calculate which results to include.
final List<DiscoItem> rsmResults;
final List<DiscoItem> allItems = new ArrayList<DiscoItem>();
while (itemsItr.hasNext()) {
allItems.add(itemsItr.next());
}
final ResultSet<DiscoItem> rs = new ResultSetImpl<DiscoItem>(
allItems);
try {
rsmResults = rs.applyRSMDirectives(rsmElement);
} catch (NullPointerException e) {
final IQ itemNotFound = IQ.createResultIQ(packet);
itemNotFound.setError(PacketError.Condition.item_not_found);
return itemNotFound;
}
// add the applicable results to the IQ-result
for (DiscoItem item : rsmResults) {
final Element resultElement = item.getElement();
resultElement.setQName(new QName(resultElement
.getName(), queryElement.getNamespace()));
queryElement.add(resultElement.createCopy());
}
// overwrite the 'set' element.
queryElement.remove(queryElement.element(
QName.get("set",
ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT)));
queryElement.add(rs.generateSetElementFromResults(rsmResults));
} else {
// don't apply RSM:
// Add to the reply all the items provided by the DiscoItemsProvider
Element item;
while (itemsItr.hasNext()) {
item = itemsItr.next().getElement();
item.setQName(new QName(item.getName(), queryElement.getNamespace()));
queryElement.add(item.createCopy());
}
}
}
else {
// If the DiscoItemsProvider has no items for the requested name and node
// then answer a not found error
reply.setChildElement(packet.getChildElement().createCopy());
reply.setError(PacketError.Condition.item_not_found);
}
}
else {
// If we didn't find a DiscoItemsProvider then answer a not found error
reply.setChildElement(packet.getChildElement().createCopy());
reply.setError(PacketError.Condition.item_not_found);
}
return reply;
}
/**
* Returns the DiscoItemsProvider responsible for providing the items related to a given entity
* or null if none was found.
*
* @param name the name of the identity.
* @return the DiscoItemsProvider responsible for providing the items related to a given entity
* or null if none was found.
*/
private DiscoItemsProvider getProvider(String name) {
return entities.get(name);
}
/**
* Sets that a given DiscoItemsProvider will provide the items related to a given entity. This
* message must be used when new modules (e.g. MUC) are implemented and need to provide
* the items related to them.
*
* @param name the name of the entity.
* @param provider the DiscoItemsProvider that will provide the entity's items.
*/
protected void setProvider(String name, DiscoItemsProvider provider) {
entities.put(name, provider);
}
/**
* Removes the DiscoItemsProvider related to a given entity.
*
* @param name the name of the entity.
*/
protected void removeProvider(String name) {
entities.remove(name);
}
/**
* Adds the items provided by the new service that implements the ServerItemsProvider
* interface. This information will be used whenever a disco for items is made against
* the server (i.e. the packet's target is the server).
* Example of item is: <item jid='conference.localhost' name='Public chatrooms'/>
*
* @param provider the ServerItemsProvider that provides new server items.
*/
public void addServerItemsProvider(ServerItemsProvider provider) {
DiscoServerItem discoItem;
Iterator<DiscoServerItem> items = provider.getItems();
if (items == null) {
// Do nothing
return;
}
while (items.hasNext()) {
discoItem = items.next();
// Add the element to the list of items related to the server
addComponentItem(discoItem.getJID().toString(), discoItem.getNode(), discoItem.getName());
// Add the new item as a valid entity that could receive info and items disco requests
String host = discoItem.getJID().getDomain();
infoHandler.setProvider(host, discoItem.getDiscoInfoProvider());
setProvider(host, discoItem.getDiscoItemsProvider());
}
}
/**
* Removes the provided items as a service of the service.
*
* @param provider The provider that is being removed.
*/
public void removeServerItemsProvider(ServerItemsProvider provider) {
DiscoServerItem discoItem;
Iterator<DiscoServerItem> items = provider.getItems();
if (items == null) {
// Do nothing
return;
}
while (items.hasNext()) {
discoItem = items.next();
// Remove the item from the server items list
removeComponentItem(discoItem.getJID().toString());
// Remove the item as a valid entity that could receive info and items disco requests
String host = discoItem.getJID().getDomain();
infoHandler.removeProvider(host);
removeProvider(host);
}
}
/**
* Sets the DiscoItemsProvider to use when a disco#items packet is sent to the server itself
* and the specified node. For instance, if node matches "http://jabber.org/protocol/offline"
* then a special DiscoItemsProvider should be use to return information about offline messages.
*
* @param node the node that the provider will handle.
* @param provider the DiscoItemsProvider that will handle disco#items packets sent with the
* specified node.
*/
public void setServerNodeInfoProvider(String node, DiscoItemsProvider provider) {
serverNodeProviders.put(node, provider);
}
/**
* Removes the DiscoItemsProvider to use when a disco#items packet is sent to the server itself
* and the specified node.
*
* @param node the node that the provider was handling.
*/
public void removeServerNodeInfoProvider(String node) {
serverNodeProviders.remove(node);
}
/**
* Registers a new disco item for a component. The jid attribute of the item will match the jid
* of the component and the name should be the name of the component discovered using disco.
*
* @param jid the jid of the component.
* @param name the discovered name of the component.
*/
public void addComponentItem(String jid, String name) {
addComponentItem(jid, null, name);
}
/**
* Registers a new disco item for a component. The jid attribute of the item will match the jid
* of the component and the name should be the name of the component discovered using disco.
*
* @param jid the jid of the component.
* @param node the node that complements the jid address.
* @param name the discovered name of the component.
*/
public void addComponentItem(String jid, String node, String name) {
Lock lock = CacheFactory.getLock(jid, serverItems);
try {
lock.lock();
ClusteredServerItem item = serverItems.get(jid);
if (item == null) {
// First time a node registers a server item for this component
item = new ClusteredServerItem();
Element element = DocumentHelper.createElement("item");
element.addAttribute("jid", jid);
element.addAttribute("node", node);
element.addAttribute("name", name);
item.element = element;
}
if (item.nodes.add(XMPPServer.getInstance().getNodeID())) {
// Update the cache with latest info
serverItems.put(jid, item);
}
// Keep track of the new server item added by this JVM
localServerItems.put(jid, item.element);
}
finally {
lock.unlock();
}
}
/**
* Removes a disco item for a component that has been removed from the server.
*
* @param jid the jid of the component being removed.
*/
public void removeComponentItem(String jid) {
if (serverItems == null) {
// Safety check
return;
}
Lock lock = CacheFactory.getLock(jid, serverItems);
try {
lock.lock();
ClusteredServerItem item = serverItems.get(jid);
if (item != null && item.nodes.remove(XMPPServer.getInstance().getNodeID())) {
// Update the cache with latest info
if (item.nodes.isEmpty()) {
serverItems.remove(jid);
}
else {
serverItems.put(jid, item);
}
}
}
finally {
lock.unlock();
}
// Remove locally added server item
localServerItems.remove(jid);
}
@Override
public void initialize(XMPPServer server) {
super.initialize(server);
serverItems = CacheFactory.createCache("Disco Server Items");
// Track the implementors of ServerItemsProvider so that we can collect the items
// provided by the server
infoHandler = server.getIQDiscoInfoHandler();
setProvider(server.getServerInfo().getXMPPDomain(), getServerItemsProvider());
// Listen to cluster events
ClusterManager.addListener(this);
}
@Override
public void start() throws IllegalStateException {
super.start();
for (ServerItemsProvider provider : XMPPServer.getInstance().getServerItemsProviders()) {
addServerItemsProvider(provider);
}
}
public Iterator<String> getFeatures() {
List<String> features = new ArrayList<String>();
features.add("http://jabber.org/protocol/disco#items");
// TODO Comment out this line when publishing of client items is implemented
//features.add("http://jabber.org/protocol/disco#publish");
return features.iterator();
}
public void joinedCluster() {
restoreCacheContent();
}
public void joinedCluster(byte[] nodeID) {
// Do nothing
}
public void leftCluster() {
if (!XMPPServer.getInstance().isShuttingDown()) {
restoreCacheContent();
}
}
public void leftCluster(byte[] nodeID) {
if (ClusterManager.isSeniorClusterMember()) {
NodeID leftNode = NodeID.getInstance(nodeID);
for (Map.Entry<String, ClusteredServerItem> entry : serverItems.entrySet()) {
String jid = entry.getKey();
Lock lock = CacheFactory.getLock(jid, serverItems);
try {
lock.lock();
ClusteredServerItem item = entry.getValue();
if (item.nodes.remove(leftNode)) {
// Update the cache with latest info
if (item.nodes.isEmpty()) {
serverItems.remove(jid);
}
else {
serverItems.put(jid, item);
}
}
}
finally {
lock.unlock();
}
}
}
}
public void markedAsSeniorClusterMember() {
// Do nothing
}
private void restoreCacheContent() {
for (Map.Entry<String, Element> entry : localServerItems.entrySet()) {
String jid = entry.getKey();
Element element = entry.getValue();
Lock lock = CacheFactory.getLock(jid, serverItems);
try {
lock.lock();
ClusteredServerItem item = serverItems.get(jid);
if (item == null) {
// First time a node registers a server item for this component
item = new ClusteredServerItem();
item.element = element;
}
if (item.nodes.add(XMPPServer.getInstance().getNodeID())) {
// Update the cache with latest info
serverItems.put(jid, item);
}
}
finally {
lock.unlock();
}
}
}
private DiscoItemsProvider getServerItemsProvider() {
return new DiscoItemsProvider() {
public Iterator<DiscoItem> getItems(String name, String node, JID senderJID) {
if (node != null) {
// Check if there is a provider for the requested node
if (serverNodeProviders.get(node) != null) {
return serverNodeProviders.get(node).getItems(name, node, senderJID);
}
return null;
}
if (name == null) {
List<DiscoItem> answer = new ArrayList<DiscoItem>();
for (ClusteredServerItem item : serverItems.values()) {
answer.add(new DiscoItem(item.element));
}
return answer.iterator();
}
else {
// If addressed to user@domain, add items from UserItemsProviders to
// the reply.
List<UserItemsProvider> itemsProviders = XMPPServer.getInstance().getUserItemsProviders();
if (itemsProviders.isEmpty()) {
// If we didn't find any UserItemsProviders, then answer a not found error
return null;
}
List<DiscoItem> answer = new ArrayList<DiscoItem>();
for (UserItemsProvider itemsProvider : itemsProviders) {
// Check if we have items associated with the requested name
Iterator<Element> itemsItr = itemsProvider.getUserItems(name, senderJID);
if (itemsItr != null) {
// Add to the reply all the items provided by the UserItemsProvider
Element item;
while (itemsItr.hasNext()) {
item = itemsItr.next();
JID itemJid = new JID(item.attributeValue("jid"));
String itemName = item.attributeValue("name");
String itemNode = item.attributeValue("node");
String itemAction = item.attributeValue("action");
answer.add(new DiscoItem(itemJid, itemName, itemNode, itemAction));
}
}
}
return answer.iterator();
}
}
};
}
private static class ClusteredServerItem implements Externalizable {
private Element element;
private Set<NodeID> nodes = new HashSet<NodeID>();
public ClusteredServerItem() {
}
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeSerializable(out, (DefaultElement) element);
ExternalizableUtil.getInstance().writeExternalizableCollection(out, nodes);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
element = (Element) ExternalizableUtil.getInstance().readSerializable(in);
ExternalizableUtil.getInstance().readExternalizableCollection(in, nodes, getClass().getClassLoader());
}
}
public Iterator<Element> getUserItems(String name, JID senderJID) {
List<Element> answer = new ArrayList<Element>();
try {
User user = UserManager.getInstance().getUser(name);
RosterItem item = user.getRoster().getRosterItem(senderJID);
// If the requesting entity is subscribed to the account's presence then
// answer the user's "available resources"
if (item.getSubStatus() == RosterItem.SUB_FROM ||
item.getSubStatus() == RosterItem.SUB_BOTH) {
for (Session session : SessionManager.getInstance().getSessions(name)) {
Element element = DocumentHelper.createElement("item");
element.addAttribute("jid", session.getAddress().toString());
answer.add(element);
}
}
return answer.iterator();
}
catch (UserNotFoundException e) {
return answer.iterator();
}
}
}