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; String tld;
/** References to hosts that are the nameservers for the domain. */ /** References to hosts that are the nameservers for the domain. */
@Transient Set<VKey<Host>> nsHosts; @Expose @Transient Set<VKey<Host>> nsHosts;
/** Contacts. */ /** Contacts. */
VKey<Contact> adminContact; @Expose VKey<Contact> adminContact;
VKey<Contact> billingContact; @Expose VKey<Contact> billingContact;
VKey<Contact> techContact; @Expose VKey<Contact> techContact;
VKey<Contact> registrantContact; @Expose VKey<Contact> registrantContact;
/** Authorization info (aka transfer secret) of the domain. */ /** Authorization info (aka transfer secret) of the domain. */
@Embedded @Embedded

View file

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

View file

@ -21,11 +21,14 @@ import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter; import com.google.gson.stream.JsonWriter;
import google.registry.model.adapters.ClassProcessingTypeAdapterFactory;
import google.registry.model.adapters.CurrencyJsonAdapter; import google.registry.model.adapters.CurrencyJsonAdapter;
import google.registry.model.adapters.SerializableJsonTypeAdapter;
import google.registry.util.CidrAddressBlock; import google.registry.util.CidrAddressBlock;
import google.registry.util.CidrAddressBlock.CidrAddressBlockAdapter; import google.registry.util.CidrAddressBlock.CidrAddressBlockAdapter;
import google.registry.util.DateTimeTypeAdapter; import google.registry.util.DateTimeTypeAdapter;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable;
import org.joda.money.CurrencyUnit; import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -69,9 +72,11 @@ public class GsonUtils {
public static Gson provideGson() { public static Gson provideGson() {
return new GsonBuilder() return new GsonBuilder()
.registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())
.registerTypeAdapter(CidrAddressBlock.class, new CidrAddressBlockAdapter()) .registerTypeAdapter(CidrAddressBlock.class, new CidrAddressBlockAdapter())
.registerTypeAdapter(CurrencyUnit.class, new CurrencyJsonAdapter()) .registerTypeAdapter(CurrencyUnit.class, new CurrencyJsonAdapter())
.registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())
.registerTypeAdapter(Serializable.class, new SerializableJsonTypeAdapter())
.registerTypeAdapterFactory(new ClassProcessingTypeAdapterFactory())
.registerTypeAdapterFactory(new GsonPostProcessableTypeAdapterFactory()) .registerTypeAdapterFactory(new GsonPostProcessableTypeAdapterFactory())
.excludeFieldsWithoutExposeAnnotation() .excludeFieldsWithoutExposeAnnotation()
.create(); .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.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
assertThat(RESPONSE.getPayload()) assertThat(RESPONSE.getPayload())
.isEqualTo( .isEqualTo(
"{\"domainName\":\"exists.tld\",\"registrationExpirationTime\":" "{\"domainName\":\"exists.tld\",\"adminContact\":{\"key\":\"3-ROID\",\"kind\":"
+ "\"294247-01-10T04:00:54.775Z\",\"lastTransferTime\":\"null\",\"repoId\":" + "\"google.registry.model.contact.Contact\"},\"techContact\":{\"key\":\"3-ROID\","
+ "\"2-TLD\",\"currentSponsorRegistrarId\":\"TheRegistrar\",\"creationRegistrarId\"" + "\"kind\":\"google.registry.model.contact.Contact\"},\"registrantContact\":"
+ ":\"TheRegistrar\",\"creationTime\":{\"creationTime\":" + "{\"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\":" + "\"1970-01-01T00:00:00.000Z\"},\"lastEppUpdateTime\":\"null\",\"statuses\":"
+ "[\"INACTIVE\"]}"); + "[\"INACTIVE\"]}");
} }