Add TypeAdapters for VKey objects (#2194)

GSON doesn't allow for clean (de)serialization of Class or Serializable
objects which we'll need for converting VKeys to/from JSON.
This commit is contained in:
gbrodman 2023-10-31 15:14:41 -04:00 committed by GitHub
parent 9330e3a50d
commit 7332b1fa38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 195 additions and 11 deletions

View file

@ -0,0 +1,40 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// 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 google.registry.model.adapters;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
/**
* Adapter factory that allows for (de)serialization of Class objects in GSON.
*
* <p>GSON's built-in adapter for Class objects throws an exception, but there are situations where
* we want to (de)serialize these, such as in VKeys. This instructs GSON to look for our custom
* {@link ClassTypeAdapter} rather than the default.
*/
public class ClassProcessingTypeAdapterFactory implements TypeAdapterFactory {
@Override
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
if (Class.class.isAssignableFrom(typeToken.getRawType())) {
// in this case, T is a class object
return (TypeAdapter<T>) new ClassTypeAdapter();
}
return null;
}
}

View file

@ -0,0 +1,48 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// 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 google.registry.model.adapters;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
/**
* TypeAdapter for {@link Class} objects.
*
* <p>GSON's default adapter doesn't allow this, but we want to allow for (de)serialization of Class
* objects for containers like VKeys using the full name of the class.
*/
public class ClassTypeAdapter extends TypeAdapter<Class<?>> {
@Override
public void write(JsonWriter out, Class value) throws IOException {
out.value(value.getName());
}
@Override
public Class<?> read(JsonReader reader) throws IOException {
String stringValue = reader.nextString();
if (stringValue.equals("null")) {
return null;
}
try {
return Class.forName(stringValue);
} catch (ClassNotFoundException e) {
// this should not happen...
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,38 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// 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 google.registry.model.adapters;
import google.registry.util.StringBaseTypeAdapter;
import java.io.IOException;
import java.io.Serializable;
/**
* TypeAdapter for {@link Serializable} objects.
*
* <p>VKey keys (primary keys in SQL) are usually represented by either a long or a String. There
* are a couple situations (CursorId, HistoryEntryId) where the Serializable in question is a
* complex object, but we do not need to worry about (de)serializing those objects to/from JSON.
*/
public class SerializableJsonTypeAdapter extends StringBaseTypeAdapter<Serializable> {
@Override
protected Serializable fromString(String stringValue) throws IOException {
try {
return Long.parseLong(stringValue);
} catch (NumberFormatException e) {
return stringValue;
}
}
}

View file

@ -128,14 +128,14 @@ public class DomainBase extends EppResource
String tld;
/** References to hosts that are the nameservers for the domain. */
@Transient Set<VKey<Host>> nsHosts;
@Expose @Transient Set<VKey<Host>> nsHosts;
/** Contacts. */
VKey<Contact> adminContact;
@Expose VKey<Contact> adminContact;
VKey<Contact> billingContact;
VKey<Contact> techContact;
VKey<Contact> registrantContact;
@Expose VKey<Contact> billingContact;
@Expose VKey<Contact> techContact;
@Expose VKey<Contact> registrantContact;
/** Authorization info (aka transfer secret) of the domain. */
@Embedded

View file

@ -57,7 +57,7 @@ public class VKey<T> extends ImmutableObject implements Serializable {
// The primary key for the referenced entity.
@Expose Serializable key;
Class<? extends T> kind;
@Expose Class<? extends T> kind;
@SuppressWarnings("unused")
VKey() {}

View file

@ -21,11 +21,14 @@ import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import google.registry.model.adapters.ClassProcessingTypeAdapterFactory;
import google.registry.model.adapters.CurrencyJsonAdapter;
import google.registry.model.adapters.SerializableJsonTypeAdapter;
import google.registry.util.CidrAddressBlock;
import google.registry.util.CidrAddressBlock.CidrAddressBlockAdapter;
import google.registry.util.DateTimeTypeAdapter;
import java.io.IOException;
import java.io.Serializable;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime;
@ -69,9 +72,11 @@ public class GsonUtils {
public static Gson provideGson() {
return new GsonBuilder()
.registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())
.registerTypeAdapter(CidrAddressBlock.class, new CidrAddressBlockAdapter())
.registerTypeAdapter(CurrencyUnit.class, new CurrencyJsonAdapter())
.registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())
.registerTypeAdapter(Serializable.class, new SerializableJsonTypeAdapter())
.registerTypeAdapterFactory(new ClassProcessingTypeAdapterFactory())
.registerTypeAdapterFactory(new GsonPostProcessableTypeAdapterFactory())
.excludeFieldsWithoutExposeAnnotation()
.create();

View file

@ -0,0 +1,49 @@
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
//
// 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 google.registry.model.adapters;
import static com.google.common.truth.Truth.assertThat;
import com.google.gson.Gson;
import google.registry.model.billing.BillingEvent;
import google.registry.model.domain.Domain;
import google.registry.persistence.VKey;
import google.registry.tools.GsonUtils;
import org.junit.jupiter.api.Test;
/** Tests for {@link ClassTypeAdapter} and {@link SerializableJsonTypeAdapter}. */
public class VKeyAdapterTest {
private static final Gson GSON = GsonUtils.provideGson();
@Test
void testVKeyConversion_string() {
VKey<Domain> vkey = VKey.create(Domain.class, "someRepoId");
String vkeyJson = GSON.toJson(vkey);
assertThat(vkeyJson)
.isEqualTo(
"{\"key\":\"someRepoId\",\"kind\":" + "\"google.registry.model.domain.Domain\"}");
assertThat(GSON.fromJson(vkeyJson, VKey.class)).isEqualTo(vkey);
}
@Test
void testVKeyConversion_number() {
VKey<BillingEvent> vkey = VKey.create(BillingEvent.class, 203L);
String vkeyJson = GSON.toJson(vkey);
assertThat(vkeyJson)
.isEqualTo("{\"key\":203,\"kind\":" + "\"google.registry.model.billing.BillingEvent\"}");
assertThat(GSON.fromJson(vkeyJson, VKey.class)).isEqualTo(vkey);
}
}

View file

@ -66,10 +66,14 @@ public class ConsoleDomainGetActionTest {
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
assertThat(RESPONSE.getPayload())
.isEqualTo(
"{\"domainName\":\"exists.tld\",\"registrationExpirationTime\":"
+ "\"294247-01-10T04:00:54.775Z\",\"lastTransferTime\":\"null\",\"repoId\":"
+ "\"2-TLD\",\"currentSponsorRegistrarId\":\"TheRegistrar\",\"creationRegistrarId\""
+ ":\"TheRegistrar\",\"creationTime\":{\"creationTime\":"
"{\"domainName\":\"exists.tld\",\"adminContact\":{\"key\":\"3-ROID\",\"kind\":"
+ "\"google.registry.model.contact.Contact\"},\"techContact\":{\"key\":\"3-ROID\","
+ "\"kind\":\"google.registry.model.contact.Contact\"},\"registrantContact\":"
+ "{\"key\":\"3-ROID\",\"kind\":\"google.registry.model.contact.Contact\"},"
+ "\"registrationExpirationTime\":\"294247-01-10T04:00:54.775Z\","
+ "\"lastTransferTime\":\"null\",\"repoId\":\"2-TLD\","
+ "\"currentSponsorRegistrarId\":\"TheRegistrar\",\"creationRegistrarId\":"
+ "\"TheRegistrar\",\"creationTime\":{\"creationTime\":"
+ "\"1970-01-01T00:00:00.000Z\"},\"lastEppUpdateTime\":\"null\",\"statuses\":"
+ "[\"INACTIVE\"]}");
}