Skip to content

Commit fd5ef90

Browse files
authored
feat: Implementation of Firestore Bundle Builder (#293)
1 parent 58d75a7 commit fd5ef90

File tree

9 files changed

+809
-45
lines changed

9 files changed

+809
-45
lines changed

google-cloud-firestore-bom/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@
7070
<artifactId>proto-google-cloud-firestore-admin-v1</artifactId>
7171
<version>2.1.1-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-firestore-admin-v1:current} -->
7272
</dependency>
73+
<dependency>
74+
<groupId>com.google.cloud</groupId>
75+
<artifactId>proto-google-cloud-firestore-bundle-v1</artifactId>
76+
<version>2.1.1-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-firestore:current} -->
77+
</dependency>
7378
<dependency>
7479
<groupId>com.google.api.grpc</groupId>
7580
<artifactId>proto-google-cloud-firestore-v1</artifactId>

google-cloud-firestore/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
<groupId>com.google.api.grpc</groupId>
2929
<artifactId>proto-google-cloud-firestore-v1</artifactId>
3030
</dependency>
31+
<dependency>
32+
<groupId>com.google.cloud</groupId>
33+
<artifactId>proto-google-cloud-firestore-bundle-v1</artifactId>
34+
</dependency>
3135
<dependency>
3236
<groupId>com.google.auto.value</groupId>
3337
<artifactId>auto-value-annotations</artifactId>

google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,16 @@ Write.Builder toPb() {
412412
return write;
413413
}
414414

