Skip to content

Commit f184491

Browse files
Add Bundle Reader (#2333)
1 parent 73f2d29 commit f184491

File tree

11 files changed

+876
-55
lines changed

11 files changed

+876
-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 implements 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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
public interface BundleElement {}

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 implements BundleElement {
2121
private final String bundleId;
2222
private final int version;
2323
private final SnapshotVersion createTime;
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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 java.nio.charset.Charset;
23+
import org.json.JSONException;
24+
import org.json.JSONObject;
25+
26+
/**
27+
* Reads the length-prefixed JSON stream for Bundles.
28+
*
29+
* <p>The class takes a bundle stream and presents abstractions to read bundled elements out of the
30+
* underlying content.
31+
*/
32+
public class BundleReader {
33+
/** The capacity for the internal char buffer. */
34+
protected static final int BUFFER_CAPACITY = 1024;
35+
36+
private final BundleSerializer serializer;
37+
private final InputStreamReader dataReader;
38+
private final Charset charset = Charset.forName("UTF-8");
39+
40+
@Nullable BundleMetadata metadata;
41+
private CharBuffer buffer;
42+
long bytesRead;
43+
44+
public BundleReader(BundleSerializer serializer, InputStream data) {
45+
this.serializer = serializer;
46+
dataReader = new InputStreamReader(data, charset);
47+
buffer = CharBuffer.allocate(BUFFER_CAPACITY);
48+
49+
buffer.flip(); // Start the buffer in "reading mode"
50+
}
51+
52+
/** Returns the metadata element from the bundle. */
53+
public BundleMetadata getBundleMetadata() throws IOException, JSONException {
54+
if (metadata != null) {
55+
return metadata;
56+
}
57+
BundleElement element = readNextElement();
58+
if (!(element instanceof BundleMetadata)) {
59+
throw new IllegalArgumentException(
60+
"Expected first element in bundle to be a metadata object");
61+
}
62+
metadata = (BundleMetadata) element;
63+
// We don't consider the metadata as part ot the bundle size, as it used to encode the size of
64+
// all remaining elements.
65+
bytesRead = 0;
66+
return metadata;
67+
}
68+
69+
/**
70+
* Returns the next element from the bundle. Metadata elements can be accessed by invoking {@link
71+
* #getBundleMetadata} are not returned from this method.
72+
*/
73+
public BundleElement getNextElement() throws IOException, JSONException {
74+
// Makes sure metadata is read before proceeding. The metadata element is the first element
75+
// in the bundle stream.
76+
getBundleMetadata();
77+
return readNextElement();
78+
}
79+
80+
/** Returns the number of bytes processed so far. */
81+
public long getBytesRead() {
82+
return bytesRead;
83+
}
84+
85+
public void close() throws IOException {
86+
dataReader.close();
87+
}
88+
89+
/**
90+
* Reads from the head of internal buffer, Pulls more data from underlying stream until a complete
91+
* element is found (including the prefixed length and the JSON string).
92+
*
93+
* <p>Once a complete element is read, it is dropped from internal buffer.
94+
*
95+
* <p>Returns either the bundled element, or null if we have reached the end of the stream.
96+
*/
97+
@Nullable
98+
private BundleElement readNextElement() throws IOException, JSONException {
99+
String lengthPrefix = readLengthPrefix();
100+
if (lengthPrefix == null) {
101+
return null;
102+
}
103+
104+
String json = readJsonString(Integer.parseInt(lengthPrefix));
105+
bytesRead += lengthPrefix.length() + json.getBytes(charset).length;
106+
return decodeBundleElement(json);
107+
}
108+
109+
/**
110+
* Reads the length prefix from the beginning of the internal buffer until the first '{'. Returns
111+
* the integer-decoded length.
112+
*
113+
* <p>If it reached the end of the stream, returns null.
114+
*/
115+
private @Nullable String readLengthPrefix() throws IOException {
116+
int nextOpenBracket;
117+
118+
while ((nextOpenBracket = indexOfOpenBracket()) == -1) {
119+
if (!pullMoreData()) {
120+
break;
121+
}
122+
}
123+
124+
// We broke out of the loop because underlying stream is closed, and there happens to be no
125+
// more data to process.
126+
if (buffer.remaining() == 0) {
127+
return null;
128+
}
129+
130+
// We broke out of the loop because underlying stream is closed, but still cannot find an
131+
// open bracket.
132+
if (nextOpenBracket == -1) {
133+
raiseError("Reached the end of bundle when a length string is expected.");
134+
}
135+
136+
char[] c = new char[nextOpenBracket];
137+
buffer.get(c);
138+
return new String(c);
139+
}
140+
141+
/** Returns the index of the first open bracket, or -1 if none is found. */
142+
private int indexOfOpenBracket() {
143+
buffer.mark();
144+
try {
145+
for (int i = 0; i < buffer.remaining(); ++i) {
146+
if (buffer.get() == '{') {
147+
return i;
148+
}
149+
}
150+
return -1;
151+
} finally {
152+
buffer.reset();
153+
}
154+
}
155+
156+
/**
157+
* Reads from a specified position from the internal buffer, for a specified number of bytes,
158+
* pulling more data from the underlying stream if needed.
159+
*
160+
* <p>Returns a string decoded from the read bytes.
161+
*/
162+
private String readJsonString(int length) throws IOException {
163+
StringBuilder json = new StringBuilder(length);
164+
165+
int remaining = length;
166+
while (remaining > 0) {
167+
if (!pullMoreData()) {
168+
raiseError("Reached the end of bundle when more data was expected.");
169+
}
170+
171+
int read = Math.min(remaining, buffer.remaining());
172+
json.append(buffer, 0, read);
173+
buffer.position(buffer.position() + read);
174+
175+
remaining -= read;
176+
}
177+
178+
return json.toString();
179+
}
180+
181+
/**
182+
* Pulls more data from underlying stream into the internal buffer.
183+
*
184+
* @return whether more data is available
185+
*/
186+
private boolean pullMoreData() throws IOException {
187+
if (buffer.remaining() == 0) {
188+
buffer.compact();
189+
dataReader.read(buffer);
190+
buffer.flip();
191+
}
192+
return buffer.remaining() > 0;
193+
}
194+
195+
/** Converts a JSON-encoded bundle element into its model class. */
196+
private BundleElement decodeBundleElement(String json) throws JSONException {
197+
JSONObject object = new JSONObject(json);
198+
199+
if (object.has("metadata")) {
200+
return serializer.decodeBundleMetadata(object.getJSONObject("metadata"));
201+
} else if (object.has("namedQuery")) {
202+
return serializer.decodeNamedQuery(object.getJSONObject("namedQuery"));
203+
} else if (object.has("documentMetadata")) {
204+
return serializer.decodeBundledDocumentMetadata(object.getJSONObject("documentMetadata"));
205+
} else if (object.has("document")) {
206+
return serializer.decodeDocument(object.getJSONObject("document"));
207+
} else {
208+
throw new IllegalArgumentException("Cannot decode unknown Bundle element: " + json);
209+
}
210+
}
211+
212+
/** Closes the underlying stream and raises an IllegalArgumentException. */
213+
private void raiseError(String message) throws IOException {
214+
close();
215+
throw new IllegalArgumentException("Invalid bundle format: " + message);
216+
}
217+
}

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 {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
// TODO(bundles): Figure out whether we need this class (it is not needed in Web).
2222

2323
/** Metadata describing a Firestore document saved in the bundle. */
24-
public class BundledDocumentMetadata {
24+
public class BundledDocumentMetadata implements BundleElement {
2525
private final DocumentKey key;
2626
private final SnapshotVersion readTime;
2727
private final boolean exists;
@@ -46,7 +46,7 @@ public SnapshotVersion getReadTime() {
4646
}
4747

4848
/** Returns whether the document exists. */
49-
public boolean isExists() {
49+
public boolean exists() {
5050
return exists;
5151
}
5252

0 commit comments

Comments
 (0)