summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/domain/Record.java
blob: 145704c1c8d489c0fe27f0628176a4566b78ce6c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
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);
}