summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java')
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java308
1 files changed, 308 insertions, 0 deletions
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java
new file mode 100644
index 000000000..145704c1c
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java
@@ -0,0 +1,308 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.domain;
+
+import java.io.UnsupportedEncodingException;
+
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+/**
+ * Record is the abstract base class for all entries that Sync processes:
+ * bookmarks, passwords, history, and such.
+ *
+ * A Record can be initialized from or serialized to a CryptoRecord for
+ * submission to an encrypted store.
+ *
+ * Records should be considered to be conventionally immutable: modifications
+ * should be completed before the new record object escapes its constructing
+ * scope. Note that this is a critically important part of equality. As Rich
+ * Hickey notes:
+ *
+ * … the only things you can really compare for equality are immutable things,
+ * because if you compare two things for equality that are mutable, and ever
+ * say true, and they're ever not the same thing, you are wrong. Or you will
+ * become wrong at some point in the future.
+ *
+ * Records have a layered definition of equality. Two records can be said to be
+ * "equal" if:
+ *
+ * * They have the same GUID and collection. Two crypto/keys records are in some
+ * way "the same".
+ * This is `equalIdentifiers`.
+ *
+ * * Their most significant fields are the same. That is to say, they share a
+ * GUID, a collection, deletion, and domain-specific fields. Two copies of
+ * crypto/keys, neither deleted, with the same encrypted data but different
+ * modified times and sortIndex are in a stronger way "the same".
+ * This is `equalPayloads`.
+ *
+ * * Their most significant fields are the same, and their local fields (e.g.,
+ * the androidID to which we have decided that this record maps) are congruent.
+ * A record with the same androidID, or one whose androidID has not been set,
+ * can be considered "the same".
+ * This concept can be extended by Record subclasses. The key point is that
+ * reconciling should be applied to the contents of these records. For example,
+ * two history records with the same URI and GUID, but different visit arrays,
+ * can be said to be congruent.
+ * This is `congruentWith`.
+ *
+ * * They are strictly identical. Every field that is persisted, including
+ * lastModified and androidID, is equal.
+ * This is `equals`.
+ *
+ * Different parts of the codebase have use for different layers of this
+ * comparison hierarchy. For instance, lastModified times change every time a
+ * record is stored; a store followed by a retrieval will return a Record that
+ * shares its most significant fields with the input, but has a later
+ * lastModified time and might not yet have values set for others. Reconciling
+ * will thus ignore the modification time of a record.
+ *
+ * @author rnewman
+ *
+ */
+public abstract class Record {
+
+ public String guid;
+ public String collection;
+ public long lastModified;
+ public boolean deleted;
+ public long androidID;
+ /**
+ * An integer indicating the relative importance of this item in the collection.
+ * <p>
+ * Default is 0.
+ */
+ public long sortIndex;
+ /**
+ * The number of seconds to keep this record. After that time this item will
+ * no longer be returned in response to any request, and it may be pruned from
+ * the database.
+ * <p>
+ * Negative values mean never forget this record.
+ * <p>
+ * Default is 1 year.
+ */
+ public long ttl;
+
+ public Record(String guid, String collection, long lastModified, boolean deleted) {
+ this.guid = guid;
+ this.collection = collection;
+ this.lastModified = lastModified;
+ this.deleted = deleted;
+ this.sortIndex = 0;
+ this.ttl = 365 * 24 * 60 * 60; // Seconds.
+ this.androidID = -1;
+ }
+
+ /**
+ * Return true iff the input is a Record and has the same
+ * collection and guid as this object.
+ */
+ public boolean equalIdentifiers(Object o) {
+ if (!(o instanceof Record)) {
+ return false;
+ }
+
+ Record other = (Record) o;
+ if (this.guid == null) {
+ if (other.guid != null) {
+ return false;
+ }
+ } else {
+ if (!this.guid.equals(other.guid)) {
+ return false;
+ }
+ }
+ if (this.collection == null) {
+ if (other.collection != null) {
+ return false;
+ }
+ } else {
+ if (!this.collection.equals(other.collection)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @param o
+ * The object to which this object should be compared.
+ * @return
+ * true iff the input is a Record which is substantially the
+ * same as this object.
+ */
+ public boolean equalPayloads(Object o) {
+ if (!this.equalIdentifiers(o)) {
+ return false;
+ }
+ Record other = (Record) o;
+ return this.deleted == other.deleted;
+ }
+
+ /**
+ *
+ *
+ * @param o
+ * The object to which this object should be compared.
+ * @return
+ * true iff the input is a Record which is substantially the
+ * same as this object, considering the ability and desire to
+ * reconcile the two objects if possible.
+ */
+ public boolean congruentWith(Object o) {
+ if (!this.equalIdentifiers(o)) {
+ return false;
+ }
+ Record other = (Record) o;
+ return congruentAndroidIDs(other) &&
+ (this.deleted == other.deleted);
+ }
+
+ public boolean congruentAndroidIDs(Record other) {
+ // We treat -1 as "unset", and treat this as
+ // congruent with any other value.
+ if (this.androidID != -1 &&
+ other.androidID != -1 &&
+ this.androidID != other.androidID) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Return true iff the input is both equal in terms of payload,
+ * and also shares transient values such as timestamps.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Record)) {
+ return false;
+ }
+
+ Record other = (Record) o;
+ return equalTimestamps(other) &&
+ equalSortIndices(other) &&
+ equalAndroidIDs(other) &&
+ equalPayloads(o);
+ }
+
+ public boolean equalAndroidIDs(Record other) {
+ return this.androidID == other.androidID;
+ }
+
+ public boolean equalSortIndices(Record other) {
+ return this.sortIndex == other.sortIndex;
+ }
+
+ public boolean equalTimestamps(Object o) {
+ if (!(o instanceof Record)) {
+ return false;
+ }
+ return ((Record) o).lastModified == this.lastModified;
+ }
+
+ protected abstract void populatePayload(ExtendedJSONObject payload);
+ protected abstract void initFromPayload(ExtendedJSONObject payload);
+
+ public void initFromEnvelope(CryptoRecord envelope) {
+ ExtendedJSONObject p = envelope.payload;
+ this.guid = envelope.guid;
+ checkGUIDs(p);
+
+ this.collection = envelope.collection;
+ this.lastModified = envelope.lastModified;
+
+ final Object del = p.get("deleted");
+ if (del instanceof Boolean) {
+ this.deleted = (Boolean) del;
+ } else {
+ this.initFromPayload(p);
+ }
+
+ }
+
+ public CryptoRecord getEnvelope() {
+ CryptoRecord rec = new CryptoRecord(this);
+ ExtendedJSONObject payload = new ExtendedJSONObject();
+ payload.put("id", this.guid);
+
+ if (this.deleted) {
+ payload.put("deleted", true);
+ } else {
+ populatePayload(payload);
+ }
+ rec.payload = payload;
+ return rec;
+ }
+
+ @SuppressWarnings("static-method")
+ public String toJSONString() {
+ throw new RuntimeException("Cannot JSONify non-CryptoRecord Records.");
+ }
+
+ public byte[] toJSONBytes() {
+ try {
+ return this.toJSONString().getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // Can't happen.
+ return null;
+ }
+ }
+
+ /**
+ * Utility for safely populating an output CryptoRecord.
+ *
+ * @param rec
+ * @param key
+ * @param value
+ */
+ @SuppressWarnings("static-method")
+ protected void putPayload(CryptoRecord rec, String key, String value) {
+ if (value == null) {
+ return;
+ }
+ rec.payload.put(key, value);
+ }
+
+ protected void putPayload(ExtendedJSONObject payload, String key, String value) {
+ this.putPayload(payload, key, value, false);
+ }
+
+ @SuppressWarnings("static-method")
+ protected void putPayload(ExtendedJSONObject payload, String key, String value, boolean excludeEmpty) {
+ if (value == null) {
+ return;
+ }
+ if (excludeEmpty && value.equals("")) {
+ return;
+ }
+ payload.put(key, value);
+ }
+
+ protected void checkGUIDs(ExtendedJSONObject payload) {
+ String payloadGUID = (String) payload.get("id");
+ if (this.guid == null ||
+ payloadGUID == null) {
+ String detailMessage = "Inconsistency: either envelope or payload GUID missing.";
+ throw new IllegalStateException(detailMessage);
+ }
+ if (!this.guid.equals(payloadGUID)) {
+ String detailMessage = "Inconsistency: record has envelope ID " + this.guid + ", payload ID " + payloadGUID;
+ throw new IllegalStateException(detailMessage);
+ }
+ }
+
+ /**
+ * Oh for persistent data structures.
+ *
+ * @param guid
+ * @param androidID
+ * @return
+ * An identical copy of this record with the provided two values.
+ */
+ public abstract Record copyWithIDs(String guid, long androidID);
+}