All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jivesoftware.smackx.caps.EntityCapsManager Maven / Gradle / Ivy

Go to download

Smack extensions. Classes and methods that implement support for the various XMPP XEPs (Multi-User Chat, PubSub, …) and other XMPP extensions.

There is a newer version: 4.5.0-beta5
Show newest version
/**
 *
 * Copyright © 2009 Jonas Ådahl, 2011-2022 Florian Schmaus
 *
 * 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.smackx.caps;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPConnectionRegistry;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.PresenceTypeFilter;
import org.jivesoftware.smack.filter.StanzaExtensionFilter;
import org.jivesoftware.smack.filter.StanzaFilter;
import org.jivesoftware.smack.filter.StanzaTypeFilter;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.PresenceBuilder;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.roster.AbstractPresenceEventListener;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.stringencoder.Base64;

import org.jivesoftware.smackx.caps.cache.EntityCapsPersistentCache;
import org.jivesoftware.smackx.caps.packet.CapsExtension;
import org.jivesoftware.smackx.disco.AbstractNodeInformationProvider;
import org.jivesoftware.smackx.disco.DiscoInfoLookupShortcutMechanism;
import org.jivesoftware.smackx.disco.EntityCapabilitiesChangedListener;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Feature;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity;
import org.jivesoftware.smackx.disco.packet.DiscoverInfoBuilder;
import org.jivesoftware.smackx.disco.packet.DiscoverInfoView;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.packet.DataForm;

import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.FullJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.util.cache.LruCache;

/**
 * Manages own and others Entity Capabilities (XEP-0115).
 * 

* Entity Capabilities is an XMPP extension which, in order to minimize network impact, caches the capabilities of * remote XMPP entities. Those capabilities are determine with the help of the Service Discovery Protocol * (XEP-0030, {@link ServiceDiscoveryManager}). *

* *

Usage

*

* Entity Capabilities work silently in the background when enabled. If the remote XMPP entity does not support XEP-0115 * but XEP-0030 then XEP-0030 mechanisms are transparently used. *

*

* The caches used by Smack for Entity Capabilities is non-persistent per default. However, it is is also possible to set * a persistent Entity Capabilities cache, which is recommended. *

*

Examples

* *

Enable Entity Capabilities

*
{@code
 * // Get an instance of entity caps manager for the specified connection
 * EntityCapsManager mgr = EntityCapsManager.getInstanceFor(connection);
 * // Enable entity capabilities
 * mgr.enableEntityCaps();
 * }
* *

Configure a persistent cache for Entity Capabilities

*
{@code
 * // Get an instance of entity caps manager for the specified connection
 * EntityCapsManager mgr = EntityCapsManager.getInstanceFor(connection);
 * // Create an cache, see smackx.entitycaps.cache for pre-defined cache implementations
 * EntityCapsPersistentCache cache = new SimpleDirectoryPersistentCache(new File("/foo/cachedir"));
 * // Set the cache
 * mgr.setPersistentCache(cache);
 * }
* * @author Florian Schmaus * @see XEP-0115: Entity Capabilities */ public final class EntityCapsManager extends Manager { public static final String NAMESPACE = CapsExtension.NAMESPACE; public static final String ELEMENT = CapsExtension.ELEMENT; private static final Map SUPPORTED_HASHES = new HashMap(); /** * The default hash. Currently 'sha-1'. */ private static final String DEFAULT_HASH = StringUtils.SHA1; private static String DEFAULT_ENTITY_NODE = SmackConfiguration.SMACK_URL_STRING; static EntityCapsPersistentCache persistentCache; private static boolean autoEnableEntityCaps = true; private static final Map instances = new WeakHashMap<>(); private static final StanzaFilter PRESENCES_WITH_CAPS = new AndFilter(new StanzaTypeFilter(Presence.class), new StanzaExtensionFilter( ELEMENT, NAMESPACE)); /** * Map of "node + '#' + hash" to DiscoverInfo data */ static final LruCache CAPS_CACHE = new LruCache<>(1000); /** * Map of Full JID -> DiscoverInfo/null. In case of c2s connection the * key is formed as user@server/resource (resource is required) In case of * link-local connection the key is formed as user@host (no resource) In * case of a server or component the key is formed as domain */ static final LruCache JID_TO_NODEVER_CACHE = new LruCache<>(10000); static { XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { @Override public void connectionCreated(XMPPConnection connection) { getInstanceFor(connection); } }); try { MessageDigest sha1MessageDigest = MessageDigest.getInstance(DEFAULT_HASH); SUPPORTED_HASHES.put(DEFAULT_HASH, sha1MessageDigest); } catch (NoSuchAlgorithmException e) { // Ignore } ServiceDiscoveryManager.addDiscoInfoLookupShortcutMechanism(new DiscoInfoLookupShortcutMechanism("XEP-0115: Entity Capabilities", 100) { @Override public DiscoverInfo getDiscoverInfoByUser(ServiceDiscoveryManager serviceDiscoveryManager, Jid jid) { DiscoverInfo info = EntityCapsManager.getDiscoverInfoByUser(jid); if (info != null) { return info; } NodeVerHash nodeVerHash = getNodeVerHashByJid(jid); if (nodeVerHash == null) { return null; } try { info = serviceDiscoveryManager.discoverInfo(jid, nodeVerHash.getNodeVer()); } catch (NoResponseException | XMPPErrorException | NotConnectedException | InterruptedException e) { // TODO log return null; } if (verifyDiscoverInfoVersion(nodeVerHash.getVer(), nodeVerHash.getHash(), info)) { addDiscoverInfoByNode(nodeVerHash.getNodeVer(), info); } else { // TODO log } return info; } }); } /** * Set the default entity node that will be used for new EntityCapsManagers. * * @param entityNode TODO javadoc me please */ public static void setDefaultEntityNode(String entityNode) { DEFAULT_ENTITY_NODE = entityNode; } /** * Add DiscoverInfo to the database. * * @param nodeVer TODO javadoc me please * The node and verification String (e.g. * "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w="). * @param info TODO javadoc me please * DiscoverInfo for the specified node. */ static void addDiscoverInfoByNode(String nodeVer, DiscoverInfo info) { CAPS_CACHE.put(nodeVer, info); if (persistentCache != null) persistentCache.addDiscoverInfoByNodePersistent(nodeVer, info); } /** * Get the Node version (node#ver) of a JID. Returns a String or null if * EntityCapsManager does not have any information. * * @param jid TODO javadoc me please * the user (Full JID) * @return the node version (node#ver) or null */ public static String getNodeVersionByJid(Jid jid) { NodeVerHash nvh = JID_TO_NODEVER_CACHE.lookup(jid); if (nvh != null) { return nvh.nodeVer; } else { return null; } } public static NodeVerHash getNodeVerHashByJid(Jid jid) { return JID_TO_NODEVER_CACHE.lookup(jid); } /** * Get the discover info given a user name. The discover info is returned if * the user has a node#ver associated with it and the node#ver has a * discover info associated with it. * * @param user TODO javadoc me please * user name (Full JID) * @return the discovered info */ public static DiscoverInfo getDiscoverInfoByUser(Jid user) { NodeVerHash nvh = JID_TO_NODEVER_CACHE.lookup(user); if (nvh == null) return null; return getDiscoveryInfoByNodeVer(nvh.nodeVer); } /** * Retrieve DiscoverInfo for a specific node. * * @param nodeVer TODO javadoc me please * The node name (e.g. * "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w="). * @return The corresponding DiscoverInfo or null if none is known. */ public static DiscoverInfo getDiscoveryInfoByNodeVer(String nodeVer) { DiscoverInfo info = CAPS_CACHE.lookup(nodeVer); // If it was not in CAPS_CACHE, try to retrieve the information from persistentCache if (info == null && persistentCache != null) { info = persistentCache.lookup(nodeVer); // Promote the information to CAPS_CACHE if one was found if (info != null) { CAPS_CACHE.put(nodeVer, info); } } // If we were able to retrieve information from one of the caches, copy it before returning if (info != null) info = new DiscoverInfo(info); return info; } /** * Set the persistent cache implementation. * * @param cache TODO javadoc me please */ public static void setPersistentCache(EntityCapsPersistentCache cache) { persistentCache = cache; } /** * Sets the maximum cache sizes. * * @param maxJidToNodeVerSize TODO javadoc me please * @param maxCapsCacheSize TODO javadoc me please */ public static void setMaxsCacheSizes(int maxJidToNodeVerSize, int maxCapsCacheSize) { JID_TO_NODEVER_CACHE.setMaxCacheSize(maxJidToNodeVerSize); CAPS_CACHE.setMaxCacheSize(maxCapsCacheSize); } /** * Clears the memory cache. */ public static void clearMemoryCache() { JID_TO_NODEVER_CACHE.clear(); CAPS_CACHE.clear(); } private static void addCapsExtensionInfo(Jid from, CapsExtension capsExtension) { String capsExtensionHash = capsExtension.getHash(); String hashInUppercase = capsExtensionHash.toUpperCase(Locale.US); // SUPPORTED_HASHES uses the format of MessageDigest, which is uppercase, e.g. "SHA-1" instead of "sha-1" if (!SUPPORTED_HASHES.containsKey(hashInUppercase)) return; String hash = capsExtensionHash.toLowerCase(Locale.US); String node = capsExtension.getNode(); String ver = capsExtension.getVer(); JID_TO_NODEVER_CACHE.put(from, new NodeVerHash(node, ver, hash)); } private final Queue lastLocalCapsVersions = new ConcurrentLinkedQueue<>(); private final ServiceDiscoveryManager sdm; private boolean entityCapsEnabled; private CapsVersionAndHash currentCapsVersion; /** * The entity node String used by this EntityCapsManager instance. */ private String entityNode = DEFAULT_ENTITY_NODE; // 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. private void addCapsExtension(PresenceBuilder presenceBuilder) { final CapsVersionAndHash capsVersionAndHash = getCapsVersionAndHash(); if (capsVersionAndHash == null) { return; } CapsExtension caps = new CapsExtension(entityNode, capsVersionAndHash.version, capsVersionAndHash.hash); presenceBuilder.overrideExtension(caps); } private EntityCapsManager(XMPPConnection connection) { super(connection); this.sdm = ServiceDiscoveryManager.getInstanceFor(connection); instances.put(connection, this); connection.addConnectionListener(new ConnectionListener() { @Override public void connected(XMPPConnection connection) { // It's not clear when a server would report the caps stream // feature, so we try to process it after we are connected and // once after we are authenticated. processCapsStreamFeatureIfAvailable(connection); } @Override public void authenticated(XMPPConnection connection, boolean resumed) { // It's not clear when a server would report the caps stream // feature, so we try to process it after we are connected and // once after we are authenticated. processCapsStreamFeatureIfAvailable(connection); } private void processCapsStreamFeatureIfAvailable(XMPPConnection connection) { CapsExtension capsExtension = connection.getFeature( CapsExtension.class); if (capsExtension == null) { return; } DomainBareJid from = connection.getXMPPServiceDomain(); addCapsExtensionInfo(from, capsExtension); } }); if (autoEnableEntityCaps) enableEntityCaps(); // Note that this is a *synchronous* stanza listener to avoid unnecessary feature lookups. If this were to be an // asynchronous listener, then it would be possible that the entity caps information was not processed when the // features of entity are looked up. See SMACK-937. connection.addStanzaListener(new StanzaListener() { // Listen for remote presence stanzas with the caps extension // If we receive such a stanza, record the JID and nodeVer @Override public void processStanza(Stanza packet) { if (!entityCapsEnabled()) return; CapsExtension capsExtension = CapsExtension.from(packet); Jid from = packet.getFrom(); addCapsExtensionInfo(from, capsExtension); } }, PRESENCES_WITH_CAPS); Roster.getInstanceFor(connection).addPresenceEventListener(new AbstractPresenceEventListener() { @Override public void presenceUnavailable(FullJid from, Presence presence) { JID_TO_NODEVER_CACHE.remove(from); } }); // It's important to do this as last action. Since it changes the // behavior of the SDM in some ways sdm.addEntityCapabilitiesChangedListener(new EntityCapabilitiesChangedListener() { @Override public void onEntityCapabilitiesChanged(DiscoverInfo synthesizedDiscoveryInfo) { if (!entityCapsEnabled()) { return; } updateLocalEntityCaps(synthesizedDiscoveryInfo); } }); } public static synchronized EntityCapsManager getInstanceFor(XMPPConnection connection) { if (SUPPORTED_HASHES.size() <= 0) throw new IllegalStateException("No supported hashes for EntityCapsManager"); EntityCapsManager entityCapsManager = instances.get(connection); if (entityCapsManager == null) { entityCapsManager = new EntityCapsManager(connection); } return entityCapsManager; } public synchronized void enableEntityCaps() { connection().addPresenceInterceptor(this::addCapsExtension, p -> { return PresenceTypeFilter.AVAILABLE.accept(p); }); // Add Entity Capabilities (XEP-0115) feature node. sdm.addFeature(NAMESPACE); entityCapsEnabled = true; } public synchronized void disableEntityCaps() { entityCapsEnabled = false; sdm.removeFeature(NAMESPACE); connection().removePresenceInterceptor(this::addCapsExtension); } public boolean entityCapsEnabled() { return entityCapsEnabled; } /** * Remove a record telling what entity caps node a user has. * * @param user TODO javadoc me please * the user (Full JID) */ public static void removeUserCapsNode(Jid user) { // While JID_TO_NODEVER_CHACHE has the generic types , it is ok to call remove with String // arguments, since the same Jid and String representations would be equal and have the same hash code. JID_TO_NODEVER_CACHE.remove(user); } /** * Get our own caps version or {@code null} if none is yet set. The version depends on the enabled features. * A caps version looks like '66/0NaeaBKkwk85efJTGmU47vXI=' * * @return our own caps version or {@code null}. */ public CapsVersionAndHash getCapsVersionAndHash() { return currentCapsVersion; } /** * Returns the local entity's NodeVer (e.g. * "http://www.igniterealtime.org/projects/smack/#66/0NaeaBKkwk85efJTGmU47vXI= * ) * * @return the local NodeVer */ public String getLocalNodeVer() { CapsVersionAndHash capsVersionAndHash = getCapsVersionAndHash(); if (capsVersionAndHash == null) { return null; } return entityNode + '#' + capsVersionAndHash.version; } /** * Returns true if Entity Caps are supported by a given JID. * * @param jid TODO javadoc me please * @return true if the entity supports Entity Capabilities. * @throws XMPPErrorException if there was an XMPP error returned. * @throws NoResponseException if there was no response from the remote entity. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ public boolean areEntityCapsSupported(Jid jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { return sdm.supportsFeature(jid, NAMESPACE); } /** * Returns true if Entity Caps are supported by the local service/server. * * @return true if the user's server supports Entity Capabilities. * @throws XMPPErrorException if there was an XMPP error returned. * @throws NoResponseException if there was no response from the remote entity. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ public boolean areEntityCapsSupportedByServer() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { return areEntityCapsSupported(connection().getXMPPServiceDomain()); } /** * Updates the local user Entity Caps information with the data provided * * If we are connected and there was already a presence send, another * presence is send to inform others about your new Entity Caps node string. * */ private void updateLocalEntityCaps(DiscoverInfo synthesizedDiscoveryInfo) { XMPPConnection connection = connection(); DiscoverInfoBuilder discoverInfoBuilder = synthesizedDiscoveryInfo.asBuilder("synthesized-disco-info-result"); // getLocalNodeVer() will return a result only after currentCapsVersion is set. Therefore // set it first and then call getLocalNodeVer() currentCapsVersion = generateVerificationString(discoverInfoBuilder); final String localNodeVer = getLocalNodeVer(); discoverInfoBuilder.setNode(localNodeVer); final DiscoverInfo discoverInfo = discoverInfoBuilder.build(); addDiscoverInfoByNode(localNodeVer, discoverInfo); if (lastLocalCapsVersions.size() > 10) { CapsVersionAndHash oldCapsVersion = lastLocalCapsVersions.poll(); sdm.removeNodeInformationProvider(entityNode + '#' + oldCapsVersion.version); } lastLocalCapsVersions.add(currentCapsVersion); if (connection != null) JID_TO_NODEVER_CACHE.put(connection.getUser(), new NodeVerHash(entityNode, currentCapsVersion)); final List identities = new LinkedList<>(ServiceDiscoveryManager.getInstanceFor(connection).getIdentities()); sdm.setNodeInformationProvider(localNodeVer, new AbstractNodeInformationProvider() { List features = sdm.getFeatures(); List packetExtensions = sdm.getExtendedInfo(); @Override public List getNodeFeatures() { return features; } @Override public List getNodeIdentities() { return identities; } @Override public List getNodePacketExtensions() { return packetExtensions; } }); } /** * Verify DiscoverInfo and Caps Node as defined in XEP-0115 5.4 Processing * Method. * * @see XEP-0115 * 5.4 Processing Method * * @param ver TODO javadoc me please * @param hash TODO javadoc me please * @param info TODO javadoc me please * @return true if it's valid and should be cache, false if not */ public static boolean verifyDiscoverInfoVersion(String ver, String hash, DiscoverInfo info) { // step 3.3 check for duplicate identities if (info.containsDuplicateIdentities()) return false; // step 3.4 check for duplicate features if (info.containsDuplicateFeatures()) return false; // step 3.5 check for well-formed packet extensions if (!verifyPacketExtensions(info)) return false; String calculatedVer = generateVerificationString(info, hash).version; if (!ver.equals(calculatedVer)) return false; return true; } /** * Verify that the given discovery info is not ill-formed. * * @param info the discovery info to verify. * @return true if the stanza extensions is not ill-formed */ private static boolean verifyPacketExtensions(DiscoverInfo info) { Set foundFormTypes = new HashSet<>(); List dataForms = info.getExtensions(DataForm.class); for (DataForm dataForm : dataForms) { FormField formFieldTypeField = dataForm.getHiddenFormTypeField(); if (formFieldTypeField == null) { continue; } String type = formFieldTypeField.getFirstValue(); boolean noDuplicate = foundFormTypes.add(type); if (!noDuplicate) { // Ill-formed extension: duplicate forms (by form field type string). return false; } } return true; } static CapsVersionAndHash generateVerificationString(DiscoverInfoView discoverInfo) { return generateVerificationString(discoverInfo, null); } /** * Generates a XEP-115 Verification String * * @see XEP-115 * Verification String * * @param discoverInfo TODO javadoc me please * @param hash TODO javadoc me please * the used hash function, if null, default hash will be used * @return The generated verification String or null if the hash is not * supported */ static CapsVersionAndHash generateVerificationString(DiscoverInfoView discoverInfo, String hash) { if (hash == null) { hash = DEFAULT_HASH; } // SUPPORTED_HASHES uses the format of MessageDigest, which is uppercase, e.g. "SHA-1" instead of "sha-1" MessageDigest md = SUPPORTED_HASHES.get(hash.toUpperCase(Locale.US)); if (md == null) return null; // Then transform the hash to lowercase, as this value will be put on the wire within the caps element's hash // attribute. I'm not sure if the standard is case insensitive here, but let's assume that even it is, there could // be "broken" implementation in the wild, so we *always* transform to lowercase. hash = hash.toLowerCase(Locale.US); // 1. Initialize an empty string S ('sb' in this method). StringBuilder sb = new StringBuilder(); // Use StringBuilder as we don't // need thread-safe StringBuffer // 2. Sort the service discovery identities by category and then by // type and then by xml:lang // (if it exists), formatted as CATEGORY '/' [TYPE] '/' [LANG] '/' // [NAME]. Note that each slash is included even if the LANG or // NAME is not included (in accordance with XEP-0030, the category and // type MUST be included. SortedSet sortedIdentities = new TreeSet<>(); sortedIdentities.addAll(discoverInfo.getIdentities()); // 3. For each identity, append the 'category/type/lang/name' to S, // followed by the '<' character. for (DiscoverInfo.Identity identity : sortedIdentities) { sb.append(identity.getCategory()); sb.append('/'); sb.append(identity.getType()); sb.append('/'); sb.append(identity.getLanguage() == null ? "" : identity.getLanguage()); sb.append('/'); sb.append(identity.getName() == null ? "" : identity.getName()); sb.append('<'); } // 4. Sort the supported service discovery features. SortedSet features = new TreeSet<>(); for (Feature f : discoverInfo.getFeatures()) features.add(f.getVar()); // 5. For each feature, append the feature to S, followed by the '<' // character for (String f : features) { sb.append(f); sb.append('<'); } List extendedInfos = discoverInfo.getExtensions(DataForm.class); final Iterator iter = extendedInfos.iterator(); while (iter.hasNext()) { if (!iter.next().hasHiddenFormTypeField()) { // Only use the data form for calculation is it has a hidden FORM_TYPE field. // See XEP-0115 5.4 step 3.f iter.remove(); } } // 6. If the service discovery information response includes // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e., // by the XML character data of the element). Collections.sort(extendedInfos, new Comparator() { @Override public int compare(DataForm dataFormLeft, DataForm dataFormRight) { final String formTypeLeft = dataFormLeft.getFormType(); assert formTypeLeft != null; // ensured by the previous step. final String formTypeRight = dataFormRight.getFormType(); assert formTypeRight != null; // ensured by the previous step. return formTypeLeft.compareTo(formTypeRight); } }); for (DataForm extendedInfo : extendedInfos) { SortedSet fs = new TreeSet<>(new Comparator() { @Override public int compare(FormField f1, FormField f2) { return f1.getFieldName().compareTo(f2.getFieldName()); } }); for (FormField f : extendedInfo.getFields()) { if (!f.getFieldName().equals("FORM_TYPE")) { fs.add(f); } } // Add FORM_TYPE values formFieldValuesToCaps(Collections.singletonList(extendedInfo.getFormType()), sb); // 7. 3. For each field other than FORM_TYPE: // 1. Append the value of the "var" attribute, followed by the // '<' character. // 2. Sort values by the XML character data of the // element. // 3. For each element, append the XML character data, // followed by the '<' character. for (FormField f : fs) { sb.append(f.getFieldName()); sb.append('<'); formFieldValuesToCaps(f.getRawValueCharSequences(), sb); } } // 8. Ensure that S is encoded according to the UTF-8 encoding (RFC // 3269). // 9. Compute the verification string by hashing S using the algorithm // specified in the 'hash' attribute (e.g., SHA-1 as defined in RFC // 3174). // The hashed data MUST be generated with binary output and // encoded using Base64 as specified in Section 4 of RFC 4648 // (note: the Base64 output MUST NOT include whitespace and MUST set // padding bits to zero). byte[] bytes = sb.toString().getBytes(StandardCharsets.UTF_8); byte[] digest; synchronized (md) { digest = md.digest(bytes); } String version = Base64.encodeToString(digest); return new CapsVersionAndHash(version, hash); } private static void formFieldValuesToCaps(List i, StringBuilder sb) { SortedSet fvs = new TreeSet<>(); fvs.addAll(i); for (CharSequence fv : fvs) { sb.append(fv); sb.append('<'); } } public static class NodeVerHash { private String node; private String hash; private String ver; private String nodeVer; NodeVerHash(String node, CapsVersionAndHash capsVersionAndHash) { this(node, capsVersionAndHash.version, capsVersionAndHash.hash); } NodeVerHash(String node, String ver, String hash) { this.node = node; this.ver = ver; this.hash = hash; nodeVer = node + "#" + ver; } public String getNodeVer() { return nodeVer; } public String getNode() { return node; } public String getHash() { return hash; } public String getVer() { return ver; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy