EntityCapsManager.java

  1. /**
  2.  *
  3.  * Copyright © 2009 Jonas Ådahl, 2011-2014 Florian Schmaus
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.caps;

  18. import org.jivesoftware.smack.AbstractConnectionListener;
  19. import org.jivesoftware.smack.SmackException.NoResponseException;
  20. import org.jivesoftware.smack.SmackException.NotConnectedException;
  21. import org.jivesoftware.smack.XMPPConnection;
  22. import org.jivesoftware.smack.ConnectionCreationListener;
  23. import org.jivesoftware.smack.Manager;
  24. import org.jivesoftware.smack.StanzaListener;
  25. import org.jivesoftware.smack.XMPPConnectionRegistry;
  26. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  27. import org.jivesoftware.smack.packet.IQ;
  28. import org.jivesoftware.smack.packet.Stanza;
  29. import org.jivesoftware.smack.packet.ExtensionElement;
  30. import org.jivesoftware.smack.packet.Presence;
  31. import org.jivesoftware.smack.filter.NotFilter;
  32. import org.jivesoftware.smack.filter.StanzaFilter;
  33. import org.jivesoftware.smack.filter.AndFilter;
  34. import org.jivesoftware.smack.filter.StanzaTypeFilter;
  35. import org.jivesoftware.smack.filter.StanzaExtensionFilter;
  36. import org.jivesoftware.smack.util.StringUtils;
  37. import org.jivesoftware.smack.util.stringencoder.Base64;
  38. import org.jivesoftware.smackx.caps.cache.EntityCapsPersistentCache;
  39. import org.jivesoftware.smackx.caps.packet.CapsExtension;
  40. import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider;
  41. import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
  42. import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
  43. import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Feature;
  44. import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity;
  45. import org.jivesoftware.smackx.xdata.FormField;
  46. import org.jivesoftware.smackx.xdata.packet.DataForm;
  47. import org.jxmpp.jid.DomainBareJid;
  48. import org.jxmpp.jid.Jid;
  49. import org.jxmpp.util.cache.LruCache;

  50. import java.util.Comparator;
  51. import java.util.HashMap;
  52. import java.util.LinkedList;
  53. import java.util.List;
  54. import java.util.Locale;
  55. import java.util.Map;
  56. import java.util.Queue;
  57. import java.util.SortedSet;
  58. import java.util.TreeSet;
  59. import java.util.WeakHashMap;
  60. import java.util.concurrent.ConcurrentLinkedQueue;
  61. import java.util.logging.Level;
  62. import java.util.logging.Logger;
  63. import java.security.MessageDigest;
  64. import java.security.NoSuchAlgorithmException;

  65. /**
  66.  * Keeps track of entity capabilities.
  67.  *
  68.  * @author Florian Schmaus
  69.  * @see <a href="http://www.xmpp.org/extensions/xep-0115.html">XEP-0115: Entity Capabilities</a>
  70.  */
  71. public class EntityCapsManager extends Manager {
  72.     private static final Logger LOGGER = Logger.getLogger(EntityCapsManager.class.getName());

  73.     public static final String NAMESPACE = CapsExtension.NAMESPACE;
  74.     public static final String ELEMENT = CapsExtension.ELEMENT;

  75.     private static final Map<String, MessageDigest> SUPPORTED_HASHES = new HashMap<String, MessageDigest>();

  76.     /**
  77.      * The default hash. Currently 'sha-1'.
  78.      */
  79.     private static final String DEFAULT_HASH = StringUtils.SHA1;

  80.     private static String DEFAULT_ENTITY_NODE = "http://www.igniterealtime.org/projects/smack";

  81.     protected static EntityCapsPersistentCache persistentCache;

  82.     private static boolean autoEnableEntityCaps = true;

  83.     private static Map<XMPPConnection, EntityCapsManager> instances = new WeakHashMap<>();

  84.     private static final StanzaFilter PRESENCES_WITH_CAPS = new AndFilter(new StanzaTypeFilter(Presence.class), new StanzaExtensionFilter(
  85.                     ELEMENT, NAMESPACE));
  86.     private static final StanzaFilter PRESENCES_WITHOUT_CAPS = new AndFilter(new StanzaTypeFilter(Presence.class), new NotFilter(new StanzaExtensionFilter(
  87.                     ELEMENT, NAMESPACE)));
  88.     private static final StanzaFilter PRESENCES = StanzaTypeFilter.PRESENCE;

  89.     /**
  90.      * Map of "node + '#' + hash" to DiscoverInfo data
  91.      */
  92.     private static final LruCache<String, DiscoverInfo> CAPS_CACHE = new LruCache<String, DiscoverInfo>(1000);

  93.     /**
  94.      * Map of Full JID -&gt; DiscoverInfo/null. In case of c2s connection the
  95.      * key is formed as user@server/resource (resource is required) In case of
  96.      * link-local connection the key is formed as user@host (no resource) In
  97.      * case of a server or component the key is formed as domain
  98.      */
  99.     private static final LruCache<Jid, NodeVerHash> JID_TO_NODEVER_CACHE = new LruCache<>(10000);

  100.     static {
  101.         XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
  102.             public void connectionCreated(XMPPConnection connection) {
  103.                 getInstanceFor(connection);
  104.             }
  105.         });

  106.         try {
  107.             MessageDigest sha1MessageDigest = MessageDigest.getInstance(DEFAULT_HASH);
  108.             SUPPORTED_HASHES.put(DEFAULT_HASH, sha1MessageDigest);
  109.         } catch (NoSuchAlgorithmException e) {
  110.             // Ignore
  111.         }
  112.     }

  113.     /**
  114.      * Set the default entity node that will be used for new EntityCapsManagers
  115.      *
  116.      * @param entityNode
  117.      */
  118.     public static void setDefaultEntityNode(String entityNode) {
  119.         DEFAULT_ENTITY_NODE = entityNode;
  120.     }

  121.     /**
  122.      * Add DiscoverInfo to the database.
  123.      *
  124.      * @param nodeVer
  125.      *            The node and verification String (e.g.
  126.      *            "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
  127.      * @param info
  128.      *            DiscoverInfo for the specified node.
  129.      */
  130.     public static void addDiscoverInfoByNode(String nodeVer, DiscoverInfo info) {
  131.         CAPS_CACHE.put(nodeVer, info);

  132.         if (persistentCache != null)
  133.             persistentCache.addDiscoverInfoByNodePersistent(nodeVer, info);
  134.     }

  135.     /**
  136.      * Get the Node version (node#ver) of a JID. Returns a String or null if
  137.      * EntiyCapsManager does not have any information.
  138.      *
  139.      * @param jid
  140.      *            the user (Full JID)
  141.      * @return the node version (node#ver) or null
  142.      */
  143.     public static String getNodeVersionByJid(String jid) {
  144.         NodeVerHash nvh = JID_TO_NODEVER_CACHE.get(jid);
  145.         if (nvh != null) {
  146.             return nvh.nodeVer;
  147.         } else {
  148.             return null;
  149.         }
  150.     }

  151.     public static NodeVerHash getNodeVerHashByJid(Jid jid) {
  152.         return JID_TO_NODEVER_CACHE.get(jid);
  153.     }

  154.     /**
  155.      * Get the discover info given a user name. The discover info is returned if
  156.      * the user has a node#ver associated with it and the node#ver has a
  157.      * discover info associated with it.
  158.      *
  159.      * @param user
  160.      *            user name (Full JID)
  161.      * @return the discovered info
  162.      */
  163.     public static DiscoverInfo getDiscoverInfoByUser(Jid user) {
  164.         NodeVerHash nvh = JID_TO_NODEVER_CACHE.get(user);
  165.         if (nvh == null)
  166.             return null;

  167.         return getDiscoveryInfoByNodeVer(nvh.nodeVer);
  168.     }

  169.     /**
  170.      * Retrieve DiscoverInfo for a specific node.
  171.      *
  172.      * @param nodeVer
  173.      *            The node name (e.g.
  174.      *            "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
  175.      * @return The corresponding DiscoverInfo or null if none is known.
  176.      */
  177.     public static DiscoverInfo getDiscoveryInfoByNodeVer(String nodeVer) {
  178.         DiscoverInfo info = CAPS_CACHE.get(nodeVer);

  179.         // If it was not in CAPS_CACHE, try to retrieve the information from persistentCache
  180.         if (info == null && persistentCache != null) {
  181.             info = persistentCache.lookup(nodeVer);
  182.             // Promote the information to CAPS_CACHE if one was found
  183.             if (info != null) {
  184.                 CAPS_CACHE.put(nodeVer, info);
  185.             }
  186.         }

  187.         // If we were able to retrieve information from one of the caches, copy it before returning
  188.         if (info != null)
  189.             info = new DiscoverInfo(info);

  190.         return info;
  191.     }

  192.     /**
  193.      * Set the persistent cache implementation
  194.      *
  195.      * @param cache
  196.      */
  197.     public static void setPersistentCache(EntityCapsPersistentCache cache) {
  198.         persistentCache = cache;
  199.     }

  200.     /**
  201.      * Sets the maximum cache sizes
  202.      *
  203.      * @param maxJidToNodeVerSize
  204.      * @param maxCapsCacheSize
  205.      */
  206.     public static void setMaxsCacheSizes(int maxJidToNodeVerSize, int maxCapsCacheSize) {
  207.         JID_TO_NODEVER_CACHE.setMaxCacheSize(maxJidToNodeVerSize);
  208.         CAPS_CACHE.setMaxCacheSize(maxCapsCacheSize);
  209.     }

  210.     /**
  211.      * Clears the memory cache.
  212.      */
  213.     public static void clearMemoryCache() {
  214.         JID_TO_NODEVER_CACHE.clear();
  215.         CAPS_CACHE.clear();
  216.     }

  217.     private static void addCapsExtensionInfo(Jid from, CapsExtension capsExtension) {
  218.         String capsExtensionHash = capsExtension.getHash();
  219.         String hashInUppercase = capsExtensionHash.toUpperCase(Locale.US);
  220.         // SUPPORTED_HASHES uses the format of MessageDigest, which is uppercase, e.g. "SHA-1" instead of "sha-1"
  221.         if (!SUPPORTED_HASHES.containsKey(hashInUppercase))
  222.             return;
  223.         String hash = capsExtensionHash.toLowerCase(Locale.US);

  224.         String node = capsExtension.getNode();
  225.         String ver = capsExtension.getVer();

  226.         JID_TO_NODEVER_CACHE.put(from, new NodeVerHash(node, ver, hash));
  227.     }

  228.     private final Queue<CapsVersionAndHash> lastLocalCapsVersions = new ConcurrentLinkedQueue<>();

  229.     private final ServiceDiscoveryManager sdm;

  230.     private boolean entityCapsEnabled;
  231.     private CapsVersionAndHash currentCapsVersion;
  232.     private boolean presenceSend = false;

  233.     /**
  234.      * The entity node String used by this EntityCapsManager instance.
  235.      */
  236.     private String entityNode = DEFAULT_ENTITY_NODE;

  237.     private EntityCapsManager(XMPPConnection connection) {
  238.         super(connection);
  239.         this.sdm = ServiceDiscoveryManager.getInstanceFor(connection);
  240.         instances.put(connection, this);

  241.         connection.addConnectionListener(new AbstractConnectionListener() {
  242.             @Override
  243.             public void connected(XMPPConnection connection) {
  244.                 // It's not clear when a server would report the caps stream
  245.                 // feature, so we try to process it after we are connected and
  246.                 // once after we are authenticated.
  247.                 processCapsStreamFeatureIfAvailable(connection);
  248.             }
  249.             @Override
  250.             public void authenticated(XMPPConnection connection, boolean resumed) {
  251.                 // It's not clear when a server would report the caps stream
  252.                 // feature, so we try to process it after we are connected and
  253.                 // once after we are authenticated.
  254.                 processCapsStreamFeatureIfAvailable(connection);

  255.                 // Reset presenceSend when the connection was not resumed
  256.                 if (!resumed) {
  257.                     presenceSend = false;
  258.                 }
  259.             }
  260.             private void processCapsStreamFeatureIfAvailable(XMPPConnection connection) {
  261.                 CapsExtension capsExtension = connection.getFeature(
  262.                                 CapsExtension.ELEMENT, CapsExtension.NAMESPACE);
  263.                 if (capsExtension == null) {
  264.                     return;
  265.                 }
  266.                 DomainBareJid from = connection.getServiceName();
  267.                 addCapsExtensionInfo(from, capsExtension);
  268.             }
  269.         });

  270.         // This calculates the local entity caps version
  271.         updateLocalEntityCaps();

  272.         if (autoEnableEntityCaps)
  273.             enableEntityCaps();

  274.         connection.addAsyncStanzaListener(new StanzaListener() {
  275.             // Listen for remote presence stanzas with the caps extension
  276.             // If we receive such a stanza, record the JID and nodeVer
  277.             @Override
  278.             public void processPacket(Stanza packet) {
  279.                 if (!entityCapsEnabled())
  280.                     return;

  281.                 CapsExtension capsExtension = CapsExtension.from(packet);
  282.                 Jid from = packet.getFrom();
  283.                 addCapsExtensionInfo(from, capsExtension);
  284.             }

  285.         }, PRESENCES_WITH_CAPS);

  286.         connection.addAsyncStanzaListener(new StanzaListener() {
  287.             @Override
  288.             public void processPacket(Stanza packet) {
  289.                 // always remove the JID from the map, even if entityCaps are
  290.                 // disabled
  291.                 Jid from = packet.getFrom();
  292.                 JID_TO_NODEVER_CACHE.remove(from);
  293.             }
  294.         }, PRESENCES_WITHOUT_CAPS);

  295.         connection.addPacketSendingListener(new StanzaListener() {
  296.             @Override
  297.             public void processPacket(Stanza packet) {
  298.                 presenceSend = true;
  299.             }
  300.         }, PRESENCES);

  301.         // Intercept presence packages and add caps data when intended.
  302.         // XEP-0115 specifies that a client SHOULD include entity capabilities
  303.         // with every presence notification it sends.
  304.         StanzaListener packetInterceptor = new StanzaListener() {
  305.             public void processPacket(Stanza packet) {
  306.                 if (!entityCapsEnabled)
  307.                     return;
  308.                 CapsVersionAndHash capsVersionAndHash = getCapsVersion();
  309.                 CapsExtension caps = new CapsExtension(entityNode, capsVersionAndHash.version, capsVersionAndHash.hash);
  310.                 packet.addExtension(caps);
  311.             }
  312.         };
  313.         connection.addPacketInterceptor(packetInterceptor, PRESENCES);
  314.         // It's important to do this as last action. Since it changes the
  315.         // behavior of the SDM in some ways
  316.         sdm.setEntityCapsManager(this);
  317.     }

  318.     public static synchronized EntityCapsManager getInstanceFor(XMPPConnection connection) {
  319.         if (SUPPORTED_HASHES.size() <= 0)
  320.             throw new IllegalStateException("No supported hashes for EntityCapsManager");

  321.         EntityCapsManager entityCapsManager = instances.get(connection);

  322.         if (entityCapsManager == null) {
  323.             entityCapsManager = new EntityCapsManager(connection);
  324.         }

  325.         return entityCapsManager;
  326.     }

  327.     public synchronized void enableEntityCaps() {
  328.         // Add Entity Capabilities (XEP-0115) feature node.
  329.         sdm.addFeature(NAMESPACE);
  330.         updateLocalEntityCaps();
  331.         entityCapsEnabled = true;
  332.     }

  333.     public synchronized void disableEntityCaps() {
  334.         entityCapsEnabled = false;
  335.         sdm.removeFeature(NAMESPACE);
  336.     }

  337.     public boolean entityCapsEnabled() {
  338.         return entityCapsEnabled;
  339.     }

  340.     public void setEntityNode(String entityNode) throws NotConnectedException {
  341.         this.entityNode = entityNode;
  342.         updateLocalEntityCaps();
  343.     }

  344.     /**
  345.      * Remove a record telling what entity caps node a user has.
  346.      *
  347.      * @param user
  348.      *            the user (Full JID)
  349.      */
  350.     public void removeUserCapsNode(String user) {
  351.         JID_TO_NODEVER_CACHE.remove(user);
  352.     }

  353.     /**
  354.      * Get our own caps version. The version depends on the enabled features. A
  355.      * caps version looks like '66/0NaeaBKkwk85efJTGmU47vXI='
  356.      *
  357.      * @return our own caps version
  358.      */
  359.     public CapsVersionAndHash getCapsVersion() {
  360.         return currentCapsVersion;
  361.     }

  362.     /**
  363.      * Returns the local entity's NodeVer (e.g.
  364.      * "http://www.igniterealtime.org/projects/smack/#66/0NaeaBKkwk85efJTGmU47vXI=
  365.      * )
  366.      *
  367.      * @return the local NodeVer
  368.      */
  369.     public String getLocalNodeVer() {
  370.         return entityNode + '#' + getCapsVersion();
  371.     }

  372.     /**
  373.      * Returns true if Entity Caps are supported by a given JID
  374.      *
  375.      * @param jid
  376.      * @return true if the entity supports Entity Capabilities.
  377.      * @throws XMPPErrorException
  378.      * @throws NoResponseException
  379.      * @throws NotConnectedException
  380.      * @throws InterruptedException
  381.      */
  382.     public boolean areEntityCapsSupported(Jid jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  383.         return sdm.supportsFeature(jid, NAMESPACE);
  384.     }

  385.     /**
  386.      * Returns true if Entity Caps are supported by the local service/server
  387.      *
  388.      * @return true if the user's server supports Entity Capabilities.
  389.      * @throws XMPPErrorException
  390.      * @throws NoResponseException
  391.      * @throws NotConnectedException
  392.      * @throws InterruptedException
  393.      */
  394.     public boolean areEntityCapsSupportedByServer() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
  395.         return areEntityCapsSupported(connection().getServiceName());
  396.     }

  397.     /**
  398.      * Updates the local user Entity Caps information with the data provided
  399.      *
  400.      * If we are connected and there was already a presence send, another
  401.      * presence is send to inform others about your new Entity Caps node string.
  402.      *
  403.      */
  404.     public void updateLocalEntityCaps() {
  405.         XMPPConnection connection = connection();

  406.         DiscoverInfo discoverInfo = new DiscoverInfo();
  407.         discoverInfo.setType(IQ.Type.result);
  408.         discoverInfo.setNode(getLocalNodeVer());
  409.         if (connection != null)
  410.             discoverInfo.setFrom(connection.getUser());
  411.         sdm.addDiscoverInfoTo(discoverInfo);

  412.         currentCapsVersion = generateVerificationString(discoverInfo);
  413.         addDiscoverInfoByNode(entityNode + '#' + currentCapsVersion.version, discoverInfo);
  414.         if (lastLocalCapsVersions.size() > 10) {
  415.             CapsVersionAndHash oldCapsVersion = lastLocalCapsVersions.poll();
  416.             sdm.removeNodeInformationProvider(entityNode + '#' + oldCapsVersion.version);
  417.         }
  418.         lastLocalCapsVersions.add(currentCapsVersion);

  419.         if (connection != null)
  420.             JID_TO_NODEVER_CACHE.put(connection.getUser(), new NodeVerHash(entityNode, currentCapsVersion));

  421.         final List<Identity> identities = new LinkedList<Identity>(ServiceDiscoveryManager.getInstanceFor(connection).getIdentities());
  422.         sdm.setNodeInformationProvider(entityNode + '#' + currentCapsVersion, new AbstractNodeInformationProvider() {
  423.             List<String> features = sdm.getFeatures();
  424.             List<ExtensionElement> packetExtensions = sdm.getExtendedInfoAsList();
  425.             @Override
  426.             public List<String> getNodeFeatures() {
  427.                 return features;
  428.             }
  429.             @Override
  430.             public List<Identity> getNodeIdentities() {
  431.                 return identities;
  432.             }
  433.             @Override
  434.             public List<ExtensionElement> getNodePacketExtensions() {
  435.                 return packetExtensions;
  436.             }
  437.         });

  438.         // Send an empty presence, and let the packet interceptor
  439.         // add a <c/> node to it.
  440.         // See http://xmpp.org/extensions/xep-0115.html#advertise
  441.         // We only send a presence packet if there was already one send
  442.         // to respect ConnectionConfiguration.isSendPresence()
  443.         if (connection != null && connection.isAuthenticated() && presenceSend) {
  444.             Presence presence = new Presence(Presence.Type.available);
  445.             try {
  446.                 connection.sendStanza(presence);
  447.             }
  448.             catch (InterruptedException | NotConnectedException e) {
  449.                 LOGGER.log(Level.WARNING, "Could could not update presence with caps info", e);
  450.             }
  451.         }
  452.     }

  453.     /**
  454.      * Verify DisoverInfo and Caps Node as defined in XEP-0115 5.4 Processing
  455.      * Method
  456.      *
  457.      * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0115
  458.      *      5.4 Processing Method</a>
  459.      *
  460.      * @param ver
  461.      * @param hash
  462.      * @param info
  463.      * @return true if it's valid and should be cache, false if not
  464.      */
  465.     public static boolean verifyDiscoverInfoVersion(String ver, String hash, DiscoverInfo info) {
  466.         // step 3.3 check for duplicate identities
  467.         if (info.containsDuplicateIdentities())
  468.             return false;

  469.         // step 3.4 check for duplicate features
  470.         if (info.containsDuplicateFeatures())
  471.             return false;

  472.         // step 3.5 check for well-formed packet extensions
  473.         if (verifyPacketExtensions(info))
  474.             return false;

  475.         String calculatedVer = generateVerificationString(info, hash).version;

  476.         if (!ver.equals(calculatedVer))
  477.             return false;

  478.         return true;
  479.     }

  480.     /**
  481.      *
  482.      * @param info
  483.      * @return true if the packet extensions is ill-formed
  484.      */
  485.     protected static boolean verifyPacketExtensions(DiscoverInfo info) {
  486.         List<FormField> foundFormTypes = new LinkedList<FormField>();
  487.         for (ExtensionElement pe : info.getExtensions()) {
  488.             if (pe.getNamespace().equals(DataForm.NAMESPACE)) {
  489.                 DataForm df = (DataForm) pe;
  490.                 for (FormField f : df.getFields()) {
  491.                     if (f.getVariable().equals("FORM_TYPE")) {
  492.                         for (FormField fft : foundFormTypes) {
  493.                             if (f.equals(fft))
  494.                                 return true;
  495.                         }
  496.                         foundFormTypes.add(f);
  497.                     }
  498.                 }
  499.             }
  500.         }
  501.         return false;
  502.     }

  503.     protected static CapsVersionAndHash generateVerificationString(DiscoverInfo discoverInfo) {
  504.         return generateVerificationString(discoverInfo, null);
  505.     }

  506.     /**
  507.      * Generates a XEP-115 Verification String
  508.      *
  509.      * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver">XEP-115
  510.      *      Verification String</a>
  511.      *
  512.      * @param discoverInfo
  513.      * @param hash
  514.      *            the used hash function, if null, default hash will be used
  515.      * @return The generated verification String or null if the hash is not
  516.      *         supported
  517.      */
  518.     protected static CapsVersionAndHash generateVerificationString(DiscoverInfo discoverInfo, String hash) {
  519.         if (hash == null) {
  520.             hash = DEFAULT_HASH;
  521.         }
  522.         // SUPPORTED_HASHES uses the format of MessageDigest, which is uppercase, e.g. "SHA-1" instead of "sha-1"
  523.         MessageDigest md = SUPPORTED_HASHES.get(hash.toUpperCase(Locale.US));
  524.         if (md == null)
  525.             return null;
  526.         // Then transform the hash to lowercase, as this value will be put on the wire within the caps element's hash
  527.         // attribute. I'm not sure if the standard is case insensitive here, but let's assume that even it is, there could
  528.         // be "broken" implementation in the wild, so we *always* transform to lowercase.
  529.         hash = hash.toLowerCase(Locale.US);

  530.         DataForm extendedInfo =  DataForm.from(discoverInfo);

  531.         // 1. Initialize an empty string S ('sb' in this method).
  532.         StringBuilder sb = new StringBuilder(); // Use StringBuilder as we don't
  533.                                                 // need thread-safe StringBuffer

  534.         // 2. Sort the service discovery identities by category and then by
  535.         // type and then by xml:lang
  536.         // (if it exists), formatted as CATEGORY '/' [TYPE] '/' [LANG] '/'
  537.         // [NAME]. Note that each slash is included even if the LANG or
  538.         // NAME is not included (in accordance with XEP-0030, the category and
  539.         // type MUST be included.
  540.         SortedSet<DiscoverInfo.Identity> sortedIdentities = new TreeSet<DiscoverInfo.Identity>();

  541.         for (DiscoverInfo.Identity i : discoverInfo.getIdentities())
  542.             sortedIdentities.add(i);

  543.         // 3. For each identity, append the 'category/type/lang/name' to S,
  544.         // followed by the '<' character.
  545.         for (DiscoverInfo.Identity identity : sortedIdentities) {
  546.             sb.append(identity.getCategory());
  547.             sb.append("/");
  548.             sb.append(identity.getType());
  549.             sb.append("/");
  550.             sb.append(identity.getLanguage() == null ? "" : identity.getLanguage());
  551.             sb.append("/");
  552.             sb.append(identity.getName() == null ? "" : identity.getName());
  553.             sb.append("<");
  554.         }

  555.         // 4. Sort the supported service discovery features.
  556.         SortedSet<String> features = new TreeSet<String>();
  557.         for (Feature f : discoverInfo.getFeatures())
  558.             features.add(f.getVar());

  559.         // 5. For each feature, append the feature to S, followed by the '<'
  560.         // character
  561.         for (String f : features) {
  562.             sb.append(f);
  563.             sb.append("<");
  564.         }

  565.         // only use the data form for calculation is it has a hidden FORM_TYPE
  566.         // field
  567.         // see XEP-0115 5.4 step 3.6
  568.         if (extendedInfo != null && extendedInfo.hasHiddenFormTypeField()) {
  569.             synchronized (extendedInfo) {
  570.                 // 6. If the service discovery information response includes
  571.                 // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e.,
  572.                 // by the XML character data of the <value/> element).
  573.                 SortedSet<FormField> fs = new TreeSet<FormField>(new Comparator<FormField>() {
  574.                     public int compare(FormField f1, FormField f2) {
  575.                         return f1.getVariable().compareTo(f2.getVariable());
  576.                     }
  577.                 });

  578.                 FormField ft = null;

  579.                 for (FormField f : extendedInfo.getFields()) {
  580.                     if (!f.getVariable().equals("FORM_TYPE")) {
  581.                         fs.add(f);
  582.                     } else {
  583.                         ft = f;
  584.                     }
  585.                 }

  586.                 // Add FORM_TYPE values
  587.                 if (ft != null) {
  588.                     formFieldValuesToCaps(ft.getValues(), sb);
  589.                 }

  590.                 // 7. 3. For each field other than FORM_TYPE:
  591.                 // 1. Append the value of the "var" attribute, followed by the
  592.                 // '<' character.
  593.                 // 2. Sort values by the XML character data of the <value/>
  594.                 // element.
  595.                 // 3. For each <value/> element, append the XML character data,
  596.                 // followed by the '<' character.
  597.                 for (FormField f : fs) {
  598.                     sb.append(f.getVariable());
  599.                     sb.append("<");
  600.                     formFieldValuesToCaps(f.getValues(), sb);
  601.                 }
  602.             }
  603.         }
  604.         // 8. Ensure that S is encoded according to the UTF-8 encoding (RFC
  605.         // 3269).
  606.         // 9. Compute the verification string by hashing S using the algorithm
  607.         // specified in the 'hash' attribute (e.g., SHA-1 as defined in RFC
  608.         // 3174).
  609.         // The hashed data MUST be generated with binary output and
  610.         // encoded using Base64 as specified in Section 4 of RFC 4648
  611.         // (note: the Base64 output MUST NOT include whitespace and MUST set
  612.         // padding bits to zero).
  613.         byte[] digest;
  614.         synchronized(md) {
  615.             digest = md.digest(sb.toString().getBytes());
  616.         }
  617.         String version = Base64.encodeToString(digest);
  618.         return new CapsVersionAndHash(version, hash);
  619.     }

  620.     private static void formFieldValuesToCaps(List<String> i, StringBuilder sb) {
  621.         SortedSet<String> fvs = new TreeSet<String>();
  622.         for (String s : i) {
  623.             fvs.add(s);
  624.         }
  625.         for (String fv : fvs) {
  626.             sb.append(fv);
  627.             sb.append("<");
  628.         }
  629.     }

  630.     public static class NodeVerHash {
  631.         private String node;
  632.         private String hash;
  633.         private String ver;
  634.         private String nodeVer;

  635.         NodeVerHash(String node, CapsVersionAndHash capsVersionAndHash) {
  636.             this(node, capsVersionAndHash.version, capsVersionAndHash.hash);
  637.         }

  638.         NodeVerHash(String node, String ver, String hash) {
  639.             this.node = node;
  640.             this.ver = ver;
  641.             this.hash = hash;
  642.             nodeVer = node + "#" + ver;
  643.         }

  644.         public String getNodeVer() {
  645.             return nodeVer;
  646.         }

  647.         public String getNode() {
  648.             return node;
  649.         }

  650.         public String getHash() {
  651.             return hash;
  652.         }

  653.         public String getVer() {
  654.             return ver;
  655.         }
  656.     }
  657. }