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}