37
37
import com .google .protobuf .ByteString ;
38
38
import com .google .protobuf .NullValue ;
39
39
import com .google .type .LatLng ;
40
+ import java .text .ParseException ;
41
+ import java .text .SimpleDateFormat ;
40
42
import java .util .ArrayList ;
43
+ import java .util .Date ;
44
+ import java .util .GregorianCalendar ;
41
45
import java .util .Iterator ;
42
46
import java .util .List ;
47
+ import java .util .Locale ;
48
+ import java .util .TimeZone ;
43
49
import org .json .JSONArray ;
44
50
import org .json .JSONException ;
45
51
import org .json .JSONObject ;
46
52
47
53
/** A JSON serializer to deserialize Firestore Bundles. */
48
54
class BundleSerializer {
49
55
56
+ private static final long MILLIS_PER_SECOND = 1000 ;
57
+
58
+ private final SimpleDateFormat timestampFormat ;
50
59
private final RemoteSerializer remoteSerializer ;
51
60
52
61
public BundleSerializer (RemoteSerializer remoteSerializer ) {
53
62
this .remoteSerializer = remoteSerializer ;
63
+
64
+ timestampFormat = new SimpleDateFormat ("yyyy-MM-dd'T'HH:mm:ss" , Locale .ENGLISH );
65
+ GregorianCalendar calendar = new GregorianCalendar (TimeZone .getTimeZone ("UTC" ));
66
+ // We use Proleptic Gregorian Calendar (i.e., Gregorian calendar extends backwards to year one)
67
+ // for timestamp formatting.
68
+ calendar .setGregorianChange (new Date (Long .MIN_VALUE ));
69
+ timestampFormat .setCalendar (calendar );
54
70
}
55
71
56
72
public NamedQuery decodeNamedQuery (JSONObject namedQuery ) throws JSONException {
57
73
String name = namedQuery .getString ("name" );
58
74
BundledQuery bundledQuery = decodeBundledQuery (namedQuery .getJSONObject ("bundledQuery" ));
59
- SnapshotVersion readTime = decodeSnapshotVersion (namedQuery .getJSONObject ("readTime" ));
75
+ SnapshotVersion readTime = decodeSnapshotVersion (namedQuery .get ("readTime" ));
60
76
return new NamedQuery (name , bundledQuery , readTime );
61
77
}
62
78
63
79
public BundleMetadata decodeBundleMetadata (JSONObject bundleMetadata ) throws JSONException {
64
80
String bundleId = bundleMetadata .getString ("id" );
65
81
int version = bundleMetadata .getInt ("version" );
66
- SnapshotVersion createTime = decodeSnapshotVersion (bundleMetadata .getJSONObject ("createTime" ));
82
+ SnapshotVersion createTime = decodeSnapshotVersion (bundleMetadata .get ("createTime" ));
67
83
return new BundleMetadata (bundleId , version , createTime );
68
84
}
69
85
70
86
public BundledDocumentMetadata decodeBundledDocumentMetadata (JSONObject bundledDocumentMetadata )
71
87
throws JSONException {
72
88
DocumentKey key = DocumentKey .fromPath (decodeName (bundledDocumentMetadata .getString ("name" )));
73
- SnapshotVersion readTime =
74
- decodeSnapshotVersion (bundledDocumentMetadata .getJSONObject ("readTime" ));
89
+ SnapshotVersion readTime = decodeSnapshotVersion (bundledDocumentMetadata .get ("readTime" ));
75
90
boolean exists = bundledDocumentMetadata .optBoolean ("exists" , false );
76
91
JSONArray queriesJson = bundledDocumentMetadata .optJSONArray ("queries" );
77
92
List <String > queries = new ArrayList <>();
@@ -88,7 +103,7 @@ public BundledDocumentMetadata decodeBundledDocumentMetadata(JSONObject bundledD
88
103
BundleDocument decodeDocument (JSONObject document ) throws JSONException {
89
104
String name = document .getString ("name" );
90
105
DocumentKey key = DocumentKey .fromPath (decodeName (name ));
91
- SnapshotVersion updateTime = decodeSnapshotVersion (document .getJSONObject ("updateTime" ));
106
+ SnapshotVersion updateTime = decodeSnapshotVersion (document .get ("updateTime" ));
92
107
93
108
Value .Builder value = Value .newBuilder ();
94
109
decodeMapValue (value , document .getJSONObject ("fields" ));
@@ -110,10 +125,8 @@ private ResourcePath decodeName(String name) {
110
125
return resourcePath .popFirst (5 );
111
126
}
112
127
113
- private SnapshotVersion decodeSnapshotVersion (JSONObject timestamp ) {
114
- long seconds = timestamp .optLong ("seconds" , 0 );
115
- int nanos = timestamp .optInt ("nanos" , 0 );
116
- return new SnapshotVersion (new Timestamp (seconds , nanos ));
128
+ private SnapshotVersion decodeSnapshotVersion (Object timestamp ) throws JSONException {
129
+ return new SnapshotVersion (decodeTimestamp (timestamp ));
117
130
}
118
131
119
132
private BundledQuery decodeBundledQuery (JSONObject bundledQuery ) throws JSONException {
@@ -240,7 +253,7 @@ private Value decodeValue(JSONObject value) throws JSONException {
240
253
} else if (value .has ("doubleValue" )) {
241
254
builder .setDoubleValue (value .optDouble ("doubleValue" , 0.0 ));
242
255
} else if (value .has ("timestampValue" )) {
243
- decodeTimestamp (builder , value .getJSONObject ("timestampValue" ));
256
+ decodeTimestamp (builder , value .get ("timestampValue" ));
244
257
} else if (value .has ("stringValue" )) {
245
258
builder .setStringValue (value .optString ("stringValue" , "" ));
246
259
} else if (value .has ("bytesValue" )) {
@@ -291,11 +304,107 @@ private void decodeGeoPoint(Value.Builder builder, JSONObject geoPoint) {
291
304
.setLongitude (geoPoint .optDouble ("longitude" , 0.0 )));
292
305
}
293
306
294
- private void decodeTimestamp (Value .Builder builder , JSONObject timestamp ) {
307
+ private Timestamp decodeTimestamp (JSONObject timestamp ) throws JSONException {
308
+ return new Timestamp (timestamp .getLong ("seconds" ), timestamp .getInt ("nanos" ));
309
+ }
310
+
311
+ private Timestamp decodeTimestamp (String timestamp ) {
312
+ // This method is copied from com.google.protobuf.util.Timestamps. See:
313
+ // https://chromium.googlesource.com/external/github.com/google/protobuf/+/HEAD/java/util/src/main/java/com/google/protobuf/util/Timestamps.java?autodive=0%2F#232
314
+
315
+ try {
316
+ int dayOffset = timestamp .indexOf ('T' );
317
+ if (dayOffset == -1 ) {
318
+ throw new IllegalArgumentException ("Invalid timestamp: " + timestamp );
319
+ }
320
+ int timezoneOffsetPosition = timestamp .indexOf ('Z' , dayOffset );
321
+ if (timezoneOffsetPosition == -1 ) {
322
+ timezoneOffsetPosition = timestamp .indexOf ('+' , dayOffset );
323
+ }
324
+ if (timezoneOffsetPosition == -1 ) {
325
+ timezoneOffsetPosition = timestamp .indexOf ('-' , dayOffset );
326
+ }
327
+ if (timezoneOffsetPosition == -1 ) {
328
+ throw new IllegalArgumentException (
329
+ "Invalid timestamp: Missing valid timezone offset: " + timestamp );
330
+ }
331
+ // Parse seconds and nanos.
332
+ String timeValue = timestamp .substring (0 , timezoneOffsetPosition );
333
+ String secondValue = timeValue ;
334
+ String nanoValue = "" ;
335
+ int pointPosition = timeValue .indexOf ('.' );
336
+ if (pointPosition != -1 ) {
337
+ secondValue = timeValue .substring (0 , pointPosition );
338
+ nanoValue = timeValue .substring (pointPosition + 1 );
339
+ }
340
+ Date date = timestampFormat .parse (secondValue );
341
+ long seconds = date .getTime () / MILLIS_PER_SECOND ;
342
+ int nanos = nanoValue .isEmpty () ? 0 : parseNanos (nanoValue );
343
+ // Parse timezone offsets.
344
+ if (timestamp .charAt (timezoneOffsetPosition ) == 'Z' ) {
345
+ if (timestamp .length () != timezoneOffsetPosition + 1 ) {
346
+ throw new IllegalArgumentException (
347
+ "Invalid timestamp: Invalid trailing data \" "
348
+ + timestamp .substring (timezoneOffsetPosition )
349
+ + "\" " );
350
+ }
351
+ } else {
352
+ String offsetValue = timestamp .substring (timezoneOffsetPosition + 1 );
353
+ long offset = decodeTimezoneOffset (offsetValue );
354
+ if (timestamp .charAt (timezoneOffsetPosition ) == '+' ) {
355
+ seconds -= offset ;
356
+ } else {
357
+ seconds += offset ;
358
+ }
359
+ }
360
+ return new Timestamp (seconds , nanos );
361
+ } catch (ParseException e ) {
362
+ throw new IllegalArgumentException ("Failed to parse timestamp" , e );
363
+ }
364
+ }
365
+
366
+ private Timestamp decodeTimestamp (Object timestamp ) throws JSONException {
367
+ if (timestamp instanceof String ) {
368
+ return decodeTimestamp ((String ) timestamp );
369
+ } else {
370
+ if (!(timestamp instanceof JSONObject )) {
371
+ throw new IllegalArgumentException (
372
+ "Timestamps must be either ISO 8601-formatted strings or JSON objects" );
373
+ }
374
+ return decodeTimestamp ((JSONObject ) timestamp );
375
+ }
376
+ }
377
+
378
+ private void decodeTimestamp (Value .Builder builder , Object timestamp ) throws JSONException {
379
+ Timestamp decoded = decodeTimestamp (timestamp );
295
380
builder .setTimestampValue (
296
381
com .google .protobuf .Timestamp .newBuilder ()
297
- .setSeconds (timestamp .optLong ("seconds" , 0 ))
298
- .setNanos (timestamp .optInt ("nanos" , 0 )));
382
+ .setSeconds (decoded .getSeconds ())
383
+ .setNanos (decoded .getNanoseconds ()));
384
+ }
385
+
386
+ private static int parseNanos (String value ) {
387
+ int result = 0 ;
388
+ for (int i = 0 ; i < 9 ; ++i ) {
389
+ result = result * 10 ;
390
+ if (i < value .length ()) {
391
+ if (value .charAt (i ) < '0' || value .charAt (i ) > '9' ) {
392
+ throw new IllegalArgumentException ("Invalid nanoseconds: " + value );
393
+ }
394
+ result += value .charAt (i ) - '0' ;
395
+ }
396
+ }
397
+ return result ;
398
+ }
399
+
400
+ private static long decodeTimezoneOffset (String value ) {
401
+ int pos = value .indexOf (':' );
402
+ if (pos == -1 ) {
403
+ throw new IllegalArgumentException ("Invalid offset value: " + value );
404
+ }
405
+ String hours = value .substring (0 , pos );
406
+ String minutes = value .substring (pos + 1 );
407
+ return (Long .parseLong (hours ) * 60 + Long .parseLong (minutes )) * 60 ;
299
408
}
300
409
301
410
private FieldFilter .Operator decodeFieldFilterOperator (String operator ) {
0 commit comments