415+
Document.Builder toDocumentPb() {
416+
Preconditions.checkState(exists(), "Can't call toDocument() on a document that doesn't exist");
417+
Document.Builder document = Document.newBuilder();
418+
return document
419+
.setName(docRef.getName())
420+
.putAllFields(fields)
421+
.setCreateTime(createTime.toProto())
422+
.setUpdateTime(updateTime.toProto());
423+
}
424+
415425
/**
416426
* Returns true if the document's data and path in this DocumentSnapshot equals the provided
417427
* snapshot.
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.firestore;
18+
19+
import com.google.cloud.Timestamp;
20+
import com.google.common.base.Optional;
21+
import com.google.common.collect.Lists;
22+
import com.google.firestore.bundle.BundleElement;
23+
import com.google.firestore.bundle.BundleMetadata;
24+
import com.google.firestore.bundle.BundledDocumentMetadata;
25+
import com.google.firestore.bundle.BundledQuery;
26+
import com.google.firestore.bundle.NamedQuery;
27+
import com.google.firestore.v1.Document;
28+
import com.google.protobuf.InvalidProtocolBufferException;
29+
import com.google.protobuf.util.JsonFormat;
30+
import java.nio.ByteBuffer;
31+
import java.nio.charset.StandardCharsets;
32+
import java.util.HashMap;
33+
import java.util.List;
34+
import java.util.Map;
35+
36+
/** Represents a Firestore data bundle with results from the given document and query snapshots. */
37+
public final class FirestoreBundle {
38+
39+
static final int BUNDLE_SCHEMA_VERSION = 1;
40+
// Printer to encode protobuf objects into JSON string.
41+
private static final JsonFormat.Printer PRINTER = JsonFormat.printer();
42+
43+
// Raw byte array to hold the content of the bundle.
44+
private byte[] bundleData;
45+
46+
/** Builds a Firestore data bundle with results from the given document and query snapshots. */
47+
public static final class Builder {
48+
// Id of the bundle.
49+
private String id;
50+
// Resulting documents for the bundle, keyed by full document path.
51+
private Map<String, BundledDocument> documents = new HashMap<>();
52+
// Named queries saved in the bundle, keyed by query name.
53+
private Map<String, NamedQuery> namedQueries = new HashMap<>();
54+
// The latest read time among all bundled documents and queries.
55+
private Timestamp latestReadTime = Timestamp.MIN_VALUE;
56+
57+
public Builder(String id) {
58+
this.id = id;
59+
}
60+
61+
/**
62+
* Adds a Firestore document snapshot to the bundle. Both the documents data and the document
63+
* read time will be included in the bundle.
64+
*
65+
* @param documentSnapshot A document snapshot to add.
66+
* @returns This instance.
67+
*/
68+
public Builder add(DocumentSnapshot documentSnapshot) {
69+
return add(documentSnapshot, Optional.<String>absent());
70+
}
71+
72+
private Builder add(DocumentSnapshot documentSnapshot, Optional<String> queryName) {
73+
String documentName = documentSnapshot.getReference().getName();
74+
BundledDocument originalDocument = documents.get(documentSnapshot.getReference().getName());
75+
List<String> queries =
76+
originalDocument == null
77+
? Lists.<String>newArrayList()
78+
: Lists.newArrayList(originalDocument.getMetadata().getQueriesList());
79+
80+
// Update with document built from `documentSnapshot` because it is newer.
81+
Timestamp snapReadTime =
82+
documentSnapshot.getReadTime() == null
83+
? Timestamp.MIN_VALUE
84+
: documentSnapshot.getReadTime();
85+
if (originalDocument == null
86+
|| originalDocument.getMetadata().getReadTime() == null
87+
|| snapReadTime.compareTo(
88+
Timestamp.fromProto(originalDocument.getMetadata().getReadTime()))
89+
> 0) {
90+
BundledDocumentMetadata metadata =
91+
BundledDocumentMetadata.newBuilder()
92+
.setName(documentName)
93+
.setReadTime(snapReadTime.toProto())
94+
.setExists(documentSnapshot.exists())
95+
.build();
96+
Document document =
97+
documentSnapshot.exists() ? documentSnapshot.toDocumentPb().build() : null;
98+
documents.put(documentName, new BundledDocument(metadata, document));
99+
}
100+
101+
// Update queries to include all queries whose results include this document.
102+
if (queryName.isPresent()) {
103+
queries.add(queryName.get());
104+
}
105+
documents
106+
.get(documentName)
107+
.setMetadata(
108+
documents
109+
.get(documentName)
110+
.getMetadata()
111+
.toBuilder()
112+
.clearQueries()
113+
.addAllQueries(queries)
114+
.build());
115+
116+
if (documentSnapshot.getReadTime().compareTo(latestReadTime) > 0) {
117+
latestReadTime = documentSnapshot.getReadTime();
118+
}
119+
120+
return this;
121+
}
122+
123+
/**
124+
* Adds a Firestore query snapshots to the bundle. Both the documents in the query snapshots and
125+
* the query read time will be included in the bundle.
126+
*
127+
* @param queryName The name of the query to add.
128+
* @param querySnap The query snapshot to add.
129+
* @returns This instance.
130+
*/
131+
public Builder add(String queryName, QuerySnapshot querySnap) {
132+
BundledQuery query = querySnap.getQuery().toBundledQuery();
133+
NamedQuery namedQuery =
134+
NamedQuery.newBuilder()
135+
.setName(queryName)
136+
.setReadTime(querySnap.getReadTime().toProto())
137+
.setBundledQuery(query)
138+
.build();
139+
namedQueries.put(queryName, namedQuery);
140+
141+
for (QueryDocumentSnapshot snapshot : querySnap.getDocuments()) {
142+
add(snapshot, Optional.of(queryName));
143+
}
144+
145+
if (querySnap.getReadTime().compareTo(latestReadTime) > 0) {
146+
latestReadTime = querySnap.getReadTime();
147+
}
148+
149+
return this;
150+
}
151+
152+
public FirestoreBundle build() {
153+
StringBuilder buffer = new StringBuilder();
154+
155+
for (NamedQuery namedQuery : namedQueries.values()) {
156+
buffer.append(
157+
elementToLengthPrefixedStringBuilder(
158+
BundleElement.newBuilder().setNamedQuery(namedQuery).build()));
159+
}
160+
161+
for (BundledDocument bundledDocument : documents.values()) {
162+
buffer.append(
163+
elementToLengthPrefixedStringBuilder(
164+
BundleElement.newBuilder()
165+
.setDocumentMetadata(bundledDocument.getMetadata())
166+
.build()));
167+
if (bundledDocument.getDocument() != null) {
168+
buffer.append(
169+
elementToLengthPrefixedStringBuilder(
170+
BundleElement.newBuilder().setDocument(bundledDocument.getDocument()).build()));
171+
}
172+
}
173+
174+
BundleMetadata metadata =
175+
BundleMetadata.newBuilder()
176+
.setId(id)
177+
.setCreateTime(latestReadTime.toProto())
178+
.setVersion(BUNDLE_SCHEMA_VERSION)
179+
.setTotalDocuments(documents.size())
180+
.setTotalBytes(buffer.toString().getBytes().length)
181+
.build();
182+
BundleElement element = BundleElement.newBuilder().setMetadata(metadata).build();
183+
buffer.insert(0, elementToLengthPrefixedStringBuilder(element));
184+
185+
return new FirestoreBundle(buffer.toString().getBytes(StandardCharsets.UTF_8));
186+
}
187+
188+
private StringBuilder elementToLengthPrefixedStringBuilder(BundleElement element) {
189+
String elementJson = null;
190+
try {
191+
elementJson = PRINTER.print(element);
192+
} catch (InvalidProtocolBufferException e) {
193+
throw new RuntimeException(e);
194+
}
195+
return new StringBuilder().append(elementJson.getBytes().length).append(elementJson);
196+
}
197+
}
198+
199+
private FirestoreBundle(byte[] data) {
200+
bundleData = data;
201+
}
202+
203+
/** Returns the bundle content as a readonly {@link ByteBuffer}. */
204+
public ByteBuffer toByteBuffer() {
205+
return ByteBuffer.wrap(bundleData).asReadOnlyBuffer();
206+
}
207+
208+
/**
209+
* Convenient class to hold both the metadata and the actual content of a document to be bundled.
210+
*/
211+
private static class BundledDocument {
212+
private BundledDocumentMetadata metadata;
213+
private final Document document;
214+
215+
BundledDocument(BundledDocumentMetadata metadata, Document document) {
216+
this.metadata = metadata;
217+
this.document = document;
218+
}
219+
220+
public BundledDocumentMetadata getMetadata() {
221+
return metadata;
222+
}
223+
224+
void setMetadata(BundledDocumentMetadata metadata) {
225+
this.metadata = metadata;
226+
}
227+
228+
public Document getDocument() {
229+
return document;
230+
}
231+
}
232+
}

0 commit comments

Comments
 (0)