diff --git a/core/src/main/java/google/registry/persistence/PersistenceModule.java b/core/src/main/java/google/registry/persistence/PersistenceModule.java index acea14b8d..172521884 100644 --- a/core/src/main/java/google/registry/persistence/PersistenceModule.java +++ b/core/src/main/java/google/registry/persistence/PersistenceModule.java @@ -61,6 +61,8 @@ public class PersistenceModule { // SessionFactory is created. Setting it to 'none' to turn off the feature. properties.put(Environment.HBM2DDL_AUTO, "none"); + // Hibernate converts any date to this timezone when writing to the database. + properties.put(Environment.JDBC_TIME_ZONE, "UTC"); properties.put( Environment.PHYSICAL_NAMING_STRATEGY, NomulusNamingStrategy.class.getCanonicalName()); diff --git a/core/src/main/java/google/registry/persistence/ZonedDateTimeConverter.java b/core/src/main/java/google/registry/persistence/ZonedDateTimeConverter.java new file mode 100644 index 000000000..cab9dbe08 --- /dev/null +++ b/core/src/main/java/google/registry/persistence/ZonedDateTimeConverter.java @@ -0,0 +1,48 @@ +// Copyright 2019 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; + +import java.sql.Timestamp; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import javax.annotation.Nullable; +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +/** + * JPA converter to for storing/retrieving {@link ZonedDateTime} objects. + * + *

Hibernate provides a default converter for {@link ZonedDateTime}, but it converts timestamp to + * a non-normalized format, e.g., 2019-09-01T01:01:01Z will be converted to + * 2019-09-01T01:01:01Z[UTC]. This converter solves that problem by explicitly calling {@link + * ZoneId#normalized()} to normalize the zone id. + */ +@Converter(autoApply = true) +public class ZonedDateTimeConverter implements AttributeConverter { + + @Override + @Nullable + public Timestamp convertToDatabaseColumn(@Nullable ZonedDateTime attribute) { + return attribute == null ? null : Timestamp.from(attribute.toInstant()); + } + + @Override + @Nullable + public ZonedDateTime convertToEntityAttribute(@Nullable Timestamp dbData) { + return dbData == null + ? null + : ZonedDateTime.ofInstant(dbData.toInstant(), ZoneId.of("UTC").normalized()); + } +} diff --git a/core/src/main/java/google/registry/tools/GenerateSqlSchemaCommand.java b/core/src/main/java/google/registry/tools/GenerateSqlSchemaCommand.java index 22788c655..4f2e1bef2 100644 --- a/core/src/main/java/google/registry/tools/GenerateSqlSchemaCommand.java +++ b/core/src/main/java/google/registry/tools/GenerateSqlSchemaCommand.java @@ -31,6 +31,7 @@ import google.registry.persistence.CreateAutoTimestampConverter; import google.registry.persistence.NomulusNamingStrategy; import google.registry.persistence.NomulusPostgreSQLDialect; import google.registry.persistence.UpdateAutoTimestampConverter; +import google.registry.persistence.ZonedDateTimeConverter; import google.registry.schema.domain.RegistryLock; import google.registry.schema.tld.PremiumList; import google.registry.schema.tmch.ClaimsList; @@ -74,7 +75,8 @@ public class GenerateSqlSchemaCommand implements Command { RegistryLock.class, TransferData.class, Trid.class, - UpdateAutoTimestampConverter.class); + UpdateAutoTimestampConverter.class, + ZonedDateTimeConverter.class); @VisibleForTesting public static final String DB_OPTIONS_CLASH = diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml index b68a9010e..705541169 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.persistence.CreateAutoTimestampConverter google.registry.persistence.UpdateAutoTimestampConverter + google.registry.persistence.ZonedDateTimeConverter NONE diff --git a/core/src/test/java/google/registry/persistence/ZonedDateTimeConverterTest.java b/core/src/test/java/google/registry/persistence/ZonedDateTimeConverterTest.java new file mode 100644 index 000000000..a6697fc77 --- /dev/null +++ b/core/src/test/java/google/registry/persistence/ZonedDateTimeConverterTest.java @@ -0,0 +1,121 @@ +// Copyright 2019 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; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.model.transaction.TransactionManagerFactory.jpaTm; + +import google.registry.model.ImmutableObject; +import google.registry.model.transaction.JpaTransactionManagerRule; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.ZonedDateTime; +import javax.persistence.Entity; +import javax.persistence.Id; +import org.hibernate.cfg.Environment; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ZonedDateTimeConverter}. */ +@RunWith(JUnit4.class) +public class ZonedDateTimeConverterTest { + + @Rule + public final JpaTransactionManagerRule jpaTmRule = + new JpaTransactionManagerRule.Builder() + .withEntityClass(TestEntity.class, ZonedDateTimeConverter.class) + .withProperty(Environment.HBM2DDL_AUTO, "update") + .build(); + + private final ZonedDateTimeConverter converter = new ZonedDateTimeConverter(); + + @Test + public void convertToDatabaseColumn_returnsNullIfInputIsNull() { + assertThat(converter.convertToDatabaseColumn(null)).isNull(); + } + + @Test + public void convertToDatabaseColumn_convertsCorrectly() { + ZonedDateTime zonedDateTime = ZonedDateTime.parse("2019-09-01T01:01:01Z"); + assertThat(converter.convertToDatabaseColumn(zonedDateTime).toInstant()) + .isEqualTo(zonedDateTime.toInstant()); + } + + @Test + public void convertToEntityAttribute_returnsNullIfInputIsNull() { + assertThat(converter.convertToEntityAttribute(null)).isNull(); + } + + @Test + public void convertToEntityAttribute_convertsCorrectly() { + ZonedDateTime zonedDateTime = ZonedDateTime.parse("2019-09-01T01:01:01Z"); + Instant instant = zonedDateTime.toInstant(); + assertThat(converter.convertToEntityAttribute(Timestamp.from(instant))) + .isEqualTo(zonedDateTime); + } + + @Test + public void converter_generatesTimestampWithNormalizedZone() { + ZonedDateTime zdt = ZonedDateTime.parse("2019-09-01T01:01:01Z"); + TestEntity entity = new TestEntity("normalized_utc_time", zdt); + jpaTm().transact(() -> jpaTm().getEntityManager().persist(entity)); + TestEntity retrievedEntity = + jpaTm() + .transact( + () -> jpaTm().getEntityManager().find(TestEntity.class, "normalized_utc_time")); + assertThat(retrievedEntity.zdt.toString()).isEqualTo("2019-09-01T01:01:01Z"); + } + + @Test + public void converter_convertsNonNormalizedZoneCorrectly() { + ZonedDateTime zdt = ZonedDateTime.parse("2019-09-01T01:01:01Z[UTC]"); + TestEntity entity = new TestEntity("non_normalized_utc_time", zdt); + + jpaTm().transact(() -> jpaTm().getEntityManager().persist(entity)); + TestEntity retrievedEntity = + jpaTm() + .transact( + () -> jpaTm().getEntityManager().find(TestEntity.class, "non_normalized_utc_time")); + assertThat(retrievedEntity.zdt.toString()).isEqualTo("2019-09-01T01:01:01Z"); + } + + @Test + public void converter_convertsNonUtcZoneCorrectly() { + ZonedDateTime zdt = ZonedDateTime.parse("2019-09-01T01:01:01+05:00"); + TestEntity entity = new TestEntity("new_york_time", zdt); + + jpaTm().transact(() -> jpaTm().getEntityManager().persist(entity)); + TestEntity retrievedEntity = + jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "new_york_time")); + assertThat(retrievedEntity.zdt.toString()).isEqualTo("2019-08-31T20:01:01Z"); + } + + @Entity(name = "TestEntity") // Override entity name to avoid the nested class reference. + private static class TestEntity extends ImmutableObject { + + @Id String name; + + ZonedDateTime zdt; + + public TestEntity() {} + + public TestEntity(String name, ZonedDateTime zdt) { + this.name = name; + this.zdt = zdt; + } + } +} diff --git a/core/src/test/java/google/registry/rde/EscrowTaskRunnerTest.java b/core/src/test/java/google/registry/rde/EscrowTaskRunnerTest.java index 33edcaf99..e6e6797ba 100644 --- a/core/src/test/java/google/registry/rde/EscrowTaskRunnerTest.java +++ b/core/src/test/java/google/registry/rde/EscrowTaskRunnerTest.java @@ -35,6 +35,7 @@ import google.registry.testing.FakeClock; import google.registry.testing.FakeLockHandler; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -54,9 +55,11 @@ public class EscrowTaskRunnerTest { private final EscrowTask task = mock(EscrowTask.class); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); + private DateTimeZone previousDateTimeZone; private EscrowTaskRunner runner; private Registry registry; + @Before public void before() { createTld("lol"); @@ -64,9 +67,15 @@ public class EscrowTaskRunnerTest { runner = new EscrowTaskRunner(); runner.clock = clock; runner.lockHandler = new FakeLockHandler(true); + previousDateTimeZone = DateTimeZone.getDefault(); DateTimeZone.setDefault(DateTimeZone.forID("America/New_York")); // Make sure UTC stuff works. } + @After + public void after() { + DateTimeZone.setDefault(previousDateTimeZone); + } + @Test public void testRun_cursorIsToday_advancesCursorToTomorrow() throws Exception { clock.setTo(DateTime.parse("2006-06-06T00:30:00Z"));