Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for java record serialization #1223

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions google-cloud-firestore/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -321,5 +321,49 @@
</dependency>
</dependencies>
</profile>
<profile>
<!-- And different set up for JDK 17 -->
<id>java17</id>
<activation>
<jdk>17</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-test-source</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/test-jdk17/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<inherited>true</inherited>
<configuration>
<!-- Enable Java 17 for all sources so that Intellij picks the right language level -->
<source>17</source>
<release>17</release>
<compilerArgs>
<arg>-parameters</arg>
<arg>--add-opens=java.base/java.lang=ALL-UNNAMED</arg>
<arg>--add-opens=java.base/java.util=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* 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.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<T> {
private final Class<T> 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<String> 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<String> documentIdPropertyNames;

BeanMapper(Class<T> clazz) {
this.clazz = clazz;
throwOnUnknownProperties = clazz.isAnnotationPresent(ThrowOnExtraProperties.class);
warnOnUnknownProperties = !clazz.isAnnotationPresent(IgnoreExtraProperties.class);
serverTimestamps = new HashSet<>();
documentIdPropertyNames = new HashSet<>();
}

Class<T> 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<String, Object> 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<String, Object> values,
Map<TypeVariable<Class<T>>, Type> types,
DeserializeContext context);

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.");
}
}

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);
}
}

Type resolveType(Type type, Map<TypeVariable<Class<T>>, 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;
}

void checkForDocIdConflict(
String docIdPropertyName,
Collection<String> 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);
}
}

T deserialize(Map<String, Object> values, DeserializeContext context) {
return deserialize(values, Collections.emptyMap(), context);
}

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));
}
}

static String propertyName(Field field) {
String annotatedName = annotatedName(field);
return annotatedName != null ? annotatedName : field.getName();
}

static String annotatedName(AccessibleObject obj) {
if (obj.isAnnotationPresent(PropertyName.class)) {
PropertyName annotation = obj.getAnnotation(PropertyName.class);
return annotation.value();
}

return null;
}
}
Loading