/* 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 == null || !(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 == null || !(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 == null || !(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); }