1package im.conversations.android.xmpp;
2
3import com.google.common.base.Strings;
4import com.google.common.collect.Collections2;
5import com.google.common.collect.ComparisonChain;
6import com.google.common.collect.Ordering;
7import com.google.common.hash.Hashing;
8import com.google.common.io.BaseEncoding;
9import im.conversations.android.xmpp.model.data.Data;
10import im.conversations.android.xmpp.model.data.Field;
11import im.conversations.android.xmpp.model.disco.info.Feature;
12import im.conversations.android.xmpp.model.disco.info.Identity;
13import im.conversations.android.xmpp.model.disco.info.InfoQuery;
14import java.nio.charset.StandardCharsets;
15import java.util.Arrays;
16import java.util.Comparator;
17import java.util.List;
18
19public final class EntityCapabilities {
20 public static EntityCapsHash hash(final InfoQuery info) {
21 final StringBuilder s = new StringBuilder();
22 final List<Identity> orderedIdentities =
23 Ordering.from(
24 (Comparator<Identity>)
25 (a, b) ->
26 ComparisonChain.start()
27 .compare(
28 blankNull(a.getCategory()),
29 blankNull(b.getCategory()))
30 .compare(
31 blankNull(a.getType()),
32 blankNull(b.getType()))
33 .compare(
34 blankNull(a.getLang()),
35 blankNull(b.getLang()))
36 .compare(
37 blankNull(a.getIdentityName()),
38 blankNull(b.getIdentityName()))
39 .result())
40 .sortedCopy(info.getIdentities());
41
42 for (final Identity id : orderedIdentities) {
43 s.append(blankNull(id.getCategory()))
44 .append("/")
45 .append(blankNull(id.getType()))
46 .append("/")
47 .append(blankNull(id.getLang()))
48 .append("/")
49 .append(blankNull(id.getIdentityName()))
50 .append("<");
51 }
52
53 final List<String> features =
54 Ordering.natural()
55 .sortedCopy(Collections2.transform(info.getFeatures(), Feature::getVar));
56 for (final String feature : features) {
57 s.append(clean(feature)).append("<");
58 }
59
60 final List<Data> extensions =
61 Ordering.from(Comparator.comparing(Data::getFormType))
62 .sortedCopy(info.getExtensions(Data.class));
63
64 for (final Data extension : extensions) {
65 s.append(clean(extension.getFormType())).append("<");
66 final List<Field> fields =
67 Ordering.from(
68 Comparator.comparing(
69 (Field lhs) -> Strings.nullToEmpty(lhs.getFieldName())))
70 .sortedCopy(extension.getFields());
71 for (final Field field : fields) {
72 s.append(Strings.nullToEmpty(field.getFieldName())).append("<");
73 final List<String> values = Ordering.natural().sortedCopy(field.getValues());
74 for (final String value : values) {
75 s.append(blankNull(value)).append("<");
76 }
77 }
78 }
79 return new EntityCapsHash(
80 Hashing.sha1().hashString(s.toString(), StandardCharsets.UTF_8).asBytes());
81 }
82
83 private static String clean(String s) {
84 return s.replace("<", "<");
85 }
86
87 private static String blankNull(String s) {
88 return s == null ? "" : clean(s);
89 }
90
91 public abstract static class Hash {
92 public final byte[] hash;
93
94 protected Hash(byte[] hash) {
95 this.hash = hash;
96 }
97
98 public String encoded() {
99 return BaseEncoding.base64().encode(hash);
100 }
101
102 public abstract String capabilityNode(final String node);
103
104 @Override
105 public boolean equals(Object o) {
106 if (this == o) return true;
107 if (o == null || getClass() != o.getClass()) return false;
108 Hash hash1 = (Hash) o;
109 return Arrays.equals(hash, hash1.hash);
110 }
111
112 @Override
113 public int hashCode() {
114 return Arrays.hashCode(hash);
115 }
116 }
117
118 public static class EntityCapsHash extends Hash {
119
120 protected EntityCapsHash(byte[] hash) {
121 super(hash);
122 }
123
124 @Override
125 public String capabilityNode(String node) {
126 return String.format("%s#%s", node, encoded());
127 }
128
129 public static EntityCapsHash of(final String encoded) {
130 return new EntityCapsHash(BaseEncoding.base64().decode(encoded));
131 }
132 }
133}