mobile/android/base/background/healthreport/HealthReportGenerator.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 package org.mozilla.gecko.background.healthreport;
michael@0 6
michael@0 7 import java.util.HashSet;
michael@0 8 import java.util.Iterator;
michael@0 9 import java.util.Set;
michael@0 10
michael@0 11 import org.json.JSONException;
michael@0 12 import org.json.JSONObject;
michael@0 13 import org.mozilla.gecko.background.common.DateUtils.DateFormatter;
michael@0 14 import org.mozilla.gecko.background.common.log.Logger;
michael@0 15 import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
michael@0 16
michael@0 17 import android.database.Cursor;
michael@0 18 import android.util.SparseArray;
michael@0 19
michael@0 20 public class HealthReportGenerator {
michael@0 21 private static final int PAYLOAD_VERSION = 3;
michael@0 22
michael@0 23 private static final String LOG_TAG = "GeckoHealthGen";
michael@0 24
michael@0 25 private final HealthReportStorage storage;
michael@0 26 private final DateFormatter dateFormatter;
michael@0 27
michael@0 28 public HealthReportGenerator(HealthReportStorage storage) {
michael@0 29 this.storage = storage;
michael@0 30 this.dateFormatter = new DateFormatter();
michael@0 31 }
michael@0 32
michael@0 33 @SuppressWarnings("static-method")
michael@0 34 protected long now() {
michael@0 35 return System.currentTimeMillis();
michael@0 36 }
michael@0 37
michael@0 38 /**
michael@0 39 * Ensure that you have initialized the Locale to your satisfaction
michael@0 40 * prior to calling this method.
michael@0 41 *
michael@0 42 * @return null if no environment could be computed, or else the resulting document.
michael@0 43 * @throws JSONException if there was an error adding environment data to the resulting document.
michael@0 44 */
michael@0 45 public JSONObject generateDocument(long since, long lastPingTime, String profilePath) throws JSONException {
michael@0 46 Logger.info(LOG_TAG, "Generating FHR document from " + since + "; last ping " + lastPingTime);
michael@0 47 Logger.pii(LOG_TAG, "Generating for profile " + profilePath);
michael@0 48
michael@0 49 ProfileInformationCache cache = new ProfileInformationCache(profilePath);
michael@0 50 if (!cache.restoreUnlessInitialized()) {
michael@0 51 Logger.warn(LOG_TAG, "Not enough profile information to compute current environment.");
michael@0 52 return null;
michael@0 53 }
michael@0 54 Environment current = EnvironmentBuilder.getCurrentEnvironment(cache);
michael@0 55 return generateDocument(since, lastPingTime, current);
michael@0 56 }
michael@0 57
michael@0 58 /**
michael@0 59 * The document consists of:
michael@0 60 *
michael@0 61 *<ul>
michael@0 62 *<li>Basic metadata: last ping time, current ping time, version.</li>
michael@0 63 *<li>A map of environments: <code>current</code> and others named by hash. <code>current</code> is fully specified,
michael@0 64 * and others are deltas from current.</li>
michael@0 65 *<li>A <code>data</code> object. This includes <code>last</code> and <code>days</code>.</li>
michael@0 66 *</ul>
michael@0 67 *
michael@0 68 * <code>days</code> is a map from date strings to <tt>{hash: {measurement: {_v: version, fields...}}}</tt>.
michael@0 69 * @throws JSONException if there was an error adding environment data to the resulting document.
michael@0 70 */
michael@0 71 public JSONObject generateDocument(long since, long lastPingTime, Environment currentEnvironment) throws JSONException {
michael@0 72 final String currentHash = currentEnvironment.getHash();
michael@0 73
michael@0 74 Logger.debug(LOG_TAG, "Current environment hash: " + currentHash);
michael@0 75 if (currentHash == null) {
michael@0 76 Logger.warn(LOG_TAG, "Current hash is null; aborting.");
michael@0 77 return null;
michael@0 78 }
michael@0 79
michael@0 80 // We want to map field IDs to some strings as we go.
michael@0 81 SparseArray<Environment> envs = storage.getEnvironmentRecordsByID();
michael@0 82
michael@0 83 JSONObject document = new JSONObject();
michael@0 84
michael@0 85 if (lastPingTime >= HealthReportConstants.EARLIEST_LAST_PING) {
michael@0 86 document.put("lastPingDate", dateFormatter.getDateString(lastPingTime));
michael@0 87 }
michael@0 88
michael@0 89 document.put("thisPingDate", dateFormatter.getDateString(now()));
michael@0 90 document.put("version", PAYLOAD_VERSION);
michael@0 91
michael@0 92 document.put("environments", getEnvironmentsJSON(currentEnvironment, envs));
michael@0 93 document.put("data", getDataJSON(currentEnvironment, envs, since));
michael@0 94
michael@0 95 return document;
michael@0 96 }
michael@0 97
michael@0 98 protected JSONObject getDataJSON(Environment currentEnvironment,
michael@0 99 SparseArray<Environment> envs, long since) throws JSONException {
michael@0 100 SparseArray<Field> fields = storage.getFieldsByID();
michael@0 101
michael@0 102 JSONObject days = getDaysJSON(currentEnvironment, envs, fields, since);
michael@0 103
michael@0 104 JSONObject last = new JSONObject();
michael@0 105
michael@0 106 JSONObject data = new JSONObject();
michael@0 107 data.put("days", days);
michael@0 108 data.put("last", last);
michael@0 109 return data;
michael@0 110 }
michael@0 111
michael@0 112 protected JSONObject getDaysJSON(Environment currentEnvironment, SparseArray<Environment> envs, SparseArray<Field> fields, long since) throws JSONException {
michael@0 113 if (Logger.shouldLogVerbose(LOG_TAG)) {
michael@0 114 for (int i = 0; i < envs.size(); ++i) {
michael@0 115 Logger.trace(LOG_TAG, "Days environment " + envs.keyAt(i) + ": " + envs.get(envs.keyAt(i)).getHash());
michael@0 116 }
michael@0 117 }
michael@0 118
michael@0 119 JSONObject days = new JSONObject();
michael@0 120 Cursor cursor = storage.getRawEventsSince(since);
michael@0 121 try {
michael@0 122 if (!cursor.moveToFirst()) {
michael@0 123 return days;
michael@0 124 }
michael@0 125
michael@0 126 // A classic walking partition.
michael@0 127 // Columns are "date", "env", "field", "value".
michael@0 128 // Note that we care about the type (integer, string) and kind
michael@0 129 // (last/counter, discrete) of each field.
michael@0 130 // Each field will be accessed once for each date/env pair, so
michael@0 131 // Field memoizes these facts.
michael@0 132 // We also care about which measurement contains each field.
michael@0 133 int lastDate = -1;
michael@0 134 int lastEnv = -1;
michael@0 135 JSONObject dateObject = null;
michael@0 136 JSONObject envObject = null;
michael@0 137
michael@0 138 while (!cursor.isAfterLast()) {
michael@0 139 int cEnv = cursor.getInt(1);
michael@0 140 if (cEnv == -1 ||
michael@0 141 (cEnv != lastEnv &&
michael@0 142 envs.indexOfKey(cEnv) < 0)) {
michael@0 143 Logger.warn(LOG_TAG, "Invalid environment " + cEnv + " in cursor. Skipping.");
michael@0 144 cursor.moveToNext();
michael@0 145 continue;
michael@0 146 }
michael@0 147
michael@0 148 int cDate = cursor.getInt(0);
michael@0 149 int cField = cursor.getInt(2);
michael@0 150
michael@0 151 Logger.trace(LOG_TAG, "Event row: " + cDate + ", " + cEnv + ", " + cField);
michael@0 152 boolean dateChanged = cDate != lastDate;
michael@0 153 boolean envChanged = cEnv != lastEnv;
michael@0 154
michael@0 155 if (dateChanged) {
michael@0 156 if (dateObject != null) {
michael@0 157 days.put(dateFormatter.getDateStringForDay(lastDate), dateObject);
michael@0 158 }
michael@0 159 dateObject = new JSONObject();
michael@0 160 lastDate = cDate;
michael@0 161 }
michael@0 162
michael@0 163 if (dateChanged || envChanged) {
michael@0 164 envObject = new JSONObject();
michael@0 165 // This is safe because we checked above that cEnv is valid.
michael@0 166 dateObject.put(envs.get(cEnv).getHash(), envObject);
michael@0 167 lastEnv = cEnv;
michael@0 168 }
michael@0 169
michael@0 170 final Field field = fields.get(cField);
michael@0 171 JSONObject measurement = envObject.optJSONObject(field.measurementName);
michael@0 172 if (measurement == null) {
michael@0 173 // We will never have more than one measurement version within a
michael@0 174 // single environment -- to do so involves changing the build ID. And
michael@0 175 // even if we did, we have no way to represent it. So just build the
michael@0 176 // output object once.
michael@0 177 measurement = new JSONObject();
michael@0 178 measurement.put("_v", field.measurementVersion);
michael@0 179 envObject.put(field.measurementName, measurement);
michael@0 180 }
michael@0 181
michael@0 182 // How we record depends on the type of the field, so we
michael@0 183 // break this out into a separate method for clarity.
michael@0 184 recordMeasurementFromCursor(field, measurement, cursor);
michael@0 185
michael@0 186 cursor.moveToNext();
michael@0 187 continue;
michael@0 188 }
michael@0 189 days.put(dateFormatter.getDateStringForDay(lastDate), dateObject);
michael@0 190 } finally {
michael@0 191 cursor.close();
michael@0 192 }
michael@0 193 return days;
michael@0 194 }
michael@0 195
michael@0 196 /**
michael@0 197 * Return the {@link JSONObject} parsed from the provided index of the given
michael@0 198 * cursor, or {@link JSONObject#NULL} if either SQL <code>NULL</code> or
michael@0 199 * string <code>"null"</code> is present at that index.
michael@0 200 */
michael@0 201 private static Object getJSONAtIndex(Cursor cursor, int index) throws JSONException {
michael@0 202 if (cursor.isNull(index)) {
michael@0 203 return JSONObject.NULL;
michael@0 204 }
michael@0 205 final String value = cursor.getString(index);
michael@0 206 if ("null".equals(value)) {
michael@0 207 return JSONObject.NULL;
michael@0 208 }
michael@0 209 return new JSONObject(value);
michael@0 210 }
michael@0 211
michael@0 212 protected static void recordMeasurementFromCursor(final Field field,
michael@0 213 JSONObject measurement,
michael@0 214 Cursor cursor)
michael@0 215 throws JSONException {
michael@0 216 if (field.isDiscreteField()) {
michael@0 217 // Discrete counted. Increment the named counter.
michael@0 218 if (field.isCountedField()) {
michael@0 219 if (!field.isStringField()) {
michael@0 220 throw new IllegalStateException("Unable to handle non-string counted types.");
michael@0 221 }
michael@0 222 HealthReportUtils.count(measurement, field.fieldName, cursor.getString(3));
michael@0 223 return;
michael@0 224 }
michael@0 225
michael@0 226 // Discrete string or integer. Append it.
michael@0 227 if (field.isStringField()) {
michael@0 228 HealthReportUtils.append(measurement, field.fieldName, cursor.getString(3));
michael@0 229 return;
michael@0 230 }
michael@0 231 if (field.isJSONField()) {
michael@0 232 HealthReportUtils.append(measurement, field.fieldName, getJSONAtIndex(cursor, 3));
michael@0 233 return;
michael@0 234 }
michael@0 235 if (field.isIntegerField()) {
michael@0 236 HealthReportUtils.append(measurement, field.fieldName, cursor.getLong(3));
michael@0 237 return;
michael@0 238 }
michael@0 239 throw new IllegalStateException("Unknown field type: " + field.flags);
michael@0 240 }
michael@0 241
michael@0 242 // Non-discrete -- must be LAST or COUNTER, so just accumulate the value.
michael@0 243 if (field.isStringField()) {
michael@0 244 measurement.put(field.fieldName, cursor.getString(3));
michael@0 245 return;
michael@0 246 }
michael@0 247 if (field.isJSONField()) {
michael@0 248 measurement.put(field.fieldName, getJSONAtIndex(cursor, 3));
michael@0 249 return;
michael@0 250 }
michael@0 251 measurement.put(field.fieldName, cursor.getLong(3));
michael@0 252 }
michael@0 253
michael@0 254 public static JSONObject getEnvironmentsJSON(Environment currentEnvironment,
michael@0 255 SparseArray<Environment> envs) throws JSONException {
michael@0 256 JSONObject environments = new JSONObject();
michael@0 257
michael@0 258 // Always do this, even if it hasn't recorded anything in the DB.
michael@0 259 environments.put("current", jsonify(currentEnvironment, null));
michael@0 260
michael@0 261 String currentHash = currentEnvironment.getHash();
michael@0 262 for (int i = 0; i < envs.size(); i++) {
michael@0 263 Environment e = envs.valueAt(i);
michael@0 264 if (currentHash.equals(e.getHash())) {
michael@0 265 continue;
michael@0 266 }
michael@0 267 environments.put(e.getHash(), jsonify(e, currentEnvironment));
michael@0 268 }
michael@0 269 return environments;
michael@0 270 }
michael@0 271
michael@0 272 public static JSONObject jsonify(Environment e, Environment current) throws JSONException {
michael@0 273 JSONObject age = getProfileAge(e, current);
michael@0 274 JSONObject sysinfo = getSysInfo(e, current);
michael@0 275 JSONObject gecko = getGeckoInfo(e, current);
michael@0 276 JSONObject appinfo = getAppInfo(e, current);
michael@0 277 JSONObject counts = getAddonCounts(e, current);
michael@0 278
michael@0 279 JSONObject out = new JSONObject();
michael@0 280 if (age != null)
michael@0 281 out.put("org.mozilla.profile.age", age);
michael@0 282 if (sysinfo != null)
michael@0 283 out.put("org.mozilla.sysinfo.sysinfo", sysinfo);
michael@0 284 if (gecko != null)
michael@0 285 out.put("geckoAppInfo", gecko);
michael@0 286 if (appinfo != null)
michael@0 287 out.put("org.mozilla.appInfo.appinfo", appinfo);
michael@0 288 if (counts != null)
michael@0 289 out.put("org.mozilla.addons.counts", counts);
michael@0 290
michael@0 291 JSONObject active = getActiveAddons(e, current);
michael@0 292 if (active != null)
michael@0 293 out.put("org.mozilla.addons.active", active);
michael@0 294
michael@0 295 if (current == null) {
michael@0 296 out.put("hash", e.getHash());
michael@0 297 }
michael@0 298 return out;
michael@0 299 }
michael@0 300
michael@0 301 private static JSONObject getProfileAge(Environment e, Environment current) throws JSONException {
michael@0 302 JSONObject age = new JSONObject();
michael@0 303 int changes = 0;
michael@0 304 if (current == null || current.profileCreation != e.profileCreation) {
michael@0 305 age.put("profileCreation", e.profileCreation);
michael@0 306 changes++;
michael@0 307 }
michael@0 308 if (current != null && changes == 0) {
michael@0 309 return null;
michael@0 310 }
michael@0 311 age.put("_v", 1);
michael@0 312 return age;
michael@0 313 }
michael@0 314
michael@0 315 private static JSONObject getSysInfo(Environment e, Environment current) throws JSONException {
michael@0 316 JSONObject sysinfo = new JSONObject();
michael@0 317 int changes = 0;
michael@0 318 if (current == null || current.cpuCount != e.cpuCount) {
michael@0 319 sysinfo.put("cpuCount", e.cpuCount);
michael@0 320 changes++;
michael@0 321 }
michael@0 322 if (current == null || current.memoryMB != e.memoryMB) {
michael@0 323 sysinfo.put("memoryMB", e.memoryMB);
michael@0 324 changes++;
michael@0 325 }
michael@0 326 if (current == null || !current.architecture.equals(e.architecture)) {
michael@0 327 sysinfo.put("architecture", e.architecture);
michael@0 328 changes++;
michael@0 329 }
michael@0 330 if (current == null || !current.sysName.equals(e.sysName)) {
michael@0 331 sysinfo.put("name", e.sysName);
michael@0 332 changes++;
michael@0 333 }
michael@0 334 if (current == null || !current.sysVersion.equals(e.sysVersion)) {
michael@0 335 sysinfo.put("version", e.sysVersion);
michael@0 336 changes++;
michael@0 337 }
michael@0 338 if (current != null && changes == 0) {
michael@0 339 return null;
michael@0 340 }
michael@0 341 sysinfo.put("_v", 1);
michael@0 342 return sysinfo;
michael@0 343 }
michael@0 344
michael@0 345 private static JSONObject getGeckoInfo(Environment e, Environment current) throws JSONException {
michael@0 346 JSONObject gecko = new JSONObject();
michael@0 347 int changes = 0;
michael@0 348 if (current == null || !current.vendor.equals(e.vendor)) {
michael@0 349 gecko.put("vendor", e.vendor);
michael@0 350 changes++;
michael@0 351 }
michael@0 352 if (current == null || !current.appName.equals(e.appName)) {
michael@0 353 gecko.put("name", e.appName);
michael@0 354 changes++;
michael@0 355 }
michael@0 356 if (current == null || !current.appID.equals(e.appID)) {
michael@0 357 gecko.put("id", e.appID);
michael@0 358 changes++;
michael@0 359 }
michael@0 360 if (current == null || !current.appVersion.equals(e.appVersion)) {
michael@0 361 gecko.put("version", e.appVersion);
michael@0 362 changes++;
michael@0 363 }
michael@0 364 if (current == null || !current.appBuildID.equals(e.appBuildID)) {
michael@0 365 gecko.put("appBuildID", e.appBuildID);
michael@0 366 changes++;
michael@0 367 }
michael@0 368 if (current == null || !current.platformVersion.equals(e.platformVersion)) {
michael@0 369 gecko.put("platformVersion", e.platformVersion);
michael@0 370 changes++;
michael@0 371 }
michael@0 372 if (current == null || !current.platformBuildID.equals(e.platformBuildID)) {
michael@0 373 gecko.put("platformBuildID", e.platformBuildID);
michael@0 374 changes++;
michael@0 375 }
michael@0 376 if (current == null || !current.os.equals(e.os)) {
michael@0 377 gecko.put("os", e.os);
michael@0 378 changes++;
michael@0 379 }
michael@0 380 if (current == null || !current.xpcomabi.equals(e.xpcomabi)) {
michael@0 381 gecko.put("xpcomabi", e.xpcomabi);
michael@0 382 changes++;
michael@0 383 }
michael@0 384 if (current == null || !current.updateChannel.equals(e.updateChannel)) {
michael@0 385 gecko.put("updateChannel", e.updateChannel);
michael@0 386 changes++;
michael@0 387 }
michael@0 388 if (current != null && changes == 0) {
michael@0 389 return null;
michael@0 390 }
michael@0 391 gecko.put("_v", 1);
michael@0 392 return gecko;
michael@0 393 }
michael@0 394
michael@0 395 // Null-safe string comparison.
michael@0 396 private static boolean stringsDiffer(final String a, final String b) {
michael@0 397 if (a == null) {
michael@0 398 return b != null;
michael@0 399 }
michael@0 400 return !a.equals(b);
michael@0 401 }
michael@0 402
michael@0 403 private static JSONObject getAppInfo(Environment e, Environment current) throws JSONException {
michael@0 404 JSONObject appinfo = new JSONObject();
michael@0 405
michael@0 406 Logger.debug(LOG_TAG, "Generating appinfo for v" + e.version + " env " + e.hash);
michael@0 407
michael@0 408 // Is the environment in question newer than the diff target, or is
michael@0 409 // there no diff target?
michael@0 410 final boolean outdated = current == null ||
michael@0 411 e.version > current.version;
michael@0 412
michael@0 413 // Is the environment in question a different version (lower or higher),
michael@0 414 // or is there no diff target?
michael@0 415 final boolean differ = outdated || current.version > e.version;
michael@0 416
michael@0 417 // Always produce an output object if there's a version mismatch or this
michael@0 418 // isn't a diff. Otherwise, track as we go if there's any difference.
michael@0 419 boolean changed = differ;
michael@0 420
michael@0 421 switch (e.version) {
michael@0 422 // There's a straightforward correspondence between environment versions
michael@0 423 // and appinfo versions.
michael@0 424 case 2:
michael@0 425 appinfo.put("_v", 3);
michael@0 426 break;
michael@0 427 case 1:
michael@0 428 appinfo.put("_v", 2);
michael@0 429 break;
michael@0 430 default:
michael@0 431 Logger.warn(LOG_TAG, "Unknown environment version: " + e.version);
michael@0 432 return appinfo;
michael@0 433 }
michael@0 434
michael@0 435 switch (e.version) {
michael@0 436 case 2:
michael@0 437 if (populateAppInfoV2(appinfo, e, current, outdated)) {
michael@0 438 changed = true;
michael@0 439 }
michael@0 440 // Fall through.
michael@0 441
michael@0 442 case 1:
michael@0 443 // There is no older version than v1, so don't check outdated.
michael@0 444 if (populateAppInfoV1(e, current, appinfo)) {
michael@0 445 changed = true;
michael@0 446 }
michael@0 447 }
michael@0 448
michael@0 449 if (!changed) {
michael@0 450 return null;
michael@0 451 }
michael@0 452
michael@0 453 return appinfo;
michael@0 454 }
michael@0 455
michael@0 456 private static boolean populateAppInfoV1(Environment e,
michael@0 457 Environment current,
michael@0 458 JSONObject appinfo)
michael@0 459 throws JSONException {
michael@0 460 boolean changes = false;
michael@0 461 if (current == null || current.isBlocklistEnabled != e.isBlocklistEnabled) {
michael@0 462 appinfo.put("isBlocklistEnabled", e.isBlocklistEnabled);
michael@0 463 changes = true;
michael@0 464 }
michael@0 465
michael@0 466 if (current == null || current.isTelemetryEnabled != e.isTelemetryEnabled) {
michael@0 467 appinfo.put("isTelemetryEnabled", e.isTelemetryEnabled);
michael@0 468 changes = true;
michael@0 469 }
michael@0 470
michael@0 471 return changes;
michael@0 472 }
michael@0 473
michael@0 474 private static boolean populateAppInfoV2(JSONObject appinfo,
michael@0 475 Environment e,
michael@0 476 Environment current,
michael@0 477 final boolean outdated)
michael@0 478 throws JSONException {
michael@0 479 boolean changes = false;
michael@0 480 if (outdated ||
michael@0 481 stringsDiffer(current.osLocale, e.osLocale)) {
michael@0 482 appinfo.put("osLocale", e.osLocale);
michael@0 483 changes = true;
michael@0 484 }
michael@0 485
michael@0 486 if (outdated ||
michael@0 487 stringsDiffer(current.appLocale, e.appLocale)) {
michael@0 488 appinfo.put("appLocale", e.appLocale);
michael@0 489 changes = true;
michael@0 490 }
michael@0 491
michael@0 492 if (outdated ||
michael@0 493 stringsDiffer(current.distribution, e.distribution)) {
michael@0 494 appinfo.put("distribution", e.distribution);
michael@0 495 changes = true;
michael@0 496 }
michael@0 497
michael@0 498 if (outdated ||
michael@0 499 current.acceptLangSet != e.acceptLangSet) {
michael@0 500 appinfo.put("acceptLangIsUserSet", e.acceptLangSet);
michael@0 501 changes = true;
michael@0 502 }
michael@0 503 return changes;
michael@0 504 }
michael@0 505
michael@0 506 private static JSONObject getAddonCounts(Environment e, Environment current) throws JSONException {
michael@0 507 JSONObject counts = new JSONObject();
michael@0 508 int changes = 0;
michael@0 509 if (current == null || current.extensionCount != e.extensionCount) {
michael@0 510 counts.put("extension", e.extensionCount);
michael@0 511 changes++;
michael@0 512 }
michael@0 513 if (current == null || current.pluginCount != e.pluginCount) {
michael@0 514 counts.put("plugin", e.pluginCount);
michael@0 515 changes++;
michael@0 516 }
michael@0 517 if (current == null || current.themeCount != e.themeCount) {
michael@0 518 counts.put("theme", e.themeCount);
michael@0 519 changes++;
michael@0 520 }
michael@0 521 if (current != null && changes == 0) {
michael@0 522 return null;
michael@0 523 }
michael@0 524 counts.put("_v", 1);
michael@0 525 return counts;
michael@0 526 }
michael@0 527
michael@0 528 /**
michael@0 529 * Compute the *tree* difference set between the two objects. If the two
michael@0 530 * objects are identical, returns <code>null</code>. If <code>from</code> is
michael@0 531 * <code>null</code>, returns <code>to</code>. If <code>to</code> is
michael@0 532 * <code>null</code>, behaves as if <code>to</code> were an empty object.
michael@0 533 *
michael@0 534 * (Note that this method does not check for {@link JSONObject#NULL}, because
michael@0 535 * by definition it can't be provided as input to this method.)
michael@0 536 *
michael@0 537 * This behavior is intended to simplify life for callers: a missing object
michael@0 538 * can be viewed as (and behaves as) an empty map, to a useful extent, rather
michael@0 539 * than throwing an exception.
michael@0 540 *
michael@0 541 * @param from
michael@0 542 * a JSONObject.
michael@0 543 * @param to
michael@0 544 * a JSONObject.
michael@0 545 * @param includeNull
michael@0 546 * if true, keys present in <code>from</code> but not in
michael@0 547 * <code>to</code> are included as {@link JSONObject#NULL} in the
michael@0 548 * output.
michael@0 549 *
michael@0 550 * @return a JSONObject, or null if the two objects are identical.
michael@0 551 * @throws JSONException
michael@0 552 * should not occur, but...
michael@0 553 */
michael@0 554 public static JSONObject diff(JSONObject from,
michael@0 555 JSONObject to,
michael@0 556 boolean includeNull) throws JSONException {
michael@0 557 if (from == null) {
michael@0 558 return to;
michael@0 559 }
michael@0 560
michael@0 561 if (to == null) {
michael@0 562 return diff(from, new JSONObject(), includeNull);
michael@0 563 }
michael@0 564
michael@0 565 JSONObject out = new JSONObject();
michael@0 566
michael@0 567 HashSet<String> toKeys = includeNull ? new HashSet<String>(to.length())
michael@0 568 : null;
michael@0 569
michael@0 570 @SuppressWarnings("unchecked")
michael@0 571 Iterator<String> it = to.keys();
michael@0 572 while (it.hasNext()) {
michael@0 573 String key = it.next();
michael@0 574
michael@0 575 // Track these as we go if we'll need them later.
michael@0 576 if (includeNull) {
michael@0 577 toKeys.add(key);
michael@0 578 }
michael@0 579
michael@0 580 Object value = to.get(key);
michael@0 581 if (!from.has(key)) {
michael@0 582 // It must be new.
michael@0 583 out.put(key, value);
michael@0 584 continue;
michael@0 585 }
michael@0 586
michael@0 587 // Not new? Then see if it changed.
michael@0 588 Object old = from.get(key);
michael@0 589
michael@0 590 // Two JSONObjects should be diffed.
michael@0 591 if (old instanceof JSONObject && value instanceof JSONObject) {
michael@0 592 JSONObject innerDiff = diff(((JSONObject) old), ((JSONObject) value),
michael@0 593 includeNull);
michael@0 594 // No change? No output.
michael@0 595 if (innerDiff == null) {
michael@0 596 continue;
michael@0 597 }
michael@0 598
michael@0 599 // Otherwise include the diff.
michael@0 600 out.put(key, innerDiff);
michael@0 601 continue;
michael@0 602 }
michael@0 603
michael@0 604 // A regular value, or a type change. Only skip if they're the same.
michael@0 605 if (value.equals(old)) {
michael@0 606 continue;
michael@0 607 }
michael@0 608 out.put(key, value);
michael@0 609 }
michael@0 610
michael@0 611 // Now -- if requested -- include any removed keys.
michael@0 612 if (includeNull) {
michael@0 613 Set<String> fromKeys = HealthReportUtils.keySet(from);
michael@0 614 fromKeys.removeAll(toKeys);
michael@0 615 for (String notPresent : fromKeys) {
michael@0 616 out.put(notPresent, JSONObject.NULL);
michael@0 617 }
michael@0 618 }
michael@0 619
michael@0 620 if (out.length() == 0) {
michael@0 621 return null;
michael@0 622 }
michael@0 623 return out;
michael@0 624 }
michael@0 625
michael@0 626 private static JSONObject getActiveAddons(Environment e, Environment current) throws JSONException {
michael@0 627 // Just return the current add-on set, with a version annotation.
michael@0 628 // To do so requires copying.
michael@0 629 if (current == null) {
michael@0 630 JSONObject out = e.getNonIgnoredAddons();
michael@0 631 if (out == null) {
michael@0 632 Logger.warn(LOG_TAG, "Null add-ons to return in FHR document. Returning {}.");
michael@0 633 out = new JSONObject(); // So that we always return something.
michael@0 634 }
michael@0 635 out.put("_v", 1);
michael@0 636 return out;
michael@0 637 }
michael@0 638
michael@0 639 // Otherwise, return the diff.
michael@0 640 JSONObject diff = diff(current.getNonIgnoredAddons(), e.getNonIgnoredAddons(), true);
michael@0 641 if (diff == null) {
michael@0 642 return null;
michael@0 643 }
michael@0 644 if (diff == e.addons) {
michael@0 645 // Again, needs to copy.
michael@0 646 return getActiveAddons(e, null);
michael@0 647 }
michael@0 648
michael@0 649 diff.put("_v", 1);
michael@0 650 return diff;
michael@0 651 }
michael@0 652 }

mercurial