Skip to content

Commit 3bbf997

Browse files
Add BundleReader
1 parent f96ed35 commit 3bbf997

File tree

11 files changed

+854
-55
lines changed

11 files changed

+854
-55
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.bundle;
16+
17+
import com.google.firebase.firestore.model.Document;
18+
import com.google.firebase.firestore.model.DocumentKey;
19+
20+
/** A document that was saved to a bundle. */
21+
public class BundleDocument extends BundleElement {
22+
private Document document;
23+
24+
public BundleDocument(Document document) {
25+
this.document = document;
26+
}
27+
28+
/** Returns the key for this document. */
29+
public DocumentKey getKey() {
30+
return document.getKey();
31+
}
32+
33+
/** Returns the document. */
34+
public Document getDocument() {
35+
return document;
36+
}
37+
38+
@Override
39+
public boolean equals(Object o) {
40+
if (this == o) return true;
41+
if (o == null || getClass() != o.getClass()) return false;
42+
43+
BundleDocument that = (BundleDocument) o;
44+
45+
return document.equals(that.document);
46+
}
47+
48+
@Override
49+
public int hashCode() {
50+
return document.hashCode();
51+
}
52+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.bundle;
16+
17+
import org.json.JSONException;
18+
import org.json.JSONObject;
19+
20+
public abstract class BundleElement {
21+
22+
public static BundleElement fromJson(BundleSerializer serializer, String json)
23+
throws JSONException {
24+
JSONObject object = new JSONObject(json);
25+
26+
if (object.has("metadata")) {
27+
return serializer.decodeBundleMetadata(object.getJSONObject("metadata"));
28+
} else if (object.has("namedQuery")) {
29+
return serializer.decodeNamedQuery(object.getJSONObject("namedQuery"));
30+
} else if (object.has("documentMetadata")) {
31+
return serializer.decodeBundledDocumentMetadata(object.getJSONObject("documentMetadata"));
32+
} else if (object.has("document")) {
33+
return serializer.decodeDocument(object.getJSONObject("document"));
34+
} else {
35+
throw new IllegalArgumentException("Cannot decode unknown Bundle element: " + json);
36+
}
37+
}
38+
39+
protected BundleElement() {}
40+
}

firebase-firestore/src/main/java/com/google/firebase/firestore/bundle/BundleMetadata.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import com.google.firebase.firestore.model.SnapshotVersion;
1818

1919
/** Represents a Firestore bundle saved by the SDK in its local storage. */
20-
public class BundleMetadata {
20+
public class BundleMetadata extends BundleElement {
2121
private final String bundleId;
2222
private final int version;
2323
private final SnapshotVersion createTime;
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.bundle;
16+
17+
import androidx.annotation.Nullable;
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
import java.io.InputStreamReader;
21+
import java.nio.CharBuffer;
22+
import org.json.JSONException;
23+
24+
/**
25+
* Reads the length-prefixed JSON stream for Bundles.
26+
*
27+
* <p>The class takes a bundle stream and presents abstractions to read bundled elements out of the
28+
* underlying content.
29+
*/
30+
public class BundleReader extends BundleElement {
31+
/** The capacity for the internal char buffer. */
32+
protected static final int BUFFER_CAPACITY = 1024;
33+
34+
private final BundleSerializer serializer;
35+
private final InputStreamReader dataReader;
36+
37+
@Nullable BundleMetadata metadata;
38+
private CharBuffer buffer;
39+
long bytesRead;
40+
41+
public BundleReader(BundleSerializer serializer, InputStream data) {
42+
this.serializer = serializer;
43+
dataReader = new InputStreamReader(data);
44+
buffer = CharBuffer.allocate(BUFFER_CAPACITY);
45+
}
46+
47+
/** Returns the metadata element from the bundle. */
48+
public BundleMetadata getBundleMetadata() throws IOException, JSONException {
49+
if (metadata != null) {
50+
return metadata;
51+
}
52+
BundleElement element = readNextElement();
53+
if (!(element instanceof BundleMetadata)) {
54+
throw new IllegalArgumentException(
55+
"Expected first element in bundle to be a metadata object");
56+
}
57+
metadata = (BundleMetadata) element;
58+
// We don't consider the metadata as part ot the bundle size, as it used to encode the size of
59+
// all remaining elements.
60+
bytesRead = 0;
61+
return metadata;
62+
}
63+
64+
/**
65+
* Returns the next element from the bundle. Metadata elements can be accessed by invoking {@link
66+
* #getBundleMetadata} are not returned from this method.
67+
*/
68+
public BundleElement getNextElement() throws IOException, JSONException {
69+
// Makes sure metadata is read before proceeding. The metadata element is the first element
70+
// in the bundle stream.
71+
getBundleMetadata();
72+
return readNextElement();
73+
}
74+
75+
/** Returns the number of bytes processed so far. */
76+
public long getBytesRead() {
77+
return bytesRead;
78+
}
79+
80+
public void close() throws IOException {
81+
dataReader.close();
82+
}
83+
84+
/**
85+
* Reads from the head of internal buffer, Pulls more data from underlying stream until a complete
86+
* element is found (including the prefixed length and the JSON string).
87+
*
88+
* <p>Once a complete element is read, it is dropped from internal buffer.
89+
*
90+
* <p>Returns either the bundled element, or null if we have reached the end of the stream.
91+
*/
92+
@Nullable
93+
private BundleElement readNextElement() throws IOException, JSONException {
94+
int length = readLength();
95+
if (length == -1) {
96+
return null;
97+
}
98+
99+
String json = readJsonString(length);
100+
bytesRead += (int) (Math.log10(length) + 1) + length;
101+
return BundleElement.fromJson(serializer, json);
102+
}
103+
104+
/**
105+
* Reads the length prefix from the beginning of the internal buffer until the first '{'. Returns
106+
* the integer-decoded length.
107+
*
108+
* <p>If it reached the end of the stream, returns -1.
109+
*/
110+
private int readLength() throws IOException {
111+
int nextOpenBracket;
112+
113+
while ((nextOpenBracket = indexOfOpenBracket()) == -1) {
114+
if (!pullMoreData()) {
115+
break;
116+
}
117+
}
118+
119+
// We broke out of the loop because underlying stream is closed, and there happens to be no
120+
// more data to process.
121+
if (buffer.remaining() == 0) {
122+
return -1;
123+
}
124+
125+
// We broke out of the loop because underlying stream is closed, but still cannot find an
126+
// open bracket.
127+
if (nextOpenBracket == -1) {
128+
raiseError("Reached the end of bundle when a length string is expected.");
129+
}
130+
131+
char[] c = new char[nextOpenBracket];
132+
buffer.get(c);
133+
return Integer.parseInt(new String(c));
134+
}
135+
136+
/** Returns the index of the first open bracket, or -1 if none is found. */
137+
private int indexOfOpenBracket() {
138+
buffer.mark();
139+
try {
140+
for (int i = 0; i < buffer.limit(); ++i) {
141+
if (buffer.get() == '{') {
142+
return i;
143+
}
144+
}
145+
return -1;
146+
} finally {
147+
buffer.reset();
148+
}
149+
}
150+
151+
/**
152+
* Reads from a specified position from the internal buffer, for a specified number of bytes,
153+
* pulling more data from the underlying stream if needed.
154+
*
155+
* <p>Returns a string decoded from the read bytes.
156+
*/
157+
private String readJsonString(int length) throws IOException {
158+
char[] c = new char[length];
159+
160+
int read = Math.min(length, buffer.remaining());
161+
buffer.get(c, 0, read);
162+
163+
while (read < length) {
164+
if (!pullMoreData()) {
165+
raiseError("Reached the end of bundle when more data was expected.");
166+
}
167+
int toRead = Math.min(length, buffer.remaining());
168+
buffer.get(c, read, toRead);
169+
read += toRead;
170+
}
171+
return new String(c);
172+
}
173+
174+
/**
175+
* Pulls more data from underlying stream into the internal buffer.
176+
*
177+
* @return whether more data is available
178+
*/
179+
private boolean pullMoreData() throws IOException {
180+
if (buffer.remaining() == 0) {
181+
buffer.compact();
182+
}
183+
184+
int read;
185+
do {
186+
read = dataReader.read(buffer);
187+
} while (read > 0);
188+
buffer.flip();
189+
190+
return buffer.remaining() > 0;
191+
}
192+
193+
/** Closes the underlying stream and raises an IllegalArgumentException. */
194+
private void raiseError(String message) throws IOException {
195+
close();
196+
throw new IllegalArgumentException("Invalid bundle format: " + message);
197+
}
198+
}

firebase-firestore/src/main/java/com/google/firebase/firestore/bundle/BundleSerializer.java

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
import android.util.Base64;
1818
import androidx.annotation.Nullable;
19-
import androidx.annotation.VisibleForTesting;
2019
import com.google.firebase.Timestamp;
2120
import com.google.firebase.firestore.core.Bound;
2221
import com.google.firebase.firestore.core.FieldFilter;
@@ -54,32 +53,20 @@ public BundleSerializer(RemoteSerializer remoteSerializer) {
5453
this.remoteSerializer = remoteSerializer;
5554
}
5655

57-
public NamedQuery decodeNamedQuery(String json) throws JSONException {
58-
return decodeNamedQuery(new JSONObject(json));
59-
}
60-
6156
public NamedQuery decodeNamedQuery(JSONObject namedQuery) throws JSONException {
6257
String name = namedQuery.getString("name");
6358
BundledQuery bundledQuery = decodeBundledQuery(namedQuery.getJSONObject("bundledQuery"));
6459
SnapshotVersion readTime = decodeSnapshotVersion(namedQuery.getJSONObject("readTime"));
6560
return new NamedQuery(name, bundledQuery, readTime);
6661
}
6762

68-
public BundleMetadata decodeBundleMetadata(String json) throws JSONException {
69-
return decodeBundleMetadata(new JSONObject(json));
70-
}
71-
7263
public BundleMetadata decodeBundleMetadata(JSONObject bundleMetadata) throws JSONException {
7364
String bundleId = bundleMetadata.getString("id");
7465
int version = bundleMetadata.getInt("version");
7566
SnapshotVersion createTime = decodeSnapshotVersion(bundleMetadata.getJSONObject("createTime"));
7667
return new BundleMetadata(bundleId, version, createTime);
7768
}
7869

79-
public BundledDocumentMetadata decodeBundledDocumentMetadata(String json) throws JSONException {
80-
return decodeBundledDocumentMetadata(new JSONObject(json));
81-
}
82-
8370
public BundledDocumentMetadata decodeBundledDocumentMetadata(JSONObject bundledDocumentMetadata)
8471
throws JSONException {
8572
DocumentKey key = DocumentKey.fromPath(decodeName(bundledDocumentMetadata.getString("name")));
@@ -98,24 +85,20 @@ public BundledDocumentMetadata decodeBundledDocumentMetadata(JSONObject bundledD
9885
return new BundledDocumentMetadata(key, readTime, exists, queries);
9986
}
10087

101-
@VisibleForTesting
102-
Document decodeDocument(String json) throws JSONException {
103-
return decodeDocument(new JSONObject(json));
104-
}
105-
106-
Document decodeDocument(JSONObject document) throws JSONException {
88+
BundleDocument decodeDocument(JSONObject document) throws JSONException {
10789
String name = document.getString("name");
10890
DocumentKey key = DocumentKey.fromPath(decodeName(name));
10991
SnapshotVersion updateTime = decodeSnapshotVersion(document.getJSONObject("updateTime"));
11092

11193
Value.Builder value = Value.newBuilder();
11294
decodeMapValue(value, document.getJSONObject("fields"));
11395

114-
return new Document(
115-
key,
116-
updateTime,
117-
ObjectValue.fromMap(value.getMapValue().getFieldsMap()),
118-
Document.DocumentState.SYNCED);
96+
return new BundleDocument(
97+
new Document(
98+
key,
99+
updateTime,
100+
ObjectValue.fromMap(value.getMapValue().getFieldsMap()),
101+
Document.DocumentState.SYNCED));
119102
}
120103

121104
private ResourcePath decodeName(String name) {
@@ -163,7 +146,8 @@ private BundledQuery decodeBundledQuery(JSONObject bundledQuery) throws JSONExce
163146
}
164147

165148
private int decodeLimit(JSONObject structuredQuery) {
166-
return structuredQuery.optInt("limit", -1);
149+
JSONObject limit = structuredQuery.optJSONObject("limit");
150+
return limit != null ? limit.optInt("value", -1) : -1;
167151
}
168152

169153
private Bound decodeBound(@Nullable JSONObject bound) throws JSONException {

0 commit comments

Comments
 (0)