1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.util.Base64;
6import java.io.UnsupportedEncodingException;
7import java.lang.Comparable;
8import java.security.MessageDigest;
9import java.security.NoSuchAlgorithmException;
10import java.util.ArrayList;
11import java.util.Collections;
12import java.util.List;
13import org.json.JSONArray;
14import org.json.JSONException;
15import org.json.JSONObject;
16
17import eu.siacs.conversations.xml.Element;
18import eu.siacs.conversations.xmpp.stanzas.IqPacket;
19
20public class ServiceDiscoveryResult {
21 public static final String TABLENAME = "discovery_results";
22 public static final String HASH = "hash";
23 public static final String VER = "ver";
24 public static final String RESULT = "result";
25
26 protected static String blankNull(String s) {
27 return s == null ? "" : s;
28 }
29
30 public static class Identity implements Comparable {
31 protected final String category;
32 protected final String type;
33 protected final String lang;
34 protected final String name;
35
36 public Identity(final String category, final String type, final String lang, final String name) {
37 this.category = category;
38 this.type = type;
39 this.lang = lang;
40 this.name = name;
41 }
42
43 public Identity(final Element el) {
44 this(
45 el.getAttribute("category"),
46 el.getAttribute("type"),
47 el.getAttribute("xml:lang"),
48 el.getAttribute("name")
49 );
50 }
51
52 public Identity(final JSONObject o) {
53 this(
54 o.optString("category", null),
55 o.optString("type", null),
56 o.optString("lang", null),
57 o.optString("name", null)
58 );
59 }
60
61 public String getCategory() {
62 return this.category;
63 }
64
65 public String getType() {
66 return this.type;
67 }
68
69 public String getLang() {
70 return this.lang;
71 }
72
73 public String getName() {
74 return this.name;
75 }
76
77 public int compareTo(Object other) {
78 Identity o = (Identity)other;
79 int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory()));
80 if(r == 0) {
81 r = blankNull(this.getType()).compareTo(blankNull(o.getType()));
82 }
83 if(r == 0) {
84 r = blankNull(this.getLang()).compareTo(blankNull(o.getLang()));
85 }
86 if(r == 0) {
87 r = blankNull(this.getName()).compareTo(blankNull(o.getName()));
88 }
89
90 return r;
91 }
92
93 public JSONObject toJSON() {
94 try {
95 JSONObject o = new JSONObject();
96 o.put("category", this.getCategory());
97 o.put("type", this.getType());
98 o.put("lang", this.getLang());
99 o.put("name", this.getName());
100 return o;
101 } catch(JSONException e) {
102 return null;
103 }
104 }
105 }
106
107 protected final String hash;
108 protected final byte[] ver;
109 protected final List<Identity> identities;
110 protected final List<String> features;
111
112 public ServiceDiscoveryResult(final IqPacket packet) {
113 this.identities = new ArrayList<>();
114 this.features = new ArrayList<>();
115 this.hash = "sha-1"; // We only support sha-1 for now
116
117 final List<Element> elements = packet.query().getChildren();
118
119 for (final Element element : elements) {
120 if (element.getName().equals("identity")) {
121 Identity id = new Identity(element);
122 if (id.getType() != null && id.getCategory() != null) {
123 identities.add(id);
124 }
125 } else if (element.getName().equals("feature")) {
126 if (element.getAttribute("var") != null) {
127 features.add(element.getAttribute("var"));
128 }
129 }
130 }
131
132 this.ver = this.mkCapHash();
133 }
134
135 public ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException {
136 this.identities = new ArrayList<>();
137 this.features = new ArrayList<>();
138 this.hash = hash;
139 this.ver = ver;
140
141 JSONArray identities = o.optJSONArray("identities");
142 for(int i = 0; i < identities.length(); i++) {
143 this.identities.add(new Identity(identities.getJSONObject(i)));
144 }
145
146 JSONArray features = o.optJSONArray("features");
147 for(int i = 0; i < features.length(); i++) {
148 this.features.add(features.getString(i));
149 }
150 }
151
152 public ServiceDiscoveryResult(Cursor cursor) throws JSONException {
153 this(
154 cursor.getString(cursor.getColumnIndex(HASH)),
155 Base64.decode(cursor.getString(cursor.getColumnIndex(VER)), Base64.DEFAULT),
156 new JSONObject(cursor.getString(cursor.getColumnIndex(RESULT)))
157 );
158 }
159
160 public List<Identity> getIdentities() {
161 return this.identities;
162 }
163
164 public List<String> getFeatures() {
165 return this.features;
166 }
167
168 public boolean hasIdentity(String category, String type) {
169 for(Identity id : this.getIdentities()) {
170 if((category == null || id.getCategory().equals(category)) &&
171 (type == null || id.getType().equals(type))) {
172 return true;
173 }
174 }
175
176 return false;
177 }
178
179 protected byte[] mkCapHash() {
180 StringBuilder s = new StringBuilder();
181
182 List<Identity> identities = this.getIdentities();
183 Collections.sort(identities);
184
185 for(Identity id : identities) {
186 s.append(
187 blankNull(id.getCategory()) + "/" +
188 blankNull(id.getType()) + "/" +
189 blankNull(id.getLang()) + "/" +
190 blankNull(id.getName()) + "<"
191 );
192 }
193
194 List<String> features = this.getFeatures();
195 Collections.sort(features);
196
197 for (String feature : features) {
198 s.append(feature + "<");
199 }
200
201 // TODO: data forms?
202
203 MessageDigest md;
204 try {
205 md = MessageDigest.getInstance("SHA-1");
206 } catch (NoSuchAlgorithmException e) {
207 return null;
208 }
209
210 try {
211 return md.digest(s.toString().getBytes("UTF-8"));
212 } catch(UnsupportedEncodingException e) {
213 return null;
214 }
215 }
216
217 public JSONObject toJSON() {
218 try {
219 JSONObject o = new JSONObject();
220
221 JSONArray ids = new JSONArray();
222 for(Identity id : this.getIdentities()) {
223 ids.put(id.toJSON());
224 }
225 o.put("identites", ids);
226
227 o.put("features", new JSONArray(this.getFeatures()));
228
229 return o;
230 } catch(JSONException e) {
231 return null;
232 }
233 }
234
235 public ContentValues getContentValues() {
236 final ContentValues values = new ContentValues();
237 values.put(HASH, this.hash);
238 values.put(VER, new String(Base64.encode(this.ver, Base64.DEFAULT)).trim());
239 values.put(RESULT, this.toJSON().toString());
240 return values;
241 }
242}