();
this.cacheNonCaps = cacheNonCaps;
DiscoverInfo.Identity identity
= new DiscoverInfo.Identity(
"client",
ServiceDiscoveryManager.getIdentityName());
identity.setType(ServiceDiscoveryManager.getIdentityType());
identities.add(identity);
//add support for capabilities
discoveryManager.addFeature(CapsPacketExtension.NAMESPACE);
/*
* Reflect featuresToRemove and featuresToAdd before
* updateEntityCapsVersion() in order to persist only the complete
* node#ver association with our own DiscoverInfo. Otherwise, we'd
* persist all intermediate ones upon each addFeature() and
* removeFeature().
*/
// featuresToRemove
if (featuresToRemove != null)
{
for (String featureToRemove : featuresToRemove)
discoveryManager.removeFeature(featureToRemove);
}
// featuresToAdd
if (featuresToAdd != null)
{
for (String featureToAdd : featuresToAdd)
if (!discoveryManager.includesFeature(featureToAdd))
discoveryManager.addFeature(featureToAdd);
}
// For every XMPPConnection, add one EntityCapsManager.
this.capsManager = new EntityCapsManager();
capsManager.addPacketListener(connection);
/*
* XXX initFeatures() has to happen before updateEntityCapsVersion().
* Otherwise, updateEntityCapsVersion() will not include the features of
* the wrapped discoveryManager.
*/
initFeatures();
updateEntityCapsVersion();
// Now, make sure we intercept presence packages and add caps data when
// intended. XEP-0115 specifies that a client SHOULD include entity
// capabilities with every presence notification it sends.
connection.addPacketInterceptor(
this,
new PacketTypeFilter(Presence.class));
}
/**
* Registers that a new feature is supported by this XMPP entity. When this
* client is queried for its information the registered features will be
* answered.
*
* Since no packet is actually sent to the server it is safe to perform
* this operation before logging to the server. In fact, you may want to
* configure the supported features before logging to the server so that
* the information is already available if it is required upon login.
*
* @param feature the feature to register as supported.
*/
public void addFeature(String feature)
{
synchronized (features)
{
features.add(feature);
discoveryManager.addFeature(feature);
}
updateEntityCapsVersion();
}
/**
* Recalculates the entity capabilities caps ver string according to what's
* currently available in our own discovery info.
*/
private void updateEntityCapsVersion()
{
// If a XMPPConnection is the managed one, see that the new version is
// updated
if ((connection != null) && (capsManager != null))
capsManager.calculateEntityCapsVersion(getOwnDiscoverInfo());
}
/**
* Returns a reference to our local copy of the feature list supported by
* this implementation.
*
* @return a reference to our local copy of the feature list supported by
* this implementation.
*/
public List getFeatures()
{
return unmodifiableFeatures;
}
/**
* Get a DiscoverInfo for the current entity caps node.
*
* @return a DiscoverInfo for the current entity caps node
*/
public DiscoverInfo getOwnDiscoverInfo()
{
DiscoverInfo di = new DiscoverInfo();
di.setType(IQ.Type.RESULT);
di.setNode(capsManager.getNode() + "#" + getEntityCapsVersion());
// Add discover info
addDiscoverInfoTo(di);
return di;
}
/**
* Returns the caps version as returned by our caps manager or null
* if we don't have a caps manager yet.
*
* @return the caps version as returned by our caps manager or null
* if we don't have a caps manager yet.
*/
private String getEntityCapsVersion()
{
return (capsManager == null) ? null : capsManager.getCapsVersion();
}
/**
* Populates a specific DiscoverInfo with the identity and features
* of the current entity caps node.
*
* @param response the discover info response packet
*/
private void addDiscoverInfoTo(DiscoverInfo response)
{
// Set this client identity
DiscoverInfo.Identity identity
= new DiscoverInfo.Identity(
"client",
ServiceDiscoveryManager.getIdentityName());
identity.setType(ServiceDiscoveryManager.getIdentityType());
response.addIdentity(identity);
// Add the registered features to the response
// Add Entity Capabilities (XEP-0115) feature node.
/*
* XXX Only addFeature if !containsFeature. Otherwise, the DiscoverInfo
* may end up with repeating features.
*/
if (!response.containsFeature(CapsPacketExtension.NAMESPACE))
response.addFeature(CapsPacketExtension.NAMESPACE);
Iterable features = getFeatures();
synchronized (features)
{
for (String feature : features)
if (!response.containsFeature(feature))
response.addFeature(feature);
}
}
/**
* Returns true if the specified feature is registered in our
* {@link ServiceDiscoveryManager} and false otherwise.
*
* @param feature the feature to look for.
*
* @return a boolean indicating if the specified featured is registered or
* not.
*/
public boolean includesFeature(String feature)
{
return this.discoveryManager.includesFeature(feature);
}
/**
* Removes the specified feature from the supported features by the
* encapsulated ServiceDiscoveryManager.
*
* Since no packet is actually sent to the server it is safe to perform
* this operation before logging to the server.
*
* @param feature the feature to remove from the supported features.
*/
public void removeFeature(String feature)
{
synchronized (features)
{
features.remove(feature);
discoveryManager.removeFeature(feature);
}
updateEntityCapsVersion();
}
/**
* Add feature to put in "ext" attribute.
*
* @param ext ext feature to add
*/
public void addExtFeature(String ext)
{
synchronized(extCapabilities)
{
extCapabilities.add(ext);
}
}
/**
* Remove "ext" feature.
*
* @param ext ext feature to remove
*/
public void removeExtFeature(String ext)
{
synchronized(extCapabilities)
{
extCapabilities.remove(ext);
}
}
/**
* Get "ext" value.
*
* @return string that represents "ext" value
*/
public synchronized String getExtFeatures()
{
StringBuilder bldr = new StringBuilder("");
for(String e : extCapabilities)
{
bldr.append(e);
bldr.append(" ");
}
return bldr.toString();
}
/**
* Intercepts outgoing presence packets and adds entity capabilities at
* their ends.
*
* @param packet the (hopefully presence) packet we need to add a "c"
* element to.
*/
public void interceptPacket(Packet packet)
{
if ((packet instanceof Presence) && (capsManager != null))
{
String ver = getEntityCapsVersion();
CapsPacketExtension caps
= new CapsPacketExtension(
getExtFeatures(),
capsManager.getNode(),
CapsPacketExtension.HASH_METHOD,
ver);
//make sure we'll be able to handle requests for the newly generated
//node once we've used it.
discoveryManager.setNodeInformationProvider(
caps.getNode() + "#" + caps.getVersion(),
this);
// Remove old capabilities extension if present
PacketExtension oldCaps
= packet.getExtension(
CapsPacketExtension.ELEMENT_NAME,
CapsPacketExtension.NAMESPACE);
if (oldCaps != null)
{
packet.removeExtension(oldCaps);
}
// Put new capabilities extension
packet.addExtension(caps);
}
}
/**
* Returns a list of the Items
* {@link org.jivesoftware.smackx.packet.DiscoverItems.Item} defined in the
* node or in other words null since we don't support any.
*
* @return always null since we don't support items.
*/
public List getNodeItems()
{
return null;
}
/**
* Returns a list of the features defined in the node. For
* example, the entity caps protocol specifies that an XMPP client
* should answer with each feature supported by the client version
* or extension.
*
* @return a list of the feature strings defined in the node.
*/
public List getNodeFeatures()
{
return getFeatures();
}
/**
* Returns a list of the identities defined in the node. For example, the
* x-command protocol must provide an identity of category automation and
* type command-node for each command.
*
* @return a list of the Identities defined in the node.
*/
public List getNodeIdentities()
{
return identities;
}
/**
* Initialize our local features copy in a way that would
*/
private void initFeatures()
{
Iterator defaultFeatures = discoveryManager.getFeatures();
synchronized (features)
{
while (defaultFeatures.hasNext())
{
String feature = defaultFeatures.next();
this.features.add( feature );
}
}
}
/**
* Returns the discovered information of a given XMPP entity addressed by
* its JID.
*
* @param entityID the address of the XMPP entity.
* @return the discovered information.
* @throws XMPPException if the operation failed for some reason.
*/
public DiscoverInfo discoverInfo(String entityID)
throws XMPPException
{
DiscoverInfo discoverInfo = capsManager.getDiscoverInfoByUser(entityID);
if (discoverInfo != null)
return discoverInfo;
EntityCapsManager.Caps caps = capsManager.getCapsByUser(entityID);
// if caps is not valid, has empty hash
if (cacheNonCaps && (caps == null || !caps.isValid(discoverInfo)))
{
discoverInfo = nonCapsCache.get(entityID);
if (discoverInfo != null)
return discoverInfo;
}
discoverInfo
= discoverInfo(
entityID,
(caps == null) ? null : caps.getNodeVer());
if ((caps != null) && !caps.isValid(discoverInfo))
{
if(!caps.hash.equals(""))
{
logger.error(
"Invalid DiscoverInfo for " + caps.getNodeVer() + ": "
+ discoverInfo);
}
caps = null;
}
if (caps == null)
{
if (cacheNonCaps)
nonCapsCache.put(entityID, discoverInfo);
}
else
EntityCapsManager.addDiscoverInfoByCaps(caps, discoverInfo);
return discoverInfo;
}
/**
* Returns the discovered information of a given XMPP entity addressed by
* its JID if locally cached, otherwise schedules for retrieval.
*
* @param entityID the address of the XMPP entity.
* @return the discovered information.
* @throws XMPPException if the operation failed for some reason.
*/
public DiscoverInfo discoverInfoNonBlocking(String entityID)
throws XMPPException
{
DiscoverInfo discoverInfo = capsManager.getDiscoverInfoByUser(entityID);
if (discoverInfo != null)
return discoverInfo;
EntityCapsManager.Caps caps = capsManager.getCapsByUser(entityID);
// if caps is not valid, has empty hash
if (cacheNonCaps && (caps == null || !caps.isValid(discoverInfo)))
{
discoverInfo = nonCapsCache.get(entityID);
if (discoverInfo != null)
return discoverInfo;
}
// add to retrieve thread
retriever.addEntityForRetrieve(
entityID,
caps);
return null;
}
/**
* Returns the discovered information of a given XMPP entity addressed by
* its JID and note attribute. Use this message only when trying to query
* information which is not directly addressable.
*
* @param entityID the address of the XMPP entity.
* @param node the attribute that supplements the 'jid' attribute.
*
* @return the discovered information.
*
* @throws XMPPException if the operation failed for some reason.
*/
public DiscoverInfo discoverInfo(String entityID, String node)
throws XMPPException
{
return discoveryManager.discoverInfo(entityID, node);
}
/**
* Returns the discovered items of a given XMPP entity addressed by its JID.
*
* @param entityID the address of the XMPP entity.
*
* @return the discovered information.
*
* @throws XMPPException if the operation failed for some reason.
*/
public DiscoverItems discoverItems(String entityID) throws XMPPException
{
return discoveryManager.discoverItems(entityID);
}
/**
* Returns the discovered items of a given XMPP entity addressed by its JID
* and note attribute. Use this message only when trying to query
* information which is not directly addressable.
*
* @param entityID the address of the XMPP entity.
* @param node the attribute that supplements the 'jid' attribute.
*
* @return the discovered items.
*
* @throws XMPPException if the operation failed for some reason.
*/
public DiscoverItems discoverItems(String entityID, String node)
throws XMPPException
{
return discoveryManager.discoverItems(entityID, node);
}
/**
* Returns true if jid supports the specified
* feature and false otherwise. The method may check the
* information locally if we've already cached this jid's disco
* info, or retrieve it from the network.
*
* @param jid the jabber ID we'd like to test for support
* @param feature the URN feature we are interested in
*
* @return true if jid is discovered to support feature
* and false otherwise.
*/
public boolean supportsFeature(String jid, String feature)
{
DiscoverInfo info;
try
{
info = this.discoverInfo(jid);
}
catch(XMPPException ex)
{
logger.info("failed to retrieve disco info for " + jid
+ " feature " + feature, ex);
return false;
}
return ((info != null) && info.containsFeature(feature));
}
/**
* Gets the EntityCapsManager which handles the entity capabilities
* for this ScServiceDiscoveryManager.
*
* @return the EntityCapsManager which handles the entity
* capabilities for this ScServiceDiscoveryManager
*/
public EntityCapsManager getCapsManager()
{
return capsManager;
}
/**
* Clears/stops what's needed.
*/
public void stop()
{
if(retriever != null)
retriever.stop();
}
/**
* Thread that runs the discovery info.
*/
private class DiscoveryInfoRetriever
implements Runnable
{
/**
* start/stop.
*/
private boolean stopped = true;
/**
* The thread that runs this dispatcher.
*/
private Thread retrieverThread = null;
/**
* Entities to be processed and their caps.
* HashMap so we can store null caps.
*/
private Map entities
= new HashMap();
/**
* Our capability operation set.
*/
private OperationSetContactCapabilitiesJabberImpl capabilitiesOpSet;
/**
* Runs in different thread.
*/
public void run()
{
try
{
stopped = false;
while(!stopped)
{
Map.Entry
entityToProcess = null;
synchronized(entities)
{
if(entities.size() == 0)
{
try
{
entities.wait();
}
catch (InterruptedException iex){}
}
Iterator>
iter = entities.entrySet().iterator();
if(iter.hasNext())
{
entityToProcess = iter.next();
iter.remove();
}
}
if(entityToProcess != null)
{
// process
requestDiscoveryInfo(
entityToProcess.getKey(),
entityToProcess.getValue());
}
entityToProcess = null;
}
} catch(Throwable t)
{
logger.error("Error requesting discovery info, " +
"thread ended unexpectedly", t);
}
}
/**
* Requests the discovery info and fires the event if
* retrieved.
* @param entityID the entity to request
* @param caps and its capability.
*/
private void requestDiscoveryInfo(final String entityID,
EntityCapsManager.Caps caps)
{
try
{
DiscoverInfo discoverInfo = discoverInfo(
entityID,
(caps == null ) ? null : caps.getNodeVer());
if ((caps != null) && !caps.isValid(discoverInfo))
{
if(!caps.hash.equals(""))
{
logger.error("Invalid DiscoverInfo for "
+ caps.getNodeVer() + ": " + discoverInfo);
}
caps = null;
}
boolean fireEvent = false;
if (caps == null)
{
if (cacheNonCaps)
{
nonCapsCache.put(entityID, discoverInfo);
fireEvent = true;
}
}
else
{
EntityCapsManager.addDiscoverInfoByCaps(caps, discoverInfo);
fireEvent = true;
}
// fire event
if(fireEvent && capabilitiesOpSet != null)
{
capabilitiesOpSet.fireContactCapabilitiesChanged(
entityID,
capsManager.getFullJidsByBareJid(
StringUtils.parseBareAddress(entityID))
);
}
}
catch(XMPPException ex)
{
// print discovery info errors only when trace is enabled
if(logger.isTraceEnabled())
logger.error("Error requesting discover info for "
+ entityID, ex);
}
}
/**
* Queue entities for retrieval.
* @param entityID the entity.
* @param caps and its capability.
*/
public void addEntityForRetrieve(String entityID,
EntityCapsManager.Caps caps)
{
synchronized(entities)
{
if(!entities.containsKey(entityID))
{
entities.put(entityID, caps);
entities.notifyAll();
if(retrieverThread == null)
{
start();
}
}
}
}
/**
* Start thread.
*/
private void start()
{
capabilitiesOpSet = (OperationSetContactCapabilitiesJabberImpl)
parentProvider.getOperationSet(
OperationSetContactCapabilities.class);
retrieverThread = new Thread(
this,
ScServiceDiscoveryManager.class.getName());
retrieverThread.setDaemon(true);
retrieverThread.start();
}
/**
* Stops and clears.
*/
void stop()
{
synchronized(entities)
{
stopped = true;
entities.notifyAll();
retrieverThread = null;
}
}
}
}