001/**
002 *
003 * Copyright © 2009 Jonas Ådahl, 2011-2014 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.caps;
018
019import org.jivesoftware.smack.AbstractConnectionListener;
020import org.jivesoftware.smack.SmackException.NoResponseException;
021import org.jivesoftware.smack.SmackException.NotConnectedException;
022import org.jivesoftware.smack.XMPPConnection;
023import org.jivesoftware.smack.ConnectionCreationListener;
024import org.jivesoftware.smack.Manager;
025import org.jivesoftware.smack.StanzaListener;
026import org.jivesoftware.smack.XMPPConnectionRegistry;
027import org.jivesoftware.smack.XMPPException.XMPPErrorException;
028import org.jivesoftware.smack.packet.IQ;
029import org.jivesoftware.smack.packet.Stanza;
030import org.jivesoftware.smack.packet.ExtensionElement;
031import org.jivesoftware.smack.packet.Presence;
032import org.jivesoftware.smack.filter.NotFilter;
033import org.jivesoftware.smack.filter.PresenceTypeFilter;
034import org.jivesoftware.smack.filter.StanzaFilter;
035import org.jivesoftware.smack.filter.AndFilter;
036import org.jivesoftware.smack.filter.StanzaTypeFilter;
037import org.jivesoftware.smack.filter.StanzaExtensionFilter;
038import org.jivesoftware.smack.util.StringUtils;
039import org.jivesoftware.smack.util.stringencoder.Base64;
040import org.jivesoftware.smackx.caps.cache.EntityCapsPersistentCache;
041import org.jivesoftware.smackx.caps.packet.CapsExtension;
042import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider;
043import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
044import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
045import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Feature;
046import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity;
047import org.jivesoftware.smackx.xdata.FormField;
048import org.jivesoftware.smackx.xdata.packet.DataForm;
049import org.jxmpp.util.cache.LruCache;
050
051import java.util.Comparator;
052import java.util.HashMap;
053import java.util.LinkedList;
054import java.util.List;
055import java.util.Locale;
056import java.util.Map;
057import java.util.Queue;
058import java.util.SortedSet;
059import java.util.TreeSet;
060import java.util.WeakHashMap;
061import java.util.concurrent.ConcurrentLinkedQueue;
062import java.util.logging.Level;
063import java.util.logging.Logger;
064import java.security.MessageDigest;
065import java.security.NoSuchAlgorithmException;
066
067/**
068 * Keeps track of entity capabilities.
069 * 
070 * @author Florian Schmaus
071 * @see <a href="http://www.xmpp.org/extensions/xep-0115.html">XEP-0115: Entity Capabilities</a>
072 */
073public class EntityCapsManager extends Manager {
074    private static final Logger LOGGER = Logger.getLogger(EntityCapsManager.class.getName());
075
076    public static final String NAMESPACE = CapsExtension.NAMESPACE;
077    public static final String ELEMENT = CapsExtension.ELEMENT;
078
079    private static final Map<String, MessageDigest> SUPPORTED_HASHES = new HashMap<String, MessageDigest>();
080
081    /**
082     * The default hash. Currently 'sha-1'.
083     */
084    private static final String DEFAULT_HASH = StringUtils.SHA1;
085
086    private static String DEFAULT_ENTITY_NODE = "http://www.igniterealtime.org/projects/smack";
087
088    protected static EntityCapsPersistentCache persistentCache;
089
090    private static boolean autoEnableEntityCaps = true;
091
092    private static Map<XMPPConnection, EntityCapsManager> instances = new WeakHashMap<>();
093
094    private static final StanzaFilter PRESENCES_WITH_CAPS = new AndFilter(new StanzaTypeFilter(Presence.class), new StanzaExtensionFilter(
095                    ELEMENT, NAMESPACE));
096    private static final StanzaFilter PRESENCES_WITHOUT_CAPS = new AndFilter(new StanzaTypeFilter(Presence.class), new NotFilter(new StanzaExtensionFilter(
097                    ELEMENT, NAMESPACE)));
098
099    /**
100     * Map of "node + '#' + hash" to DiscoverInfo data
101     */
102    private static final LruCache<String, DiscoverInfo> CAPS_CACHE = new LruCache<String, DiscoverInfo>(1000);
103
104    /**
105     * Map of Full JID -&gt; DiscoverInfo/null. In case of c2s connection the
106     * key is formed as user@server/resource (resource is required) In case of
107     * link-local connection the key is formed as user@host (no resource) In
108     * case of a server or component the key is formed as domain
109     */
110    private static final LruCache<String, NodeVerHash> JID_TO_NODEVER_CACHE = new LruCache<String, NodeVerHash>(10000);
111
112    static {
113        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
114            public void connectionCreated(XMPPConnection connection) {
115                getInstanceFor(connection);
116            }
117        });
118
119        try {
120            MessageDigest sha1MessageDigest = MessageDigest.getInstance(DEFAULT_HASH);
121            SUPPORTED_HASHES.put(DEFAULT_HASH, sha1MessageDigest);
122        } catch (NoSuchAlgorithmException e) {
123            // Ignore
124        }
125    }
126
127    /**
128     * Set the default entity node that will be used for new EntityCapsManagers
129     *
130     * @param entityNode
131     */
132    public static void setDefaultEntityNode(String entityNode) {
133        DEFAULT_ENTITY_NODE = entityNode;
134    }
135
136    /**
137     * Add DiscoverInfo to the database.
138     * 
139     * @param nodeVer
140     *            The node and verification String (e.g.
141     *            "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
142     * @param info
143     *            DiscoverInfo for the specified node.
144     */
145    public static void addDiscoverInfoByNode(String nodeVer, DiscoverInfo info) {
146        CAPS_CACHE.put(nodeVer, info);
147
148        if (persistentCache != null)
149            persistentCache.addDiscoverInfoByNodePersistent(nodeVer, info);
150    }
151
152    /**
153     * Get the Node version (node#ver) of a JID. Returns a String or null if
154     * EntiyCapsManager does not have any information.
155     * 
156     * @param jid
157     *            the user (Full JID)
158     * @return the node version (node#ver) or null
159     */
160    public static String getNodeVersionByJid(String jid) {
161        NodeVerHash nvh = JID_TO_NODEVER_CACHE.get(jid);
162        if (nvh != null) {
163            return nvh.nodeVer;
164        } else {
165            return null;
166        }
167    }
168
169    public static NodeVerHash getNodeVerHashByJid(String jid) {
170        return JID_TO_NODEVER_CACHE.get(jid);
171    }
172
173    /**
174     * Get the discover info given a user name. The discover info is returned if
175     * the user has a node#ver associated with it and the node#ver has a
176     * discover info associated with it.
177     * 
178     * @param user
179     *            user name (Full JID)
180     * @return the discovered info
181     */
182    public static DiscoverInfo getDiscoverInfoByUser(String user) {
183        NodeVerHash nvh = JID_TO_NODEVER_CACHE.get(user);
184        if (nvh == null)
185            return null;
186
187        return getDiscoveryInfoByNodeVer(nvh.nodeVer);
188    }
189
190    /**
191     * Retrieve DiscoverInfo for a specific node.
192     * 
193     * @param nodeVer
194     *            The node name (e.g.
195     *            "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
196     * @return The corresponding DiscoverInfo or null if none is known.
197     */
198    public static DiscoverInfo getDiscoveryInfoByNodeVer(String nodeVer) {
199        DiscoverInfo info = CAPS_CACHE.get(nodeVer);
200
201        // If it was not in CAPS_CACHE, try to retrieve the information from persistentCache
202        if (info == null && persistentCache != null) {
203            info = persistentCache.lookup(nodeVer);
204            // Promote the information to CAPS_CACHE if one was found
205            if (info != null) {
206                CAPS_CACHE.put(nodeVer, info);
207            }
208        }
209
210        // If we were able to retrieve information from one of the caches, copy it before returning
211        if (info != null)
212            info = new DiscoverInfo(info);
213
214        return info;
215    }
216
217    /**
218     * Set the persistent cache implementation
219     * 
220     * @param cache
221     */
222    public static void setPersistentCache(EntityCapsPersistentCache cache) {
223        persistentCache = cache;
224    }
225
226    /**
227     * Sets the maximum cache sizes
228     *
229     * @param maxJidToNodeVerSize
230     * @param maxCapsCacheSize
231     */
232    public static void setMaxsCacheSizes(int maxJidToNodeVerSize, int maxCapsCacheSize) {
233        JID_TO_NODEVER_CACHE.setMaxCacheSize(maxJidToNodeVerSize);
234        CAPS_CACHE.setMaxCacheSize(maxCapsCacheSize);
235    }
236
237    /**
238     * Clears the memory cache.
239     */
240    public static void clearMemoryCache() {
241        JID_TO_NODEVER_CACHE.clear();
242        CAPS_CACHE.clear();
243    }
244
245    private static void addCapsExtensionInfo(String from, CapsExtension capsExtension) {
246        String capsExtensionHash = capsExtension.getHash();
247        String hashInUppercase = capsExtensionHash.toUpperCase(Locale.US);
248        // SUPPORTED_HASHES uses the format of MessageDigest, which is uppercase, e.g. "SHA-1" instead of "sha-1"
249        if (!SUPPORTED_HASHES.containsKey(hashInUppercase))
250            return;
251        String hash = capsExtensionHash.toLowerCase(Locale.US);
252
253        String node = capsExtension.getNode();
254        String ver = capsExtension.getVer();
255
256        JID_TO_NODEVER_CACHE.put(from, new NodeVerHash(node, ver, hash));
257    }
258
259    private final Queue<CapsVersionAndHash> lastLocalCapsVersions = new ConcurrentLinkedQueue<>();
260
261    private final ServiceDiscoveryManager sdm;
262
263    private boolean entityCapsEnabled;
264    private CapsVersionAndHash currentCapsVersion;
265    private volatile Presence presenceSend;
266
267    /**
268     * The entity node String used by this EntityCapsManager instance.
269     */
270    private String entityNode = DEFAULT_ENTITY_NODE;
271
272    private EntityCapsManager(XMPPConnection connection) {
273        super(connection);
274        this.sdm = ServiceDiscoveryManager.getInstanceFor(connection);
275        instances.put(connection, this);
276
277        connection.addConnectionListener(new AbstractConnectionListener() {
278            @Override
279            public void connected(XMPPConnection connection) {
280                // It's not clear when a server would report the caps stream
281                // feature, so we try to process it after we are connected and
282                // once after we are authenticated.
283                processCapsStreamFeatureIfAvailable(connection);
284            }
285            @Override
286            public void authenticated(XMPPConnection connection, boolean resumed) {
287                // It's not clear when a server would report the caps stream
288                // feature, so we try to process it after we are connected and
289                // once after we are authenticated.
290                processCapsStreamFeatureIfAvailable(connection);
291
292                // Reset presenceSend when the connection was not resumed
293                if (!resumed) {
294                    presenceSend = null;
295                }
296            }
297            private void processCapsStreamFeatureIfAvailable(XMPPConnection connection) {
298                CapsExtension capsExtension = connection.getFeature(
299                                CapsExtension.ELEMENT, CapsExtension.NAMESPACE);
300                if (capsExtension == null) {
301                    return;
302                }
303                String from = connection.getServiceName();
304                addCapsExtensionInfo(from, capsExtension);
305            }
306        });
307
308        // This calculates the local entity caps version
309        updateLocalEntityCaps();
310
311        if (autoEnableEntityCaps)
312            enableEntityCaps();
313
314        connection.addAsyncStanzaListener(new StanzaListener() {
315            // Listen for remote presence stanzas with the caps extension
316            // If we receive such a stanza, record the JID and nodeVer
317            @Override
318            public void processPacket(Stanza packet) {
319                if (!entityCapsEnabled())
320                    return;
321
322                CapsExtension capsExtension = CapsExtension.from(packet);
323                String from = packet.getFrom();
324                addCapsExtensionInfo(from, capsExtension);
325            }
326
327        }, PRESENCES_WITH_CAPS);
328
329        connection.addAsyncStanzaListener(new StanzaListener() {
330            @Override
331            public void processPacket(Stanza packet) {
332                // always remove the JID from the map, even if entityCaps are
333                // disabled
334                String from = packet.getFrom();
335                JID_TO_NODEVER_CACHE.remove(from);
336            }
337        }, PRESENCES_WITHOUT_CAPS);
338
339        connection.addPacketSendingListener(new StanzaListener() {
340            @Override
341            public void processPacket(Stanza packet) {
342                presenceSend = (Presence) packet;
343            }
344        }, PresenceTypeFilter.AVAILABLE);
345
346        // Intercept presence packages and add caps data when intended.
347        // XEP-0115 specifies that a client SHOULD include entity capabilities
348        // with every presence notification it sends.
349        StanzaListener packetInterceptor = new StanzaListener() {
350            public void processPacket(Stanza packet) {
351                if (!entityCapsEnabled) {
352                    // Be sure to not send stanzas with the caps extension if it's not enabled
353                    packet.removeExtension(CapsExtension.ELEMENT, CapsExtension.NAMESPACE);
354                    return;
355                }
356                CapsVersionAndHash capsVersionAndHash = getCapsVersionAndHash();
357                CapsExtension caps = new CapsExtension(entityNode, capsVersionAndHash.version, capsVersionAndHash.hash);
358                packet.overrideExtension(caps);
359            }
360        };
361        connection.addPacketInterceptor(packetInterceptor, PresenceTypeFilter.AVAILABLE);
362        // It's important to do this as last action. Since it changes the
363        // behavior of the SDM in some ways
364        sdm.setEntityCapsManager(this);
365    }
366
367    public static synchronized EntityCapsManager getInstanceFor(XMPPConnection connection) {
368        if (SUPPORTED_HASHES.size() <= 0)
369            throw new IllegalStateException("No supported hashes for EntityCapsManager");
370
371        EntityCapsManager entityCapsManager = instances.get(connection);
372
373        if (entityCapsManager == null) {
374            entityCapsManager = new EntityCapsManager(connection);
375        }
376
377        return entityCapsManager;
378    }
379
380    public synchronized void enableEntityCaps() {
381        // Add Entity Capabilities (XEP-0115) feature node.
382        sdm.addFeature(NAMESPACE);
383        updateLocalEntityCaps();
384        entityCapsEnabled = true;
385    }
386
387    public synchronized void disableEntityCaps() {
388        entityCapsEnabled = false;
389        sdm.removeFeature(NAMESPACE);
390    }
391
392    public boolean entityCapsEnabled() {
393        return entityCapsEnabled;
394    }
395
396    public void setEntityNode(String entityNode) throws NotConnectedException {
397        this.entityNode = entityNode;
398        updateLocalEntityCaps();
399    }
400
401    /**
402     * Remove a record telling what entity caps node a user has.
403     * 
404     * @param user
405     *            the user (Full JID)
406     */
407    public void removeUserCapsNode(String user) {
408        JID_TO_NODEVER_CACHE.remove(user);
409    }
410
411    /**
412     * Get our own caps version. The version depends on the enabled features. A
413     * caps version looks like '66/0NaeaBKkwk85efJTGmU47vXI='
414     * 
415     * @return our own caps version
416     */
417    public CapsVersionAndHash getCapsVersionAndHash() {
418        return currentCapsVersion;
419    }
420
421    /**
422     * Returns the local entity's NodeVer (e.g.
423     * "http://www.igniterealtime.org/projects/smack/#66/0NaeaBKkwk85efJTGmU47vXI=
424     * )
425     * 
426     * @return the local NodeVer
427     */
428    public String getLocalNodeVer() {
429        CapsVersionAndHash capsVersionAndHash = getCapsVersionAndHash();
430        if (capsVersionAndHash == null) {
431            return null;
432        }
433        return entityNode + '#' + capsVersionAndHash.version;
434    }
435
436    /**
437     * Returns true if Entity Caps are supported by a given JID
438     * 
439     * @param jid
440     * @return true if the entity supports Entity Capabilities.
441     * @throws XMPPErrorException 
442     * @throws NoResponseException 
443     * @throws NotConnectedException 
444     */
445    public boolean areEntityCapsSupported(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException {
446        return sdm.supportsFeature(jid, NAMESPACE);
447    }
448
449    /**
450     * Returns true if Entity Caps are supported by the local service/server
451     * 
452     * @return true if the user's server supports Entity Capabilities.
453     * @throws XMPPErrorException 
454     * @throws NoResponseException 
455     * @throws NotConnectedException 
456     */
457    public boolean areEntityCapsSupportedByServer() throws NoResponseException, XMPPErrorException, NotConnectedException  {
458        return areEntityCapsSupported(connection().getServiceName());
459    }
460
461    /**
462     * Updates the local user Entity Caps information with the data provided
463     *
464     * If we are connected and there was already a presence send, another
465     * presence is send to inform others about your new Entity Caps node string.
466     *
467     */
468    public void updateLocalEntityCaps() {
469        XMPPConnection connection = connection();
470
471        DiscoverInfo discoverInfo = new DiscoverInfo();
472        discoverInfo.setType(IQ.Type.result);
473        sdm.addDiscoverInfoTo(discoverInfo);
474
475        // getLocalNodeVer() will return a result only after currentCapsVersion is set. Therefore
476        // set it first and then call getLocalNodeVer()
477        currentCapsVersion = generateVerificationString(discoverInfo);
478        final String localNodeVer = getLocalNodeVer();
479        discoverInfo.setNode(localNodeVer);
480        addDiscoverInfoByNode(localNodeVer, discoverInfo);
481        if (lastLocalCapsVersions.size() > 10) {
482            CapsVersionAndHash oldCapsVersion = lastLocalCapsVersions.poll();
483            sdm.removeNodeInformationProvider(entityNode + '#' + oldCapsVersion.version);
484        }
485        lastLocalCapsVersions.add(currentCapsVersion);
486
487        if (connection != null)
488            JID_TO_NODEVER_CACHE.put(connection.getUser(), new NodeVerHash(entityNode, currentCapsVersion));
489
490        final List<Identity> identities = new LinkedList<Identity>(ServiceDiscoveryManager.getInstanceFor(connection).getIdentities());
491        sdm.setNodeInformationProvider(localNodeVer, new AbstractNodeInformationProvider() {
492            List<String> features = sdm.getFeatures();
493            List<ExtensionElement> packetExtensions = sdm.getExtendedInfoAsList();
494            @Override
495            public List<String> getNodeFeatures() {
496                return features;
497            }
498            @Override
499            public List<Identity> getNodeIdentities() {
500                return identities;
501            }
502            @Override
503            public List<ExtensionElement> getNodePacketExtensions() {
504                return packetExtensions;
505            }
506        });
507
508        // Re-send the last sent presence, and let the stanza interceptor
509        // add a <c/> node to it.
510        // See http://xmpp.org/extensions/xep-0115.html#advertise
511        // We only send a presence packet if there was already one send
512        // to respect ConnectionConfiguration.isSendPresence()
513        if (connection != null && connection.isAuthenticated() && presenceSend != null) {
514            try {
515                connection.sendStanza(presenceSend.cloneWithNewId());
516            }
517            catch (NotConnectedException e) {
518                LOGGER.log(Level.WARNING, "Could could not update presence with caps info", e);
519            }
520        }
521    }
522
523    /**
524     * Verify DisoverInfo and Caps Node as defined in XEP-0115 5.4 Processing
525     * Method
526     * 
527     * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0115
528     *      5.4 Processing Method</a>
529     * 
530     * @param ver
531     * @param hash
532     * @param info
533     * @return true if it's valid and should be cache, false if not
534     */
535    public static boolean verifyDiscoverInfoVersion(String ver, String hash, DiscoverInfo info) {
536        // step 3.3 check for duplicate identities
537        if (info.containsDuplicateIdentities())
538            return false;
539
540        // step 3.4 check for duplicate features
541        if (info.containsDuplicateFeatures())
542            return false;
543
544        // step 3.5 check for well-formed packet extensions
545        if (verifyPacketExtensions(info))
546            return false;
547
548        String calculatedVer = generateVerificationString(info, hash).version;
549
550        if (!ver.equals(calculatedVer))
551            return false;
552
553        return true;
554    }
555
556    /**
557     * 
558     * @param info
559     * @return true if the stanza(/packet) extensions is ill-formed
560     */
561    protected static boolean verifyPacketExtensions(DiscoverInfo info) {
562        List<FormField> foundFormTypes = new LinkedList<FormField>();
563        for (ExtensionElement pe : info.getExtensions()) {
564            if (pe.getNamespace().equals(DataForm.NAMESPACE)) {
565                DataForm df = (DataForm) pe;
566                for (FormField f : df.getFields()) {
567                    if (f.getVariable().equals("FORM_TYPE")) {
568                        for (FormField fft : foundFormTypes) {
569                            if (f.equals(fft))
570                                return true;
571                        }
572                        foundFormTypes.add(f);
573                    }
574                }
575            }
576        }
577        return false;
578    }
579
580    protected static CapsVersionAndHash generateVerificationString(DiscoverInfo discoverInfo) {
581        return generateVerificationString(discoverInfo, null);
582    }
583
584    /**
585     * Generates a XEP-115 Verification String
586     * 
587     * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver">XEP-115
588     *      Verification String</a>
589     * 
590     * @param discoverInfo
591     * @param hash
592     *            the used hash function, if null, default hash will be used
593     * @return The generated verification String or null if the hash is not
594     *         supported
595     */
596    protected static CapsVersionAndHash generateVerificationString(DiscoverInfo discoverInfo, String hash) {
597        if (hash == null) {
598            hash = DEFAULT_HASH;
599        }
600        // SUPPORTED_HASHES uses the format of MessageDigest, which is uppercase, e.g. "SHA-1" instead of "sha-1"
601        MessageDigest md = SUPPORTED_HASHES.get(hash.toUpperCase(Locale.US));
602        if (md == null)
603            return null;
604        // Then transform the hash to lowercase, as this value will be put on the wire within the caps element's hash
605        // attribute. I'm not sure if the standard is case insensitive here, but let's assume that even it is, there could
606        // be "broken" implementation in the wild, so we *always* transform to lowercase.
607        hash = hash.toLowerCase(Locale.US);
608
609        DataForm extendedInfo =  DataForm.from(discoverInfo);
610
611        // 1. Initialize an empty string S ('sb' in this method).
612        StringBuilder sb = new StringBuilder(); // Use StringBuilder as we don't
613                                                // need thread-safe StringBuffer
614
615        // 2. Sort the service discovery identities by category and then by
616        // type and then by xml:lang
617        // (if it exists), formatted as CATEGORY '/' [TYPE] '/' [LANG] '/'
618        // [NAME]. Note that each slash is included even if the LANG or
619        // NAME is not included (in accordance with XEP-0030, the category and
620        // type MUST be included.
621        SortedSet<DiscoverInfo.Identity> sortedIdentities = new TreeSet<DiscoverInfo.Identity>();
622
623        for (DiscoverInfo.Identity i : discoverInfo.getIdentities())
624            sortedIdentities.add(i);
625
626        // 3. For each identity, append the 'category/type/lang/name' to S,
627        // followed by the '<' character.
628        for (DiscoverInfo.Identity identity : sortedIdentities) {
629            sb.append(identity.getCategory());
630            sb.append("/");
631            sb.append(identity.getType());
632            sb.append("/");
633            sb.append(identity.getLanguage() == null ? "" : identity.getLanguage());
634            sb.append("/");
635            sb.append(identity.getName() == null ? "" : identity.getName());
636            sb.append("<");
637        }
638
639        // 4. Sort the supported service discovery features.
640        SortedSet<String> features = new TreeSet<String>();
641        for (Feature f : discoverInfo.getFeatures())
642            features.add(f.getVar());
643
644        // 5. For each feature, append the feature to S, followed by the '<'
645        // character
646        for (String f : features) {
647            sb.append(f);
648            sb.append("<");
649        }
650
651        // only use the data form for calculation is it has a hidden FORM_TYPE
652        // field
653        // see XEP-0115 5.4 step 3.6
654        if (extendedInfo != null && extendedInfo.hasHiddenFormTypeField()) {
655            synchronized (extendedInfo) {
656                // 6. If the service discovery information response includes
657                // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e.,
658                // by the XML character data of the <value/> element).
659                SortedSet<FormField> fs = new TreeSet<FormField>(new Comparator<FormField>() {
660                    public int compare(FormField f1, FormField f2) {
661                        return f1.getVariable().compareTo(f2.getVariable());
662                    }
663                });
664
665                FormField ft = null;
666
667                for (FormField f : extendedInfo.getFields()) {
668                    if (!f.getVariable().equals("FORM_TYPE")) {
669                        fs.add(f);
670                    } else {
671                        ft = f;
672                    }
673                }
674
675                // Add FORM_TYPE values
676                if (ft != null) {
677                    formFieldValuesToCaps(ft.getValues(), sb);
678                }
679
680                // 7. 3. For each field other than FORM_TYPE:
681                // 1. Append the value of the "var" attribute, followed by the
682                // '<' character.
683                // 2. Sort values by the XML character data of the <value/>
684                // element.
685                // 3. For each <value/> element, append the XML character data,
686                // followed by the '<' character.
687                for (FormField f : fs) {
688                    sb.append(f.getVariable());
689                    sb.append("<");
690                    formFieldValuesToCaps(f.getValues(), sb);
691                }
692            }
693        }
694        // 8. Ensure that S is encoded according to the UTF-8 encoding (RFC
695        // 3269).
696        // 9. Compute the verification string by hashing S using the algorithm
697        // specified in the 'hash' attribute (e.g., SHA-1 as defined in RFC
698        // 3174).
699        // The hashed data MUST be generated with binary output and
700        // encoded using Base64 as specified in Section 4 of RFC 4648
701        // (note: the Base64 output MUST NOT include whitespace and MUST set
702        // padding bits to zero).
703        byte[] digest;
704        synchronized(md) {
705            digest = md.digest(sb.toString().getBytes());
706        }
707        String version = Base64.encodeToString(digest);
708        return new CapsVersionAndHash(version, hash);
709    }
710
711    private static void formFieldValuesToCaps(List<String> i, StringBuilder sb) {
712        SortedSet<String> fvs = new TreeSet<String>();
713        for (String s : i) {
714            fvs.add(s);
715        }
716        for (String fv : fvs) {
717            sb.append(fv);
718            sb.append("<");
719        }
720    }
721
722    public static class NodeVerHash {
723        private String node;
724        private String hash;
725        private String ver;
726        private String nodeVer;
727
728        NodeVerHash(String node, CapsVersionAndHash capsVersionAndHash) {
729            this(node, capsVersionAndHash.version, capsVersionAndHash.hash);
730        }
731
732        NodeVerHash(String node, String ver, String hash) {
733            this.node = node;
734            this.ver = ver;
735            this.hash = hash;
736            nodeVer = node + "#" + ver;
737        }
738
739        public String getNodeVer() {
740            return nodeVer;
741        }
742
743        public String getNode() {
744            return node;
745        }
746
747        public String getHash() {
748            return hash;
749        }
750
751        public String getVer() {
752            return ver;
753        }
754    }
755}