Implement ZonedDateTimeConverter (#287)

* Implement ZonedDateTimeConverter

* Use dedicated TestEntity for ZonedDateTimeConverterTest
This commit is contained in:
Shicong Huang 2019-10-08 16:46:07 -04:00 committed by GitHub
parent 3d33820a82
commit 682f3be767
6 changed files with 184 additions and 1 deletions

View file

@ -61,6 +61,8 @@ public class PersistenceModule {
// SessionFactory is created. Setting it to 'none' to turn off the feature. // SessionFactory is created. Setting it to 'none' to turn off the feature.
properties.put(Environment.HBM2DDL_AUTO, "none"); 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( properties.put(
Environment.PHYSICAL_NAMING_STRATEGY, NomulusNamingStrategy.class.getCanonicalName()); Environment.PHYSICAL_NAMING_STRATEGY, NomulusNamingStrategy.class.getCanonicalName());

View file

@ -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.
*
* <p>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<ZonedDateTime, Timestamp> {
@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());
}
}

View file

@ -31,6 +31,7 @@ import google.registry.persistence.CreateAutoTimestampConverter;
import google.registry.persistence.NomulusNamingStrategy; import google.registry.persistence.NomulusNamingStrategy;
import google.registry.persistence.NomulusPostgreSQLDialect; import google.registry.persistence.NomulusPostgreSQLDialect;
import google.registry.persistence.UpdateAutoTimestampConverter; import google.registry.persistence.UpdateAutoTimestampConverter;
import google.registry.persistence.ZonedDateTimeConverter;
import google.registry.schema.domain.RegistryLock; import google.registry.schema.domain.RegistryLock;
import google.registry.schema.tld.PremiumList; import google.registry.schema.tld.PremiumList;
import google.registry.schema.tmch.ClaimsList; import google.registry.schema.tmch.ClaimsList;
@ -74,7 +75,8 @@ public class GenerateSqlSchemaCommand implements Command {
RegistryLock.class, RegistryLock.class,
TransferData.class, TransferData.class,
Trid.class, Trid.class,
UpdateAutoTimestampConverter.class); UpdateAutoTimestampConverter.class,
ZonedDateTimeConverter.class);
@VisibleForTesting @VisibleForTesting
public static final String DB_OPTIONS_CLASH = public static final String DB_OPTIONS_CLASH =

View file

@ -35,6 +35,7 @@
<!-- Customized type converters --> <!-- Customized type converters -->
<class>google.registry.persistence.CreateAutoTimestampConverter</class> <class>google.registry.persistence.CreateAutoTimestampConverter</class>
<class>google.registry.persistence.UpdateAutoTimestampConverter</class> <class>google.registry.persistence.UpdateAutoTimestampConverter</class>
<class>google.registry.persistence.ZonedDateTimeConverter</class>
<!-- TODO(weiminyu): check out application-layer validation. --> <!-- TODO(weiminyu): check out application-layer validation. -->
<validation-mode>NONE</validation-mode> <validation-mode>NONE</validation-mode>

View file

@ -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;
}
}
}

View file

@ -35,6 +35,7 @@ import google.registry.testing.FakeClock;
import google.registry.testing.FakeLockHandler; import google.registry.testing.FakeLockHandler;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.DateTimeZone; import org.joda.time.DateTimeZone;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@ -54,9 +55,11 @@ public class EscrowTaskRunnerTest {
private final EscrowTask task = mock(EscrowTask.class); private final EscrowTask task = mock(EscrowTask.class);
private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ")); private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ"));
private DateTimeZone previousDateTimeZone;
private EscrowTaskRunner runner; private EscrowTaskRunner runner;
private Registry registry; private Registry registry;
@Before @Before
public void before() { public void before() {
createTld("lol"); createTld("lol");
@ -64,9 +67,15 @@ public class EscrowTaskRunnerTest {
runner = new EscrowTaskRunner(); runner = new EscrowTaskRunner();
runner.clock = clock; runner.clock = clock;
runner.lockHandler = new FakeLockHandler(true); runner.lockHandler = new FakeLockHandler(true);
previousDateTimeZone = DateTimeZone.getDefault();
DateTimeZone.setDefault(DateTimeZone.forID("America/New_York")); // Make sure UTC stuff works. DateTimeZone.setDefault(DateTimeZone.forID("America/New_York")); // Make sure UTC stuff works.
} }
@After
public void after() {
DateTimeZone.setDefault(previousDateTimeZone);
}
@Test @Test
public void testRun_cursorIsToday_advancesCursorToTomorrow() throws Exception { public void testRun_cursorIsToday_advancesCursorToTomorrow() throws Exception {
clock.setTo(DateTime.parse("2006-06-06T00:30:00Z")); clock.setTo(DateTime.parse("2006-06-06T00:30:00Z"));