[firestore][android] re-write serialize class and add support for NaN & Infinity numbers

This commit is contained in:
Salakar
2018-09-24 02:48:29 +01:00
parent 571d9b6031
commit 855b8fab53
2 changed files with 331 additions and 169 deletions

View File

@@ -18,6 +18,7 @@ import com.google.firebase.firestore.FieldValue;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.GeoPoint;
import com.google.firebase.firestore.QuerySnapshot;
import com.google.firebase.firestore.SnapshotMetadata;
import java.util.ArrayList;
import java.util.Date;
@@ -25,19 +26,53 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import io.invertase.firebase.Utils;
public class FirestoreSerialize {
class FirestoreSerialize {
private static final String TAG = "FirestoreSerialize";
private static final String KEY_CHANGES = "changes";
// Keys
private static final String TYPE = "type";
private static final String VALUE = "value";
private static final String KEY_DATA = "data";
private static final String KEY_PATH = "path";
private static final String KEY_META = "metadata";
private static final String KEY_CHANGES = "changes";
private static final String KEY_OPTIONS = "options";
private static final String KEY_LATITUDE = "latitude";
private static final String KEY_LONGITUDE = "longitude";
private static final String KEY_DOCUMENTS = "documents";
private static final String KEY_DOC_CHANGE_TYPE = "type";
private static final String KEY_META_FROM_CACHE = "fromCache";
private static final String KEY_DOC_CHANGE_DOCUMENT = "document";
private static final String KEY_DOC_CHANGE_NEW_INDEX = "newIndex";
private static final String KEY_DOC_CHANGE_OLD_INDEX = "oldIndex";
private static final String KEY_DOC_CHANGE_TYPE = "type";
private static final String KEY_DOCUMENTS = "documents";
private static final String KEY_METADATA = "metadata";
private static final String KEY_PATH = "path";
private static final String KEY_META_HAS_PENDING_WRITES = "hasPendingWrites";
// Types
private static final String TYPE_NAN = "nan";
private static final String TYPE_NULL = "null";
private static final String TYPE_BLOB = "blob";
private static final String TYPE_DATE = "date";
private static final String TYPE_ARRAY = "array";
private static final String TYPE_STRING = "string";
private static final String TYPE_NUMBER = "number";
private static final String TYPE_OBJECT = "object";
private static final String TYPE_BOOLEAN = "boolean";
private static final String TYPE_GEOPOINT = "geopoint";
private static final String TYPE_INFINITY = "infinity";
private static final String TYPE_REFERENCE = "reference";
private static final String TYPE_DOCUMENTID = "documentid";
private static final String TYPE_FIELDVALUE = "fieldvalue";
private static final String TYPE_FIELDVALUE_DELETE = "delete";
private static final String TYPE_FIELDVALUE_TIMESTAMP = "timestamp";
// Document Change Types
private static final String CHANGE_ADDED = "added";
private static final String CHANGE_MODIFIED = "modified";
private static final String CHANGE_REMOVED = "removed";
/**
* Convert a DocumentSnapshot instance into a React Native WritableMap
@@ -46,67 +81,59 @@ public class FirestoreSerialize {
* @return WritableMap
*/
static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) {
WritableMap documentMap = Arguments.createMap();
documentMap.putString(
KEY_PATH,
documentSnapshot
.getReference()
.getPath()
);
if (documentSnapshot.exists()) {
documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData()));
}
// metadata
WritableMap metadata = Arguments.createMap();
metadata.putBoolean(
"fromCache",
documentSnapshot
.getMetadata()
.isFromCache()
);
metadata.putBoolean(
"hasPendingWrites",
documentSnapshot
.getMetadata()
.hasPendingWrites()
);
documentMap.putMap(KEY_METADATA, metadata);
WritableMap documentMap = Arguments.createMap();
SnapshotMetadata snapshotMetadata = documentSnapshot.getMetadata();
// build metadata
metadata.putBoolean(KEY_META_FROM_CACHE, snapshotMetadata.isFromCache());
metadata.putBoolean(KEY_META_HAS_PENDING_WRITES, snapshotMetadata.hasPendingWrites());
documentMap.putMap(KEY_META, metadata);
documentMap.putString(KEY_PATH, documentSnapshot.getReference().getPath());
if (documentSnapshot.exists())
documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData()));
return documentMap;
}
/**
* Convert a Firestore QuerySnapshot instance to a RN serializable WritableMap type map
*
* @param querySnapshot QuerySnapshot
* @return WritableMap
*/
static WritableMap snapshotToWritableMap(QuerySnapshot querySnapshot) {
WritableMap queryMap = Arguments.createMap();
List<DocumentChange> documentChanges = querySnapshot.getDocumentChanges();
queryMap.putArray(KEY_CHANGES, documentChangesToWritableArray(documentChanges));
// documents
WritableMap metadata = Arguments.createMap();
WritableMap writableMap = Arguments.createMap();
WritableArray documents = Arguments.createArray();
SnapshotMetadata snapshotMetadata = querySnapshot.getMetadata();
List<DocumentSnapshot> documentSnapshots = querySnapshot.getDocuments();
List<DocumentChange> documentChanges = querySnapshot.getDocumentChanges();
// convert documents documents
for (DocumentSnapshot documentSnapshot : documentSnapshots) {
documents.pushMap(snapshotToWritableMap(documentSnapshot));
}
queryMap.putArray(KEY_DOCUMENTS, documents);
// metadata
WritableMap metadata = Arguments.createMap();
metadata.putBoolean(
"fromCache",
querySnapshot
.getMetadata()
.isFromCache()
);
metadata.putBoolean(
"hasPendingWrites",
querySnapshot
.getMetadata()
.hasPendingWrites()
);
queryMap.putMap(KEY_METADATA, metadata);
// build metadata
metadata.putBoolean(KEY_META_FROM_CACHE, snapshotMetadata.isFromCache());
metadata.putBoolean(KEY_META_HAS_PENDING_WRITES, snapshotMetadata.hasPendingWrites());
return queryMap;
// set metadata
writableMap.putMap(KEY_META, metadata);
// set documents
writableMap.putArray(KEY_DOCUMENTS, documents);
// set document changes
writableMap.putArray(
KEY_CHANGES,
documentChangesToWritableArray(documentChanges)
);
return writableMap;
}
/**
@@ -117,9 +144,11 @@ public class FirestoreSerialize {
*/
private static WritableArray documentChangesToWritableArray(List<DocumentChange> documentChanges) {
WritableArray documentChangesWritable = Arguments.createArray();
for (DocumentChange documentChange : documentChanges) {
documentChangesWritable.pushMap(documentChangeToWritableMap(documentChange));
}
return documentChangesWritable;
}
@@ -134,19 +163,21 @@ public class FirestoreSerialize {
switch (documentChange.getType()) {
case ADDED:
documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, "added");
break;
case REMOVED:
documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, "removed");
documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, CHANGE_ADDED);
break;
case MODIFIED:
documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, "modified");
documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, CHANGE_MODIFIED);
break;
case REMOVED:
documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, CHANGE_REMOVED);
break;
}
documentChangeMap.putMap(
KEY_DOC_CHANGE_DOCUMENT,
snapshotToWritableMap(documentChange.getDocument())
);
documentChangeMap.putInt(KEY_DOC_CHANGE_NEW_INDEX, documentChange.getNewIndex());
documentChangeMap.putInt(KEY_DOC_CHANGE_OLD_INDEX, documentChange.getOldIndex());
@@ -161,15 +192,17 @@ public class FirestoreSerialize {
*/
private static WritableMap objectMapToWritable(Map<String, Object> map) {
WritableMap writableMap = Arguments.createMap();
for (Map.Entry<String, Object> entry : map.entrySet()) {
WritableMap typeMap = buildTypeMap(entry.getValue());
writableMap.putMap(entry.getKey(), typeMap);
}
return writableMap;
}
/**
* Converts an Object array into a React Native WritableArray.
* Converts a Object array into a React Native WritableArray.
*
* @param array Object[]
* @return WritableArray
@@ -186,155 +219,285 @@ public class FirestoreSerialize {
}
/**
* Detects an objects type and creates a Map to represent the type and value.
* Convert an Object to a type map for use in JS land to convert to JS equivalent.
*
* @param value Object
* @return WritableMap
*/
private static WritableMap buildTypeMap(Object value) {
WritableMap typeMap = Arguments.createMap();
if (value == null) {
typeMap.putString("type", "null");
typeMap.putNull("value");
} else {
if (value instanceof Boolean) {
typeMap.putString("type", "boolean");
typeMap.putBoolean("value", (Boolean) value);
} else if (value instanceof Integer) {
typeMap.putString("type", "number");
typeMap.putDouble("value", ((Integer) value).doubleValue());
} else if (value instanceof Long) {
typeMap.putString("type", "number");
typeMap.putDouble("value", ((Long) value).doubleValue());
} else if (value instanceof Double) {
typeMap.putString("type", "number");
typeMap.putDouble("value", (Double) value);
} else if (value instanceof Float) {
typeMap.putString("type", "number");
typeMap.putDouble("value", ((Float) value).doubleValue());
} else if (value instanceof String) {
typeMap.putString("type", "string");
typeMap.putString("value", (String) value);
} else if (Map.class.isAssignableFrom(value.getClass())) {
typeMap.putString("type", "object");
typeMap.putMap("value", objectMapToWritable((Map<String, Object>) value));
} else if (List.class.isAssignableFrom(value.getClass())) {
typeMap.putString("type", "array");
List<Object> list = (List<Object>) value;
Object[] array = list.toArray(new Object[list.size()]);
typeMap.putArray("value", objectArrayToWritable(array));
} else if (value instanceof DocumentReference) {
typeMap.putString("type", "reference");
typeMap.putString("value", ((DocumentReference) value).getPath());
} else if (value instanceof GeoPoint) {
typeMap.putString("type", "geopoint");
WritableMap geoPoint = Arguments.createMap();
geoPoint.putDouble("latitude", ((GeoPoint) value).getLatitude());
geoPoint.putDouble("longitude", ((GeoPoint) value).getLongitude());
typeMap.putMap("value", geoPoint);
} else if (value instanceof Date) {
typeMap.putString("type", "date");
typeMap.putDouble("value", ((Date) value).getTime());
} else if (value instanceof Blob) {
typeMap.putString("type", "blob");
typeMap.putString("value", Base64.encodeToString(((Blob) value).toBytes(), Base64.NO_WRAP));
} else {
Log.e(TAG, "buildTypeMap: Cannot convert object of type " + value.getClass());
typeMap.putString("type", "null");
typeMap.putNull("value");
}
typeMap.putString(TYPE, TYPE_NULL);
typeMap.putNull(VALUE);
return typeMap;
}
if (value instanceof Boolean) {
typeMap.putString(TYPE, TYPE_BOOLEAN);
typeMap.putBoolean(VALUE, (Boolean) value);
return typeMap;
}
if (value instanceof Integer) {
typeMap.putString(TYPE, TYPE_NUMBER);
typeMap.putDouble(VALUE, ((Integer) value).doubleValue());
return typeMap;
}
if (value instanceof Double) {
Double doubleValue = (Double) value;
if (Double.isInfinite(doubleValue)) {
typeMap.putString(TYPE, TYPE_INFINITY);
return typeMap;
}
if (Double.isNaN(doubleValue)) {
typeMap.putString(TYPE, TYPE_NAN);
return typeMap;
}
typeMap.putString(TYPE, TYPE_NUMBER);
typeMap.putDouble(VALUE, doubleValue);
return typeMap;
}
if (value instanceof Float) {
typeMap.putString(TYPE, TYPE_NUMBER);
typeMap.putDouble(VALUE, ((Float) value).doubleValue());
return typeMap;
}
if (value instanceof Long) {
typeMap.putString(TYPE, TYPE_NUMBER);
typeMap.putDouble(VALUE, ((Long) value).doubleValue());
return typeMap;
}
if (value instanceof String) {
typeMap.putString(TYPE, TYPE_STRING);
typeMap.putString(VALUE, (String) value);
return typeMap;
}
if (value instanceof Date) {
typeMap.putString(TYPE, TYPE_DATE);
typeMap.putDouble(VALUE, ((Date) value).getTime());
return typeMap;
}
if (Map.class.isAssignableFrom(value.getClass())) {
typeMap.putString(TYPE, TYPE_OBJECT);
typeMap.putMap(VALUE, objectMapToWritable((Map<String, Object>) value));
return typeMap;
}
if (List.class.isAssignableFrom(value.getClass())) {
typeMap.putString(TYPE, TYPE_ARRAY);
List<Object> list = (List<Object>) value;
Object[] array = list.toArray(new Object[list.size()]);
typeMap.putArray(VALUE, objectArrayToWritable(array));
return typeMap;
}
if (value instanceof DocumentReference) {
typeMap.putString(TYPE, TYPE_REFERENCE);
typeMap.putString(VALUE, ((DocumentReference) value).getPath());
return typeMap;
}
if (value instanceof GeoPoint) {
WritableMap geoPoint = Arguments.createMap();
geoPoint.putDouble(KEY_LATITUDE, ((GeoPoint) value).getLatitude());
geoPoint.putDouble(KEY_LONGITUDE, ((GeoPoint) value).getLongitude());
typeMap.putMap(VALUE, geoPoint);
typeMap.putString(TYPE, TYPE_GEOPOINT);
return typeMap;
}
if (value instanceof Blob) {
typeMap.putString(TYPE, TYPE_BLOB);
typeMap.putString(VALUE, Base64.encodeToString(((Blob) value).toBytes(), Base64.NO_WRAP));
return typeMap;
}
Log.w(TAG, "Unknown object of type " + value.getClass());
typeMap.putString(TYPE, TYPE_NULL);
typeMap.putNull(VALUE);
return typeMap;
}
/**
* Converts a ReadableMap to a usable format for Firestore
*
* @param firestore FirebaseFirestore
* @param readableMap ReadableMap
* @return Map<>
*/
static Map<String, Object> parseReadableMap(
FirebaseFirestore firestore,
ReadableMap readableMap
@Nullable ReadableMap readableMap
) {
Map<String, Object> map = new HashMap<>();
if (readableMap != null) {
ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
map.put(key, parseTypeMap(firestore, readableMap.getMap(key)));
}
if (readableMap == null) return map;
ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
map.put(key, parseTypeMap(firestore, readableMap.getMap(key)));
}
return map;
}
static List<Object> parseReadableArray(FirebaseFirestore firestore, ReadableArray readableArray) {
/**
* Convert a RN array to a valid Firestore array
*
* @param firestore FirebaseFirestore
* @param readableArray ReadableArray
* @return List<Object>
*/
static List<Object> parseReadableArray(
FirebaseFirestore firestore,
@Nullable ReadableArray readableArray
) {
List<Object> list = new ArrayList<>();
if (readableArray != null) {
for (int i = 0; i < readableArray.size(); i++) {
list.add(parseTypeMap(firestore, readableArray.getMap(i)));
}
if (readableArray == null) return list;
for (int i = 0; i < readableArray.size(); i++) {
list.add(parseTypeMap(firestore, readableArray.getMap(i)));
}
return list;
}
/**
* Convert a JS type to a Firestore type
*
* @param firestore FirebaseFirestore
* @param typeMap ReadableMap
* @return Object
*/
static Object parseTypeMap(FirebaseFirestore firestore, ReadableMap typeMap) {
String type = typeMap.getString("type");
if ("boolean".equals(type)) {
return typeMap.getBoolean("value");
} else if ("number".equals(type)) {
return typeMap.getDouble("value");
} else if ("string".equals(type)) {
return typeMap.getString("value");
} else if ("null".equals(type)) {
return null;
} else if ("array".equals(type)) {
return parseReadableArray(firestore, typeMap.getArray("value"));
} else if ("object".equals(type)) {
return parseReadableMap(firestore, typeMap.getMap("value"));
} else if ("reference".equals(type)) {
String path = typeMap.getString("value");
return firestore.document(path);
} else if ("geopoint".equals(type)) {
ReadableMap geoPoint = typeMap.getMap("value");
return new GeoPoint(geoPoint.getDouble("latitude"), geoPoint.getDouble("longitude"));
} else if ("blob".equals(type)) {
String base64String = typeMap.getString("value");
return Blob.fromBytes(Base64.decode(base64String, Base64.NO_WRAP));
} else if ("date".equals(type)) {
Double time = typeMap.getDouble("value");
return new Date(time.longValue());
} else if ("documentid".equals(type)) {
return FieldPath.documentId();
} else if ("fieldvalue".equals(type)) {
String value = typeMap.getString("value");
if ("delete".equals(value)) {
return FieldValue.delete();
} else if ("timestamp".equals(value)) {
return FieldValue.serverTimestamp();
} else {
Log.e(TAG, "parseTypeMap: Invalid fieldvalue: " + value);
return null;
}
} else {
Log.e(TAG, "parseTypeMap: Cannot convert object of type " + type);
String type = typeMap.getString(TYPE);
if (TYPE_NULL.equals(type)) {
return null;
}
if (TYPE_BOOLEAN.equals(type)) {
return typeMap.getBoolean(VALUE);
}
if (TYPE_NAN.equals(type)) {
return Double.NaN;
}
if (TYPE_NUMBER.equals(type)) {
return typeMap.getDouble(VALUE);
}
if (TYPE_INFINITY.equals(type)) {
return Double.POSITIVE_INFINITY;
}
if (TYPE_STRING.equals(type)) {
return typeMap.getString(VALUE);
}
if (TYPE_ARRAY.equals(type)) {
return parseReadableArray(firestore, typeMap.getArray(VALUE));
}
if (TYPE_OBJECT.equals(type)) {
return parseReadableMap(firestore, typeMap.getMap(VALUE));
}
if (TYPE_DATE.equals(type)) {
Double time = typeMap.getDouble(VALUE);
return new Date(time.longValue());
}
/* --------------------------
* Firestore Specific Types
* -------------------------- */
if (TYPE_DOCUMENTID.equals(type)) {
return FieldPath.documentId();
}
if (TYPE_GEOPOINT.equals(type)) {
ReadableMap geoPoint = typeMap.getMap(VALUE);
return new GeoPoint(geoPoint.getDouble(KEY_LATITUDE), geoPoint.getDouble(KEY_LONGITUDE));
}
if (TYPE_BLOB.equals(type)) {
String base64String = typeMap.getString(VALUE);
return Blob.fromBytes(Base64.decode(base64String, Base64.NO_WRAP));
}
if (TYPE_REFERENCE.equals(type)) {
String path = typeMap.getString(VALUE);
return firestore.document(path);
}
if (TYPE_FIELDVALUE.equals(type)) {
String fieldValueType = typeMap.getString(VALUE);
if (TYPE_FIELDVALUE_TIMESTAMP.equals(fieldValueType)) {
return FieldValue.serverTimestamp();
}
if (TYPE_FIELDVALUE_DELETE.equals(fieldValueType)) {
return FieldValue.delete();
}
Log.w(TAG, "Unknown FieldValue type: " + fieldValueType);
return null;
}
Log.w(TAG, "Unknown object of type " + type);
return null;
}
/**
* Parse JS batches array
*
* @param firestore FirebaseFirestore
* @param readableArray ReadableArray
* @return List<Object>
*/
static List<Object> parseDocumentBatches(
FirebaseFirestore firestore,
ReadableArray readableArray
) {
List<Object> writes = new ArrayList<>(readableArray.size());
for (int i = 0; i < readableArray.size(); i++) {
Map<String, Object> write = new HashMap<>();
ReadableMap map = readableArray.getMap(i);
if (map.hasKey("data")) {
write.put("data", parseReadableMap(firestore, map.getMap("data")));
write.put(TYPE, map.getString(TYPE));
write.put(KEY_PATH, map.getString(KEY_PATH));
if (map.hasKey(KEY_DATA)) {
write.put(KEY_DATA, parseReadableMap(firestore, map.getMap(KEY_DATA)));
}
if (map.hasKey("options")) {
write.put("options", Utils.recursivelyDeconstructReadableMap(map.getMap("options")));
if (map.hasKey(KEY_OPTIONS)) {
write.put(
KEY_OPTIONS,
Utils.recursivelyDeconstructReadableMap(map.getMap(KEY_OPTIONS))
);
}
write.put("path", map.getString("path"));
write.put("type", map.getString("type"));
writes.add(write);
}
return writes;
}
}