From 7332b1fa386544ecb13ac930e5bec372961e3cc0 Mon Sep 17 00:00:00 2001 From: gbrodman Date: Tue, 31 Oct 2023 15:14:41 -0400 Subject: [PATCH] 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. --- .../ClassProcessingTypeAdapterFactory.java | 40 +++++++++++++++ .../model/adapters/ClassTypeAdapter.java | 48 ++++++++++++++++++ .../adapters/SerializableJsonTypeAdapter.java | 38 ++++++++++++++ .../registry/model/domain/DomainBase.java | 10 ++-- .../google/registry/persistence/VKey.java | 2 +- .../java/google/registry/tools/GsonUtils.java | 7 ++- .../model/adapters/VKeyAdapterTest.java | 49 +++++++++++++++++++ .../console/ConsoleDomainGetActionTest.java | 12 +++-- 8 files changed, 195 insertions(+), 11 deletions(-) create mode 100644 core/src/main/java/google/registry/model/adapters/ClassProcessingTypeAdapterFactory.java create mode 100644 core/src/main/java/google/registry/model/adapters/ClassTypeAdapter.java create mode 100644 core/src/main/java/google/registry/model/adapters/SerializableJsonTypeAdapter.java create mode 100644 core/src/test/java/google/registry/model/adapters/VKeyAdapterTest.java diff --git a/core/src/main/java/google/registry/model/adapters/ClassProcessingTypeAdapterFactory.java b/core/src/main/java/google/registry/model/adapters/ClassProcessingTypeAdapterFactory.java new file mode 100644 index 000000000..5abefb679 --- /dev/null +++ b/core/src/main/java/google/registry/model/adapters/ClassProcessingTypeAdapterFactory.java @@ -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. + * + *

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 TypeAdapter create(Gson gson, TypeToken typeToken) { + if (Class.class.isAssignableFrom(typeToken.getRawType())) { + // in this case, T is a class object + return (TypeAdapter) new ClassTypeAdapter(); + } + return null; + } +} diff --git a/core/src/main/java/google/registry/model/adapters/ClassTypeAdapter.java b/core/src/main/java/google/registry/model/adapters/ClassTypeAdapter.java new file mode 100644 index 000000000..abdfc9a77 --- /dev/null +++ b/core/src/main/java/google/registry/model/adapters/ClassTypeAdapter.java @@ -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. + * + *

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> { + + @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); + } + } +} diff --git a/core/src/main/java/google/registry/model/adapters/SerializableJsonTypeAdapter.java b/core/src/main/java/google/registry/model/adapters/SerializableJsonTypeAdapter.java new file mode 100644 index 000000000..e2933d808 --- /dev/null +++ b/core/src/main/java/google/registry/model/adapters/SerializableJsonTypeAdapter.java @@ -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. + * + *

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 { + + @Override + protected Serializable fromString(String stringValue) throws IOException { + try { + return Long.parseLong(stringValue); + } catch (NumberFormatException e) { + return stringValue; + } + } +} diff --git a/core/src/main/java/google/registry/model/domain/DomainBase.java b/core/src/main/java/google/registry/model/domain/DomainBase.java index ec0a13b2d..95b84ff9c 100644 --- a/core/src/main/java/google/registry/model/domain/DomainBase.java +++ b/core/src/main/java/google/registry/model/domain/DomainBase.java @@ -128,14 +128,14 @@ public class DomainBase extends EppResource String tld; /** References to hosts that are the nameservers for the domain. */ - @Transient Set> nsHosts; + @Expose @Transient Set> nsHosts; /** Contacts. */ - VKey adminContact; + @Expose VKey adminContact; - VKey billingContact; - VKey techContact; - VKey registrantContact; + @Expose VKey billingContact; + @Expose VKey techContact; + @Expose VKey registrantContact; /** Authorization info (aka transfer secret) of the domain. */ @Embedded diff --git a/core/src/main/java/google/registry/persistence/VKey.java b/core/src/main/java/google/registry/persistence/VKey.java index b2c9fb83b..e1a65b995 100644 --- a/core/src/main/java/google/registry/persistence/VKey.java +++ b/core/src/main/java/google/registry/persistence/VKey.java @@ -57,7 +57,7 @@ public class VKey extends ImmutableObject implements Serializable { // The primary key for the referenced entity. @Expose Serializable key; - Class kind; + @Expose Class kind; @SuppressWarnings("unused") VKey() {} diff --git a/core/src/main/java/google/registry/tools/GsonUtils.java b/core/src/main/java/google/registry/tools/GsonUtils.java index be85145e2..96a22dc4d 100644 --- a/core/src/main/java/google/registry/tools/GsonUtils.java +++ b/core/src/main/java/google/registry/tools/GsonUtils.java @@ -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(); diff --git a/core/src/test/java/google/registry/model/adapters/VKeyAdapterTest.java b/core/src/test/java/google/registry/model/adapters/VKeyAdapterTest.java new file mode 100644 index 000000000..e2abde604 --- /dev/null +++ b/core/src/test/java/google/registry/model/adapters/VKeyAdapterTest.java @@ -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 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 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); + } +} diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java index 46658e833..0463c0d92 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java @@ -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\"]}"); }