From 148430b6cc5643123b9c67fd3058512689aa9f3d Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:11:53 -0400 Subject: [PATCH] Revert "Feat: Record serialization/deserialization (#1706)" This reverts commit f5613b4ecb46c61cd7c24ae55606a63f92303360. --- google-cloud-firestore/pom.xml | 56 - .../cloud/firestore/CollectionReference.java | 1 - .../cloud/firestore/CustomClassMapper.java | 1279 +++++++++++++++++ .../cloud/firestore/DocumentSnapshot.java | 1 - .../google/cloud/firestore/FieldValue.java | 1 - .../com/google/cloud/firestore/Internal.java | 1 - .../com/google/cloud/firestore/Query.java | 1 - .../google/cloud/firestore/QuerySnapshot.java | 1 - .../google/cloud/firestore/UpdateBuilder.java | 1 - .../cloud/firestore/encoding/BeanMapper.java | 204 --- .../firestore/encoding/CustomClassMapper.java | 628 -------- .../encoding/DeserializeContext.java | 92 -- .../firestore/encoding/PojoBeanMapper.java | 494 ------- .../firestore/encoding/RecordMapper.java | 230 --- .../RecordDocumentReferenceTest.java | 407 ------ .../cloud/firestore/RecordMapperTest.java | 1134 --------------- .../cloud/firestore/RecordTestHelper.java | 224 --- .../google/cloud/firestore/MapperTest.java | 1 - .../google/cloud/firestore/ToStringTest.java | 1 - 19 files changed, 1279 insertions(+), 3478 deletions(-) create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/CustomClassMapper.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/DeserializeContext.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java delete mode 100644 google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java delete mode 100644 google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java delete mode 100644 google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordTestHelper.java diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 3b0648529..bc68c0738 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -321,61 +321,5 @@ - - - java17-test - - 17 - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - add-test-source - generate-test-sources - - add-test-source - - - - src/test-jdk17/java - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - true - - - 17 - 17 - - -parameters - --add-opens=java.base/java.lang=ALL-UNNAMED - --add-opens=java.base/java.util=ALL-UNNAMED - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - src/test-jdk17/**/*.java - - 17 - 17 - - - - - diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java index e1c2841d6..c736d7028 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java @@ -22,7 +22,6 @@ import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.ApiExceptions; import com.google.api.gax.rpc.UnaryCallable; -import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.cloud.firestore.telemetry.TraceUtil.Scope; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CustomClassMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CustomClassMapper.java new file mode 100644 index 000000000..1369091e4 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CustomClassMapper.java @@ -0,0 +1,1279 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore; + +import com.google.cloud.Timestamp; +import com.google.cloud.firestore.annotation.DocumentId; +import com.google.cloud.firestore.annotation.Exclude; +import com.google.cloud.firestore.annotation.IgnoreExtraProperties; +import com.google.cloud.firestore.annotation.PropertyName; +import com.google.cloud.firestore.annotation.ServerTimestamp; +import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; +import com.google.firestore.v1.Value; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Logger; + +/** Helper class to convert to/from custom POJO classes and plain Java types. */ +class CustomClassMapper { + private static final Logger LOGGER = Logger.getLogger(CustomClassMapper.class.getName()); + + /** Maximum depth before we give up and assume it's a recursive object graph. */ + private static final int MAX_DEPTH = 500; + + private static final ConcurrentMap, BeanMapper> mappers = new ConcurrentHashMap<>(); + + private static void hardAssert(boolean assertion) { + hardAssert(assertion, "Internal inconsistency"); + } + + private static void hardAssert(boolean assertion, String message) { + if (!assertion) { + throw new RuntimeException("Hard assert failed: " + message); + } + } + + /** + * 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 + */ + static Object convertToPlainJavaTypes(Object object) { + return serialize(object); + } + + public static Map convertToPlainJavaTypes(Map update) { + Object converted = serialize(update); + hardAssert(converted instanceof Map); + @SuppressWarnings("unchecked") + Map convertedMap = (Map) converted; + return convertedMap; + } + + /** + * Converts a standard library Java representation of JSON data to an object of the provided + * class. + * + * @param object The representation of the JSON data + * @param clazz The class of the object to convert to + * @param docRef The value to set to {@link DocumentId} annotated fields in the custom class. + * @return The POJO object. + */ + static T convertToCustomClass(Object object, Class clazz, DocumentReference docRef) { + return deserializeToClass(object, clazz, new DeserializeContext(ErrorPath.EMPTY, docRef)); + } + + static Object serialize(T o) { + return serialize(o, ErrorPath.EMPTY); + } + + @SuppressWarnings("unchecked") + private static Object serialize(T o, ErrorPath path) { + if (path.getLength() > MAX_DEPTH) { + throw serializeError( + path, + "Exceeded maximum depth of " + + MAX_DEPTH + + ", which likely indicates there's an object cycle"); + } + if (o == null) { + return null; + } else if (o instanceof Number) { + if (o instanceof Long || o instanceof Integer || o instanceof Double || o instanceof Float) { + return o; + } else if (o instanceof BigDecimal) { + return String.valueOf(o); + } else { + throw serializeError( + path, + String.format( + "Numbers of type %s are not supported, please use an int, long, float, double or BigDecimal", + o.getClass().getSimpleName())); + } + } else if (o instanceof String) { + return o; + } else if (o instanceof Boolean) { + return o; + } else if (o instanceof Character) { + throw serializeError(path, "Characters are not supported, please use Strings"); + } else if (o instanceof Map) { + Map result = new HashMap<>(); + for (Map.Entry entry : ((Map) o).entrySet()) { + Object key = entry.getKey(); + if (key instanceof String) { + String keyString = (String) key; + result.put(keyString, serialize(entry.getValue(), path.child(keyString))); + } else { + throw serializeError(path, "Maps with non-string keys are not supported"); + } + } + return result; + } else if (o instanceof Collection) { + if (o instanceof List) { + List list = (List) o; + List result = new ArrayList<>(list.size()); + for (int i = 0; i < list.size(); i++) { + result.add(serialize(list.get(i), path.child("[" + i + "]"))); + } + return result; + } else { + throw serializeError( + path, "Serializing Collections is not supported, please use Lists instead"); + } + } else if (o.getClass().isArray()) { + throw serializeError(path, "Serializing Arrays is not supported, please use Lists instead"); + } else if (o instanceof Enum) { + String enumName = ((Enum) o).name(); + try { + Field enumField = o.getClass().getField(enumName); + return BeanMapper.propertyName(enumField); + } catch (NoSuchFieldException ex) { + return enumName; + } + } else if (o instanceof Date + || o instanceof Timestamp + || o instanceof GeoPoint + || o instanceof Blob + || o instanceof DocumentReference + || o instanceof FieldValue + || o instanceof Value + || o instanceof VectorValue) { + return o; + } else if (o instanceof Instant) { + Instant instant = (Instant) o; + return Timestamp.ofTimeSecondsAndNanos(instant.getEpochSecond(), instant.getNano()); + } else { + Class clazz = (Class) o.getClass(); + BeanMapper mapper = loadOrCreateBeanMapperForClass(clazz); + return mapper.serialize(o, path); + } + } + + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + private static T deserializeToType(Object o, Type type, DeserializeContext context) { + if (o == null) { + return null; + } else if (type instanceof ParameterizedType) { + return deserializeToParameterizedType(o, (ParameterizedType) type, context); + } else if (type instanceof Class) { + return deserializeToClass(o, (Class) type, context); + } else if (type instanceof WildcardType) { + Type[] lowerBounds = ((WildcardType) type).getLowerBounds(); + if (lowerBounds.length > 0) { + throw deserializeError( + context.errorPath, "Generic lower-bounded wildcard types are not supported"); + } + + // Upper bounded wildcards are of the form . Multiple upper bounds are allowed + // but if any of the bounds are of class type, that bound must come first in this array. Note + // that this array always has at least one element, since the unbounded wildcard always + // has at least an upper bound of Object. + Type[] upperBounds = ((WildcardType) type).getUpperBounds(); + hardAssert(upperBounds.length > 0, "Unexpected type bounds on wildcard " + type); + return deserializeToType(o, upperBounds[0], context); + } else if (type instanceof TypeVariable) { + // As above, TypeVariables always have at least one upper bound of Object. + Type[] upperBounds = ((TypeVariable) type).getBounds(); + hardAssert(upperBounds.length > 0, "Unexpected type bounds on type variable " + type); + return deserializeToType(o, upperBounds[0], context); + + } else if (type instanceof GenericArrayType) { + throw deserializeError( + context.errorPath, "Generic Arrays are not supported, please use Lists instead"); + } else { + throw deserializeError(context.errorPath, "Unknown type encountered: " + type); + } + } + + @SuppressWarnings("unchecked") + private static T deserializeToClass(Object o, Class clazz, DeserializeContext context) { + if (o == null) { + return null; + } else if (clazz.isPrimitive() + || Number.class.isAssignableFrom(clazz) + || Boolean.class.isAssignableFrom(clazz) + || Character.class.isAssignableFrom(clazz)) { + return deserializeToPrimitive(o, clazz, context); + } else if (String.class.isAssignableFrom(clazz)) { + return (T) convertString(o, context); + } else if (Date.class.isAssignableFrom(clazz)) { + return (T) convertDate(o, context); + } else if (Timestamp.class.isAssignableFrom(clazz)) { + return (T) convertTimestamp(o, context); + } else if (Instant.class.isAssignableFrom(clazz)) { + return (T) convertInstant(o, context); + } else if (Blob.class.isAssignableFrom(clazz)) { + return (T) convertBlob(o, context); + } else if (GeoPoint.class.isAssignableFrom(clazz)) { + return (T) convertGeoPoint(o, context); + } else if (VectorValue.class.isAssignableFrom(clazz)) { + return (T) convertVectorValue(o, context); + } else if (DocumentReference.class.isAssignableFrom(clazz)) { + return (T) convertDocumentReference(o, context); + } else if (clazz.isArray()) { + throw deserializeError( + context.errorPath, "Converting to Arrays is not supported, please use Lists instead"); + } else if (clazz.getTypeParameters().length > 0) { + throw deserializeError( + context.errorPath, + "Class " + + clazz.getName() + + " has generic type parameters, please use GenericTypeIndicator instead"); + } else if (clazz.equals(Object.class)) { + return (T) o; + } else if (clazz.isEnum()) { + return deserializeToEnum(o, clazz, context); + } else { + return convertBean(o, clazz, context); + } + } + + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + private static T deserializeToParameterizedType( + Object o, ParameterizedType type, DeserializeContext context) { + // getRawType should always return a Class + Class rawType = (Class) type.getRawType(); + if (List.class.isAssignableFrom(rawType)) { + Type genericType = type.getActualTypeArguments()[0]; + if (o instanceof List) { + List list = (List) o; + List result; + try { + result = + (rawType == List.class) + ? new ArrayList<>(list.size()) + : (List) rawType.getDeclaredConstructor().newInstance(); + } catch (InstantiationException + | IllegalAccessException + | NoSuchMethodException + | InvocationTargetException e) { + throw deserializeError( + context.errorPath, + String.format( + "Unable to deserialize to %s: %s", rawType.getSimpleName(), e.toString())); + } + for (int i = 0; i < list.size(); i++) { + result.add( + deserializeToType( + list.get(i), + genericType, + context.newInstanceWithErrorPath(context.errorPath.child("[" + i + "]")))); + } + return (T) result; + } else { + throw deserializeError(context.errorPath, "Expected a List, but got a " + o.getClass()); + } + } else if (Map.class.isAssignableFrom(rawType)) { + Type keyType = type.getActualTypeArguments()[0]; + Type valueType = type.getActualTypeArguments()[1]; + if (!keyType.equals(String.class)) { + throw deserializeError( + context.errorPath, + "Only Maps with string keys are supported, but found Map with key type " + keyType); + } + Map map = expectMap(o, context); + HashMap result; + try { + result = + (rawType == Map.class) + ? new HashMap<>() + : (HashMap) rawType.getDeclaredConstructor().newInstance(); + } catch (InstantiationException + | IllegalAccessException + | NoSuchMethodException + | InvocationTargetException e) { + throw deserializeError( + context.errorPath, + String.format( + "Unable to deserialize to %s: %s", rawType.getSimpleName(), e.toString())); + } + for (Map.Entry entry : map.entrySet()) { + result.put( + entry.getKey(), + deserializeToType( + entry.getValue(), + valueType, + context.newInstanceWithErrorPath(context.errorPath.child(entry.getKey())))); + } + return (T) result; + } else if (Collection.class.isAssignableFrom(rawType)) { + throw deserializeError( + context.errorPath, "Collections are not supported, please use Lists instead"); + } else { + Map map = expectMap(o, context); + BeanMapper mapper = (BeanMapper) loadOrCreateBeanMapperForClass(rawType); + HashMap>, Type> typeMapping = new HashMap<>(); + TypeVariable>[] typeVariables = mapper.clazz.getTypeParameters(); + Type[] types = type.getActualTypeArguments(); + if (types.length != typeVariables.length) { + throw new IllegalStateException("Mismatched lengths for type variables and actual types"); + } + for (int i = 0; i < typeVariables.length; i++) { + typeMapping.put(typeVariables[i], types[i]); + } + return mapper.deserialize(map, typeMapping, context); + } + } + + @SuppressWarnings("unchecked") + private static T deserializeToPrimitive( + Object o, Class clazz, DeserializeContext context) { + if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) { + return (T) convertInteger(o, context); + } else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) { + return (T) convertBoolean(o, context); + } else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) { + return (T) convertDouble(o, context); + } else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) { + return (T) convertLong(o, context); + } else if (BigDecimal.class.isAssignableFrom(clazz)) { + return (T) convertBigDecimal(o, context); + } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) { + return (T) (Float) convertDouble(o, context).floatValue(); + } else { + throw deserializeError( + context.errorPath, + String.format("Deserializing values to %s is not supported", clazz.getSimpleName())); + } + } + + @SuppressWarnings("unchecked") + private static T deserializeToEnum( + Object object, Class clazz, DeserializeContext context) { + if (object instanceof String) { + String value = (String) object; + // We cast to Class without generics here since we can't prove the bound + // T extends Enum statically + + // try to use PropertyName if exist + Field[] enumFields = clazz.getFields(); + for (Field field : enumFields) { + if (field.isEnumConstant()) { + String propertyName = BeanMapper.propertyName(field); + if (value.equals(propertyName)) { + value = field.getName(); + break; + } + } + } + + try { + return (T) Enum.valueOf((Class) clazz, value); + } catch (IllegalArgumentException e) { + throw deserializeError( + context.errorPath, + "Could not find enum value of " + clazz.getName() + " for value \"" + value + "\""); + } + } else { + throw deserializeError( + context.errorPath, + "Expected a String while deserializing to enum " + + clazz + + " but got a " + + object.getClass()); + } + } + + private static BeanMapper loadOrCreateBeanMapperForClass(Class clazz) { + @SuppressWarnings("unchecked") + BeanMapper mapper = (BeanMapper) mappers.get(clazz); + if (mapper == null) { + mapper = new BeanMapper<>(clazz); + // Inserting without checking is fine because mappers are "pure" and it's okay + // if we create and use multiple by different threads temporarily + mappers.put(clazz, mapper); + } + return mapper; + } + + @SuppressWarnings("unchecked") + private static Map expectMap(Object object, DeserializeContext context) { + if (object instanceof Map) { + // TODO: runtime validation of keys? + return (Map) object; + } else { + throw deserializeError( + context.errorPath, "Expected a Map while deserializing, but got a " + object.getClass()); + } + } + + private static Integer convertInteger(Object o, DeserializeContext context) { + if (o instanceof Integer) { + return (Integer) o; + } else if (o instanceof Long || o instanceof Double) { + double value = ((Number) o).doubleValue(); + if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { + return ((Number) o).intValue(); + } else { + throw deserializeError( + context.errorPath, + "Numeric value out of 32-bit integer range: " + + value + + ". Did you mean to use a long or double instead of an int?"); + } + } else { + throw deserializeError( + context.errorPath, + "Failed to convert a value of type " + o.getClass().getName() + " to int"); + } + } + + private static Long convertLong(Object o, DeserializeContext context) { + if (o instanceof Integer) { + return ((Integer) o).longValue(); + } else if (o instanceof Long) { + return (Long) o; + } else if (o instanceof Double) { + Double value = (Double) o; + if (value >= Long.MIN_VALUE && value <= Long.MAX_VALUE) { + return value.longValue(); + } else { + throw deserializeError( + context.errorPath, + "Numeric value out of 64-bit long range: " + + value + + ". Did you mean to use a double instead of a long?"); + } + } else { + throw deserializeError( + context.errorPath, + "Failed to convert a value of type " + o.getClass().getName() + " to long"); + } + } + + private static Double convertDouble(Object o, DeserializeContext context) { + if (o instanceof Integer) { + return ((Integer) o).doubleValue(); + } else if (o instanceof Long) { + Long value = (Long) o; + Double doubleValue = ((Long) o).doubleValue(); + if (doubleValue.longValue() == value) { + return doubleValue; + } else { + throw deserializeError( + context.errorPath, + "Loss of precision while converting number to " + + "double: " + + o + + ". Did you mean to use a 64-bit long instead?"); + } + } else if (o instanceof Double) { + return (Double) o; + } else { + throw deserializeError( + context.errorPath, + "Failed to convert a value of type " + o.getClass().getName() + " to double"); + } + } + + private static BigDecimal convertBigDecimal(Object o, DeserializeContext context) { + if (o instanceof Integer) { + return BigDecimal.valueOf(((Integer) o).intValue()); + } else if (o instanceof Long) { + return BigDecimal.valueOf(((Long) o).longValue()); + } else if (o instanceof Double) { + return BigDecimal.valueOf(((Double) o).doubleValue()).abs(); + } else if (o instanceof BigDecimal) { + return (BigDecimal) o; + } else if (o instanceof String) { + return new BigDecimal((String) o); + } else { + throw deserializeError( + context.errorPath, + "Failed to convert a value of type " + o.getClass().getName() + " to BigDecimal"); + } + } + + private static Boolean convertBoolean(Object o, DeserializeContext context) { + if (o instanceof Boolean) { + return (Boolean) o; + } else { + throw deserializeError( + context.errorPath, + "Failed to convert value of type " + o.getClass().getName() + " to boolean"); + } + } + + private static String convertString(Object o, DeserializeContext context) { + if (o instanceof String) { + return (String) o; + } else { + throw deserializeError( + context.errorPath, + "Failed to convert value of type " + o.getClass().getName() + " to String"); + } + } + + private static Date convertDate(Object o, DeserializeContext context) { + if (o instanceof Date) { + return (Date) o; + } else if (o instanceof Timestamp) { + return ((Timestamp) o).toDate(); + } else { + throw deserializeError( + context.errorPath, + "Failed to convert value of type " + o.getClass().getName() + " to Date"); + } + } + + private static Timestamp convertTimestamp(Object o, DeserializeContext context) { + if (o instanceof Timestamp) { + return (Timestamp) o; + } else if (o instanceof Date) { + return Timestamp.of((Date) o); + } else { + throw deserializeError( + context.errorPath, + "Failed to convert value of type " + o.getClass().getName() + " to Timestamp"); + } + } + + private static Instant convertInstant(Object o, DeserializeContext context) { + if (o instanceof Timestamp) { + Timestamp timestamp = (Timestamp) o; + return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()); + } else if (o instanceof Date) { + return Instant.ofEpochMilli(((Date) o).getTime()); + } else { + throw deserializeError( + context.errorPath, + "Failed to convert value of type " + o.getClass().getName() + " to Instant"); + } + } + + private static Blob convertBlob(Object o, DeserializeContext context) { + if (o instanceof Blob) { + return (Blob) o; + } else { + throw deserializeError( + context.errorPath, + "Failed to convert value of type " + o.getClass().getName() + " to Blob"); + } + } + + private static GeoPoint convertGeoPoint(Object o, DeserializeContext context) { + if (o instanceof GeoPoint) { + return (GeoPoint) o; + } else { + throw deserializeError( + context.errorPath, + "Failed to convert value of type " + o.getClass().getName() + " to GeoPoint"); + } + } + + private static VectorValue convertVectorValue(Object o, DeserializeContext context) { + if (o instanceof VectorValue) { + return (VectorValue) o; + } else { + throw deserializeError( + context.errorPath, + "Failed to convert value of type " + o.getClass().getName() + " to VectorValue"); + } + } + + private static DocumentReference convertDocumentReference(Object o, DeserializeContext context) { + if (o instanceof DocumentReference) { + return (DocumentReference) o; + } else { + throw deserializeError( + context.errorPath, + "Failed to convert value of type " + o.getClass().getName() + " to DocumentReference"); + } + } + + private static T convertBean(Object o, Class clazz, DeserializeContext context) { + BeanMapper mapper = loadOrCreateBeanMapperForClass(clazz); + if (o instanceof Map) { + return mapper.deserialize(expectMap(o, context), context); + } else { + throw deserializeError( + context.errorPath, + "Can't convert object of type " + o.getClass().getName() + " to type " + clazz.getName()); + } + } + + private static IllegalArgumentException serializeError(ErrorPath path, String reason) { + reason = "Could not serialize object. " + reason; + if (path.getLength() > 0) { + reason = reason + " (found in field '" + path.toString() + "')"; + } + return new IllegalArgumentException(reason); + } + + private static RuntimeException deserializeError(ErrorPath path, String reason) { + reason = "Could not deserialize object. " + reason; + if (path.getLength() > 0) { + reason = reason + " (found in field '" + path.toString() + "')"; + } + return new RuntimeException(reason); + } + + // Helper class to convert from maps to custom objects (Beans), and vice versa. + private static class BeanMapper { + private final Class clazz; + private final Constructor constructor; + // Whether to throw exception if there are properties we don't know how to set to + // custom object fields/setters during deserialization. + private final boolean throwOnUnknownProperties; + // Whether to log a message if there are properties we don't know how to set to + // custom object fields/setters during deserialization. + private final boolean warnOnUnknownProperties; + + // Case insensitive mapping of properties to their case sensitive versions + private final Map properties; + + // Below are maps to find getter/setter/field from a given property name. + // A property name is the name annotated by @PropertyName, if exists; or their property name + // following the Java Bean convention: field name is kept as-is while getters/setters will have + // their prefixes removed. See method propertyName for details. + private final Map getters; + private final Map setters; + private final Map fields; + + // A set of property names that were annotated with @ServerTimestamp. + private final HashSet serverTimestamps; + + // A set of property names that were annotated with @DocumentId. These properties will be + // populated with document ID values during deserialization, and be skipped during + // serialization. + private final HashSet documentIdPropertyNames; + + BeanMapper(Class clazz) { + this.clazz = clazz; + throwOnUnknownProperties = clazz.isAnnotationPresent(ThrowOnExtraProperties.class); + warnOnUnknownProperties = !clazz.isAnnotationPresent(IgnoreExtraProperties.class); + properties = new HashMap<>(); + + setters = new HashMap<>(); + getters = new HashMap<>(); + fields = new HashMap<>(); + + serverTimestamps = new HashSet<>(); + documentIdPropertyNames = new HashSet<>(); + + Constructor constructor; + try { + constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + } catch (NoSuchMethodException e) { + // We will only fail at deserialization time if no constructor is present + constructor = null; + } + this.constructor = constructor; + // Add any public getters to properties (including isXyz()) + for (Method method : clazz.getMethods()) { + if (shouldIncludeGetter(method)) { + String propertyName = propertyName(method); + addProperty(propertyName); + method.setAccessible(true); + if (getters.containsKey(propertyName)) { + throw new RuntimeException( + "Found conflicting getters for name " + + method.getName() + + " on class " + + clazz.getName()); + } + getters.put(propertyName, method); + applyGetterAnnotations(method); + } + } + + // Add any public fields to properties + for (Field field : clazz.getFields()) { + if (shouldIncludeField(field)) { + String propertyName = propertyName(field); + addProperty(propertyName); + applyFieldAnnotations(field); + } + } + + // We can use private setters and fields for known (public) properties/getters. Since + // getMethods/getFields only returns public methods/fields we need to traverse the + // class hierarchy to find the appropriate setter or field. + Class currentClass = clazz; + do { + // Add any setters + for (Method method : currentClass.getDeclaredMethods()) { + if (shouldIncludeSetter(method)) { + String propertyName = propertyName(method); + String existingPropertyName = properties.get(propertyName.toLowerCase(Locale.US)); + if (existingPropertyName != null) { + if (!existingPropertyName.equals(propertyName)) { + throw new RuntimeException( + "Found setter on " + + currentClass.getName() + + " with invalid case-sensitive name: " + + method.getName()); + } else { + Method existingSetter = setters.get(propertyName); + if (existingSetter == null) { + method.setAccessible(true); + setters.put(propertyName, method); + applySetterAnnotations(method); + } else if (!isSetterOverride(method, existingSetter)) { + // We require that setters with conflicting property names are + // overrides from a base class + if (currentClass == clazz) { + // TODO: Should we support overloads? + throw new RuntimeException( + "Class " + + clazz.getName() + + " has multiple setter overloads with name " + + method.getName()); + } else { + throw new RuntimeException( + "Found conflicting setters " + + "with name: " + + method.getName() + + " (conflicts with " + + existingSetter.getName() + + " defined on " + + existingSetter.getDeclaringClass().getName() + + ")"); + } + } + } + } + } + } + + for (Field field : currentClass.getDeclaredFields()) { + String propertyName = propertyName(field); + + // Case sensitivity is checked at deserialization time + // Fields are only added if they don't exist on a subclass + if (properties.containsKey(propertyName.toLowerCase(Locale.US)) + && !fields.containsKey(propertyName)) { + field.setAccessible(true); + fields.put(propertyName, field); + applyFieldAnnotations(field); + } + } + + // Traverse class hierarchy until we reach java.lang.Object which contains a bunch + // of fields/getters we don't want to serialize + currentClass = currentClass.getSuperclass(); + } while (currentClass != null && !currentClass.equals(Object.class)); + + if (properties.isEmpty()) { + throw new RuntimeException("No properties to serialize found on class " + clazz.getName()); + } + + // Make sure we can write to @DocumentId annotated properties before proceeding. + for (String docIdProperty : documentIdPropertyNames) { + if (!setters.containsKey(docIdProperty) && !fields.containsKey(docIdProperty)) { + throw new RuntimeException( + "@DocumentId is annotated on property " + + docIdProperty + + " of class " + + clazz.getName() + + " but no field or public setter was found"); + } + } + } + + private void addProperty(String property) { + String oldValue = properties.put(property.toLowerCase(Locale.US), property); + if (oldValue != null && !property.equals(oldValue)) { + throw new RuntimeException( + "Found two getters or fields with conflicting case " + + "sensitivity for property: " + + property.toLowerCase(Locale.US)); + } + } + + T deserialize(Map values, DeserializeContext context) { + return deserialize(values, Collections.emptyMap(), context); + } + + T deserialize( + Map values, + Map>, Type> types, + DeserializeContext context) { + if (constructor == null) { + throw deserializeError( + context.errorPath, + "Class " + + clazz.getName() + + " does not define a no-argument constructor. If you are using ProGuard, make " + + "sure these constructors are not stripped"); + } + + T instance; + try { + instance = constructor.newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + HashSet deserialzedProperties = new HashSet<>(); + for (Map.Entry entry : values.entrySet()) { + String propertyName = entry.getKey(); + ErrorPath childPath = context.errorPath.child(propertyName); + if (setters.containsKey(propertyName)) { + Method setter = setters.get(propertyName); + Type[] params = setter.getGenericParameterTypes(); + if (params.length != 1) { + throw deserializeError(childPath, "Setter does not have exactly one parameter"); + } + Type resolvedType = resolveType(params[0], types); + Object value = + CustomClassMapper.deserializeToType( + entry.getValue(), resolvedType, context.newInstanceWithErrorPath(childPath)); + try { + setter.invoke(instance, value); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + deserialzedProperties.add(propertyName); + } else if (fields.containsKey(propertyName)) { + Field field = fields.get(propertyName); + Type resolvedType = resolveType(field.getGenericType(), types); + Object value = + CustomClassMapper.deserializeToType( + entry.getValue(), resolvedType, context.newInstanceWithErrorPath(childPath)); + try { + field.set(instance, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + deserialzedProperties.add(propertyName); + } else { + String message = + "No setter/field for " + propertyName + " found on class " + clazz.getName(); + if (properties.containsKey(propertyName.toLowerCase(Locale.US))) { + message += " (fields/setters are case sensitive!)"; + } + if (throwOnUnknownProperties) { + throw new RuntimeException(message); + } else if (warnOnUnknownProperties) { + LOGGER.warning(message); + } + } + } + populateDocumentIdProperties(types, context, instance, deserialzedProperties); + + return instance; + } + + // Populate @DocumentId annotated fields. If there is a conflict (@DocumentId annotation is + // applied to a property that is already deserialized from the firestore document) + // a runtime exception will be thrown. + private void populateDocumentIdProperties( + Map>, Type> types, + DeserializeContext context, + T instance, + HashSet deserialzedProperties) { + for (String docIdPropertyName : documentIdPropertyNames) { + if (deserialzedProperties.contains(docIdPropertyName)) { + String message = + "'" + + docIdPropertyName + + "' was found from document " + + context.documentRef.getPath() + + ", cannot apply @DocumentId on this property for class " + + clazz.getName(); + throw new RuntimeException(message); + } + ErrorPath childPath = context.errorPath.child(docIdPropertyName); + if (setters.containsKey(docIdPropertyName)) { + Method setter = setters.get(docIdPropertyName); + Type[] params = setter.getGenericParameterTypes(); + if (params.length != 1) { + throw deserializeError(childPath, "Setter does not have exactly one parameter"); + } + Type resolvedType = resolveType(params[0], types); + try { + if (resolvedType == String.class) { + setter.invoke(instance, context.documentRef.getId()); + } else { + setter.invoke(instance, context.documentRef); + } + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } else { + Field docIdField = fields.get(docIdPropertyName); + try { + if (docIdField.getType() == String.class) { + docIdField.set(instance, context.documentRef.getId()); + } else { + docIdField.set(instance, context.documentRef); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + private Type resolveType(Type type, Map>, Type> types) { + if (type instanceof TypeVariable) { + Type resolvedType = types.get(type); + if (resolvedType == null) { + throw new IllegalStateException("Could not resolve type " + type); + } else { + return resolvedType; + } + } else { + return type; + } + } + + Map serialize(T object, ErrorPath path) { + if (!clazz.isAssignableFrom(object.getClass())) { + throw new IllegalArgumentException( + "Can't serialize object of class " + + object.getClass() + + " with BeanMapper for class " + + clazz); + } + Map result = new HashMap<>(); + for (String property : properties.values()) { + // Skip @DocumentId annotated properties; + if (documentIdPropertyNames.contains(property)) { + continue; + } + + Object propertyValue; + if (getters.containsKey(property)) { + Method getter = getters.get(property); + try { + propertyValue = getter.invoke(object); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } else { + // Must be a field + Field field = fields.get(property); + if (field == null) { + throw new IllegalStateException("Bean property without field or getter: " + property); + } + try { + propertyValue = field.get(object); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + Object serializedValue; + if (serverTimestamps.contains(property) && propertyValue == null) { + // Replace null ServerTimestamp-annotated fields with the sentinel. + serializedValue = FieldValue.serverTimestamp(); + } else { + serializedValue = CustomClassMapper.serialize(propertyValue, path.child(property)); + } + result.put(property, serializedValue); + } + return result; + } + + private void applyFieldAnnotations(Field field) { + if (field.isAnnotationPresent(ServerTimestamp.class)) { + Class fieldType = field.getType(); + if (fieldType != Date.class && fieldType != Timestamp.class && fieldType != Instant.class) { + throw new IllegalArgumentException( + "Field " + + field.getName() + + " is annotated with @ServerTimestamp but is " + + fieldType + + " instead of Date, Timestamp, or Instant."); + } + serverTimestamps.add(propertyName(field)); + } + + if (field.isAnnotationPresent(DocumentId.class)) { + Class fieldType = field.getType(); + ensureValidDocumentIdType("Field", "is", fieldType); + documentIdPropertyNames.add(propertyName(field)); + } + } + + private void applyGetterAnnotations(Method method) { + if (method.isAnnotationPresent(ServerTimestamp.class)) { + Class returnType = method.getReturnType(); + if (returnType != Date.class + && returnType != Timestamp.class + && returnType != Instant.class) { + throw new IllegalArgumentException( + "Method " + + method.getName() + + " is annotated with @ServerTimestamp but returns " + + returnType + + " instead of Date, Timestamp, or Instant."); + } + serverTimestamps.add(propertyName(method)); + } + + // Even though the value will be skipped, we still check for type matching for consistency. + if (method.isAnnotationPresent(DocumentId.class)) { + Class returnType = method.getReturnType(); + ensureValidDocumentIdType("Method", "returns", returnType); + documentIdPropertyNames.add(propertyName(method)); + } + } + + private void applySetterAnnotations(Method method) { + if (method.isAnnotationPresent(ServerTimestamp.class)) { + throw new IllegalArgumentException( + "Method " + + method.getName() + + " is annotated with @ServerTimestamp but should not be. @ServerTimestamp can" + + " only be applied to fields and getters, not setters."); + } + + if (method.isAnnotationPresent(DocumentId.class)) { + Class paramType = method.getParameterTypes()[0]; + ensureValidDocumentIdType("Method", "accepts", paramType); + documentIdPropertyNames.add(propertyName(method)); + } + } + + private void ensureValidDocumentIdType(String fieldDescription, String operation, Type type) { + if (type != String.class && type != DocumentReference.class) { + throw new IllegalArgumentException( + fieldDescription + + " is annotated with @DocumentId but " + + operation + + " " + + type + + " instead of String or DocumentReference."); + } + } + + private static boolean shouldIncludeGetter(Method method) { + if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) { + return false; + } + // Exclude methods from Object.class + if (method.getDeclaringClass().equals(Object.class)) { + return false; + } + // Non-public methods + if (!Modifier.isPublic(method.getModifiers())) { + return false; + } + // Static methods + if (Modifier.isStatic(method.getModifiers())) { + return false; + } + // No return type + if (method.getReturnType().equals(Void.TYPE)) { + return false; + } + // Non-zero parameters + if (method.getParameterTypes().length != 0) { + return false; + } + // Excluded methods + if (method.isAnnotationPresent(Exclude.class)) { + return false; + } + return true; + } + + private static boolean shouldIncludeSetter(Method method) { + if (!method.getName().startsWith("set")) { + return false; + } + // Exclude methods from Object.class + if (method.getDeclaringClass().equals(Object.class)) { + return false; + } + // Static methods + if (Modifier.isStatic(method.getModifiers())) { + return false; + } + // Has a return type + if (!method.getReturnType().equals(Void.TYPE)) { + return false; + } + // Methods without exactly one parameters + if (method.getParameterTypes().length != 1) { + return false; + } + // Excluded methods + if (method.isAnnotationPresent(Exclude.class)) { + return false; + } + return true; + } + + private static boolean shouldIncludeField(Field field) { + // Exclude methods from Object.class + if (field.getDeclaringClass().equals(Object.class)) { + return false; + } + // Non-public fields + if (!Modifier.isPublic(field.getModifiers())) { + return false; + } + // Static fields + if (Modifier.isStatic(field.getModifiers())) { + return false; + } + // Transient fields + if (Modifier.isTransient(field.getModifiers())) { + return false; + } + // Excluded fields + if (field.isAnnotationPresent(Exclude.class)) { + return false; + } + return true; + } + + private static boolean isSetterOverride(Method base, Method override) { + // We expect an overridden setter here + hardAssert( + base.getDeclaringClass().isAssignableFrom(override.getDeclaringClass()), + "Expected override from a base class"); + hardAssert(base.getReturnType().equals(Void.TYPE), "Expected void return type"); + hardAssert(override.getReturnType().equals(Void.TYPE), "Expected void return type"); + + Type[] baseParameterTypes = base.getParameterTypes(); + Type[] overrideParameterTypes = override.getParameterTypes(); + hardAssert(baseParameterTypes.length == 1, "Expected exactly one parameter"); + hardAssert(overrideParameterTypes.length == 1, "Expected exactly one parameter"); + + return base.getName().equals(override.getName()) + && baseParameterTypes[0].equals(overrideParameterTypes[0]); + } + + private static String propertyName(Field field) { + String annotatedName = annotatedName(field); + return annotatedName != null ? annotatedName : field.getName(); + } + + private static String propertyName(Method method) { + String annotatedName = annotatedName(method); + return annotatedName != null ? annotatedName : serializedName(method.getName()); + } + + private static String annotatedName(AccessibleObject obj) { + if (obj.isAnnotationPresent(PropertyName.class)) { + PropertyName annotation = obj.getAnnotation(PropertyName.class); + return annotation.value(); + } + + return null; + } + + private static String serializedName(String methodName) { + String[] prefixes = new String[] {"get", "set", "is"}; + String methodPrefix = null; + for (String prefix : prefixes) { + if (methodName.startsWith(prefix)) { + methodPrefix = prefix; + } + } + if (methodPrefix == null) { + throw new IllegalArgumentException("Unknown Bean prefix for method: " + methodName); + } + String strippedName = methodName.substring(methodPrefix.length()); + + // Make sure the first word or upper-case prefix is converted to lower-case + char[] chars = strippedName.toCharArray(); + int pos = 0; + while (pos < chars.length && Character.isUpperCase(chars[pos])) { + chars[pos] = Character.toLowerCase(chars[pos]); + pos++; + } + return new String(chars); + } + } + + /** + * Immutable class representing the path to a specific field in an object. Used to provide better + * error messages. + */ + static class ErrorPath { + private final int length; + private final ErrorPath parent; + private final String name; + + static final ErrorPath EMPTY = new ErrorPath(null, null, 0); + + ErrorPath(ErrorPath parent, String name, int length) { + this.parent = parent; + this.name = name; + this.length = length; + } + + int getLength() { + return length; + } + + ErrorPath child(String name) { + return new ErrorPath(this, name, length + 1); + } + + @Override + public String toString() { + if (length == 0) { + return ""; + } else if (length == 1) { + return name; + } else { + // This is not very efficient, but it's only hit if there's an error. + return parent.toString() + "." + name; + } + } + } + + /** Holds information a deserialization operation needs to complete the job. */ + private static class DeserializeContext { + + /** Current path to the field being deserialized, used for better error messages. */ + final ErrorPath errorPath; + + /** Value used to set to {@link DocumentId} annotated fields during deserialization, if any. */ + final DocumentReference documentRef; + + DeserializeContext(ErrorPath path, DocumentReference docRef) { + errorPath = path; + documentRef = docRef; + } + + DeserializeContext newInstanceWithErrorPath(ErrorPath newPath) { + return new DeserializeContext(newPath, documentRef); + } + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java index e1aab1cac..3f27040ad 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java @@ -19,7 +19,6 @@ import com.google.api.core.InternalExtensionOnly; import com.google.cloud.Timestamp; import com.google.cloud.firestore.UserDataConverter.EncodingOptions; -import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.common.base.Preconditions; import com.google.firestore.v1.Document; import com.google.firestore.v1.Value; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldValue.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldValue.java index 2cfc41acf..5f9e406da 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldValue.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldValue.java @@ -16,7 +16,6 @@ package com.google.cloud.firestore; -import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.common.base.Preconditions; import com.google.firestore.v1.ArrayValue; import com.google.firestore.v1.DocumentTransform.FieldTransform; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Internal.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Internal.java index b25701075..46f16f5f3 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Internal.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Internal.java @@ -20,7 +20,6 @@ import com.google.api.core.InternalApi; import com.google.cloud.Timestamp; -import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.common.base.Preconditions; import com.google.firestore.v1.Document; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index a794b6a63..4721ba93d 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -39,7 +39,6 @@ import com.google.auto.value.AutoValue; import com.google.cloud.Timestamp; import com.google.cloud.firestore.Query.QueryOptions.Builder; -import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.cloud.firestore.telemetry.TraceUtil.Scope; import com.google.cloud.firestore.v1.FirestoreSettings; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/QuerySnapshot.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/QuerySnapshot.java index 494a298e4..4f51cd583 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/QuerySnapshot.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/QuerySnapshot.java @@ -18,7 +18,6 @@ import com.google.cloud.Timestamp; import com.google.cloud.firestore.DocumentChange.Type; -import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Collections; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java index 31434667b..e93fe8310 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java @@ -25,7 +25,6 @@ import com.google.api.core.ApiFutures; import com.google.api.core.InternalExtensionOnly; import com.google.cloud.firestore.UserDataConverter.EncodingOptions; -import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.cloud.firestore.telemetry.TraceUtil.Scope; import com.google.common.base.Preconditions; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java deleted file mode 100644 index fd5067b08..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.encoding; - -import com.google.cloud.Timestamp; -import com.google.cloud.firestore.DocumentReference; -import com.google.cloud.firestore.FieldValue; -import com.google.cloud.firestore.annotation.DocumentId; -import com.google.cloud.firestore.annotation.IgnoreExtraProperties; -import com.google.cloud.firestore.annotation.PropertyName; -import com.google.cloud.firestore.annotation.ServerTimestamp; -import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; -import java.lang.reflect.AccessibleObject; -import java.lang.reflect.Field; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; -import java.time.Instant; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.Map; - -/** Base bean mapper class, providing common functionality for class and record serialization. */ -abstract class BeanMapper { - private final Class clazz; - // Whether to throw exception if there are properties we don't know how to set to - // custom object fields/setters or record components during deserialization. - private final boolean throwOnUnknownProperties; - // Whether to log a message if there are properties we don't know how to set to - // custom object fields/setters or record components during deserialization. - private final boolean warnOnUnknownProperties; - // A set of property names that were annotated with @ServerTimestamp. - final HashSet serverTimestamps; - // A set of property names that were annotated with @DocumentId. These properties will be - // populated with document ID values during deserialization, and be skipped during - // serialization. - final HashSet documentIdPropertyNames; - - BeanMapper(Class clazz) { - this.clazz = clazz; - throwOnUnknownProperties = clazz.isAnnotationPresent(ThrowOnExtraProperties.class); - warnOnUnknownProperties = !clazz.isAnnotationPresent(IgnoreExtraProperties.class); - serverTimestamps = new HashSet<>(); - documentIdPropertyNames = new HashSet<>(); - } - - Class getClazz() { - return clazz; - } - - boolean isThrowOnUnknownProperties() { - return throwOnUnknownProperties; - } - - boolean isWarnOnUnknownProperties() { - return warnOnUnknownProperties; - } - - /** - * Serialize an object to a map. - * - * @param object the object to serialize - * @param path the path to a specific field/component in an object, for use in error messages - * @return the map - */ - abstract Map serialize(T object, DeserializeContext.ErrorPath path); - - /** - * Deserialize a map to an object. - * - * @param values the map to deserialize - * @param types generic type mappings - * @param context context information about the deserialization operation - * @return the deserialized object - */ - abstract T deserialize( - Map values, - Map>, Type> types, - DeserializeContext context); - - T deserialize(Map values, DeserializeContext context) { - return deserialize(values, Collections.emptyMap(), context); - } - - protected void verifyValidType(T object) { - if (!clazz.isAssignableFrom(object.getClass())) { - throw new IllegalArgumentException( - "Can't serialize object of class " - + object.getClass() - + " with BeanMapper for class " - + clazz); - } - } - - protected Type resolveType(Type type, Map>, Type> types) { - if (type instanceof TypeVariable) { - Type resolvedType = types.get(type); - if (resolvedType == null) { - throw new IllegalStateException("Could not resolve type " + type); - } - - return resolvedType; - } - - return type; - } - - protected void checkForDocIdConflict( - String docIdPropertyName, - Collection deserializedProperties, - DeserializeContext context) { - if (deserializedProperties.contains(docIdPropertyName)) { - String message = - "'" - + docIdPropertyName - + "' was found from document " - + context.documentRef.getPath() - + ", cannot apply @DocumentId on this property for class " - + clazz.getName(); - throw new RuntimeException(message); - } - } - - protected Object getSerializedValue( - String property, Object propertyValue, DeserializeContext.ErrorPath path) { - if (serverTimestamps.contains(property) && propertyValue == null) { - // Replace null ServerTimestamp-annotated fields with the sentinel. - return FieldValue.serverTimestamp(); - } else { - return CustomClassMapper.serialize(propertyValue, path.child(property)); - } - } - - protected void applyFieldAnnotations(Field field) { - Class fieldType = field.getType(); - if (field.isAnnotationPresent(ServerTimestamp.class)) { - validateServerTimestampType("Field", "is", fieldType); - serverTimestamps.add(propertyName(field)); - } - if (field.isAnnotationPresent(DocumentId.class)) { - validateDocumentIdType("Field", "is", fieldType); - documentIdPropertyNames.add(propertyName(field)); - } - } - - protected void validateDocumentIdType(String fieldDescription, String operation, Type type) { - if (type != String.class && type != DocumentReference.class) { - throw new IllegalArgumentException( - fieldDescription - + " is annotated with @DocumentId but " - + operation - + " " - + type - + " instead of String or DocumentReference."); - } - } - - protected void validateServerTimestampType(String fieldDescription, String operation, Type type) { - if (type != Date.class && type != Timestamp.class && type != Instant.class) { - throw new IllegalArgumentException( - fieldDescription - + " is annotated with @ServerTimestamp but " - + operation - + " " - + type - + " instead of Date, Timestamp, or Instant."); - } - } - - protected String propertyName(Field field) { - String annotatedName = annotatedName(field); - return annotatedName != null ? annotatedName : field.getName(); - } - - protected String annotatedName(AccessibleObject obj) { - if (obj.isAnnotationPresent(PropertyName.class)) { - PropertyName annotation = obj.getAnnotation(PropertyName.class); - return annotation.value(); - } - - return null; - } - - protected void hardAssert(boolean assertion, String message) { - if (!assertion) { - throw new RuntimeException("Hard assert failed: " + message); - } - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java deleted file mode 100644 index 321ead3e6..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java +++ /dev/null @@ -1,628 +0,0 @@ -/* - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.encoding; - -import com.google.api.core.InternalApi; -import com.google.cloud.Timestamp; -import com.google.cloud.firestore.Blob; -import com.google.cloud.firestore.DocumentReference; -import com.google.cloud.firestore.FieldValue; -import com.google.cloud.firestore.GeoPoint; -import com.google.cloud.firestore.VectorValue; -import com.google.cloud.firestore.annotation.DocumentId; -import com.google.cloud.firestore.annotation.PropertyName; -import com.google.firestore.v1.Value; -import java.lang.reflect.AccessibleObject; -import java.lang.reflect.Field; -import java.lang.reflect.GenericArrayType; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; -import java.lang.reflect.WildcardType; -import java.math.BigDecimal; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -/** Helper class to convert to/from custom POJO classes and plain Java types. */ -@InternalApi -public class CustomClassMapper { - - /** Maximum depth before we give up and assume it's a recursive object graph. */ - private static final int MAX_DEPTH = 500; - - private static final ConcurrentMap, BeanMapper> mappers = new ConcurrentHashMap<>(); - - /** - * 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) { - return serialize(object); - } - - public static Map convertToPlainJavaTypes(Map update) { - Object converted = serialize(update); - hardAssert(converted instanceof Map); - @SuppressWarnings("unchecked") - Map convertedMap = (Map) converted; - return convertedMap; - } - - /** - * Converts a standard library Java representation of JSON data to an object of the provided - * class. - * - * @param object The representation of the JSON data - * @param clazz The class of the object to convert to - * @param docRef The value to set to {@link DocumentId} annotated fields in the custom class. - * @return The POJO object. - */ - public static T convertToCustomClass( - Object object, Class clazz, DocumentReference docRef) { - return deserializeToClass( - object, clazz, new DeserializeContext(DeserializeContext.ErrorPath.EMPTY, docRef)); - } - - public static Object serialize(T o) { - return serialize(o, DeserializeContext.ErrorPath.EMPTY); - } - - @SuppressWarnings("unchecked") - static Object serialize(T o, DeserializeContext.ErrorPath path) { - if (path.getLength() > MAX_DEPTH) { - throw path.serializeError( - "Exceeded maximum depth of " - + MAX_DEPTH - + ", which likely indicates there's an object cycle"); - } - if (o == null) { - return null; - } else if (o instanceof Number) { - if (o instanceof Long || o instanceof Integer || o instanceof Double || o instanceof Float) { - return o; - } else if (o instanceof BigDecimal) { - return String.valueOf(o); - } else { - throw path.serializeError( - String.format( - "Numbers of type %s are not supported, please use an int, long, float, double or BigDecimal", - o.getClass().getSimpleName())); - } - } else if (o instanceof String) { - return o; - } else if (o instanceof Boolean) { - return o; - } else if (o instanceof Character) { - throw path.serializeError("Characters are not supported, please use Strings"); - } else if (o instanceof Map) { - Map result = new HashMap<>(); - for (Map.Entry entry : ((Map) o).entrySet()) { - Object key = entry.getKey(); - if (key instanceof String) { - String keyString = (String) key; - result.put(keyString, serialize(entry.getValue(), path.child(keyString))); - } else { - throw path.serializeError("Maps with non-string keys are not supported"); - } - } - return result; - } else if (o instanceof Collection) { - if (o instanceof List) { - List list = (List) o; - List result = new ArrayList<>(list.size()); - for (int i = 0; i < list.size(); i++) { - result.add(serialize(list.get(i), path.child("[" + i + "]"))); - } - return result; - } else { - throw path.serializeError( - "Serializing Collections is not supported, please use Lists instead"); - } - } else if (o.getClass().isArray()) { - throw path.serializeError("Serializing Arrays is not supported, please use Lists instead"); - } else if (o instanceof Enum) { - String enumName = ((Enum) o).name(); - try { - Field enumField = o.getClass().getField(enumName); - return propertyName(enumField); - } catch (NoSuchFieldException ex) { - return enumName; - } - } else if (o instanceof Date - || o instanceof Timestamp - || o instanceof GeoPoint - || o instanceof Blob - || o instanceof DocumentReference - || o instanceof FieldValue - || o instanceof Value - || o instanceof VectorValue) { - return o; - } else if (o instanceof Instant) { - Instant instant = (Instant) o; - return Timestamp.ofTimeSecondsAndNanos(instant.getEpochSecond(), instant.getNano()); - } else { - Class clazz = (Class) o.getClass(); - BeanMapper mapper = loadOrCreateBeanMapperForClass(clazz); - return mapper.serialize(o, path); - } - } - - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - static T deserializeToType(Object o, Type type, DeserializeContext context) { - if (o == null) { - return null; - } else if (type instanceof ParameterizedType) { - return deserializeToParameterizedType(o, (ParameterizedType) type, context); - } else if (type instanceof Class) { - return deserializeToClass(o, (Class) type, context); - } else if (type instanceof WildcardType) { - Type[] lowerBounds = ((WildcardType) type).getLowerBounds(); - if (lowerBounds.length > 0) { - throw context.errorPath.deserializeError( - "Generic lower-bounded wildcard types are not supported"); - } - - // Upper bounded wildcards are of the form . Multiple upper bounds are allowed - // but if any of the bounds are of class type, that bound must come first in this array. Note - // that this array always has at least one element, since the unbounded wildcard always - // has at least an upper bound of Object. - Type[] upperBounds = ((WildcardType) type).getUpperBounds(); - hardAssert(upperBounds.length > 0, "Unexpected type bounds on wildcard " + type); - return deserializeToType(o, upperBounds[0], context); - } else if (type instanceof TypeVariable) { - // As above, TypeVariables always have at least one upper bound of Object. - Type[] upperBounds = ((TypeVariable) type).getBounds(); - hardAssert(upperBounds.length > 0, "Unexpected type bounds on type variable " + type); - return deserializeToType(o, upperBounds[0], context); - - } else if (type instanceof GenericArrayType) { - throw context.errorPath.deserializeError( - "Generic Arrays are not supported, please use Lists instead"); - } else { - throw context.errorPath.deserializeError("Unknown type encountered: " + type); - } - } - - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - private static T deserializeToParameterizedType( - Object o, ParameterizedType type, DeserializeContext context) { - // getRawType should always return a Class - Class rawType = (Class) type.getRawType(); - if (List.class.isAssignableFrom(rawType)) { - Type genericType = type.getActualTypeArguments()[0]; - if (o instanceof List) { - List list = (List) o; - List result; - try { - result = - (rawType == List.class) - ? new ArrayList<>(list.size()) - : (List) rawType.getDeclaredConstructor().newInstance(); - } catch (InstantiationException - | IllegalAccessException - | NoSuchMethodException - | InvocationTargetException e) { - throw context.errorPath.deserializeError( - String.format( - "Unable to deserialize to %s: %s", rawType.getSimpleName(), e.toString())); - } - for (int i = 0; i < list.size(); i++) { - result.add( - deserializeToType( - list.get(i), - genericType, - context.newInstanceWithErrorPath(context.errorPath.child("[" + i + "]")))); - } - return (T) result; - } else { - throw context.errorPath.deserializeError("Expected a List, but got a " + o.getClass()); - } - } else if (Map.class.isAssignableFrom(rawType)) { - Type keyType = type.getActualTypeArguments()[0]; - Type valueType = type.getActualTypeArguments()[1]; - if (!keyType.equals(String.class)) { - throw context.errorPath.deserializeError( - "Only Maps with string keys are supported, but found Map with key type " + keyType); - } - Map map = expectMap(o, context.errorPath); - HashMap result; - try { - result = - (rawType == Map.class) - ? new HashMap<>() - : (HashMap) rawType.getDeclaredConstructor().newInstance(); - } catch (InstantiationException - | IllegalAccessException - | NoSuchMethodException - | InvocationTargetException e) { - throw context.errorPath.deserializeError( - String.format( - "Unable to deserialize to %s: %s", rawType.getSimpleName(), e.toString())); - } - for (Map.Entry entry : map.entrySet()) { - result.put( - entry.getKey(), - deserializeToType( - entry.getValue(), - valueType, - context.newInstanceWithErrorPath(context.errorPath.child(entry.getKey())))); - } - return (T) result; - } else if (Collection.class.isAssignableFrom(rawType)) { - throw context.errorPath.deserializeError( - "Collections are not supported, please use Lists instead"); - } else { - Map map = expectMap(o, context.errorPath); - BeanMapper mapper = (BeanMapper) loadOrCreateBeanMapperForClass(rawType); - HashMap>, Type> typeMapping = new HashMap<>(); - TypeVariable>[] typeVariables = mapper.getClazz().getTypeParameters(); - Type[] types = type.getActualTypeArguments(); - if (types.length != typeVariables.length) { - throw new IllegalStateException("Mismatched lengths for type variables and actual types"); - } - for (int i = 0; i < typeVariables.length; i++) { - typeMapping.put(typeVariables[i], types[i]); - } - return mapper.deserialize(map, typeMapping, context); - } - } - - @SuppressWarnings("unchecked") - private static T deserializeToClass(Object o, Class clazz, DeserializeContext context) { - if (o == null) { - return null; - } else if (clazz.isPrimitive() - || Number.class.isAssignableFrom(clazz) - || Boolean.class.isAssignableFrom(clazz) - || Character.class.isAssignableFrom(clazz)) { - return deserializeToPrimitive(o, clazz, context.errorPath); - } else if (String.class.isAssignableFrom(clazz)) { - return (T) convertString(o, context.errorPath); - } else if (Date.class.isAssignableFrom(clazz)) { - return (T) convertDate(o, context.errorPath); - } else if (Timestamp.class.isAssignableFrom(clazz)) { - return (T) convertTimestamp(o, context.errorPath); - } else if (Instant.class.isAssignableFrom(clazz)) { - return (T) convertInstant(o, context.errorPath); - } else if (Blob.class.isAssignableFrom(clazz)) { - return (T) convertBlob(o, context.errorPath); - } else if (GeoPoint.class.isAssignableFrom(clazz)) { - return (T) convertGeoPoint(o, context.errorPath); - } else if (VectorValue.class.isAssignableFrom(clazz)) { - return (T) convertVectorValue(o, context.errorPath); - } else if (DocumentReference.class.isAssignableFrom(clazz)) { - return (T) convertDocumentReference(o, context.errorPath); - } else if (clazz.isArray()) { - throw context.errorPath.deserializeError( - "Converting to Arrays is not supported, please use Lists instead"); - } else if (clazz.getTypeParameters().length > 0) { - throw context.errorPath.deserializeError( - "Class " - + clazz.getName() - + " has generic type parameters, please use GenericTypeIndicator instead"); - } else if (clazz.equals(Object.class)) { - return (T) o; - } else if (clazz.isEnum()) { - return deserializeToEnum(o, clazz, context.errorPath); - } else { - return convertBean(o, clazz, context); - } - } - - private static T convertBean(Object o, Class clazz, DeserializeContext context) { - BeanMapper mapper = loadOrCreateBeanMapperForClass(clazz); - if (o instanceof Map) { - return mapper.deserialize(expectMap(o, context.errorPath), context); - } else { - throw context.errorPath.deserializeError( - "Can't convert object of type " + o.getClass().getName() + " to type " + clazz.getName()); - } - } - - private static BeanMapper loadOrCreateBeanMapperForClass(Class clazz) { - @SuppressWarnings("unchecked") - BeanMapper mapper = (BeanMapper) mappers.get(clazz); - if (mapper == null) { - if (isRecordType(clazz)) { - mapper = new RecordMapper<>(clazz); - } else { - mapper = new PojoBeanMapper<>(clazz); - } - // Inserting without checking is fine because mappers are "pure" and it's okay - // if we create and use multiple by different threads temporarily - mappers.put(clazz, mapper); - } - return mapper; - } - - @SuppressWarnings("unchecked") - private static Map expectMap( - Object object, DeserializeContext.ErrorPath errorPath) { - if (object instanceof Map) { - // TODO: runtime validation of keys? - return (Map) object; - } else { - throw errorPath.deserializeError( - "Expected a Map while deserializing, but got a " + object.getClass()); - } - } - - @SuppressWarnings("unchecked") - private static T deserializeToPrimitive( - Object o, Class clazz, DeserializeContext.ErrorPath errorPath) { - if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) { - return (T) convertInteger(o, errorPath); - } else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) { - return (T) convertBoolean(o, errorPath); - } else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) { - return (T) convertDouble(o, errorPath); - } else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) { - return (T) convertLong(o, errorPath); - } else if (BigDecimal.class.isAssignableFrom(clazz)) { - return (T) convertBigDecimal(o, errorPath); - } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) { - return (T) (Float) convertDouble(o, errorPath).floatValue(); - } else { - throw errorPath.deserializeError( - String.format("Deserializing values to %s is not supported", clazz.getSimpleName())); - } - } - - @SuppressWarnings("unchecked") - private static T deserializeToEnum( - Object object, Class clazz, DeserializeContext.ErrorPath errorPath) { - if (object instanceof String) { - String value = (String) object; - // We cast to Class without generics here since we can't prove the bound - // T extends Enum statically - - // try to use PropertyName if exist - Field[] enumFields = clazz.getFields(); - for (Field field : enumFields) { - if (field.isEnumConstant()) { - String propertyName = propertyName(field); - if (value.equals(propertyName)) { - value = field.getName(); - break; - } - } - } - - try { - return (T) Enum.valueOf((Class) clazz, value); - } catch (IllegalArgumentException e) { - throw errorPath.deserializeError( - "Could not find enum value of " + clazz.getName() + " for value \"" + value + "\""); - } - } else { - throw errorPath.deserializeError( - "Expected a String while deserializing to enum " - + clazz - + " but got a " - + object.getClass()); - } - } - - private static Integer convertInteger(Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof Integer) { - return (Integer) o; - } else if (o instanceof Long || o instanceof Double) { - double value = ((Number) o).doubleValue(); - if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { - return ((Number) o).intValue(); - } else { - throw errorPath.deserializeError( - "Numeric value out of 32-bit integer range: " - + value - + ". Did you mean to use a long or double instead of an int?"); - } - } else { - throw errorPath.deserializeError( - "Failed to convert a value of type " + o.getClass().getName() + " to int"); - } - } - - private static Long convertLong(Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof Integer) { - return ((Integer) o).longValue(); - } else if (o instanceof Long) { - return (Long) o; - } else if (o instanceof Double) { - Double value = (Double) o; - if (value >= Long.MIN_VALUE && value <= Long.MAX_VALUE) { - return value.longValue(); - } else { - throw errorPath.deserializeError( - "Numeric value out of 64-bit long range: " - + value - + ". Did you mean to use a double instead of a long?"); - } - } else { - throw errorPath.deserializeError( - "Failed to convert a value of type " + o.getClass().getName() + " to long"); - } - } - - private static Double convertDouble(Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof Integer) { - return ((Integer) o).doubleValue(); - } else if (o instanceof Long) { - Long value = (Long) o; - Double doubleValue = ((Long) o).doubleValue(); - if (doubleValue.longValue() == value) { - return doubleValue; - } else { - throw errorPath.deserializeError( - "Loss of precision while converting number to " - + "double: " - + o - + ". Did you mean to use a 64-bit long instead?"); - } - } else if (o instanceof Double) { - return (Double) o; - } else { - throw errorPath.deserializeError( - "Failed to convert a value of type " + o.getClass().getName() + " to double"); - } - } - - private static BigDecimal convertBigDecimal(Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof Integer) { - return BigDecimal.valueOf(((Integer) o).intValue()); - } else if (o instanceof Long) { - return BigDecimal.valueOf(((Long) o).longValue()); - } else if (o instanceof Double) { - return BigDecimal.valueOf(((Double) o).doubleValue()).abs(); - } else if (o instanceof BigDecimal) { - return (BigDecimal) o; - } else if (o instanceof String) { - return new BigDecimal((String) o); - } else { - throw errorPath.deserializeError( - "Failed to convert a value of type " + o.getClass().getName() + " to BigDecimal"); - } - } - - private static Boolean convertBoolean(Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof Boolean) { - return (Boolean) o; - } else { - throw errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to boolean"); - } - } - - private static String convertString(Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof String) { - return (String) o; - } else { - throw errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to String"); - } - } - - private static Date convertDate(Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof Date) { - return (Date) o; - } else if (o instanceof Timestamp) { - return ((Timestamp) o).toDate(); - } else { - throw errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to Date"); - } - } - - private static Timestamp convertTimestamp(Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof Timestamp) { - return (Timestamp) o; - } else if (o instanceof Date) { - return Timestamp.of((Date) o); - } else { - throw errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to Timestamp"); - } - } - - private static Instant convertInstant(Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof Timestamp) { - Timestamp timestamp = (Timestamp) o; - return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()); - } else if (o instanceof Date) { - return Instant.ofEpochMilli(((Date) o).getTime()); - } else { - throw errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to Instant"); - } - } - - private static Blob convertBlob(Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof Blob) { - return (Blob) o; - } else { - throw errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to Blob"); - } - } - - private static GeoPoint convertGeoPoint(Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof GeoPoint) { - return (GeoPoint) o; - } else { - throw errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to GeoPoint"); - } - } - - private static VectorValue convertVectorValue(Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof VectorValue) { - return (VectorValue) o; - } else { - throw errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to VectorValue"); - } - } - - private static DocumentReference convertDocumentReference( - Object o, DeserializeContext.ErrorPath errorPath) { - if (o instanceof DocumentReference) { - return (DocumentReference) o; - } else { - throw errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to DocumentReference"); - } - } - - private static boolean isRecordType(Class cls) { - Class parent = cls.getSuperclass(); - return parent != null && "java.lang.Record".equals(parent.getName()); - } - - private static String propertyName(Field field) { - String annotatedName = annotatedName(field); - return annotatedName != null ? annotatedName : field.getName(); - } - - private static String annotatedName(AccessibleObject obj) { - if (obj.isAnnotationPresent(PropertyName.class)) { - PropertyName annotation = obj.getAnnotation(PropertyName.class); - return annotation.value(); - } - - return null; - } - - private static void hardAssert(boolean assertion) { - hardAssert(assertion, "Internal inconsistency"); - } - - private static void hardAssert(boolean assertion, String message) { - if (!assertion) { - throw new RuntimeException("Hard assert failed: " + message); - } - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/DeserializeContext.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/DeserializeContext.java deleted file mode 100644 index df85f5817..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/DeserializeContext.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.encoding; - -import com.google.cloud.firestore.DocumentReference; -import com.google.cloud.firestore.annotation.DocumentId; - -/** Holds information a deserialization operation needs to complete the job. */ -class DeserializeContext { - /** - * Immutable class representing the path to a specific field in an object. Used to provide better - * error messages. - */ - static class ErrorPath { - static final ErrorPath EMPTY = new ErrorPath(null, null, 0); - - private final int length; - private final ErrorPath parent; - private final String name; - - ErrorPath child(String name) { - return new ErrorPath(this, name, length + 1); - } - - @Override - public String toString() { - if (length == 0) { - return ""; - } else if (length == 1) { - return name; - } else { - // This is not very efficient, but it's only hit if there's an error. - return parent.toString() + "." + name; - } - } - - ErrorPath(ErrorPath parent, String name, int length) { - this.parent = parent; - this.name = name; - this.length = length; - } - - int getLength() { - return length; - } - - IllegalArgumentException serializeError(String reason) { - reason = "Could not serialize object. " + reason; - if (getLength() > 0) { - reason = reason + " (found in field '" + toString() + "')"; - } - return new IllegalArgumentException(reason); - } - - RuntimeException deserializeError(String reason) { - reason = "Could not deserialize object. " + reason; - if (getLength() > 0) { - reason = reason + " (found in field '" + toString() + "')"; - } - return new RuntimeException(reason); - } - } - - /** Current path to the field being deserialized, used for better error messages. */ - final ErrorPath errorPath; - - /** Value used to set to {@link DocumentId} annotated fields during deserialization, if any. */ - final DocumentReference documentRef; - - DeserializeContext newInstanceWithErrorPath(ErrorPath newPath) { - return new DeserializeContext(newPath, documentRef); - } - - DeserializeContext(ErrorPath path, DocumentReference docRef) { - errorPath = path; - documentRef = docRef; - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java deleted file mode 100644 index adf7ef229..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java +++ /dev/null @@ -1,494 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.encoding; - -import com.google.cloud.firestore.annotation.DocumentId; -import com.google.cloud.firestore.annotation.Exclude; -import com.google.cloud.firestore.annotation.ServerTimestamp; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Locale; -import java.util.Map; -import java.util.logging.Logger; - -// Helper class to convert from maps to custom objects (Beans), and vice versa. -class PojoBeanMapper extends BeanMapper { - private static final Logger LOGGER = Logger.getLogger(PojoBeanMapper.class.getName()); - - private final Constructor constructor; - - // Case insensitive mapping of properties to their case sensitive versions - private final Map properties; - - // Below are maps to find getter/setter/field from a given property name. - // A property name is the name annotated by @PropertyName, if exists; or their property name - // following the Java Bean convention: field name is kept as-is while getters/setters will have - // their prefixes removed. See method propertyName for details. - private final Map getters; - private final Map setters; - private final Map fields; - - PojoBeanMapper(Class clazz) { - super(clazz); - properties = new HashMap<>(); - - setters = new HashMap<>(); - getters = new HashMap<>(); - fields = new HashMap<>(); - - Constructor constructor; - try { - constructor = clazz.getDeclaredConstructor(); - constructor.setAccessible(true); - } catch (NoSuchMethodException e) { - // We will only fail at deserialization time if no constructor is present - constructor = null; - } - this.constructor = constructor; - // Add any public getters to properties (including isXyz()) - for (Method method : clazz.getMethods()) { - if (shouldIncludeGetter(method)) { - String propertyName = propertyName(method); - addProperty(propertyName); - method.setAccessible(true); - if (getters.containsKey(propertyName)) { - throw new RuntimeException( - "Found conflicting getters for name " - + method.getName() - + " on class " - + clazz.getName()); - } - getters.put(propertyName, method); - applyGetterAnnotations(method); - } - } - - // Add any public fields to properties - for (Field field : clazz.getFields()) { - if (shouldIncludeField(field)) { - String propertyName = propertyName(field); - addProperty(propertyName); - applyFieldAnnotations(field); - } - } - - // We can use private setters and fields for known (public) properties/getters. Since - // getMethods/getFields only returns public methods/fields we need to traverse the - // class hierarchy to find the appropriate setter or field. - Class currentClass = clazz; - do { - // Add any setters - for (Method method : currentClass.getDeclaredMethods()) { - if (shouldIncludeSetter(method)) { - String propertyName = propertyName(method); - String existingPropertyName = properties.get(propertyName.toLowerCase(Locale.US)); - if (existingPropertyName != null) { - if (!existingPropertyName.equals(propertyName)) { - throw new RuntimeException( - "Found setter on " - + currentClass.getName() - + " with invalid case-sensitive name: " - + method.getName()); - } else { - Method existingSetter = setters.get(propertyName); - if (existingSetter == null) { - method.setAccessible(true); - setters.put(propertyName, method); - applySetterAnnotations(method); - } else if (!isSetterOverride(method, existingSetter)) { - // We require that setters with conflicting property names are - // overrides from a base class - if (currentClass == clazz) { - // TODO: Should we support overloads? - throw new RuntimeException( - "Class " - + clazz.getName() - + " has multiple setter overloads with name " - + method.getName()); - } else { - throw new RuntimeException( - "Found conflicting setters " - + "with name: " - + method.getName() - + " (conflicts with " - + existingSetter.getName() - + " defined on " - + existingSetter.getDeclaringClass().getName() - + ")"); - } - } - } - } - } - } - - for (Field field : currentClass.getDeclaredFields()) { - String propertyName = propertyName(field); - - // Case sensitivity is checked at deserialization time - // Fields are only added if they don't exist on a subclass - if (properties.containsKey(propertyName.toLowerCase(Locale.US)) - && !fields.containsKey(propertyName)) { - field.setAccessible(true); - fields.put(propertyName, field); - applyFieldAnnotations(field); - } - } - - // Traverse class hierarchy until we reach java.lang.Object which contains a bunch - // of fields/getters we don't want to serialize - currentClass = currentClass.getSuperclass(); - } while (currentClass != null && !currentClass.equals(Object.class)); - - if (properties.isEmpty()) { - throw new RuntimeException("No properties to serialize found on class " + clazz.getName()); - } - - // Make sure we can write to @DocumentId annotated properties before proceeding. - for (String docIdProperty : documentIdPropertyNames) { - if (!setters.containsKey(docIdProperty) && !fields.containsKey(docIdProperty)) { - throw new RuntimeException( - "@DocumentId is annotated on property " - + docIdProperty - + " of class " - + clazz.getName() - + " but no field or public setter was found"); - } - } - } - - @Override - Map serialize(T object, DeserializeContext.ErrorPath path) { - verifyValidType(object); - Map result = new HashMap<>(); - for (String property : properties.values()) { - // Skip @DocumentId annotated properties; - if (documentIdPropertyNames.contains(property)) { - continue; - } - - Object propertyValue; - if (getters.containsKey(property)) { - Method getter = getters.get(property); - try { - propertyValue = getter.invoke(object); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } else { - // Must be a field - Field field = fields.get(property); - if (field == null) { - throw new IllegalStateException("Bean property without field or getter: " + property); - } - try { - propertyValue = field.get(object); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - Object serializedValue = getSerializedValue(property, propertyValue, path); - - result.put(property, serializedValue); - } - return result; - } - - @Override - T deserialize( - Map values, - Map>, Type> types, - DeserializeContext context) { - if (constructor == null) { - throw context.errorPath.deserializeError( - "Class " - + getClazz().getName() - + " does not define a no-argument constructor. If you are using ProGuard, make " - + "sure these constructors are not stripped"); - } - - T instance; - try { - instance = constructor.newInstance(); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - HashSet deserializedProperties = new HashSet<>(); - for (Map.Entry entry : values.entrySet()) { - String propertyName = entry.getKey(); - DeserializeContext.ErrorPath childPath = context.errorPath.child(propertyName); - if (setters.containsKey(propertyName)) { - Method setter = setters.get(propertyName); - Type[] params = setter.getGenericParameterTypes(); - if (params.length != 1) { - throw childPath.deserializeError("Setter does not have exactly one parameter"); - } - Type resolvedType = resolveType(params[0], types); - Object value = - CustomClassMapper.deserializeToType( - entry.getValue(), resolvedType, context.newInstanceWithErrorPath(childPath)); - try { - setter.invoke(instance, value); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - deserializedProperties.add(propertyName); - } else if (fields.containsKey(propertyName)) { - Field field = fields.get(propertyName); - Type resolvedType = resolveType(field.getGenericType(), types); - Object value = - CustomClassMapper.deserializeToType( - entry.getValue(), resolvedType, context.newInstanceWithErrorPath(childPath)); - try { - field.set(instance, value); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - deserializedProperties.add(propertyName); - } else { - String message = - "No setter/field for " + propertyName + " found on class " + getClazz().getName(); - if (properties.containsKey(propertyName.toLowerCase(Locale.US))) { - message += " (fields/setters are case sensitive!)"; - } - if (isThrowOnUnknownProperties()) { - throw new RuntimeException(message); - } else if (isWarnOnUnknownProperties()) { - LOGGER.warning(message); - } - } - } - populateDocumentIdProperties(types, context, instance, deserializedProperties); - - return instance; - } - - private void addProperty(String property) { - String oldValue = properties.put(property.toLowerCase(Locale.US), property); - if (oldValue != null && !property.equals(oldValue)) { - throw new RuntimeException( - "Found two getters or fields with conflicting case " - + "sensitivity for property: " - + property.toLowerCase(Locale.US)); - } - } - - // Populate @DocumentId annotated fields. If there is a conflict (@DocumentId annotation is - // applied to a property that is already deserialized from the firestore document) - // a runtime exception will be thrown. - private void populateDocumentIdProperties( - Map>, Type> types, - DeserializeContext context, - T instance, - HashSet deserializedProperties) { - for (String docIdPropertyName : documentIdPropertyNames) { - checkForDocIdConflict(docIdPropertyName, deserializedProperties, context); - DeserializeContext.ErrorPath childPath = context.errorPath.child(docIdPropertyName); - if (setters.containsKey(docIdPropertyName)) { - Method setter = setters.get(docIdPropertyName); - Type[] params = setter.getGenericParameterTypes(); - if (params.length != 1) { - throw childPath.deserializeError("Setter does not have exactly one parameter"); - } - Type resolvedType = resolveType(params[0], types); - try { - if (resolvedType == String.class) { - setter.invoke(instance, context.documentRef.getId()); - } else { - setter.invoke(instance, context.documentRef); - } - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } else { - Field docIdField = fields.get(docIdPropertyName); - try { - if (docIdField.getType() == String.class) { - docIdField.set(instance, context.documentRef.getId()); - } else { - docIdField.set(instance, context.documentRef); - } - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } - } - - private void applyGetterAnnotations(Method method) { - Class returnType = method.getReturnType(); - if (method.isAnnotationPresent(ServerTimestamp.class)) { - validateServerTimestampType("Method", "returns", returnType); - serverTimestamps.add(propertyName(method)); - } - // Even though the value will be skipped, we still check for type matching for consistency. - if (method.isAnnotationPresent(DocumentId.class)) { - validateDocumentIdType("Method", "returns", returnType); - documentIdPropertyNames.add(propertyName(method)); - } - } - - private void applySetterAnnotations(Method method) { - if (method.isAnnotationPresent(ServerTimestamp.class)) { - throw new IllegalArgumentException( - "Method " - + method.getName() - + " is annotated with @ServerTimestamp but should not be. @ServerTimestamp can" - + " only be applied to fields and getters, not setters."); - } - if (method.isAnnotationPresent(DocumentId.class)) { - Class paramType = method.getParameterTypes()[0]; - validateDocumentIdType("Method", "accepts", paramType); - documentIdPropertyNames.add(propertyName(method)); - } - } - - private boolean shouldIncludeGetter(Method method) { - if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) { - return false; - } - // Exclude methods from Object.class - if (method.getDeclaringClass().equals(Object.class)) { - return false; - } - // Non-public methods - if (!Modifier.isPublic(method.getModifiers())) { - return false; - } - // Static methods - if (Modifier.isStatic(method.getModifiers())) { - return false; - } - // No return type - if (method.getReturnType().equals(Void.TYPE)) { - return false; - } - // Non-zero parameters - if (method.getParameterTypes().length != 0) { - return false; - } - // Excluded methods - if (method.isAnnotationPresent(Exclude.class)) { - return false; - } - return true; - } - - private boolean shouldIncludeSetter(Method method) { - if (!method.getName().startsWith("set")) { - return false; - } - // Exclude methods from Object.class - if (method.getDeclaringClass().equals(Object.class)) { - return false; - } - // Static methods - if (Modifier.isStatic(method.getModifiers())) { - return false; - } - // Has a return type - if (!method.getReturnType().equals(Void.TYPE)) { - return false; - } - // Methods without exactly one parameters - if (method.getParameterTypes().length != 1) { - return false; - } - // Excluded methods - if (method.isAnnotationPresent(Exclude.class)) { - return false; - } - return true; - } - - private boolean shouldIncludeField(Field field) { - // Exclude methods from Object.class - if (field.getDeclaringClass().equals(Object.class)) { - return false; - } - // Non-public fields - if (!Modifier.isPublic(field.getModifiers())) { - return false; - } - // Static fields - if (Modifier.isStatic(field.getModifiers())) { - return false; - } - // Transient fields - if (Modifier.isTransient(field.getModifiers())) { - return false; - } - // Excluded fields - if (field.isAnnotationPresent(Exclude.class)) { - return false; - } - return true; - } - - private boolean isSetterOverride(Method base, Method override) { - // We expect an overridden setter here - hardAssert( - base.getDeclaringClass().isAssignableFrom(override.getDeclaringClass()), - "Expected override from a base class"); - hardAssert(base.getReturnType().equals(Void.TYPE), "Expected void return type"); - hardAssert(override.getReturnType().equals(Void.TYPE), "Expected void return type"); - - Type[] baseParameterTypes = base.getParameterTypes(); - Type[] overrideParameterTypes = override.getParameterTypes(); - hardAssert(baseParameterTypes.length == 1, "Expected exactly one parameter"); - hardAssert(overrideParameterTypes.length == 1, "Expected exactly one parameter"); - - return base.getName().equals(override.getName()) - && baseParameterTypes[0].equals(overrideParameterTypes[0]); - } - - private String propertyName(Method method) { - String annotatedName = annotatedName(method); - return annotatedName != null ? annotatedName : serializedName(method.getName()); - } - - private String serializedName(String methodName) { - String[] prefixes = new String[] {"get", "set", "is"}; - String methodPrefix = null; - for (String prefix : prefixes) { - if (methodName.startsWith(prefix)) { - methodPrefix = prefix; - } - } - if (methodPrefix == null) { - throw new IllegalArgumentException("Unknown Bean prefix for method: " + methodName); - } - String strippedName = methodName.substring(methodPrefix.length()); - - // Make sure the first word or upper-case prefix is converted to lower-case - char[] chars = strippedName.toCharArray(); - int pos = 0; - while (pos < chars.length && Character.isUpperCase(chars[pos])) { - chars[pos] = Character.toLowerCase(chars[pos]); - pos++; - } - return new String(chars); - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java deleted file mode 100644 index f15f65823..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.encoding; - -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; - -/** - * Serializes java records. Uses automatic record constructors and accessors only. Therefore, - * exclusion of fields is not supported. Supports DocumentId, PropertyName, and ServerTimestamp - * annotations on record components. Since records are not supported in JDK versions < 16, - * reflection is used for inspecting record metadata. - */ -class RecordMapper extends BeanMapper { - private static final Logger LOGGER = Logger.getLogger(RecordMapper.class.getName()); - private static final RecordInspector RECORD_INSPECTOR = new RecordInspector(); - - // Below are maps to find an accessor and constructor parameter index from a given property name. - // A property name is the name annotated by @PropertyName, if exists; or the component name. - // See method propertyName for details. - private final Map accessors = new HashMap<>(); - private final Constructor constructor; - private final Map constructorParamIndexes = new HashMap<>(); - - RecordMapper(Class clazz) { - super(clazz); - - constructor = RECORD_INSPECTOR.getCanonicalConstructor(clazz); - - AnnotatedElement[] recordComponents = RECORD_INSPECTOR.getRecordComponents(clazz); - if (recordComponents.length == 0) { - throw new RuntimeException("No properties to serialize found on class " + clazz.getName()); - } - - try { - for (int i = 0; i < recordComponents.length; i++) { - Field field = clazz.getDeclaredField(RECORD_INSPECTOR.getName(recordComponents[i])); - String propertyName = propertyName(field); - constructorParamIndexes.put(propertyName, i); - accessors.put(propertyName, RECORD_INSPECTOR.getAccessor(recordComponents[i])); - applyFieldAnnotations(field); - } - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } - - @Override - Map serialize(T object, DeserializeContext.ErrorPath path) { - verifyValidType(object); - Map result = new HashMap<>(); - for (Map.Entry entry : accessors.entrySet()) { - String property = entry.getKey(); - // Skip @DocumentId annotated properties; - if (documentIdPropertyNames.contains(property)) { - continue; - } - - Object propertyValue; - Method accessor = entry.getValue(); - try { - propertyValue = accessor.invoke(object); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - - Object serializedValue = getSerializedValue(property, propertyValue, path); - - result.put(property, serializedValue); - } - return result; - } - - @Override - T deserialize( - Map values, - Map>, Type> types, - DeserializeContext context) { - Object[] constructorParams = new Object[constructor.getParameterCount()]; - Set deserializedProperties = new HashSet<>(values.size()); - for (Map.Entry entry : values.entrySet()) { - String propertyName = entry.getKey(); - if (accessors.containsKey(propertyName)) { - Method accessor = accessors.get(propertyName); - Type resolvedType = resolveType(accessor.getGenericReturnType(), types); - DeserializeContext.ErrorPath childPath = context.errorPath.child(propertyName); - Object value = - CustomClassMapper.deserializeToType( - entry.getValue(), resolvedType, context.newInstanceWithErrorPath(childPath)); - constructorParams[constructorParamIndexes.get(propertyName).intValue()] = value; - deserializedProperties.add(propertyName); - } else { - String message = - "No accessor for " + propertyName + " found on class " + getClazz().getName(); - if (isThrowOnUnknownProperties()) { - throw new RuntimeException(message); - } - if (isWarnOnUnknownProperties()) { - LOGGER.warning(message); - } - } - } - - populateDocumentIdProperties(types, context, constructorParams, deserializedProperties); - - try { - return constructor.newInstance(constructorParams); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - // Populate @DocumentId annotated components. If there is a conflict (@DocumentId annotation is - // applied to a property that is already deserialized from the firestore document) - // a runtime exception will be thrown. - private void populateDocumentIdProperties( - Map>, Type> types, - DeserializeContext context, - Object[] params, - Set deserializedProperties) { - for (String docIdPropertyName : documentIdPropertyNames) { - checkForDocIdConflict(docIdPropertyName, deserializedProperties, context); - - if (accessors.containsKey(docIdPropertyName)) { - Object id; - Type resolvedType = - resolveType(accessors.get(docIdPropertyName).getGenericReturnType(), types); - if (resolvedType == String.class) { - id = context.documentRef.getId(); - } else { - id = context.documentRef; - } - params[constructorParamIndexes.get(docIdPropertyName).intValue()] = id; - } - } - } - - private static final class RecordInspector { - private final Method _getRecordComponents; - private final Method _getName; - private final Method _getType; - private final Method _getAccessor; - - @SuppressWarnings("JavaReflectionMemberAccess") - private RecordInspector() { - try { - _getRecordComponents = Class.class.getMethod("getRecordComponents"); - Class recordComponentClass = Class.forName("java.lang.reflect.RecordComponent"); - _getName = recordComponentClass.getMethod("getName"); - _getType = recordComponentClass.getMethod("getType"); - _getAccessor = recordComponentClass.getMethod("getAccessor"); - } catch (ClassNotFoundException | NoSuchMethodException e) { - throw new IllegalStateException( - "Failed to access class or methods needed to support record serialization", e); - } - } - - private Constructor getCanonicalConstructor(Class cls) { - try { - Class[] paramTypes = - Arrays.stream(getRecordComponents(cls)).map(this::getType).toArray(Class[]::new); - Constructor constructor = cls.getDeclaredConstructor(paramTypes); - constructor.setAccessible(true); - return constructor; - } catch (NoSuchMethodException e) { - throw new IllegalStateException(e); - } - } - - private AnnotatedElement[] getRecordComponents(Class recordType) { - try { - return (AnnotatedElement[]) _getRecordComponents.invoke(recordType); - } catch (InvocationTargetException | IllegalAccessException e) { - throw new IllegalArgumentException( - "Failed to load components of record " + recordType.getName(), e); - } - } - - private Class getType(AnnotatedElement recordComponent) { - try { - return (Class) _getType.invoke(recordComponent); - } catch (InvocationTargetException | IllegalAccessException e) { - throw new IllegalArgumentException("Failed to get record component type", e); - } - } - - private String getName(AnnotatedElement recordComponent) { - try { - return (String) _getName.invoke(recordComponent); - } catch (InvocationTargetException | IllegalAccessException e) { - throw new IllegalArgumentException("Failed to get record component name", e); - } - } - - private Method getAccessor(AnnotatedElement recordComponent) { - try { - Method accessor = (Method) _getAccessor.invoke(recordComponent); - accessor.setAccessible(true); - return accessor; - } catch (InvocationTargetException | IllegalAccessException e) { - throw new IllegalArgumentException("Failed to get record component accessor", e); - } - } - } -} diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java deleted file mode 100644 index fb4cc57e5..000000000 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore; - -import static com.google.cloud.firestore.LocalFirestoreHelper.assertCommitEquals; -import static com.google.cloud.firestore.LocalFirestoreHelper.commit; -import static com.google.cloud.firestore.LocalFirestoreHelper.create; -import static com.google.cloud.firestore.LocalFirestoreHelper.getAllResponse; -import static com.google.cloud.firestore.LocalFirestoreHelper.map; -import static com.google.cloud.firestore.LocalFirestoreHelper.serverTimestamp; -import static com.google.cloud.firestore.LocalFirestoreHelper.set; -import static com.google.cloud.firestore.LocalFirestoreHelper.transform; -import static com.google.cloud.firestore.RecordTestHelper.*; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; - -import com.google.api.gax.rpc.ResponseObserver; -import com.google.api.gax.rpc.ServerStreamingCallable; -import com.google.api.gax.rpc.UnaryCallable; -import com.google.cloud.firestore.spi.v1.FirestoreRpc; -import com.google.firestore.v1.BatchGetDocumentsRequest; -import com.google.firestore.v1.CommitRequest; -import com.google.firestore.v1.CommitResponse; -import com.google.firestore.v1.Value; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; -import org.mockito.Captor; -import org.mockito.Mockito; -import org.mockito.Spy; -import org.mockito.junit.MockitoJUnitRunner; - -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - - -@RunWith(MockitoJUnitRunner.class) -public class RecordDocumentReferenceTest { - - @Spy - private final FirestoreImpl firestoreMock = - new FirestoreImpl( - FirestoreOptions.newBuilder().setProjectId("test-project").build(), - Mockito.mock(FirestoreRpc.class)); - - @Captor private ArgumentCaptor commitCapture; - - @Captor private ArgumentCaptor getAllCapture; - - @Captor private ArgumentCaptor> streamObserverCapture; - - private DocumentReference documentReference; - - @Before - public void before() { - documentReference = firestoreMock.document("coll/doc"); - } - - @Test - public void serializeBasicTypes() throws Exception { - doReturn(SINGLE_WRITE_COMMIT_RESPONSE) - .when(firestoreMock) - .sendRequest( - commitCapture.capture(), ArgumentMatchers.>any()); - - documentReference.set(ALL_SUPPORTED_TYPES_OBJECT).get(); - - CommitRequest expectedCommit = commit(set(ALL_SUPPORTED_TYPES_PROTO)); - assertCommitEquals(expectedCommit, commitCapture.getAllValues().get(0)); - } - - @Test - public void doesNotSerializeAdvancedNumberTypes() { - Map expectedErrorMessages = new HashMap<>(); - - InvalidRecord record = new InvalidRecord(new BigInteger("0"), null, null); - expectedErrorMessages.put( - record, - "Could not serialize object. Numbers of type BigInteger are not supported, please use an int, long, float, double or BigDecimal (found in field 'bigIntegerValue')"); - - record = new InvalidRecord(null, (byte) 0, null); - expectedErrorMessages.put( - record, - "Could not serialize object. Numbers of type Byte are not supported, please use an int, long, float, double or BigDecimal (found in field 'byteValue')"); - - record = new InvalidRecord(null, null, (short) 0); - expectedErrorMessages.put( - record, - "Could not serialize object. Numbers of type Short are not supported, please use an int, long, float, double or BigDecimal (found in field 'shortValue')"); - - for (Map.Entry testCase : expectedErrorMessages.entrySet()) { - try { - documentReference.set(testCase.getKey()); - fail(); - } catch (IllegalArgumentException e) { - assertEquals(testCase.getValue(), e.getMessage()); - } - } - } - - @Test - public void doesNotDeserializeAdvancedNumberTypes() throws Exception { - Map fieldNamesToTypeNames = - map("bigIntegerValue", "BigInteger", "shortValue", "Short", "byteValue", "Byte"); - - for (Map.Entry testCase : fieldNamesToTypeNames.entrySet()) { - String fieldName = testCase.getKey(); - String typeName = testCase.getValue(); - Map response = map(fieldName, Value.newBuilder().setIntegerValue(0).build()); - - doAnswer(getAllResponse(response)) - .when(firestoreMock) - .streamRequest( - getAllCapture.capture(), - streamObserverCapture.capture(), - ArgumentMatchers.any()); - - DocumentSnapshot snapshot = documentReference.get().get(); - try { - snapshot.toObject(InvalidRecord.class); - fail(); - } catch (RuntimeException e) { - assertEquals( - String.format( - "Could not deserialize object. Deserializing values to %s is not supported (found in field '%s')", - typeName, fieldName), - e.getMessage()); - } - } - } - - @Test - public void createDocument() throws Exception { - doReturn(SINGLE_WRITE_COMMIT_RESPONSE) - .when(firestoreMock) - .sendRequest( - commitCapture.capture(), ArgumentMatchers.>any()); - - documentReference.create(SINGLE_COMPONENT_OBJECT).get(); - - CommitRequest expectedCommit = commit(create(SINGLE_COMPONENT_PROTO)); - - List commitRequests = commitCapture.getAllValues(); - assertCommitEquals(expectedCommit, commitRequests.get(0)); - } - - @Test - public void createWithServerTimestamp() throws Exception { - doReturn(SINGLE_WRITE_COMMIT_RESPONSE) - .when(firestoreMock) - .sendRequest( - commitCapture.capture(), ArgumentMatchers.>any()); - - documentReference.create(SERVER_TIMESTAMP_OBJECT).get(); - - CommitRequest create = - commit( - create(Collections.emptyMap()), - transform("foo", serverTimestamp(), "inner.bar", serverTimestamp())); - - List commitRequests = commitCapture.getAllValues(); - assertCommitEquals(create, commitRequests.get(0)); - } - - @Test - public void setWithServerTimestamp() throws Exception { - doReturn(FIELD_TRANSFORM_COMMIT_RESPONSE) - .when(firestoreMock) - .sendRequest( - commitCapture.capture(), ArgumentMatchers.>any()); - - documentReference.set(SERVER_TIMESTAMP_OBJECT).get(); - - CommitRequest set = - commit( - set(SERVER_TIMESTAMP_PROTO), - transform("foo", serverTimestamp(), "inner.bar", serverTimestamp())); - - List commitRequests = commitCapture.getAllValues(); - assertCommitEquals(set, commitRequests.get(0)); - } - - @Test - public void mergeWithServerTimestamps() throws Exception { - doReturn(SINGLE_WRITE_COMMIT_RESPONSE) - .when(firestoreMock) - .sendRequest( - commitCapture.capture(), ArgumentMatchers.>any()); - - documentReference - .set(SERVER_TIMESTAMP_OBJECT, SetOptions.mergeFields("inner.bar")) - .get(); - - CommitRequest set = - commit( - set(SERVER_TIMESTAMP_PROTO, new ArrayList<>()), - transform("inner.bar", serverTimestamp())); - - List commitRequests = commitCapture.getAllValues(); - assertCommitEquals(set, commitRequests.get(0)); - } - - @Test - public void setDocumentWithMerge() throws Exception { - doReturn(SINGLE_WRITE_COMMIT_RESPONSE) - .when(firestoreMock) - .sendRequest( - commitCapture.capture(), ArgumentMatchers.>any()); - - documentReference.set(SINGLE_COMPONENT_OBJECT, SetOptions.merge()).get(); - documentReference.set(ALL_SUPPORTED_TYPES_OBJECT, SetOptions.mergeFields("foo")).get(); - documentReference - .set(ALL_SUPPORTED_TYPES_OBJECT, SetOptions.mergeFields(Arrays.asList("foo"))) - .get(); - documentReference - .set( - ALL_SUPPORTED_TYPES_OBJECT, - SetOptions.mergeFieldPaths(Arrays.asList(FieldPath.of("foo")))) - .get(); - - CommitRequest expectedCommit = commit(set(SINGLE_COMPONENT_PROTO, Arrays.asList("foo"))); - - for (int i = 0; i < 4; ++i) { - assertCommitEquals(expectedCommit, commitCapture.getAllValues().get(i)); - } - } - - @Test - public void setDocumentWithNestedMerge() throws Exception { - doReturn(SINGLE_WRITE_COMMIT_RESPONSE) - .when(firestoreMock) - .sendRequest( - commitCapture.capture(), ArgumentMatchers.>any()); - - documentReference.set(NESTED_RECORD_OBJECT, SetOptions.mergeFields("first.foo")).get(); - documentReference - .set(NESTED_RECORD_OBJECT, SetOptions.mergeFields(Arrays.asList("first.foo"))) - .get(); - documentReference - .set( - NESTED_RECORD_OBJECT, - SetOptions.mergeFieldPaths(Arrays.asList(FieldPath.of("first", "foo")))) - .get(); - - Map nestedUpdate = new HashMap<>(); - Value.Builder nestedProto = Value.newBuilder(); - nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); - nestedUpdate.put("first", nestedProto.build()); - - CommitRequest expectedCommit = commit(set(nestedUpdate, Arrays.asList("first.foo"))); - - for (int i = 0; i < 3; ++i) { - assertCommitEquals(expectedCommit, commitCapture.getAllValues().get(i)); - } - } - - @Test - public void setMultipleFieldsWithMerge() throws Exception { - doReturn(SINGLE_WRITE_COMMIT_RESPONSE) - .when(firestoreMock) - .sendRequest( - commitCapture.capture(), ArgumentMatchers.>any()); - - documentReference - .set( - NESTED_RECORD_OBJECT, - SetOptions.mergeFields("first.foo", "second.foo", "second.trueValue")) - .get(); - - Map nestedUpdate = new HashMap<>(); - Value.Builder nestedProto = Value.newBuilder(); - nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); - nestedUpdate.put("first", nestedProto.build()); - nestedProto - .getMapValueBuilder() - .putFields("trueValue", Value.newBuilder().setBooleanValue(true).build()); - nestedUpdate.put("second", nestedProto.build()); - - CommitRequest expectedCommit = - commit(set(nestedUpdate, Arrays.asList("first.foo", "second.foo", "second.trueValue"))); - - assertCommitEquals(expectedCommit, commitCapture.getValue()); - } - - @Test - public void setNestedMapWithMerge() throws Exception { - doReturn(SINGLE_WRITE_COMMIT_RESPONSE) - .when(firestoreMock) - .sendRequest( - commitCapture.capture(), ArgumentMatchers.>any()); - - documentReference.set(NESTED_RECORD_OBJECT, SetOptions.mergeFields("first", "second")).get(); - - Map nestedUpdate = new HashMap<>(); - Value.Builder nestedProto = Value.newBuilder(); - nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); - nestedUpdate.put("first", nestedProto.build()); - nestedProto.getMapValueBuilder().putAllFields(ALL_SUPPORTED_TYPES_PROTO); - nestedUpdate.put("second", nestedProto.build()); - - CommitRequest expectedCommit = commit(set(nestedUpdate, Arrays.asList("first", "second"))); - assertCommitEquals(expectedCommit, commitCapture.getValue()); - } - - @Test - public void extractFieldMaskFromMerge() throws Exception { - doReturn(SINGLE_WRITE_COMMIT_RESPONSE) - .when(firestoreMock) - .sendRequest( - commitCapture.capture(), ArgumentMatchers.>any()); - - documentReference.set(NESTED_RECORD_OBJECT, SetOptions.merge()).get(); - - Map nestedUpdate = new HashMap<>(); - Value.Builder nestedProto = Value.newBuilder(); - nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); - nestedUpdate.put("first", nestedProto.build()); - nestedProto.getMapValueBuilder().putAllFields(ALL_SUPPORTED_TYPES_PROTO); - nestedUpdate.put("second", nestedProto.build()); - - List updateMask = Arrays.asList( - "first.foo", - "second.arrayValue", - "second.bytesValue", - "second.dateValue", - "second.doubleValue", - "second.falseValue", - "second.foo", - "second.geoPointValue", - "second.infValue", - "second.longValue", - "second.nanValue", - "second.negInfValue", - "second.nullValue", - "second.objectValue.foo", - "second.timestampValue", - "second.trueValue", - "second.model.foo"); - - CommitRequest expectedCommit = commit(set(nestedUpdate, updateMask)); - assertCommitEquals(expectedCommit, commitCapture.getValue()); - } - - @Test - public void setNestedRecordWithPojoMapWithMerge() throws Exception { - doReturn(SINGLE_WRITE_COMMIT_RESPONSE) - .when(firestoreMock) - .sendRequest( - commitCapture.capture(), ArgumentMatchers.>any()); - - documentReference.set(NESTED_RECORD_WITH_POJO_OBJECT, SetOptions.mergeFields("first", "second")).get(); - - Map nestedUpdate = new HashMap<>(); - Value.Builder nestedProto = Value.newBuilder(); - nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); - nestedUpdate.put("first", nestedProto.build()); - nestedProto.getMapValueBuilder().putAllFields(ALL_SUPPORTED_TYPES_PROTO); - nestedUpdate.put("second", nestedProto.build()); - - CommitRequest expectedCommit = commit(set(nestedUpdate, Arrays.asList("first", "second"))); - assertCommitEquals(expectedCommit, commitCapture.getValue()); - } - - @Test - public void setNestedPojoWithRecordMapWithMerge() throws Exception { - doReturn(SINGLE_WRITE_COMMIT_RESPONSE) - .when(firestoreMock) - .sendRequest( - commitCapture.capture(), - ArgumentMatchers.>any()); - - documentReference.set(NESTED_POJO_WITH_RECORD_OBJECT, SetOptions.mergeFields("first", "second")).get(); - - Map nestedUpdate = new HashMap<>(); - Value.Builder nestedProto = Value.newBuilder(); - nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); - nestedUpdate.put("first", nestedProto.build()); - nestedProto.getMapValueBuilder().putAllFields(ALL_SUPPORTED_TYPES_PROTO); - nestedUpdate.put("second", nestedProto.build()); - - CommitRequest expectedCommit = commit(set(nestedUpdate, Arrays.asList("first", "second"))); - assertCommitEquals(expectedCommit, commitCapture.getValue()); - } -} diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java deleted file mode 100644 index daa5df99d..000000000 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java +++ /dev/null @@ -1,1134 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore; - -import com.google.cloud.firestore.annotation.DocumentId; -import com.google.cloud.firestore.annotation.PropertyName; -import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; -import com.google.cloud.firestore.encoding.CustomClassMapper; -import com.google.cloud.firestore.spi.v1.FirestoreRpc; -import com.google.common.collect.ImmutableList; -import com.google.firestore.v1.DatabaseRootName; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.Spy; -import org.mockito.junit.MockitoJUnitRunner; - -import java.io.Serializable; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static com.google.cloud.firestore.LocalFirestoreHelper.fromSingleQuotedString; -import static com.google.cloud.firestore.LocalFirestoreHelper.mapAnyType; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -@SuppressWarnings({"unused", "WeakerAccess", "SpellCheckingInspection"}) -@RunWith(MockitoJUnitRunner.class) -public class RecordMapperTest { - - @Spy - private final FirestoreImpl firestoreMock = - new FirestoreImpl( - FirestoreOptions.newBuilder().setProjectId("test-project").build(), - Mockito.mock(FirestoreRpc.class)); - - private static final double EPSILON = 0.0003; - - public record StringBean ( - String value - ){} - - public record DoubleBean ( - double value - ){} - - public record BigDecimalBean ( - BigDecimal value - ){} - - public record FloatBean ( - float value - ){} - - public record LongBean ( - long value - ){} - - public record IntBean ( - int value - ){} - - public record BooleanBean ( - boolean value - ){} - - public record ShortBean ( - short value - ){} - - public record ByteBean ( - byte value - ){} - - public record CharBean ( - char value - ){} - - public record IntArrayBean ( - int[] values - ){} - - public record StringArrayBean ( - String[] values - ){} - - public record XMLAndURLBean ( - String XMLAndURL - ){} - - public record CaseSensitiveFieldBean1 ( - String VALUE - ){} - - public record CaseSensitiveFieldBean2 ( - String value - ){} - - public record CaseSensitiveFieldBean3 ( - String Value - ){} - - public record CaseSensitiveFieldBean4 ( - String valUE - ){} - - public record NestedBean ( - StringBean bean - ){} - - public record ObjectBean ( - Object value - ){} - - public record GenericBean ( - B value - ){} - - public record DoubleGenericBean ( - A valueA, - B valueB - ){} - - public record ListBean ( - List values - ){} - - public record SetBean ( - Set values - ){} - - public record CollectionBean ( - Collection values - ){} - - public record MapBean ( - Map values - ){} - - /** - * This form is not terribly useful in Java, but Kotlin Maps are immutable and are rewritten into - * this form (b/67470108 has more details). - */ - public record UpperBoundedMapBean ( - Map values - ){} - - public record MultiBoundedMapBean ( - Map values - ){} - - public record MultiBoundedMapHolderBean ( - MultiBoundedMapBean map - ){} - - public record UnboundedMapBean ( - Map values - ){} - - public record UnboundedTypeVariableMapBean ( - Map values - ){} - - public record UnboundedTypeVariableMapHolderBean ( - UnboundedTypeVariableMapBean map - ){} - - public record NestedListBean ( - List values - ){} - - public record NestedMapBean ( - Map values - ){} - - public record IllegalKeyMapBean ( - Map values - ){} - - @ThrowOnExtraProperties - public record ThrowOnUnknownPropertiesBean ( - String value - ){} - - @ThrowOnExtraProperties - public record NoFieldBean( - ){} - - public record PropertyNameBean ( - @PropertyName("my_key") - String key, - - @PropertyName("my_value") - String value - ){} - - @SuppressWarnings({"NonAsciiCharacters"}) - public record UnicodeBean ( - String 漢字 - ){} - - private static T deserialize(String jsonString, Class clazz) { - return deserialize(jsonString, clazz, /*docRef=*/ null); - } - - private static T deserialize(Map json, Class clazz) { - return deserialize(json, clazz, /*docRef=*/ null); - } - - private static T deserialize(String jsonString, Class clazz, DocumentReference docRef) { - Map json = fromSingleQuotedString(jsonString); - return CustomClassMapper.convertToCustomClass(json, clazz, docRef); - } - - private static T deserialize( - Map json, Class clazz, DocumentReference docRef) { - return CustomClassMapper.convertToCustomClass(json, clazz, docRef); - } - - private static Object serialize(Object object) { - return CustomClassMapper.convertToPlainJavaTypes(object); - } - - private static void assertJson(String expected, Object actual) { - assertEquals(fromSingleQuotedString(expected), actual); - } - - private static void assertExceptionContains(String partialMessage, Runnable run) { - try { - run.run(); - fail("Expected exception not thrown"); - } catch (RuntimeException e) { - assertTrue(e.getMessage().contains(partialMessage)); - } - } - - private static T convertToCustomClass( - Object object, Class clazz, DocumentReference docRef) { - return CustomClassMapper.convertToCustomClass(object, clazz, docRef); - } - - private static T convertToCustomClass(Object object, Class clazz) { - return CustomClassMapper.convertToCustomClass(object, clazz, null); - } - - @Test - public void primitiveDeserializeString() { - StringBean bean = deserialize("{'value': 'foo'}", StringBean.class); - assertEquals("foo", bean.value()); - - // Double - try { - deserialize("{'value': 1.1}", StringBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // Int - try { - deserialize("{'value': 1}", StringBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // Long - try { - deserialize("{'value': 1234567890123}", StringBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // Boolean - try { - deserialize("{'value': true}", StringBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - } - - @Test - public void primitiveDeserializeBoolean() { - BooleanBean beanBoolean = deserialize("{'value': true}", BooleanBean.class); - assertEquals(true, beanBoolean.value()); - - // Double - try { - deserialize("{'value': 1.1}", BooleanBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // Long - try { - deserialize("{'value': 1234567890123}", BooleanBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // Int - try { - deserialize("{'value': 1}", BooleanBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // String - try { - deserialize("{'value': 'foo'}", BooleanBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - } - - @Test - public void primitiveDeserializeDouble() { - DoubleBean beanDouble = deserialize("{'value': 1.1}", DoubleBean.class); - assertEquals(1.1, beanDouble.value(), EPSILON); - - // Int - DoubleBean beanInt = deserialize("{'value': 1}", DoubleBean.class); - assertEquals(1, beanInt.value(), EPSILON); - // Long - DoubleBean beanLong = deserialize("{'value': 1234567890123}", DoubleBean.class); - assertEquals(1234567890123L, beanLong.value(), EPSILON); - - // Boolean - try { - deserialize("{'value': true}", DoubleBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // String - try { - deserialize("{'value': 'foo'}", DoubleBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - } - - @Test - public void primitiveDeserializeBigDecimal() { - BigDecimalBean beanBigdecimal = deserialize("{'value': 123}", BigDecimalBean.class); - assertEquals(BigDecimal.valueOf(123.0), beanBigdecimal.value()); - - beanBigdecimal = deserialize("{'value': '123'}", BigDecimalBean.class); - assertEquals(BigDecimal.valueOf(123), beanBigdecimal.value()); - - // Int - BigDecimalBean beanInt = - deserialize(Collections.singletonMap("value", 1), BigDecimalBean.class); - assertEquals(BigDecimal.valueOf(1), beanInt.value()); - - // Long - BigDecimalBean beanLong = - deserialize(Collections.singletonMap("value", 1234567890123L), BigDecimalBean.class); - assertEquals(BigDecimal.valueOf(1234567890123L), beanLong.value()); - - // Double - BigDecimalBean beanDouble = - deserialize(Collections.singletonMap("value", 1.1), BigDecimalBean.class); - assertEquals(BigDecimal.valueOf(1.1), beanDouble.value()); - - // BigDecimal - BigDecimalBean beanBigDecimal = - deserialize( - Collections.singletonMap("value", BigDecimal.valueOf(1.2)), BigDecimalBean.class); - assertEquals(BigDecimal.valueOf(1.2), beanBigDecimal.value()); - - // Boolean - try { - deserialize("{'value': true}", BigDecimalBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // String - try { - deserialize("{'value': 'foo'}", BigDecimalBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - } - - @Test - public void primitiveDeserializeFloat() { - FloatBean beanFloat = deserialize("{'value': 1.1}", FloatBean.class); - assertEquals(1.1, beanFloat.value(), EPSILON); - - // Int - FloatBean beanInt = deserialize(Collections.singletonMap("value", 1), FloatBean.class); - assertEquals(1, beanInt.value(), EPSILON); - // Long - FloatBean beanLong = - deserialize(Collections.singletonMap("value", 1234567890123L), FloatBean.class); - assertEquals((float) 1234567890123L, beanLong.value(), EPSILON); - - // Boolean - try { - deserialize("{'value': true}", FloatBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // String - try { - deserialize("{'value': 'foo'}", FloatBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - } - - @Test - public void primitiveDeserializeInt() { - IntBean beanInt = deserialize("{'value': 1}", IntBean.class); - assertEquals(1, beanInt.value()); - - // Double - IntBean beanDouble = deserialize("{'value': 1.1}", IntBean.class); - assertEquals(1, beanDouble.value()); - - // Large doubles - try { - deserialize("{'value': 1e10}", IntBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // Long - try { - deserialize("{'value': 1234567890123}", IntBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // Boolean - try { - deserialize("{'value': true}", IntBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // String - try { - deserialize("{'value': 'foo'}", IntBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - } - - @Test - public void primitiveDeserializeLong() { - LongBean beanLong = deserialize("{'value': 1234567890123}", LongBean.class); - assertEquals(1234567890123L, beanLong.value()); - - // Int - LongBean beanInt = deserialize("{'value': 1}", LongBean.class); - assertEquals(1, beanInt.value()); - - // Double - LongBean beanDouble = deserialize("{'value': 1.1}", LongBean.class); - assertEquals(1, beanDouble.value()); - - // Large doubles - try { - deserialize("{'value': 1e300}", LongBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // Boolean - try { - deserialize("{'value': true}", LongBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - - // String - try { - deserialize("{'value': 'foo'}", LongBean.class); - fail("Should throw"); - } catch (RuntimeException e) { // ignore - } - } - - @Test - public void primitiveDeserializeWrongTypeMap() { - String expectedExceptionMessage = - ".* Failed to convert value of type .*Map to String \\(found in field 'value'\\).*"; - Throwable exception = - assertThrows( - RuntimeException.class, - () -> deserialize("{'value': {'foo': 'bar'}}", StringBean.class)); - assertTrue(exception.getMessage().matches(expectedExceptionMessage)); - } - - @Test - public void primitiveDeserializeWrongTypeList() { - assertExceptionContains( - "Failed to convert value of type java.util.ArrayList to String" - + " (found in field 'value')", - () -> deserialize("{'value': ['foo']}", StringBean.class)); - } - - @Test - public void noFieldDeserialize() { - assertExceptionContains( - "No properties to serialize found on class " - + "com.google.cloud.firestore.RecordMapperTest$NoFieldBean", - () -> deserialize("{'value': 'foo'}", NoFieldBean.class)); - } - - @Test - public void throwOnUnknownProperties() { - assertExceptionContains( - "No accessor for unknown found on class " - + "com.google.cloud.firestore.RecordMapperTest$ThrowOnUnknownPropertiesBean", - () -> - deserialize("{'value': 'foo', 'unknown': 'bar'}", ThrowOnUnknownPropertiesBean.class)); - } - - @Test - public void XMLAndURLBean() { - XMLAndURLBean bean = - deserialize("{'XMLAndURL': 'foo'}", XMLAndURLBean.class); - assertEquals("foo", bean.XMLAndURL()); - } - - public record AllCapsDefaultHandlingBean ( - String UUID - ){} - - @Test - public void allCapsSerializesToUppercaseByDefault() { - AllCapsDefaultHandlingBean bean = new AllCapsDefaultHandlingBean("value"); - assertJson("{'UUID': 'value'}", serialize(bean)); - AllCapsDefaultHandlingBean deserialized = - deserialize("{'UUID': 'value'}", AllCapsDefaultHandlingBean.class); - assertEquals("value", deserialized.UUID()); - } - - public record AllCapsWithPropertyName ( - @PropertyName("uuid") - String UUID - ){} - - @Test - public void allCapsWithPropertyNameSerializesToLowercase() { - AllCapsWithPropertyName bean = new AllCapsWithPropertyName("value"); - assertJson("{'uuid': 'value'}", serialize(bean)); - AllCapsWithPropertyName deserialized = - deserialize("{'uuid': 'value'}", AllCapsWithPropertyName.class); - assertEquals("value", deserialized.UUID()); - } - - @Test - public void nestedParsingWorks() { - NestedBean bean = deserialize("{'bean': {'value': 'foo'}}", NestedBean.class); - assertEquals("foo", bean.bean().value()); - } - - @Test - public void beansCanContainLists() { - ListBean bean = deserialize("{'values': ['foo', 'bar']}", ListBean.class); - assertEquals(Arrays.asList("foo", "bar"), bean.values()); - } - - @Test - public void beansCanContainMaps() { - MapBean bean = deserialize("{'values': {'foo': 'bar'}}", MapBean.class); - Map expected = fromSingleQuotedString("{'foo': 'bar'}"); - assertEquals(expected, bean.values()); - } - - @Test - public void beansCanContainUpperBoundedMaps() { - Date date = new Date(1491847082123L); - Map source = mapAnyType("values", mapAnyType("foo", date)); - UpperBoundedMapBean bean = convertToCustomClass(source, UpperBoundedMapBean.class); - Map expected = mapAnyType("foo", date); - assertEquals(expected, bean.values()); - } - - @Test - public void beansCanContainMultiBoundedMaps() { - Date date = new Date(1491847082123L); - Map source = mapAnyType("map", mapAnyType("values", mapAnyType("foo", date))); - MultiBoundedMapHolderBean bean = convertToCustomClass(source, MultiBoundedMapHolderBean.class); - - Map expected = mapAnyType("foo", date); - assertEquals(expected, bean.map().values()); - } - - @Test - public void beansCanContainUnboundedMaps() { - UnboundedMapBean bean = deserialize("{'values': {'foo': 'bar'}}", UnboundedMapBean.class); - Map expected = mapAnyType("foo", "bar"); - assertEquals(expected, bean.values()); - } - - @Test - public void beansCanContainUnboundedTypeVariableMaps() { - Map source = mapAnyType("map", mapAnyType("values", mapAnyType("foo", "bar"))); - UnboundedTypeVariableMapHolderBean bean = - convertToCustomClass(source, UnboundedTypeVariableMapHolderBean.class); - - Map expected = mapAnyType("foo", "bar"); - assertEquals(expected, bean.map().values()); - } - - @Test - public void beansCanContainNestedUnboundedMaps() { - UnboundedMapBean bean = - deserialize("{'values': {'foo': {'bar': 'baz'}}}", UnboundedMapBean.class); - Map expected = mapAnyType("foo", mapAnyType("bar", "baz")); - assertEquals(expected, bean.values()); - } - - @Test - public void beansCanContainBeanLists() { - NestedListBean bean = deserialize("{'values': [{'value': 'foo'}]}", NestedListBean.class); - assertEquals(1, bean.values().size()); - assertEquals("foo", bean.values().get(0).value()); - } - - @Test - public void beansCanContainBeanMaps() { - NestedMapBean bean = deserialize("{'values': {'key': {'value': 'foo'}}}", NestedMapBean.class); - assertEquals(1, bean.values().size()); - assertEquals("foo", bean.values().get("key").value()); - } - - @Test - public void beanMapsMustHaveStringKeys() { - assertExceptionContains( - "Only Maps with string keys are supported, but found Map with key type class " - + "java.lang.Integer (found in field 'values')", - () -> deserialize("{'values': {'1': 'bar'}}", IllegalKeyMapBean.class)); - } - - @Test - public void serializeStringBean() { - StringBean bean = new StringBean("foo"); - assertJson("{'value': 'foo'}", serialize(bean)); - } - - @Test - public void serializeDoubleBean() { - DoubleBean bean = new DoubleBean(1.1); - assertJson("{'value': 1.1}", serialize(bean)); - } - - @Test - public void serializeIntBean() { - IntBean bean = new IntBean(1); - assertJson("{'value': 1}", serialize(Collections.singletonMap("value", 1.0))); - } - - @Test - public void serializeLongBean() { - LongBean bean = new LongBean(1234567890123L); - assertJson( - "{'value': 1.234567890123E12}", - serialize(Collections.singletonMap("value", 1.234567890123E12))); - } - - @Test - public void serializeBigDecimalBean() { - BigDecimalBean bean = new BigDecimalBean(BigDecimal.valueOf(1.1)); - assertEquals(mapAnyType("value", "1.1"), serialize(bean)); - } - - @Test - public void bigDecimalRoundTrip() { - BigDecimal doubleMaxPlusOne = BigDecimal.valueOf(Double.MAX_VALUE).add(BigDecimal.ONE); - BigDecimalBean a = new BigDecimalBean(doubleMaxPlusOne); - Map serialized = (Map) serialize(a); - BigDecimalBean b = convertToCustomClass(serialized, BigDecimalBean.class); - assertEquals(a, b); - } - - @Test - public void serializeBooleanBean() { - BooleanBean bean = new BooleanBean(true); - assertJson("{'value': true}", serialize(bean)); - } - - @Test - public void serializeFloatBean() { - FloatBean bean = new FloatBean(0.5f); - - // We don't use assertJson as it converts all floating point numbers to Double. - assertEquals(mapAnyType("value", 0.5f), serialize(bean)); - } - - @Test - public void serializePrivateFieldBean() { - final NoFieldBean bean = new NoFieldBean(); - assertExceptionContains( - "No properties to serialize found on class " - + "com.google.cloud.firestore.RecordMapperTest$NoFieldBean", - () -> serialize(bean)); - } - - @Test - public void nestedSerializingWorks() { - NestedBean bean = new NestedBean(new StringBean("foo")); - assertJson("{'bean': {'value': 'foo'}}", serialize(bean)); - } - - @Test - public void serializingListsWorks() { - ListBean bean = new ListBean(Arrays.asList("foo", "bar")); - assertJson("{'values': ['foo', 'bar']}", serialize(bean)); - } - - @Test - public void serializingMapsWorks() { - MapBean bean = new MapBean(new HashMap<>()); - bean.values().put("foo", "bar"); - assertJson("{'values': {'foo': 'bar'}}", serialize(bean)); - } - - @Test - public void serializingUpperBoundedMapsWorks() { - Date date = new Date(1491847082123L); - UpperBoundedMapBean bean = new UpperBoundedMapBean(Map.of("foo", date)); - Map expected = - mapAnyType("values", mapAnyType("foo", new Date(date.getTime()))); - assertEquals(expected, serialize(bean)); - } - - @Test - public void serializingMultiBoundedObjectsWorks() { - Date date = new Date(1491847082123L); - - HashMap values = new HashMap(); - values.put("foo", date); - - MultiBoundedMapHolderBean holder = new MultiBoundedMapHolderBean(new MultiBoundedMapBean<>(values)); - - Map expected = - mapAnyType("map", mapAnyType("values", mapAnyType("foo", new Date(date.getTime())))); - assertEquals(expected, serialize(holder)); - } - - @Test - public void serializeListOfBeansWorks() { - StringBean stringBean = new StringBean("foo"); - - NestedListBean bean = new NestedListBean(new ArrayList<>()); - bean.values().add(stringBean); - - assertJson("{'values': [{'value': 'foo'}]}", serialize(bean)); - } - - @Test - public void serializeMapOfBeansWorks() { - StringBean stringBean = new StringBean("foo"); - - NestedMapBean bean = new NestedMapBean(new HashMap<>()); - bean.values().put("key", stringBean); - - assertJson("{'values': {'key': {'value': 'foo'}}}", serialize(bean)); - } - - @Test - public void beanMapsMustHaveStringKeysForSerializing() { - StringBean stringBean = new StringBean("foo"); - - final IllegalKeyMapBean bean = new IllegalKeyMapBean(new HashMap<>()); - bean.values().put(1, stringBean); - - assertExceptionContains( - "Maps with non-string keys are not supported (found in field 'values')", - () -> serialize(bean)); - } - - @Test - public void serializeUPPERCASE() { - XMLAndURLBean bean = new XMLAndURLBean("foo"); - assertJson("{'XMLAndURL': 'foo'}", serialize(bean)); - } - - @Test - public void roundTripCaseSensitiveFieldBean1() { - CaseSensitiveFieldBean1 bean = new CaseSensitiveFieldBean1("foo"); - assertJson("{'VALUE': 'foo'}", serialize(bean)); - CaseSensitiveFieldBean1 deserialized = - deserialize("{'VALUE': 'foo'}", CaseSensitiveFieldBean1.class); - assertEquals("foo", deserialized.VALUE()); - } - - @Test - public void roundTripCaseSensitiveFieldBean2() { - CaseSensitiveFieldBean2 bean = new CaseSensitiveFieldBean2("foo"); - assertJson("{'value': 'foo'}", serialize(bean)); - CaseSensitiveFieldBean2 deserialized = - deserialize("{'value': 'foo'}", CaseSensitiveFieldBean2.class); - assertEquals("foo", deserialized.value()); - } - - @Test - public void roundTripCaseSensitiveFieldBean3() { - CaseSensitiveFieldBean3 bean = new CaseSensitiveFieldBean3("foo"); - assertJson("{'Value': 'foo'}", serialize(bean)); - CaseSensitiveFieldBean3 deserialized = - deserialize("{'Value': 'foo'}", CaseSensitiveFieldBean3.class); - assertEquals("foo", deserialized.Value()); - } - - @Test - public void roundTripCaseSensitiveFieldBean4() { - CaseSensitiveFieldBean4 bean = new CaseSensitiveFieldBean4("foo"); - assertJson("{'valUE': 'foo'}", serialize(bean)); - CaseSensitiveFieldBean4 deserialized = - deserialize("{'valUE': 'foo'}", CaseSensitiveFieldBean4.class); - assertEquals("foo", deserialized.valUE()); - } - - @Test - public void roundTripUnicodeBean() { - UnicodeBean bean = new UnicodeBean("foo"); - assertJson("{'漢字': 'foo'}", serialize(bean)); - UnicodeBean deserialized = deserialize("{'漢字': 'foo'}", UnicodeBean.class); - assertEquals("foo", deserialized.漢字()); - } - - @Test - public void shortsCantBeSerialized() { - final ShortBean bean = new ShortBean((short) 1); - assertExceptionContains( - "Numbers of type Short are not supported, please use an int, long, float, double or BigDecimal (found in field 'value')", - () -> serialize(bean)); - } - - @Test - public void bytesCantBeSerialized() { - final ByteBean bean = new ByteBean((byte) 1); - assertExceptionContains( - "Numbers of type Byte are not supported, please use an int, long, float, double or BigDecimal (found in field 'value')", - () -> serialize(bean)); - } - - @Test - public void charsCantBeSerialized() { - final CharBean bean = new CharBean((char) 1); - assertExceptionContains( - "Characters are not supported, please use Strings (found in field 'value')", - () -> serialize(bean)); - } - - @Test - public void intArraysCantBeSerialized() { - final IntArrayBean bean = new IntArrayBean(new int[] {1}); - assertExceptionContains( - "Serializing Arrays is not supported, please use Lists instead " - + "(found in field 'values')", - () -> serialize(bean)); - } - - @Test - public void objectArraysCantBeSerialized() { - final StringArrayBean bean = new StringArrayBean(new String[] {"foo"}); - assertExceptionContains( - "Serializing Arrays is not supported, please use Lists instead " - + "(found in field 'values')", - () -> serialize(bean)); - } - - @Test - public void shortsCantBeDeserialized() { - assertExceptionContains( - "Deserializing values to short is not supported (found in field 'value')", - () -> deserialize("{'value': 1}", ShortBean.class)); - } - - @Test - public void bytesCantBeDeserialized() { - assertExceptionContains( - "Deserializing values to byte is not supported (found in field 'value')", - () -> deserialize("{'value': 1}", ByteBean.class)); - } - - @Test - public void charsCantBeDeserialized() { - assertExceptionContains( - "Deserializing values to char is not supported (found in field 'value')", - () -> deserialize("{'value': '1'}", CharBean.class)); - } - - @Test - public void intArraysCantBeDeserialized() { - assertExceptionContains( - "Converting to Arrays is not supported, please use Lists instead (found in field 'values')", - () -> deserialize("{'values': [1]}", IntArrayBean.class)); - } - - @Test - public void objectArraysCantBeDeserialized() { - assertExceptionContains( - "Could not deserialize object. Converting to Arrays is not supported, please use Lists " - + "instead (found in field 'values')", - () -> deserialize("{'values': ['foo']}", StringArrayBean.class)); - } - - @Test - public void objectAcceptsAnyObject() { - ObjectBean stringValue = deserialize("{'value': 'foo'}", ObjectBean.class); - assertEquals("foo", stringValue.value()); - ObjectBean listValue = deserialize("{'value': ['foo']}", ObjectBean.class); - assertEquals(Collections.singletonList("foo"), listValue.value()); - ObjectBean mapValue = deserialize("{'value': {'foo':'bar'}}", ObjectBean.class); - assertEquals(fromSingleQuotedString("{'foo':'bar'}"), mapValue.value()); - String complex = "{'value': {'foo':['bar', ['baz'], {'bam': 'qux'}]}, 'other':{'a': ['b']}}"; - ObjectBean complexValue = deserialize(complex, ObjectBean.class); - assertEquals(fromSingleQuotedString(complex).get("value"), complexValue.value()); - } - - @Test - public void passingInGenericBeanTopLevelThrows() { - assertExceptionContains( - "Class com.google.cloud.firestore.RecordMapperTest$GenericBean has generic type " - + "parameters, please use GenericTypeIndicator instead", - () -> deserialize("{'value': 'foo'}", GenericBean.class)); - } - - @Test - public void collectionsCanBeSerializedWhenList() { - CollectionBean bean = new CollectionBean(Collections.singletonList("foo")); - assertJson("{'values': ['foo']}", serialize(bean)); - } - - @Test - public void collectionsCantBeSerializedWhenSet() { - final CollectionBean bean = new CollectionBean(Collections.singleton("foo")); - assertExceptionContains( - "Serializing Collections is not supported, please use Lists instead " - + "(found in field 'values')", - () -> serialize(bean)); - } - - @Test - public void collectionsCantBeDeserialized() { - assertExceptionContains( - "Collections are not supported, please use Lists instead (found in field 'values')", - () -> deserialize("{'values': ['foo']}", CollectionBean.class)); - } - - @Test - public void serializingGenericBeansSupported() { - GenericBean stringBean = new GenericBean("foo"); - assertJson("{'value': 'foo'}", serialize(stringBean)); - - GenericBean> mapBean = new GenericBean>(Collections.singletonMap("foo", "bar")); - assertJson("{'value': {'foo': 'bar'}}", serialize(mapBean)); - - GenericBean> listBean = new GenericBean>(Collections.singletonList("foo")); - assertJson("{'value': ['foo']}", serialize(listBean)); - - GenericBean> recursiveBean = new GenericBean>(new GenericBean<>("foo")); - assertJson("{'value': {'value': 'foo'}}", serialize(recursiveBean)); - - DoubleGenericBean doubleBean = new DoubleGenericBean("foo", 1.0); - assertJson("{'valueB': 1, 'valueA': 'foo'}", serialize(doubleBean)); - } - - @Test - public void propertyNamesAreSerialized() { - PropertyNameBean bean = new PropertyNameBean("foo", "bar"); - - assertJson("{'my_key': 'foo', 'my_value': 'bar'}", serialize(bean)); - } - - @Test - public void propertyNamesAreParsed() { - PropertyNameBean bean = - deserialize("{'my_key': 'foo', 'my_value': 'bar'}", PropertyNameBean.class); - assertEquals("foo", bean.key()); - assertEquals("bar", bean.value()); - } - - // Bean definitions with @DocumentId applied to wrong type. - public record FieldWithDocumentIdOnWrongTypeBean ( - @DocumentId Integer intField - ){} - - public record PropertyWithDocumentIdOnWrongTypeBean ( - @PropertyName("intField") - @DocumentId - int intField - ){} - - @Test - public void documentIdAnnotateWrongTypeThrows() { - final String expectedErrorMessage = "instead of String or DocumentReference"; - assertExceptionContains( - expectedErrorMessage, () -> serialize(new FieldWithDocumentIdOnWrongTypeBean(100))); - assertExceptionContains( - expectedErrorMessage, - () -> deserialize("{'intField': 1}", FieldWithDocumentIdOnWrongTypeBean.class)); - - assertExceptionContains( - expectedErrorMessage, () -> serialize(new PropertyWithDocumentIdOnWrongTypeBean(100))); - assertExceptionContains( - expectedErrorMessage, - () -> deserialize("{'intField': 1}", PropertyWithDocumentIdOnWrongTypeBean.class)); - } - - public record DocumentIdOnStringField ( - @DocumentId String docId - ){} - - public record DocumentIdOnStringFieldAsProperty ( - @PropertyName("docIdProperty") - @DocumentId - String docId, - - @PropertyName("anotherProperty") - int someOtherProperty - ){} - - public record DocumentIdOnNestedObjects ( - @PropertyName("nestedDocIdHolder") - DocumentIdOnStringField nestedDocIdHolder - ){} - - @Test - public void documentIdsDeserialize() { - DocumentReference ref = - new DocumentReference( - firestoreMock, - ResourcePath.create( - DatabaseRootName.of("test-project", "(default)"), - ImmutableList.of("coll", "doc123"))); - - assertEquals("doc123", deserialize("{}", DocumentIdOnStringField.class, ref).docId()); - - assertEquals( - "doc123", - deserialize(Collections.singletonMap("property", 100), DocumentIdOnStringField.class, ref) - .docId()); - - DocumentIdOnStringFieldAsProperty target = - deserialize("{'anotherProperty': 100}", DocumentIdOnStringFieldAsProperty.class, ref); - assertEquals("doc123", target.docId()); - assertEquals(100, target.someOtherProperty()); - - assertEquals( - "doc123", - deserialize("{'nestedDocIdHolder': {}}", DocumentIdOnNestedObjects.class, ref) - .nestedDocIdHolder() - .docId()); - } - - @Test - public void documentIdsRoundTrip() { - // Implicitly verifies @DocumentId is ignored during serialization. - - final DocumentReference ref = - new DocumentReference( - firestoreMock, - ResourcePath.create( - DatabaseRootName.of("test-project", "(default)"), - ImmutableList.of("coll", "doc123"))); - - assertEquals( - Collections.emptyMap(), serialize(deserialize("{}", DocumentIdOnStringField.class, ref))); - - assertEquals( - Collections.singletonMap("anotherProperty", 100), - serialize( - deserialize("{'anotherProperty': 100}", DocumentIdOnStringFieldAsProperty.class, ref))); - - assertEquals( - Collections.singletonMap("nestedDocIdHolder", Collections.emptyMap()), - serialize(deserialize("{'nestedDocIdHolder': {}}", DocumentIdOnNestedObjects.class, ref))); - } - - @Test - public void documentIdsDeserializeConflictThrows() { - final String expectedErrorMessage = "cannot apply @DocumentId on this property"; - final DocumentReference ref = - new DocumentReference( - firestoreMock, - ResourcePath.create( - DatabaseRootName.of("test-project", "(default)"), - ImmutableList.of("coll", "doc123"))); - - assertExceptionContains( - expectedErrorMessage, - () -> deserialize("{'docId': 'toBeOverwritten'}", DocumentIdOnStringField.class, ref)); - - assertExceptionContains( - expectedErrorMessage, - () -> - deserialize( - "{'docIdProperty': 'toBeOverwritten', 'anotherProperty': 100}", - DocumentIdOnStringFieldAsProperty.class, - ref)); - - assertExceptionContains( - expectedErrorMessage, - () -> - deserialize( - "{'nestedDocIdHolder': {'docId': 'toBeOverwritten'}}", - DocumentIdOnNestedObjects.class, - ref)); - } -} diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordTestHelper.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordTestHelper.java deleted file mode 100644 index 6bfe845be..000000000 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordTestHelper.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore; - -import com.google.api.core.ApiFuture; -import com.google.cloud.Timestamp; -import com.google.cloud.firestore.LocalFirestoreHelper.SingleField; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.firestore.v1.ArrayValue; -import com.google.firestore.v1.CommitResponse; -import com.google.firestore.v1.MapValue; -import com.google.firestore.v1.Value; -import com.google.protobuf.NullValue; -import com.google.type.LatLng; - -import static com.google.cloud.firestore.LocalFirestoreHelper.commitResponse; -import static com.google.cloud.firestore.LocalFirestoreHelper.map; - -import java.math.BigInteger; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - - -public final class RecordTestHelper { - - public static final String DATABASE_NAME; - public static final String DOCUMENT_PATH; - public static final String DOCUMENT_NAME; - public static final String DOCUMENT_ROOT; - - public static final SingleComponent SINGLE_COMPONENT_OBJECT; - public static final Map SINGLE_COMPONENT_PROTO; - - public static final NestedRecord NESTED_RECORD_OBJECT; - public static final NestedRecordWithPOJO NESTED_RECORD_WITH_POJO_OBJECT; - public static final NestedPOJOWithRecord NESTED_POJO_WITH_RECORD_OBJECT; - - public static final ServerTimestamp SERVER_TIMESTAMP_OBJECT; - public static final Map SERVER_TIMESTAMP_PROTO; - - public static final AllSupportedTypes ALL_SUPPORTED_TYPES_OBJECT; - public static final Map ALL_SUPPORTED_TYPES_PROTO; - - public static final ApiFuture SINGLE_WRITE_COMMIT_RESPONSE; - - public static final ApiFuture FIELD_TRANSFORM_COMMIT_RESPONSE; - - public static final Date DATE; - public static final Timestamp TIMESTAMP; - public static final GeoPoint GEO_POINT; - public static final Blob BLOB; - - - public record SingleComponent( - String foo) { - } - - public record NestedRecord( - SingleComponent first, - AllSupportedTypes second) { - } - - public record NestedRecordWithPOJO( - SingleField first, - AllSupportedTypes second) { - } - - public static class NestedPOJOWithRecord { - public SingleField first = new SingleField(); - public AllSupportedTypes second = ALL_SUPPORTED_TYPES_OBJECT; - } - - public record ServerTimestamp ( - - @com.google.cloud.firestore.annotation.ServerTimestamp Date foo, - Inner inner - - ){ - record Inner ( - - @com.google.cloud.firestore.annotation.ServerTimestamp Date bar - ){} - } - - public record InvalidRecord ( - BigInteger bigIntegerValue, - Byte byteValue, - Short shortValue - ){} - - - public record AllSupportedTypes ( - - String foo, - Double doubleValue, - long longValue, - double nanValue, - double infValue, - double negInfValue, - boolean trueValue, - boolean falseValue, - SingleComponent objectValue, - Date dateValue, - Timestamp timestampValue, - List arrayValue, - String nullValue, - Blob bytesValue, - GeoPoint geoPointValue, - Map model - ){} - - static { - try { - DATE = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S z").parse("1985-03-18 08:20:00.123 CET"); - } catch (ParseException e) { - throw new RuntimeException("Failed to parse date", e); - } - - TIMESTAMP = - Timestamp.ofTimeSecondsAndNanos( - TimeUnit.MILLISECONDS.toSeconds(DATE.getTime()), - 123000); // Firestore truncates to microsecond precision. - GEO_POINT = new GeoPoint(50.1430847, -122.9477780); - BLOB = Blob.fromBytes(new byte[] {1, 2, 3}); - - DATABASE_NAME = "projects/test-project/databases/(default)"; - DOCUMENT_PATH = "coll/doc"; - DOCUMENT_NAME = DATABASE_NAME + "/documents/" + DOCUMENT_PATH; - DOCUMENT_ROOT = DATABASE_NAME + "/documents/"; - - SINGLE_COMPONENT_OBJECT = new SingleComponent("bar"); - SINGLE_COMPONENT_PROTO = map("foo", Value.newBuilder().setStringValue("bar").build()); - - SERVER_TIMESTAMP_PROTO = Collections.emptyMap(); - SERVER_TIMESTAMP_OBJECT = new ServerTimestamp(null, new ServerTimestamp.Inner(null)); - - ALL_SUPPORTED_TYPES_OBJECT = new AllSupportedTypes("bar", 0.0, 0L, Double.NaN, Double.POSITIVE_INFINITY, - Double.NEGATIVE_INFINITY, true, false, - new SingleComponent("bar"), DATE, - TIMESTAMP, ImmutableList.of("foo"), null, BLOB, GEO_POINT, - ImmutableMap.of("foo", SINGLE_COMPONENT_OBJECT.foo())); - ALL_SUPPORTED_TYPES_PROTO = - ImmutableMap.builder() - .put("foo", Value.newBuilder().setStringValue("bar").build()) - .put("doubleValue", Value.newBuilder().setDoubleValue(0.0).build()) - .put("longValue", Value.newBuilder().setIntegerValue(0L).build()) - .put("nanValue", Value.newBuilder().setDoubleValue(Double.NaN).build()) - .put("infValue", Value.newBuilder().setDoubleValue(Double.POSITIVE_INFINITY).build()) - .put("negInfValue", Value.newBuilder().setDoubleValue(Double.NEGATIVE_INFINITY).build()) - .put("trueValue", Value.newBuilder().setBooleanValue(true).build()) - .put("falseValue", Value.newBuilder().setBooleanValue(false).build()) - .put( - "objectValue", - Value.newBuilder() - .setMapValue(MapValue.newBuilder().putAllFields(SINGLE_COMPONENT_PROTO)) - .build()) - .put( - "dateValue", - Value.newBuilder() - .setTimestampValue( - com.google.protobuf.Timestamp.newBuilder() - .setSeconds(479978400) - .setNanos(123000000)) // Dates only support millisecond precision. - .build()) - .put( - "timestampValue", - Value.newBuilder() - .setTimestampValue( - com.google.protobuf.Timestamp.newBuilder() - .setSeconds(479978400) - .setNanos(123000)) // Timestamps supports microsecond precision. - .build()) - .put( - "arrayValue", - Value.newBuilder() - .setArrayValue( - ArrayValue.newBuilder().addValues(Value.newBuilder().setStringValue("foo"))) - .build()) - .put("nullValue", Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()) - .put("bytesValue", Value.newBuilder().setBytesValue(BLOB.toByteString()).build()) - .put( - "geoPointValue", - Value.newBuilder() - .setGeoPointValue( - LatLng.newBuilder().setLatitude(50.1430847).setLongitude(-122.9477780)) - .build()) - .put( - "model", - Value.newBuilder() - .setMapValue(MapValue.newBuilder().putAllFields(SINGLE_COMPONENT_PROTO)) - .build()) - .build(); - SINGLE_WRITE_COMMIT_RESPONSE = commitResponse(/* adds= */ 1, /* deletes= */ 0); - - FIELD_TRANSFORM_COMMIT_RESPONSE = commitResponse(/* adds= */ 2, /* deletes= */ 0); - - NESTED_RECORD_OBJECT = new NestedRecord(SINGLE_COMPONENT_OBJECT, ALL_SUPPORTED_TYPES_OBJECT); - - NESTED_RECORD_WITH_POJO_OBJECT = new NestedRecordWithPOJO(new SingleField(), ALL_SUPPORTED_TYPES_OBJECT); - - NESTED_POJO_WITH_RECORD_OBJECT = new NestedPOJOWithRecord(); - } - -} diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/MapperTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/MapperTest.java index cf5a55c4e..b8508b787 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/MapperTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/MapperTest.java @@ -29,7 +29,6 @@ import com.google.cloud.firestore.annotation.Exclude; import com.google.cloud.firestore.annotation.PropertyName; import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; -import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.common.collect.ImmutableList; import com.google.firestore.v1.DatabaseRootName; diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/ToStringTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/ToStringTest.java index 8b5a7e64e..6779edd18 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/ToStringTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/ToStringTest.java @@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat; import com.google.cloud.Timestamp; -import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.firestore.v1.Value; import java.util.Collections;