/*
* Kontalk Java client
* Copyright (C) 2016 Kontalk Devteam <devteam@kontalk.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kontalk.client;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smackx.address.packet.MultipleAddresses;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverItems;
import org.jivesoftware.smackx.iqlast.packet.LastActivity;
import org.jivesoftware.smackx.pubsub.packet.PubSub;
import org.kontalk.misc.JID;
/**
* Feature Service discovery (XEP-0030).
*
* A cache is used for discovering each entity at most once. Assumption is that entity features
* do not change during a connection session.
*
* NOTE: Caps (XEP-0115) and caps cache is unfortunately not supported with server entities.
* The "ver=..." identifier is send with presence stanzas and server obviously don't send them.
* XEP-0115 mentions that the server connecting to can send a caps stanza as <stream:features>
* but this doesn't seem to be happen with Tigase.
*
* @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>}
*/
public final class FeatureDiscovery {
private static final Logger LOGGER = Logger.getLogger(FeatureDiscovery.class.getName());
public enum Feature {
USER_AVATAR,
MULTI_ADDRESSING,
/** New XEP-0363 upload service. */
HTTP_FILE_UPLOAD,
/** XEP-0012. */
LAST_ACTIVITY
}
private static final Map<String, Feature> FEATURE_MAP;
static {
FEATURE_MAP = new HashMap<>();
FEATURE_MAP.put(PubSub.NAMESPACE, Feature.USER_AVATAR);
FEATURE_MAP.put(MultipleAddresses.NAMESPACE, Feature.MULTI_ADDRESSING);
FEATURE_MAP.put(HTTPFileUpload.NAMESPACE, Feature.HTTP_FILE_UPLOAD);
FEATURE_MAP.put(LastActivity.NAMESPACE, Feature.LAST_ACTIVITY);
}
private final KonConnection mConn;
// NOTE: ignoring resource
private final Map<JID, EnumMap<Feature, JID>> mCache = new HashMap<>();
FeatureDiscovery(KonConnection conn) {
mConn = conn;
}
/** Discover all known features of connected server and its items. */
EnumMap<Feature, JID> getServerFeatures() {
// TODO use ServiceDiscoveryManager.serverSupportsFeature()
return getFeatures(JID.fromSmack(mConn.getServiceName()), true);
}
/** Discover all known features of an entity. */
EnumMap<Feature, JID> getFeaturesFor(JID entity) {
return getFeatures(entity, false);
}
private EnumMap<Feature, JID> getFeatures(JID entity, boolean withItems) {
if (!mCache.containsKey(entity))
mCache.put(entity, this.discover(entity, withItems));
return mCache.get(entity);
}
private EnumMap<Feature, JID> discover(JID entity, boolean withItems) {
// NOTE: smack automatically creates instances of SDM and CapsM and connects them
ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(mConn);
// 1. get features from server
EnumMap<Feature, JID> features = discover(discoManager, entity);
if (features == null)
return new EnumMap<>(FeatureDiscovery.Feature.class);
if (!withItems)
return features;
// 2. get server items
DiscoverItems items;
try {
items = discoManager.discoverItems(entity.toBareSmack());
} catch (SmackException.NoResponseException |
XMPPException.XMPPErrorException |
SmackException.NotConnectedException |
InterruptedException ex) {
LOGGER.log(Level.WARNING, "can't get service discovery items", ex);
return features;
}
// 3. get features from server items
for (DiscoverItems.Item item: items.getItems()) {
EnumMap<Feature, JID> itemFeatures = discover(discoManager, JID.fromSmack(item.getEntityID()));
if (itemFeatures != null)
features.putAll(itemFeatures);
}
LOGGER.info("supported server features: "+features);
return features;
}
private static EnumMap<Feature, JID> discover(ServiceDiscoveryManager dm, JID entity) {
DiscoverInfo info;
try {
// blocking
// NOTE: null parameter does not work
info = dm.discoverInfo(entity.toSmack());
} catch (SmackException.NoResponseException |
XMPPException.XMPPErrorException |
SmackException.NotConnectedException |
InterruptedException ex) {
// not supported by all servers/server not reachable, we only know after trying
//LOGGER.log(Level.WARNING, "can't get service discovery info", ex);
LOGGER.warning("can't get info for " + entity + " " + ex.getMessage());
return null;
}
EnumMap<Feature, JID> features = new EnumMap<>(FeatureDiscovery.Feature.class);
for (DiscoverInfo.Feature feature: info.getFeatures()) {
String var = feature.getVar();
if (FEATURE_MAP.containsKey(var)) {
features.put(FEATURE_MAP.get(var), entity);
}
}
List<DiscoverInfo.Identity> identities = info.getIdentities();
LOGGER.config("entity: " + entity
+ " identities: " + identities.stream()
.map(DiscoverInfo.Identity::toXML).collect(Collectors.toList())
+ " features: " + info.getFeatures().stream()
.map(DiscoverInfo.Feature::getVar).collect(Collectors.toList()));
return features;
}
}