diff --git a/core/src/main/java/google/registry/persistence/converter/BillingCostTransitionConverter.java b/core/src/main/java/google/registry/persistence/converter/BillingCostTransitionConverter.java new file mode 100644 index 000000000..d54fa2089 --- /dev/null +++ b/core/src/main/java/google/registry/persistence/converter/BillingCostTransitionConverter.java @@ -0,0 +1,48 @@ +// Copyright 2020 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.persistence.converter; + +import avro.shaded.com.google.common.collect.Maps; +import google.registry.model.common.TimedTransitionProperty; +import google.registry.model.registry.Registry.BillingCostTransition; +import java.util.Map; +import javax.persistence.Converter; +import org.joda.money.Money; +import org.joda.time.DateTime; + +/** + * JPA converter for storing/retrieving {@link TimedTransitionProperty } objects. + */ +@Converter(autoApply = true) +public class BillingCostTransitionConverter + extends TimedTransitionPropertyConverterBase { + + @Override + Map.Entry convertToDatabaseMapEntry( + Map.Entry entry) { + return Maps.immutableEntry(entry.getKey().toString(), entry.getValue().getValue().toString()); + } + + @Override + Map.Entry convertToEntityMapEntry(Map.Entry entry) { + return Maps.immutableEntry(DateTime.parse(entry.getKey()), Money.parse(entry.getValue())); + } + + @Override + Class getTimedTransitionSubclass() { + return BillingCostTransition.class; + } +} diff --git a/core/src/main/java/google/registry/persistence/converter/TimedTransitionPropertyConverterBase.java b/core/src/main/java/google/registry/persistence/converter/TimedTransitionPropertyConverterBase.java new file mode 100644 index 000000000..554bb308a --- /dev/null +++ b/core/src/main/java/google/registry/persistence/converter/TimedTransitionPropertyConverterBase.java @@ -0,0 +1,63 @@ +// Copyright 2020 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.persistence.converter; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; + +import com.google.common.collect.ImmutableSortedMap; +import google.registry.model.common.TimedTransitionProperty; +import google.registry.model.common.TimedTransitionProperty.TimedTransition; +import google.registry.persistence.converter.StringMapDescriptor.StringMap; +import java.util.Map; +import javax.annotation.Nullable; +import javax.persistence.AttributeConverter; +import org.joda.time.DateTime; + +/** + * Base JPA converter for {@link TimedTransitionProperty} objects that are stored in a column with + * data type of hstore in the database. + */ +public abstract class TimedTransitionPropertyConverterBase> + implements AttributeConverter, StringMap> { + + abstract Map.Entry convertToDatabaseMapEntry(Map.Entry entry); + + abstract Map.Entry convertToEntityMapEntry(Map.Entry entry); + + abstract Class getTimedTransitionSubclass(); + + @Override + public StringMap convertToDatabaseColumn(@Nullable TimedTransitionProperty attribute) { + return attribute == null + ? null + : StringMap.create( + attribute.entrySet().stream() + .map(this::convertToDatabaseMapEntry) + .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue))); + } + + @Override + public TimedTransitionProperty convertToEntityAttribute(@Nullable StringMap dbData) { + if (dbData == null) { + return null; + } + Map map = + dbData.getMap().entrySet().stream() + .map(this::convertToEntityMapEntry) + .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); + return TimedTransitionProperty.fromValueMap( + ImmutableSortedMap.copyOf(map), getTimedTransitionSubclass()); + } +} diff --git a/core/src/main/java/google/registry/persistence/converter/TldStateTransitionConverter.java b/core/src/main/java/google/registry/persistence/converter/TldStateTransitionConverter.java new file mode 100644 index 000000000..aef4344a1 --- /dev/null +++ b/core/src/main/java/google/registry/persistence/converter/TldStateTransitionConverter.java @@ -0,0 +1,48 @@ +// Copyright 2020 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.persistence.converter; + +import com.google.common.collect.Maps; +import google.registry.model.common.TimedTransitionProperty; +import google.registry.model.registry.Registry.TldState; +import google.registry.model.registry.Registry.TldStateTransition; +import java.util.Map; +import javax.persistence.Converter; +import org.joda.time.DateTime; + +/** + * JPA converter for storing/retrieving {@link TimedTransitionProperty} objects. + */ +@Converter(autoApply = true) +public class TldStateTransitionConverter + extends TimedTransitionPropertyConverterBase { + + @Override + Map.Entry convertToDatabaseMapEntry( + Map.Entry entry) { + return Maps.immutableEntry(entry.getKey().toString(), entry.getValue().getValue().name()); + } + + @Override + Map.Entry convertToEntityMapEntry(Map.Entry entry) { + return Maps.immutableEntry(DateTime.parse(entry.getKey()), TldState.valueOf(entry.getValue())); + } + + @Override + Class getTimedTransitionSubclass() { + return TldStateTransition.class; + } +} diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml index ff910329a..2ed878d4d 100644 --- a/core/src/main/resources/META-INF/persistence.xml +++ b/core/src/main/resources/META-INF/persistence.xml @@ -35,6 +35,7 @@ google.registry.model.domain.GracePeriod + google.registry.persistence.converter.BillingCostTransitionConverter google.registry.persistence.converter.BloomFilterConverter google.registry.persistence.converter.CidrAddressBlockListConverter google.registry.persistence.converter.CreateAutoTimestampConverter @@ -47,6 +48,7 @@ google.registry.persistence.converter.StatusValueSetConverter google.registry.persistence.converter.StringListConverter google.registry.persistence.converter.StringSetConverter + google.registry.persistence.converter.TldStateTransitionConverter google.registry.persistence.converter.UpdateAutoTimestampConverter google.registry.persistence.converter.ZonedDateTimeConverter diff --git a/core/src/test/java/google/registry/persistence/converter/BillingCostTransitionConverterTest.java b/core/src/test/java/google/registry/persistence/converter/BillingCostTransitionConverterTest.java new file mode 100644 index 000000000..c3457a105 --- /dev/null +++ b/core/src/test/java/google/registry/persistence/converter/BillingCostTransitionConverterTest.java @@ -0,0 +1,77 @@ +// Copyright 2020 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.persistence.converter; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; +import static google.registry.util.DateTimeUtils.START_OF_TIME; +import static org.joda.money.CurrencyUnit.USD; + +import com.google.common.collect.ImmutableSortedMap; +import google.registry.model.ImmutableObject; +import google.registry.model.common.TimedTransitionProperty; +import google.registry.model.registry.Registry.BillingCostTransition; +import google.registry.persistence.transaction.JpaTestRules; +import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule; +import javax.persistence.Entity; +import javax.persistence.Id; +import org.joda.money.Money; +import org.joda.time.DateTime; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** Unit tests for {@link BillingCostTransitionConverter}. */ +public class BillingCostTransitionConverterTest { + + @RegisterExtension + public final JpaUnitTestRule jpa = + new JpaTestRules.Builder() + .withInitScript("sql/flyway/V14__load_extension_for_hstore.sql") + .withEntityClass(TestEntity.class) + .buildUnitTestRule(); + + private static final ImmutableSortedMap values = + ImmutableSortedMap.of( + START_OF_TIME, + Money.of(USD, 8), + DateTime.parse("2001-01-01T00:00:00.0Z"), + Money.of(USD, 0)); + + @Test + void roundTripConversion_returnsSameTimedTransitionProperty() { + TimedTransitionProperty timedTransitionProperty = + TimedTransitionProperty.fromValueMap(values, BillingCostTransition.class); + TestEntity testEntity = new TestEntity(timedTransitionProperty); + jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity)); + TestEntity persisted = + jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); + assertThat(persisted.timedTransitionProperty).containsExactlyEntriesIn(timedTransitionProperty); + } + + @Entity(name = "TestEntity") + private static class TestEntity extends ImmutableObject { + + @Id String name = "id"; + + TimedTransitionProperty timedTransitionProperty; + + private TestEntity() {} + + private TestEntity( + TimedTransitionProperty timedTransitionProperty) { + this.timedTransitionProperty = timedTransitionProperty; + } + } +} diff --git a/core/src/test/java/google/registry/persistence/converter/TimedTransitionPropertyConverterBaseTest.java b/core/src/test/java/google/registry/persistence/converter/TimedTransitionPropertyConverterBaseTest.java new file mode 100644 index 000000000..c5e01452d --- /dev/null +++ b/core/src/test/java/google/registry/persistence/converter/TimedTransitionPropertyConverterBaseTest.java @@ -0,0 +1,181 @@ +// Copyright 2020 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.persistence.converter; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; +import static google.registry.util.DateTimeUtils.START_OF_TIME; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Maps; +import google.registry.model.ImmutableObject; +import google.registry.model.common.TimedTransitionProperty; +import google.registry.model.common.TimedTransitionProperty.TimedTransition; +import google.registry.persistence.transaction.JpaTestRules; +import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule; +import java.util.Map; +import javax.persistence.Converter; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.NoResultException; +import org.joda.time.DateTime; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** Unit tests for {@link TimedTransitionPropertyConverterBase}. */ +public class TimedTransitionPropertyConverterBaseTest { + + @RegisterExtension + public final JpaUnitTestRule jpa = + new JpaTestRules.Builder() + .withInitScript("sql/flyway/V14__load_extension_for_hstore.sql") + .withEntityClass(TestTimedTransitionPropertyConverter.class, TestEntity.class) + .buildUnitTestRule(); + + private static final DateTime DATE_1 = DateTime.parse("2001-01-01T00:00:00.000Z"); + private static final DateTime DATE_2 = DateTime.parse("2002-01-01T00:00:00.000Z"); + + private static final ImmutableSortedMap VALUES = + ImmutableSortedMap.of( + START_OF_TIME, "val1", + DATE_1, "val2", + DATE_2, "val3"); + + private static final TimedTransitionProperty TIMED_TRANSITION_PROPERTY = + TimedTransitionProperty.fromValueMap(VALUES, TestTransition.class); + + @Test + void roundTripConversion_returnsSameTimedTransitionProperty() { + TestEntity testEntity = new TestEntity(TIMED_TRANSITION_PROPERTY); + jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity)); + TestEntity persisted = + jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); + assertThat(persisted.property).containsExactlyEntriesIn(TIMED_TRANSITION_PROPERTY); + } + + @Test + void testUpdateColumn_succeeds() { + TestEntity testEntity = new TestEntity(TIMED_TRANSITION_PROPERTY); + jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity)); + TestEntity persisted = + jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); + assertThat(persisted.property).containsExactlyEntriesIn(TIMED_TRANSITION_PROPERTY); + ImmutableSortedMap newValues = ImmutableSortedMap.of(START_OF_TIME, "val4"); + persisted.property = TimedTransitionProperty.fromValueMap(newValues, TestTransition.class); + jpaTm().transact(() -> jpaTm().getEntityManager().merge(persisted)); + TestEntity updated = + jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); + assertThat(updated.property.toValueMap()).isEqualTo(newValues); + } + + @Test + void testNullValue_writesAndReadsNullSuccessfully() { + TestEntity testEntity = new TestEntity(null); + jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity)); + TestEntity persisted = + jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); + assertThat(persisted.property).isNull(); + } + + @Test + void testNativeQuery_succeeds() { + executeNativeQuery( + "INSERT INTO \"TestEntity\" (name, property) VALUES ('id'," + + " 'val1=>1970-01-01T00:00:00.000Z, val2=>2001-01-01T00:00:00.000Z')"); + + assertThat( + getSingleResultFromNativeQuery( + "SELECT property -> 'val1' FROM \"TestEntity\" WHERE name = 'id'")) + .isEqualTo(START_OF_TIME.toString()); + assertThat( + getSingleResultFromNativeQuery( + "SELECT property -> 'val2' FROM \"TestEntity\" WHERE name = 'id'")) + .isEqualTo(DATE_1.toString()); + + executeNativeQuery( + "UPDATE \"TestEntity\" SET property = 'val3=>2002-01-01T00:00:00.000Z' WHERE name = 'id'"); + + assertThat( + getSingleResultFromNativeQuery( + "SELECT property -> 'val3' FROM \"TestEntity\" WHERE name = 'id'")) + .isEqualTo(DATE_2.toString()); + + executeNativeQuery("DELETE FROM \"TestEntity\" WHERE name = 'id'"); + + assertThrows( + NoResultException.class, + () -> + getSingleResultFromNativeQuery( + "SELECT property -> 'val3' FROM \"TestEntity\" WHERE name = 'id'")); + } + + private static Object getSingleResultFromNativeQuery(String sql) { + return jpaTm() + .transact(() -> jpaTm().getEntityManager().createNativeQuery(sql).getSingleResult()); + } + + private static void executeNativeQuery(String sql) { + jpaTm().transact(() -> jpaTm().getEntityManager().createNativeQuery(sql).executeUpdate()); + } + + public static class TestTransition extends TimedTransition { + private String transition; + + @Override + public String getValue() { + return transition; + } + + @Override + protected void setValue(String transition) { + this.transition = transition; + } + } + + @Converter(autoApply = true) + private static class TestTimedTransitionPropertyConverter + extends TimedTransitionPropertyConverterBase { + + @Override + Map.Entry convertToEntityMapEntry(Map.Entry entry) { + return Maps.immutableEntry(DateTime.parse(entry.getKey()), entry.getValue()); + } + + @Override + Class getTimedTransitionSubclass() { + return TestTransition.class; + } + + @Override + Map.Entry convertToDatabaseMapEntry(Map.Entry entry) { + return Maps.immutableEntry(entry.getKey().toString(), entry.getValue().getValue()); + } + } + + @Entity(name = "TestEntity") // Override entity name to avoid the nested class reference. + private static class TestEntity extends ImmutableObject { + + @Id String name = "id"; + + TimedTransitionProperty property; + + private TestEntity() {} + + private TestEntity(TimedTransitionProperty timedTransitionProperty) { + this.property = timedTransitionProperty; + } + } +} diff --git a/core/src/test/java/google/registry/persistence/converter/TldStateTransitionConverterTest.java b/core/src/test/java/google/registry/persistence/converter/TldStateTransitionConverterTest.java new file mode 100644 index 000000000..852c1fe78 --- /dev/null +++ b/core/src/test/java/google/registry/persistence/converter/TldStateTransitionConverterTest.java @@ -0,0 +1,80 @@ +// Copyright 2020 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.persistence.converter; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; +import static google.registry.util.DateTimeUtils.START_OF_TIME; + +import com.google.common.collect.ImmutableSortedMap; +import google.registry.model.ImmutableObject; +import google.registry.model.common.TimedTransitionProperty; +import google.registry.model.registry.Registry.TldState; +import google.registry.model.registry.Registry.TldStateTransition; +import google.registry.persistence.transaction.JpaTestRules; +import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule; +import javax.persistence.Entity; +import javax.persistence.Id; +import org.joda.time.DateTime; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** Unit tests for {@link TldStateTransitionConverter}. */ +public class TldStateTransitionConverterTest { + + @RegisterExtension + public final JpaUnitTestRule jpa = + new JpaTestRules.Builder() + .withInitScript("sql/flyway/V14__load_extension_for_hstore.sql") + .withEntityClass(TestEntity.class) + .buildUnitTestRule(); + + private static final ImmutableSortedMap values = + ImmutableSortedMap.of( + START_OF_TIME, + TldState.PREDELEGATION, + DateTime.parse("2001-01-01T00:00:00.0Z"), + TldState.QUIET_PERIOD, + DateTime.parse("2002-01-01T00:00:00.0Z"), + TldState.PDT, + DateTime.parse("2003-01-01T00:00:00.0Z"), + TldState.GENERAL_AVAILABILITY); + + @Test + void roundTripConversion_returnsSameTimedTransitionProperty() { + TimedTransitionProperty timedTransitionProperty = + TimedTransitionProperty.fromValueMap(values, TldStateTransition.class); + TestEntity testEntity = new TestEntity(timedTransitionProperty); + jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity)); + TestEntity persisted = + jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); + assertThat(persisted.timedTransitionProperty).containsExactlyEntriesIn(timedTransitionProperty); + } + + @Entity(name = "TestEntity") + private static class TestEntity extends ImmutableObject { + + @Id String name = "id"; + + TimedTransitionProperty timedTransitionProperty; + + private TestEntity() {} + + private TestEntity( + TimedTransitionProperty timedTransitionProperty) { + this.timedTransitionProperty = timedTransitionProperty; + } + } +} diff --git a/core/src/test/java/google/registry/persistence/transaction/JpaTestRules.java b/core/src/test/java/google/registry/persistence/transaction/JpaTestRules.java index fc392f302..69c4507cf 100644 --- a/core/src/test/java/google/registry/persistence/transaction/JpaTestRules.java +++ b/core/src/test/java/google/registry/persistence/transaction/JpaTestRules.java @@ -60,10 +60,11 @@ public class JpaTestRules { /** * Junit rule for unit tests with JPA framework, when the underlying database is populated by the - * optional init script (which must not be the Nomulus Cloud SQL schema). + * optional init script (which must not be the Nomulus Cloud SQL schema). This rule can also be + * used as am extension for JUnit5 tests. */ - public static class JpaUnitTestRule extends JpaTransactionManagerRule { - + public static class JpaUnitTestRule extends JpaTransactionManagerRule + implements BeforeEachCallback, AfterEachCallback { private JpaUnitTestRule( Clock clock, Optional initScriptPath, @@ -71,6 +72,16 @@ public class JpaTestRules { ImmutableMap userProperties) { super(clock, initScriptPath, extraEntityClasses, userProperties); } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + this.before(); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + this.after(); + } } /** @@ -195,7 +206,9 @@ public class JpaTestRules { return new JpaIntegrationWithCoverageExtension(buildIntegrationTestRule()); } - /** Builds a {@link JpaUnitTestRule} instance. */ + /** + * Builds a {@link JpaUnitTestRule} instance that can also be used as an extension for JUnit5. + */ public JpaUnitTestRule buildUnitTestRule() { checkState( !Objects.equals(GOLDEN_SCHEMA_SQL_PATH, initScript),