Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
making it non-static
  • Loading branch information
wu-hui committed May 22, 2019
commit 1533c5e8bfa4479e5fc978ec0bbf808d7066a0e0
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ public DocumentReference document(@NonNull String documentPath) {
public Task<DocumentReference> add(@NonNull Object data) {
checkNotNull(data, "Provided data must not be null.");
final DocumentReference ref = document();
// TODO: Assert fields annotated with DocumentId in `data` are set to null.
return ref.setForAddition(data)
return ref.set(data)
.continueWith(
Executors.DIRECT_EXECUTOR,
task -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,14 @@
import java.lang.annotation.Target;

/**
* Annotation used to mark an object field to be the ID of a firestore document.
*
* <p>This annotation is recognized by {@link com.google.firebase.firestore.util.CustomClassMapper}.
*
* <p>During conversions from documents to java objects, fields with this annotation will be
* populated with the document ID being converted.
*
* <p>When objects with the annotation is used to create new documents, its field value must be null
* to guarantee uniqueness, otherwise a runtime exception will be thrown.
*
* <p>When objects with the annotation is used to update documents, its field value must match the
* target documents, otherwise a runtime exception will be thrown.
* Annotation used to mark a POJO field to be automatically populated with the document's ID when
* the POJO is created from a Firestore document (e.g. via {@link DocumentSnapshot#toObject}).
*
* <p>This annotation can only be applied to fields of String or {@link DocumentReference},
* otherwise a runtime exception will be thrown.
*
* <p>When writing a POJO to Firestore, the @DocumentId-annotated field must either be null or match
* the document ID of the document being written to, else a runtime exception will be thrown.
*/
@PublicApi
@Retention(RetentionPolicy.RUNTIME)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,6 @@ public Task<Void> set(@NonNull Object data) {
return set(data, SetOptions.OVERWRITE);
}

// Handles specific logic for document addition before entering comment `set` logic.
/* package */ Task<Void> setForAddition(@NonNull Object data) {
checkNotNull(data, "Provided data must not be null.");
// TODO: Assert fields annotated with DocumentId in `data` are set to null.
return setImpl(data, SetOptions.OVERWRITE);
}

/**
* Writes to the document referred to by this DocumentReference. If the document does not yet
* exist, it will be created. If you pass {@link SetOptions}, the provided data can be merged into
Expand All @@ -176,16 +169,10 @@ public Task<Void> set(@NonNull Object data) {
public Task<Void> set(@NonNull Object data, @NonNull SetOptions options) {
checkNotNull(data, "Provided data must not be null.");
checkNotNull(options, "Provided options must not be null.");
// TODO: Assert fields annotated with DocumentId in `data` match `documentRef`.
return setImpl(data, options);
}

@NonNull
private Task<Void> setImpl(@NonNull Object data, @NonNull SetOptions options) {
ParsedSetData parsed =
options.isMerge()
? firestore.getDataConverter().parseMergeData(data, options.getFieldMask())
: firestore.getDataConverter().parseSetData(data);
? firestore.getDataConverter().parseMergeData(data, options.getFieldMask(), getId())
: firestore.getDataConverter().parseSetData(data, getId());
return firestore
.getClient()
.write(parsed.toMutationList(key, Precondition.NONE))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,10 @@ public <T> T toObject(
checkNotNull(
serverTimestampBehavior, "Provided serverTimestampBehavior value must not be null.");
Map<String, Object> data = getData(serverTimestampBehavior);
return data == null ? null : CustomClassMapper.convertToCustomClass(data, valueType);
return data == null
? null
: CustomClassMapper.withDocumentId(key.getPath().getLastSegment())
.convertToCustomClass(data, valueType);
}

/**
Expand Down Expand Up @@ -353,7 +356,10 @@ public <T> T get(
@NonNull Class<T> valueType,
@NonNull ServerTimestampBehavior serverTimestampBehavior) {
Object data = get(fieldPath, serverTimestampBehavior);
return data == null ? null : CustomClassMapper.convertToCustomClass(data, valueType);
return data == null
? null
: CustomClassMapper.withDocumentId(key.getPath().getLastSegment())
.convertToCustomClass(data, valueType);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,12 @@ public Transaction set(
firestore.validateReference(documentRef);
checkNotNull(data, "Provided data must not be null.");
checkNotNull(options, "Provided options must not be null.");
// TODO: Assert fields annotated with DocumentId in `data` match `documentRef`.
ParsedSetData parsed =
options.isMerge()
? firestore.getDataConverter().parseMergeData(data, options.getFieldMask())
: firestore.getDataConverter().parseSetData(data);
? firestore
.getDataConverter()
.parseMergeData(data, options.getFieldMask(), documentRef.getId())
: firestore.getDataConverter().parseSetData(data, documentRef.getId());
transaction.set(documentRef.getKey(), parsed);
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,33 @@ public UserDataConverter(DatabaseId databaseId) {
this.databaseId = databaseId;
}

/** Parse document data from a non-merge set() call. */
public ParsedSetData parseSetData(Object input) {
/**
* Parse document data from a non-merge set() call.
*
* @param input A POJO object representing document data.
* @param expectedId If `input` has any fields annotated with {@link DocumentId}, they should
* either be null or match this parameter.
*/
public ParsedSetData parseSetData(Object input, String expectedId) {
ParseAccumulator accumulator = new ParseAccumulator(UserData.Source.Set);
ObjectValue updateData = convertAndParseDocumentData(input, accumulator.rootContext());
ObjectValue updateData =
convertAndParseDocumentData(input, accumulator.rootContext(), expectedId);
return accumulator.toSetData(updateData);
}

/** Parse document data from a set() call with SetOptions.merge() set. */
public ParsedSetData parseMergeData(Object input, @Nullable FieldMask fieldMask) {
/**
* Parse document data from a set() call with SetOptions.merge() set.
*
* @param input A POJO object representing document data.
* @param fieldMask A {@link FieldMask} object representing the fields to be merged.
* @param expectedId If `input` has any fields annotated with {@link DocumentId}, they should
* either be null or match this parameter.
*/
public ParsedSetData parseMergeData(
Object input, @Nullable FieldMask fieldMask, String expectedId) {
ParseAccumulator accumulator = new ParseAccumulator(UserData.Source.MergeSet);
ObjectValue updateData = convertAndParseDocumentData(input, accumulator.rootContext());
ObjectValue updateData =
convertAndParseDocumentData(input, accumulator.rootContext(), expectedId);

if (fieldMask != null) {
// Verify that all elements specified in the field mask are part of the parsed context.
Expand Down Expand Up @@ -198,7 +214,7 @@ public FieldValue parseQueryValue(Object input) {

/** Converts a POJO to native types and then parses it into model types. */
private FieldValue convertAndParseFieldData(Object input, ParseContext context) {
Object converted = CustomClassMapper.convertToPlainJavaTypes(input);
Object converted = CustomClassMapper.withDocumentId(null).convertToPlainJavaTypes(input);
return parseData(converted, context);
}

Expand All @@ -207,7 +223,8 @@ private FieldValue convertAndParseFieldData(Object input, ParseContext context)
* conform to document data (i.e. it must parse into an ObjectValue model type) and will throw an
* appropriate error otherwise.
*/
private ObjectValue convertAndParseDocumentData(Object input, ParseContext context) {
private ObjectValue convertAndParseDocumentData(
Object input, ParseContext context, String expectedId) {
String badDocReason =
"Invalid data. Data must be a Map<String, Object> or a suitable POJO object, but it was ";

Expand All @@ -217,7 +234,7 @@ private ObjectValue convertAndParseDocumentData(Object input, ParseContext conte
throw new IllegalArgumentException(badDocReason + "an array");
}

Object converted = CustomClassMapper.convertToPlainJavaTypes(input);
Object converted = CustomClassMapper.withDocumentId(expectedId).convertToPlainJavaTypes(input);
FieldValue value = parseData(converted, context);

if (!(value instanceof ObjectValue)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,13 @@ public WriteBatch set(
firestore.validateReference(documentRef);
checkNotNull(data, "Provided data must not be null.");
checkNotNull(options, "Provided options must not be null.");
// TODO: Assert fields annotated with DocumentId in `data` match `documentRef`.
verifyNotCommitted();
ParsedSetData parsed =
options.isMerge()
? firestore.getDataConverter().parseMergeData(data, options.getFieldMask())
: firestore.getDataConverter().parseSetData(data);
? firestore
.getDataConverter()
.parseMergeData(data, options.getFieldMask(), documentRef.getId())
: firestore.getDataConverter().parseSetData(data, documentRef.getId());
mutations.addAll(parsed.toMutationList(documentRef.getKey(), Precondition.NONE));
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.annotation.Nullable;

/** Helper class to convert to/from custom POJO classes and plain Java types. */
public class CustomClassMapper {
Expand All @@ -67,18 +66,35 @@ private static void hardAssert(boolean assertion, String message) {
}
}

/**
* Returns a mapper with a given document ID. The document ID will be used to populate the {@link
* com.google.firebase.firestore.DocumentId} annotated fields of the POJO of the custom class;
* when converting a POJO to plain Java type, the @DocumentId-annotated fields should be either
* null or match the given document ID.
*/
// TODO: actually implement documentId population/validation logic during (de)serialization.
public static CustomClassMapper withDocumentId(String documentId) {
return new CustomClassMapper(documentId);
}

private CustomClassMapper(String documentId) {
this.documentId = documentId;
}

private String documentId = null;

/**
* Converts a Java representation of JSON data to standard library Java data types: Map, Array,
* String, Double, Integer and Boolean. POJOs are converted to Java Maps.
*
* @param object The representation of the JSON data
* @return JSON representation containing only standard library Java types
*/
public static Object convertToPlainJavaTypes(Object object) {
public Object convertToPlainJavaTypes(Object object) {
return serialize(object);
}

public static Map<String, Object> convertToPlainJavaTypes(Map<?, Object> update) {
public Map<String, Object> convertToPlainJavaTypes(Map<?, Object> update) {
Object converted = serialize(update);
hardAssert(converted instanceof Map);
@SuppressWarnings("unchecked")
Expand All @@ -94,25 +110,16 @@ public static Map<String, Object> convertToPlainJavaTypes(Map<?, Object> update)
* @param clazz The class of the object to convert to
* @return The POJO object.
*/
public static <T> T convertToCustomClass(Object object, Class<T> clazz) {
public <T> T convertToCustomClass(Object object, Class<T> clazz) {
return deserializeToClass(object, clazz, ErrorPath.EMPTY);
}

/**
* If there are fields annotated with {@link com.google.firebase.firestore.DocumentId}, make sure
* they are set to expected value by throwing runtime exception when it fails to comply.
*
* @param object Java object that might have DocumentId annotated fields.
* @param expected Expected value of the DocumentId annotated fields.
*/
public static void assertAnnotatedDocumentIdEqual(Object object, @Nullable Object expected) {}

private static <T> Object serialize(T o) {
private <T> Object serialize(T o) {
return serialize(o, ErrorPath.EMPTY);
}

@SuppressWarnings("unchecked")
private static <T> Object serialize(T o, ErrorPath path) {
private <T> Object serialize(T o, ErrorPath path) {
if (path.getLength() > MAX_DEPTH) {
throw serializeError(
path,
Expand Down Expand Up @@ -182,7 +189,7 @@ private static <T> Object serialize(T o, ErrorPath path) {
} else {
Class<T> clazz = (Class<T>) o.getClass();
BeanMapper<T> mapper = loadOrCreateBeanMapperForClass(clazz);
return mapper.serialize(o, path);
return mapper.serialize(o, path, this);
}
}

Expand Down Expand Up @@ -763,7 +770,7 @@ private Type resolveType(Type type, Map<TypeVariable<Class<T>>, Type> types) {
}
}

Map<String, Object> serialize(T object, ErrorPath path) {
Map<String, Object> serialize(T object, ErrorPath path, CustomClassMapper classMapper) {
if (!clazz.isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Can't serialize object of class "
Expand Down Expand Up @@ -795,7 +802,7 @@ Map<String, Object> serialize(T object, ErrorPath path) {
// Replace null ServerTimestamp-annotated fields with the sentinel.
serializedValue = FieldValue.serverTimestamp();
} else {
serializedValue = CustomClassMapper.serialize(propertyValue, path.child(property));
serializedValue = classMapper.serialize(propertyValue, path.child(property));
}
result.put(property, serializedValue);
}
Expand Down
Loading