diff --git a/core/src/main/java/google/registry/model/common/ClassPathManager.java b/core/src/main/java/google/registry/model/common/ClassPathManager.java new file mode 100644 index 000000000..b712c23b8 --- /dev/null +++ b/core/src/main/java/google/registry/model/common/ClassPathManager.java @@ -0,0 +1,63 @@ +// Copyright 2021 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.common; + +import static com.google.common.base.Functions.identity; +import static com.google.common.base.Preconditions.checkArgument; +import static google.registry.model.EntityClasses.ALL_CLASSES; + +import com.google.common.annotations.VisibleForTesting; +import com.googlecode.objectify.annotation.EntitySubclass; +import java.util.Map; +import java.util.stream.Collectors; + +/** A helper to manage class name and class path mapping. */ +public class ClassPathManager { + /** + * Class registry allowing us to restore the original class object from the unqualified class + * name, which is all the datastore key gives us. Note that entities annotated + * with @EntitySubclass are removed because they share the same kind of the key with their parent + * class. + */ + public static final Map> CLASS_REGISTRY = + ALL_CLASSES.stream() + .filter(clazz -> !clazz.isAnnotationPresent(EntitySubclass.class)) + .collect(Collectors.toMap(com.googlecode.objectify.Key::getKind, identity())); + + /** + * Class name registry allowing us to obtain the class name the unqualified class, which is all + * the datastore key gives us. Note that entities annotated with @EntitySubclass are removed + * because they share the same kind of the key with their parent class. + */ + public static final Map, String> CLASS_NAME_REGISTRY = + ALL_CLASSES.stream() + .filter(clazz -> !clazz.isAnnotationPresent(EntitySubclass.class)) + .collect(Collectors.toMap(identity(), com.googlecode.objectify.Key::getKind)); + + @VisibleForTesting + public static void addTestEntityClass(Class clazz) { + CLASS_REGISTRY.put(com.googlecode.objectify.Key.getKind(clazz), clazz); + } + + public static Class getClass(String className) { + checkArgument(CLASS_REGISTRY.containsKey(className), "Class not found in class registry"); + return (Class) CLASS_REGISTRY.get(className); + } + + public static String getClassName(Class clazz) { + checkArgument(CLASS_NAME_REGISTRY.containsKey(clazz), "Class not found in class name registry"); + return CLASS_NAME_REGISTRY.get(clazz); + } +} diff --git a/core/src/main/java/google/registry/model/translators/VKeyTranslatorFactory.java b/core/src/main/java/google/registry/model/translators/VKeyTranslatorFactory.java index 113dc7da0..192c429e4 100644 --- a/core/src/main/java/google/registry/model/translators/VKeyTranslatorFactory.java +++ b/core/src/main/java/google/registry/model/translators/VKeyTranslatorFactory.java @@ -14,18 +14,13 @@ package google.registry.model.translators; -import static com.google.common.base.Functions.identity; import static com.google.common.base.Preconditions.checkArgument; -import static google.registry.model.EntityClasses.ALL_CLASSES; import com.google.appengine.api.datastore.Key; -import com.google.common.annotations.VisibleForTesting; -import com.googlecode.objectify.annotation.EntitySubclass; +import google.registry.model.common.ClassPathManager; import google.registry.persistence.VKey; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Map; -import java.util.stream.Collectors; import javax.annotation.Nullable; /** @@ -36,15 +31,6 @@ import javax.annotation.Nullable; */ public class VKeyTranslatorFactory extends AbstractSimpleTranslatorFactory { - // Class registry allowing us to restore the original class object from the unqualified class - // name, which is all the datastore key gives us. - // Note that entities annotated with @EntitySubclass are removed because they share the same - // kind of the key with their parent class. - private static final Map> CLASS_REGISTRY = - ALL_CLASSES.stream() - .filter(clazz -> !clazz.isAnnotationPresent(EntitySubclass.class)) - .collect(Collectors.toMap(com.googlecode.objectify.Key::getKind, identity())); - public VKeyTranslatorFactory() { super(VKey.class); } @@ -67,7 +53,7 @@ public class VKeyTranslatorFactory extends AbstractSimpleTranslatorFactory clazz = (Class) CLASS_REGISTRY.get(key.getKind()); + Class clazz = ClassPathManager.getClass(key.getKind()); checkArgument(clazz != null, "Unknown Key type: %s", key.getKind()); try { Method createVKeyMethod = @@ -92,11 +78,6 @@ public class VKeyTranslatorFactory extends AbstractSimpleTranslatorFactory clazz) { - CLASS_REGISTRY.put(com.googlecode.objectify.Key.getKind(clazz), clazz); - } - @Override public SimpleTranslator createTranslator() { return new SimpleTranslator() { diff --git a/core/src/test/java/google/registry/backup/ReplayCommitLogsToSqlActionTest.java b/core/src/test/java/google/registry/backup/ReplayCommitLogsToSqlActionTest.java index 0e4377455..5b276861f 100644 --- a/core/src/test/java/google/registry/backup/ReplayCommitLogsToSqlActionTest.java +++ b/core/src/test/java/google/registry/backup/ReplayCommitLogsToSqlActionTest.java @@ -48,6 +48,7 @@ import com.google.common.truth.Truth8; import com.google.common.util.concurrent.MoreExecutors; import com.googlecode.objectify.Key; import google.registry.gcs.GcsUtils; +import google.registry.model.common.ClassPathManager; import google.registry.model.common.DatabaseMigrationStateSchedule; import google.registry.model.common.DatabaseMigrationStateSchedule.MigrationState; import google.registry.model.contact.ContactResource; @@ -67,7 +68,6 @@ import google.registry.model.replay.SqlReplayCheckpoint; import google.registry.model.server.Lock; import google.registry.model.tld.label.PremiumList; import google.registry.model.tld.label.PremiumList.PremiumEntry; -import google.registry.model.translators.VKeyTranslatorFactory; import google.registry.persistence.VKey; import google.registry.persistence.transaction.JpaTransactionManager; import google.registry.persistence.transaction.TransactionManagerFactory; @@ -130,7 +130,7 @@ public class ReplayCommitLogsToSqlActionTest { @BeforeAll static void beforeAll() { - VKeyTranslatorFactory.addTestEntityClass(TestObject.class); + ClassPathManager.addTestEntityClass(TestObject.class); } @BeforeEach diff --git a/core/src/test/java/google/registry/model/common/ClassPathManagerTest.java b/core/src/test/java/google/registry/model/common/ClassPathManagerTest.java new file mode 100644 index 000000000..458f039ea --- /dev/null +++ b/core/src/test/java/google/registry/model/common/ClassPathManagerTest.java @@ -0,0 +1,177 @@ +// Copyright 2021 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.common; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import google.registry.model.billing.BillingEvent.Cancellation; +import google.registry.model.billing.BillingEvent.Modification; +import google.registry.model.billing.BillingEvent.OneTime; +import google.registry.model.billing.BillingEvent.Recurring; +import google.registry.model.contact.ContactResource; +import google.registry.model.domain.DomainBase; +import google.registry.model.domain.DomainHistory; +import google.registry.model.domain.token.AllocationToken; +import google.registry.model.host.HostResource; +import google.registry.model.index.EppResourceIndex; +import google.registry.model.index.EppResourceIndexBucket; +import google.registry.model.index.ForeignKeyIndex.ForeignKeyContactIndex; +import google.registry.model.index.ForeignKeyIndex.ForeignKeyDomainIndex; +import google.registry.model.index.ForeignKeyIndex.ForeignKeyHostIndex; +import google.registry.model.ofy.CommitLogBucket; +import google.registry.model.ofy.CommitLogCheckpoint; +import google.registry.model.ofy.CommitLogCheckpointRoot; +import google.registry.model.ofy.CommitLogManifest; +import google.registry.model.ofy.CommitLogMutation; +import google.registry.model.poll.PollMessage; +import google.registry.model.rde.RdeRevision; +import google.registry.model.registrar.Registrar; +import google.registry.model.registrar.RegistrarContact; +import google.registry.model.replay.LastSqlTransaction; +import google.registry.model.reporting.HistoryEntry; +import google.registry.model.server.Lock; +import google.registry.model.server.ServerSecret; +import google.registry.model.tld.Registry; +import google.registry.testing.TestObject; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ClassPathManager}. */ +public class ClassPathManagerTest { + @Test + void getClass_classInClassRegistry_returnsClass() throws ClassNotFoundException { + /** + * Class names are used in stringified vkeys, which can be present in task queues. Class name is + * required to create a vkey. Changing these names could break task queue entries that are + * present during a rollout. If you want to change the names of any of the classses supported in + * CLASS_REGISTRY, you'll need to introduce some mechanism to deal with this. One way is to find + * the corresponding class name by calling ClassPathManager.getClassName(clazz). The classes + * below are all classes supported in CLASS_REGISTRY. This test breaks if someone changes a + * classname without preserving the original name. + */ + assertThat(ClassPathManager.getClass("ForeignKeyContactIndex")) + .isEqualTo(ForeignKeyContactIndex.class); + assertThat(ClassPathManager.getClass("Modification")).isEqualTo(Modification.class); + assertThat(ClassPathManager.getClass("CommitLogCheckpoint")) + .isEqualTo(CommitLogCheckpoint.class); + assertThat(ClassPathManager.getClass("CommitLogManifest")).isEqualTo(CommitLogManifest.class); + assertThat(ClassPathManager.getClass("AllocationToken")).isEqualTo(AllocationToken.class); + assertThat(ClassPathManager.getClass("OneTime")).isEqualTo(OneTime.class); + assertThat(ClassPathManager.getClass("Cursor")).isEqualTo(Cursor.class); + assertThat(ClassPathManager.getClass("RdeRevision")).isEqualTo(RdeRevision.class); + assertThat(ClassPathManager.getClass("HostResource")).isEqualTo(HostResource.class); + assertThat(ClassPathManager.getClass("Recurring")).isEqualTo(Recurring.class); + assertThat(ClassPathManager.getClass("Registrar")).isEqualTo(Registrar.class); + assertThat(ClassPathManager.getClass("ContactResource")).isEqualTo(ContactResource.class); + assertThat(ClassPathManager.getClass("Cancellation")).isEqualTo(Cancellation.class); + assertThat(ClassPathManager.getClass("RegistrarContact")).isEqualTo(RegistrarContact.class); + assertThat(ClassPathManager.getClass("CommitLogBucket")).isEqualTo(CommitLogBucket.class); + assertThat(ClassPathManager.getClass("LastSqlTransaction")).isEqualTo(LastSqlTransaction.class); + assertThat(ClassPathManager.getClass("CommitLogCheckpointRoot")) + .isEqualTo(CommitLogCheckpointRoot.class); + assertThat(ClassPathManager.getClass("GaeUserIdConverter")).isEqualTo(GaeUserIdConverter.class); + assertThat(ClassPathManager.getClass("EppResourceIndexBucket")) + .isEqualTo(EppResourceIndexBucket.class); + assertThat(ClassPathManager.getClass("Registry")).isEqualTo(Registry.class); + assertThat(ClassPathManager.getClass("EntityGroupRoot")).isEqualTo(EntityGroupRoot.class); + assertThat(ClassPathManager.getClass("Lock")).isEqualTo(Lock.class); + assertThat(ClassPathManager.getClass("DomainBase")).isEqualTo(DomainBase.class); + assertThat(ClassPathManager.getClass("CommitLogMutation")).isEqualTo(CommitLogMutation.class); + assertThat(ClassPathManager.getClass("HistoryEntry")).isEqualTo(HistoryEntry.class); + assertThat(ClassPathManager.getClass("PollMessage")).isEqualTo(PollMessage.class); + assertThat(ClassPathManager.getClass("ForeignKeyHostIndex")) + .isEqualTo(ForeignKeyHostIndex.class); + assertThat(ClassPathManager.getClass("ServerSecret")).isEqualTo(ServerSecret.class); + assertThat(ClassPathManager.getClass("EppResourceIndex")).isEqualTo(EppResourceIndex.class); + assertThat(ClassPathManager.getClass("ForeignKeyDomainIndex")) + .isEqualTo(ForeignKeyDomainIndex.class); + } + + @Test + void getClass_classNotInClassRegistry_throwsException() { + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, () -> ClassPathManager.getClass("DomainHistory")); + assertThat(thrown).hasMessageThat().contains("Class not found in class registry"); + } + + @Test + void getClassName_classNotInClassRegistry_throwsException() { + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> ClassPathManager.getClassName(DomainHistory.class)); + assertThat(thrown).hasMessageThat().contains("Class not found in class name registry"); + } + + @Test + void getClassName() { + /** + * Class names are used in stringified vkeys, which can be present in task queues. Class name is + * required to create a vkey. Changing these names could break task queue entries that are + * present during a rollout. If you want to change the names of any of the classses supported in + * CLASS_NAME_REGISTRY, you'll need to introduce some mechanism to deal with this. + * ClassPathManager.getClassName(clazz) allows you to verify the corresponding name of a class. + * The classes below are all classes supported in CLASS_NAME_REGISTRY. This test breaks if + * someone changes a classname without preserving the original name. + */ + assertThat(ClassPathManager.getClassName(ForeignKeyContactIndex.class)) + .isEqualTo("ForeignKeyContactIndex"); + assertThat(ClassPathManager.getClassName(Modification.class)).isEqualTo("Modification"); + assertThat(ClassPathManager.getClassName(CommitLogCheckpoint.class)) + .isEqualTo("CommitLogCheckpoint"); + assertThat(ClassPathManager.getClassName(CommitLogManifest.class)) + .isEqualTo("CommitLogManifest"); + assertThat(ClassPathManager.getClassName(AllocationToken.class)).isEqualTo("AllocationToken"); + assertThat(ClassPathManager.getClassName(OneTime.class)).isEqualTo("OneTime"); + assertThat(ClassPathManager.getClassName(Cursor.class)).isEqualTo("Cursor"); + assertThat(ClassPathManager.getClassName(RdeRevision.class)).isEqualTo("RdeRevision"); + assertThat(ClassPathManager.getClassName(HostResource.class)).isEqualTo("HostResource"); + assertThat(ClassPathManager.getClassName(Recurring.class)).isEqualTo("Recurring"); + assertThat(ClassPathManager.getClassName(Registrar.class)).isEqualTo("Registrar"); + assertThat(ClassPathManager.getClassName(ContactResource.class)).isEqualTo("ContactResource"); + assertThat(ClassPathManager.getClassName(Cancellation.class)).isEqualTo("Cancellation"); + assertThat(ClassPathManager.getClassName(RegistrarContact.class)).isEqualTo("RegistrarContact"); + assertThat(ClassPathManager.getClassName(CommitLogBucket.class)).isEqualTo("CommitLogBucket"); + assertThat(ClassPathManager.getClassName(LastSqlTransaction.class)) + .isEqualTo("LastSqlTransaction"); + assertThat(ClassPathManager.getClassName(CommitLogCheckpointRoot.class)) + .isEqualTo("CommitLogCheckpointRoot"); + assertThat(ClassPathManager.getClassName(GaeUserIdConverter.class)) + .isEqualTo("GaeUserIdConverter"); + assertThat(ClassPathManager.getClassName(EppResourceIndexBucket.class)) + .isEqualTo("EppResourceIndexBucket"); + assertThat(ClassPathManager.getClassName(Registry.class)).isEqualTo("Registry"); + assertThat(ClassPathManager.getClassName(EntityGroupRoot.class)).isEqualTo("EntityGroupRoot"); + assertThat(ClassPathManager.getClassName(Lock.class)).isEqualTo("Lock"); + assertThat(ClassPathManager.getClassName(DomainBase.class)).isEqualTo("DomainBase"); + assertThat(ClassPathManager.getClassName(CommitLogMutation.class)) + .isEqualTo("CommitLogMutation"); + assertThat(ClassPathManager.getClassName(HistoryEntry.class)).isEqualTo("HistoryEntry"); + assertThat(ClassPathManager.getClassName(PollMessage.class)).isEqualTo("PollMessage"); + assertThat(ClassPathManager.getClassName(ForeignKeyHostIndex.class)) + .isEqualTo("ForeignKeyHostIndex"); + assertThat(ClassPathManager.getClassName(ServerSecret.class)).isEqualTo("ServerSecret"); + assertThat(ClassPathManager.getClassName(EppResourceIndex.class)).isEqualTo("EppResourceIndex"); + assertThat(ClassPathManager.getClassName(ForeignKeyDomainIndex.class)) + .isEqualTo("ForeignKeyDomainIndex"); + } + + @Test + void addTestEntityClass_success() { + ClassPathManager.addTestEntityClass(TestObject.class); + assertThat(ClassPathManager.getClass("TestObject")).isEqualTo(TestObject.class); + } +} diff --git a/core/src/test/java/google/registry/model/translators/VKeyTranslatorFactoryTest.java b/core/src/test/java/google/registry/model/translators/VKeyTranslatorFactoryTest.java index 557bf2ff1..f954d01db 100644 --- a/core/src/test/java/google/registry/model/translators/VKeyTranslatorFactoryTest.java +++ b/core/src/test/java/google/registry/model/translators/VKeyTranslatorFactoryTest.java @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.googlecode.objectify.Key; import google.registry.model.billing.BillingEvent; +import google.registry.model.common.ClassPathManager; import google.registry.model.domain.DomainBase; import google.registry.model.ofy.CommitLogCheckpoint; import google.registry.model.ofy.CommitLogCheckpointRoot; @@ -32,6 +33,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +/** Unit tests for {@link VKeyTranslatorFactory}. */ public class VKeyTranslatorFactoryTest { @RegisterExtension @@ -45,7 +47,7 @@ public class VKeyTranslatorFactoryTest { @BeforeAll static void beforeAll() { - VKeyTranslatorFactory.addTestEntityClass(TestObject.class); + ClassPathManager.addTestEntityClass(TestObject.class); } @Test diff --git a/core/src/test/java/google/registry/persistence/VKeyTest.java b/core/src/test/java/google/registry/persistence/VKeyTest.java index 1209975e5..c36343cc0 100644 --- a/core/src/test/java/google/registry/persistence/VKeyTest.java +++ b/core/src/test/java/google/registry/persistence/VKeyTest.java @@ -27,9 +27,9 @@ import com.google.common.collect.ImmutableMap; import com.googlecode.objectify.Key; import com.googlecode.objectify.annotation.Entity; import google.registry.model.billing.BillingEvent.OneTime; +import google.registry.model.common.ClassPathManager; import google.registry.model.domain.DomainBase; import google.registry.model.registrar.RegistrarContact; -import google.registry.model.translators.VKeyTranslatorFactory; import google.registry.testing.AppEngineExtension; import google.registry.testing.TaskQueueHelper.TaskMatcher; import google.registry.testing.TestObject; @@ -63,7 +63,7 @@ class VKeyTest { @BeforeAll static void beforeAll() { - VKeyTranslatorFactory.addTestEntityClass(TestObject.class); + ClassPathManager.addTestEntityClass(TestObject.class); } @Test