EntityCapsManager.java
- /**
- *
- * Copyright © 2009 Jonas Ådahl, 2011-2014 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 org.jivesoftware.smack.AbstractConnectionListener;
- import org.jivesoftware.smack.SmackException.NoResponseException;
- import org.jivesoftware.smack.SmackException.NotConnectedException;
- import org.jivesoftware.smack.XMPPConnection;
- import org.jivesoftware.smack.ConnectionCreationListener;
- import org.jivesoftware.smack.Manager;
- import org.jivesoftware.smack.StanzaListener;
- import org.jivesoftware.smack.XMPPConnectionRegistry;
- import org.jivesoftware.smack.XMPPException.XMPPErrorException;
- import org.jivesoftware.smack.packet.IQ;
- import org.jivesoftware.smack.packet.Stanza;
- import org.jivesoftware.smack.packet.ExtensionElement;
- import org.jivesoftware.smack.packet.Presence;
- import org.jivesoftware.smack.filter.NotFilter;
- import org.jivesoftware.smack.filter.StanzaFilter;
- import org.jivesoftware.smack.filter.AndFilter;
- import org.jivesoftware.smack.filter.StanzaTypeFilter;
- import org.jivesoftware.smack.filter.StanzaExtensionFilter;
- 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.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.xdata.FormField;
- import org.jivesoftware.smackx.xdata.packet.DataForm;
- import org.jxmpp.jid.DomainBareJid;
- import org.jxmpp.jid.Jid;
- import org.jxmpp.util.cache.LruCache;
- import java.util.Comparator;
- import java.util.HashMap;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.Locale;
- import java.util.Map;
- import java.util.Queue;
- import java.util.SortedSet;
- import java.util.TreeSet;
- import java.util.WeakHashMap;
- import java.util.concurrent.ConcurrentLinkedQueue;
- import java.util.logging.Level;
- import java.util.logging.Logger;
- import java.security.MessageDigest;
- import java.security.NoSuchAlgorithmException;
- /**
- * Keeps track of entity capabilities.
- *
- * @author Florian Schmaus
- * @see <a href="http://www.xmpp.org/extensions/xep-0115.html">XEP-0115: Entity Capabilities</a>
- */
- public class EntityCapsManager extends Manager {
- private static final Logger LOGGER = Logger.getLogger(EntityCapsManager.class.getName());
- public static final String NAMESPACE = CapsExtension.NAMESPACE;
- public static final String ELEMENT = CapsExtension.ELEMENT;
- private static final Map<String, MessageDigest> SUPPORTED_HASHES = new HashMap<String, MessageDigest>();
- /**
- * The default hash. Currently 'sha-1'.
- */
- private static final String DEFAULT_HASH = StringUtils.SHA1;
- private static String DEFAULT_ENTITY_NODE = "http://www.igniterealtime.org/projects/smack";
- protected static EntityCapsPersistentCache persistentCache;
- private static boolean autoEnableEntityCaps = true;
- private static Map<XMPPConnection, EntityCapsManager> instances = new WeakHashMap<>();
- private static final StanzaFilter PRESENCES_WITH_CAPS = new AndFilter(new StanzaTypeFilter(Presence.class), new StanzaExtensionFilter(
- ELEMENT, NAMESPACE));
- private static final StanzaFilter PRESENCES_WITHOUT_CAPS = new AndFilter(new StanzaTypeFilter(Presence.class), new NotFilter(new StanzaExtensionFilter(
- ELEMENT, NAMESPACE)));
- private static final StanzaFilter PRESENCES = StanzaTypeFilter.PRESENCE;
- /**
- * Map of "node + '#' + hash" to DiscoverInfo data
- */
- private static final LruCache<String, DiscoverInfo> CAPS_CACHE = new LruCache<String, DiscoverInfo>(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
- */
- private static final LruCache<Jid, NodeVerHash> JID_TO_NODEVER_CACHE = new LruCache<>(10000);
- static {
- XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
- public void connectionCreated(XMPPConnection connection) {
- getInstanceFor(connection);
- }
- });
- try {
- MessageDigest sha1MessageDigest = MessageDigest.getInstance(DEFAULT_HASH);
- SUPPORTED_HASHES.put(DEFAULT_HASH, sha1MessageDigest);
- } catch (NoSuchAlgorithmException e) {
- // Ignore
- }
- }
- /**
- * Set the default entity node that will be used for new EntityCapsManagers
- *
- * @param entityNode
- */
- public static void setDefaultEntityNode(String entityNode) {
- DEFAULT_ENTITY_NODE = entityNode;
- }
- /**
- * Add DiscoverInfo to the database.
- *
- * @param nodeVer
- * The node and verification String (e.g.
- * "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
- * @param info
- * DiscoverInfo for the specified node.
- */
- public 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
- * EntiyCapsManager does not have any information.
- *
- * @param jid
- * the user (Full JID)
- * @return the node version (node#ver) or null
- */
- public static String getNodeVersionByJid(String jid) {
- NodeVerHash nvh = JID_TO_NODEVER_CACHE.get(jid);
- if (nvh != null) {
- return nvh.nodeVer;
- } else {
- return null;
- }
- }
- public static NodeVerHash getNodeVerHashByJid(Jid jid) {
- return JID_TO_NODEVER_CACHE.get(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
- * user name (Full JID)
- * @return the discovered info
- */
- public static DiscoverInfo getDiscoverInfoByUser(Jid user) {
- NodeVerHash nvh = JID_TO_NODEVER_CACHE.get(user);
- if (nvh == null)
- return null;
- return getDiscoveryInfoByNodeVer(nvh.nodeVer);
- }
- /**
- * Retrieve DiscoverInfo for a specific node.
- *
- * @param nodeVer
- * 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.get(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
- */
- public static void setPersistentCache(EntityCapsPersistentCache cache) {
- persistentCache = cache;
- }
- /**
- * Sets the maximum cache sizes
- *
- * @param maxJidToNodeVerSize
- * @param maxCapsCacheSize
- */
- 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<CapsVersionAndHash> lastLocalCapsVersions = new ConcurrentLinkedQueue<>();
- private final ServiceDiscoveryManager sdm;
- private boolean entityCapsEnabled;
- private CapsVersionAndHash currentCapsVersion;
- private boolean presenceSend = false;
- /**
- * The entity node String used by this EntityCapsManager instance.
- */
- private String entityNode = DEFAULT_ENTITY_NODE;
- private EntityCapsManager(XMPPConnection connection) {
- super(connection);
- this.sdm = ServiceDiscoveryManager.getInstanceFor(connection);
- instances.put(connection, this);
- connection.addConnectionListener(new AbstractConnectionListener() {
- @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);
- // Reset presenceSend when the connection was not resumed
- if (!resumed) {
- presenceSend = false;
- }
- }
- private void processCapsStreamFeatureIfAvailable(XMPPConnection connection) {
- CapsExtension capsExtension = connection.getFeature(
- CapsExtension.ELEMENT, CapsExtension.NAMESPACE);
- if (capsExtension == null) {
- return;
- }
- DomainBareJid from = connection.getServiceName();
- addCapsExtensionInfo(from, capsExtension);
- }
- });
- // This calculates the local entity caps version
- updateLocalEntityCaps();
- if (autoEnableEntityCaps)
- enableEntityCaps();
- connection.addAsyncStanzaListener(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 processPacket(Stanza packet) {
- if (!entityCapsEnabled())
- return;
- CapsExtension capsExtension = CapsExtension.from(packet);
- Jid from = packet.getFrom();
- addCapsExtensionInfo(from, capsExtension);
- }
- }, PRESENCES_WITH_CAPS);
- connection.addAsyncStanzaListener(new StanzaListener() {
- @Override
- public void processPacket(Stanza packet) {
- // always remove the JID from the map, even if entityCaps are
- // disabled
- Jid from = packet.getFrom();
- JID_TO_NODEVER_CACHE.remove(from);
- }
- }, PRESENCES_WITHOUT_CAPS);
- connection.addPacketSendingListener(new StanzaListener() {
- @Override
- public void processPacket(Stanza packet) {
- presenceSend = true;
- }
- }, PRESENCES);
- // 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.
- StanzaListener packetInterceptor = new StanzaListener() {
- public void processPacket(Stanza packet) {
- if (!entityCapsEnabled)
- return;
- CapsVersionAndHash capsVersionAndHash = getCapsVersion();
- CapsExtension caps = new CapsExtension(entityNode, capsVersionAndHash.version, capsVersionAndHash.hash);
- packet.addExtension(caps);
- }
- };
- connection.addPacketInterceptor(packetInterceptor, PRESENCES);
- // It's important to do this as last action. Since it changes the
- // behavior of the SDM in some ways
- sdm.setEntityCapsManager(this);
- }
- 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() {
- // Add Entity Capabilities (XEP-0115) feature node.
- sdm.addFeature(NAMESPACE);
- updateLocalEntityCaps();
- entityCapsEnabled = true;
- }
- public synchronized void disableEntityCaps() {
- entityCapsEnabled = false;
- sdm.removeFeature(NAMESPACE);
- }
- public boolean entityCapsEnabled() {
- return entityCapsEnabled;
- }
- public void setEntityNode(String entityNode) throws NotConnectedException {
- this.entityNode = entityNode;
- updateLocalEntityCaps();
- }
- /**
- * Remove a record telling what entity caps node a user has.
- *
- * @param user
- * the user (Full JID)
- */
- public void removeUserCapsNode(String user) {
- JID_TO_NODEVER_CACHE.remove(user);
- }
- /**
- * Get our own caps version. The version depends on the enabled features. A
- * caps version looks like '66/0NaeaBKkwk85efJTGmU47vXI='
- *
- * @return our own caps version
- */
- public CapsVersionAndHash getCapsVersion() {
- 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() {
- return entityNode + '#' + getCapsVersion();
- }
- /**
- * Returns true if Entity Caps are supported by a given JID
- *
- * @param jid
- * @return true if the entity supports Entity Capabilities.
- * @throws XMPPErrorException
- * @throws NoResponseException
- * @throws NotConnectedException
- * @throws InterruptedException
- */
- 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
- * @throws NoResponseException
- * @throws NotConnectedException
- * @throws InterruptedException
- */
- public boolean areEntityCapsSupportedByServer() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
- return areEntityCapsSupported(connection().getServiceName());
- }
- /**
- * 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.
- *
- */
- public void updateLocalEntityCaps() {
- XMPPConnection connection = connection();
- DiscoverInfo discoverInfo = new DiscoverInfo();
- discoverInfo.setType(IQ.Type.result);
- discoverInfo.setNode(getLocalNodeVer());
- if (connection != null)
- discoverInfo.setFrom(connection.getUser());
- sdm.addDiscoverInfoTo(discoverInfo);
- currentCapsVersion = generateVerificationString(discoverInfo);
- addDiscoverInfoByNode(entityNode + '#' + currentCapsVersion.version, 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<Identity> identities = new LinkedList<Identity>(ServiceDiscoveryManager.getInstanceFor(connection).getIdentities());
- sdm.setNodeInformationProvider(entityNode + '#' + currentCapsVersion, new AbstractNodeInformationProvider() {
- List<String> features = sdm.getFeatures();
- List<ExtensionElement> packetExtensions = sdm.getExtendedInfoAsList();
- @Override
- public List<String> getNodeFeatures() {
- return features;
- }
- @Override
- public List<Identity> getNodeIdentities() {
- return identities;
- }
- @Override
- public List<ExtensionElement> getNodePacketExtensions() {
- return packetExtensions;
- }
- });
- // Send an empty presence, and let the packet interceptor
- // add a <c/> node to it.
- // See http://xmpp.org/extensions/xep-0115.html#advertise
- // We only send a presence packet if there was already one send
- // to respect ConnectionConfiguration.isSendPresence()
- if (connection != null && connection.isAuthenticated() && presenceSend) {
- Presence presence = new Presence(Presence.Type.available);
- try {
- connection.sendStanza(presence);
- }
- catch (InterruptedException | NotConnectedException e) {
- LOGGER.log(Level.WARNING, "Could could not update presence with caps info", e);
- }
- }
- }
- /**
- * Verify DisoverInfo and Caps Node as defined in XEP-0115 5.4 Processing
- * Method
- *
- * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0115
- * 5.4 Processing Method</a>
- *
- * @param ver
- * @param hash
- * @param info
- * @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;
- }
- /**
- *
- * @param info
- * @return true if the packet extensions is ill-formed
- */
- protected static boolean verifyPacketExtensions(DiscoverInfo info) {
- List<FormField> foundFormTypes = new LinkedList<FormField>();
- for (ExtensionElement pe : info.getExtensions()) {
- if (pe.getNamespace().equals(DataForm.NAMESPACE)) {
- DataForm df = (DataForm) pe;
- for (FormField f : df.getFields()) {
- if (f.getVariable().equals("FORM_TYPE")) {
- for (FormField fft : foundFormTypes) {
- if (f.equals(fft))
- return true;
- }
- foundFormTypes.add(f);
- }
- }
- }
- }
- return false;
- }
- protected static CapsVersionAndHash generateVerificationString(DiscoverInfo discoverInfo) {
- return generateVerificationString(discoverInfo, null);
- }
- /**
- * Generates a XEP-115 Verification String
- *
- * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver">XEP-115
- * Verification String</a>
- *
- * @param discoverInfo
- * @param hash
- * the used hash function, if null, default hash will be used
- * @return The generated verification String or null if the hash is not
- * supported
- */
- protected static CapsVersionAndHash generateVerificationString(DiscoverInfo 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);
- DataForm extendedInfo = DataForm.from(discoverInfo);
- // 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<DiscoverInfo.Identity> sortedIdentities = new TreeSet<DiscoverInfo.Identity>();
- for (DiscoverInfo.Identity i : discoverInfo.getIdentities())
- sortedIdentities.add(i);
- // 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<String> features = new TreeSet<String>();
- 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("<");
- }
- // only use the data form for calculation is it has a hidden FORM_TYPE
- // field
- // see XEP-0115 5.4 step 3.6
- if (extendedInfo != null && extendedInfo.hasHiddenFormTypeField()) {
- synchronized (extendedInfo) {
- // 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 <value/> element).
- SortedSet<FormField> fs = new TreeSet<FormField>(new Comparator<FormField>() {
- public int compare(FormField f1, FormField f2) {
- return f1.getVariable().compareTo(f2.getVariable());
- }
- });
- FormField ft = null;
- for (FormField f : extendedInfo.getFields()) {
- if (!f.getVariable().equals("FORM_TYPE")) {
- fs.add(f);
- } else {
- ft = f;
- }
- }
- // Add FORM_TYPE values
- if (ft != null) {
- formFieldValuesToCaps(ft.getValues(), 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 <value/>
- // element.
- // 3. For each <value/> element, append the XML character data,
- // followed by the '<' character.
- for (FormField f : fs) {
- sb.append(f.getVariable());
- sb.append("<");
- formFieldValuesToCaps(f.getValues(), 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[] digest;
- synchronized(md) {
- digest = md.digest(sb.toString().getBytes());
- }
- String version = Base64.encodeToString(digest);
- return new CapsVersionAndHash(version, hash);
- }
- private static void formFieldValuesToCaps(List<String> i, StringBuilder sb) {
- SortedSet<String> fvs = new TreeSet<String>();
- for (String s : i) {
- fvs.add(s);
- }
- for (String 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;
- }
- }
- }