Refactor foundational temporal types to java.time (#3036)

* Migrates core classes (Clock, Sleeper, TransactionManager) and extensive domain models from Joda-Time to java.time.
* Restores original public API method names while substituting parameters/return values with `java.time.Instant`.
* Updates JAXB XJC `bindings.xjb` to natively generate `java.time.Instant` and `java.time.LocalDate`, eliminating `toDateTime` wrapper methods.
* Fixes XML serializers (`DateAdapter`) to robustly convert OffsetDateTime timezone strings to UTC.
* Cleans up redundant imports and Checkstyle failures across the codebase.

Remaining Joda-Time surface area to migrate in future tasks:
* Command-line parameters (e.g. `DateTimeParameter`, `DateParameter`, `IntervalParameter`) in `google.registry.tools.params`.
* EPP/RDAP flow testing infrastructure (`EppTestCase`, `RdapActionBaseTestCase`, `FlowTestCase`).
* Beam pipelines and Load Testing modules (`Spec11PipelineTest`, `RdePipelineTest`, `RegistryJpaReadTest`, `EppClient`).
* Utility bridges and converters (`DateTimeUtils.toDateTime/toInstant`, `DateTimeConverter`, `UtcDateTimeAdapter`).
* Remaining UI Console tests and Actions.
This commit is contained in:
Ben McIlwain
2026-05-08 21:04:00 +00:00
committed by GitHub
parent 60d3653b46
commit b69d51add1
320 changed files with 2450 additions and 3488 deletions
@@ -0,0 +1,27 @@
// Copyright 2026 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.util;
import java.io.IOException;
import java.time.Duration;
/** GSON type adapter for {@link java.time.Duration} objects. */
public class DurationTypeAdapter extends StringBaseTypeAdapter<Duration> {
@Override
protected Duration fromString(String stringValue) throws IOException {
return Duration.parse(stringValue);
}
}
@@ -40,7 +40,7 @@ public class InstantTypeAdapter extends StringBaseTypeAdapter<Instant> {
@Override
public void write(JsonWriter writer, Instant value) throws IOException {
if (value == null) {
writer.value("null");
writer.nullValue();
} else {
writer.value(formatInstant(value));
}
@@ -24,11 +24,11 @@ import com.google.common.flogger.FluentLogger;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.time.Duration;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.Predicate;
import org.joda.time.Duration;
/** Wrapper that does retry with exponential backoff. */
public class Retrier implements Serializable {
@@ -164,7 +164,7 @@ public class Retrier implements Serializable {
long backoffMillis = pow(2, failures) * 100L;
long sleepDurationMillis = Math.round(randomForSkew.nextDouble(0.8, 1.2) * backoffMillis);
try {
sleeper.sleep(Duration.millis(sleepDurationMillis));
sleeper.sleep(Duration.ofMillis(sleepDurationMillis));
} catch (InterruptedException e2) {
// Since we're not rethrowing InterruptedException, set the interrupt state on the thread
// so the next blocking operation will know to abort the thread.
@@ -15,6 +15,8 @@
package google.registry.util;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.DateTimeUtils.minusHours;
import static google.registry.util.DateTimeUtils.plusDays;
import com.google.common.collect.ImmutableMap;
import java.math.BigInteger;
@@ -23,6 +25,8 @@ import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Date;
import java.util.Random;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.X500Name;
@@ -34,7 +38,6 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.joda.time.DateTime;
/** A self-signed certificate authority (CA) cert for use in tests. */
// TODO(weiminyu): make this class test-only. Requires refactor in proxy and prober.
@@ -69,21 +72,21 @@ public class SelfSignedCaCertificate {
return create(
keyGen.generateKeyPair(),
DEFAULT_ISSUER_FQDN,
clock.nowUtc().minusHours(1),
clock.nowUtc().plusDays(1));
minusHours(clock.now(), 1),
plusDays(clock.now(), 1));
}
public static SelfSignedCaCertificate create(String fqdn, Clock clock) throws Exception {
return create(fqdn, clock.nowUtc().minusHours(1), clock.nowUtc().plusDays(1));
return create(fqdn, minusHours(clock.now(), 1), plusDays(clock.now(), 1));
}
public static SelfSignedCaCertificate create(String fqdn, DateTime from, DateTime to)
public static SelfSignedCaCertificate create(String fqdn, Instant from, Instant to)
throws Exception {
return create(keyGen.generateKeyPair(), fqdn, from, to);
}
public static SelfSignedCaCertificate create(
KeyPair keyPair, String fqdn, DateTime from, DateTime to) throws Exception {
KeyPair keyPair, String fqdn, Instant from, Instant to) throws Exception {
return new SelfSignedCaCertificate(keyPair.getPrivate(), createCaCert(keyPair, fqdn, from, to));
}
@@ -98,7 +101,7 @@ public class SelfSignedCaCertificate {
}
/** Returns a self-signed Certificate Authority (CA) certificate. */
static X509Certificate createCaCert(KeyPair keyPair, String fqdn, DateTime from, DateTime to)
static X509Certificate createCaCert(KeyPair keyPair, String fqdn, Instant from, Instant to)
throws Exception {
X500Name owner = new X500Name("CN=" + fqdn);
String publicKeyAlg = keyPair.getPublic().getAlgorithm();
@@ -110,8 +113,8 @@ public class SelfSignedCaCertificate {
new JcaX509v3CertificateBuilder(
owner,
new BigInteger(64, RANDOM),
from.toDate(),
to.toDate(),
Date.from(from),
Date.from(to),
owner,
keyPair.getPublic());
@@ -16,15 +16,19 @@ package google.registry.util;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.Objects;
/** Abstract class for {@link TypeAdapter}s that can convert directly to/from strings. */
public abstract class StringBaseTypeAdapter<T> extends TypeAdapter<T> {
@Override
public T read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
String stringValue = reader.nextString();
if (stringValue.equals("null")) {
return null;
@@ -34,7 +38,11 @@ public abstract class StringBaseTypeAdapter<T> extends TypeAdapter<T> {
@Override
public void write(JsonWriter writer, T t) throws IOException {
writer.value(Objects.toString(t));
if (t == null) {
writer.nullValue();
} else {
writer.value(t.toString());
}
}
protected abstract T fromString(String stringValue) throws IOException;
@@ -40,13 +40,13 @@ import java.security.cert.CertificateRevokedException;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Base64;
import java.util.Date;
import java.util.NoSuchElementException;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.annotation.Tainted;
import org.joda.time.DateTime;
import org.joda.time.DateTimeComparator;
/** X.509 Public Key Infrastructure (PKI) helper functions. */
public final class X509Utils {
@@ -144,9 +144,9 @@ public final class X509Utils {
* currently in memory.
*/
public static void verifyCertificate(
X509Certificate rootCert, X509CRL crl, @Tainted X509Certificate cert, DateTime now)
X509Certificate rootCert, X509CRL crl, @Tainted X509Certificate cert, Instant now)
throws GeneralSecurityException {
cert.checkValidity(checkNotNull(now, "now").toDate());
cert.checkValidity(Date.from(checkNotNull(now, "now")));
cert.verify(rootCert.getPublicKey());
if (crl.isRevoked(cert)) {
X509CRLEntry entry = crl.getRevokedCertificate(cert);
@@ -168,17 +168,16 @@ public final class X509Utils {
* incorrect keys, and for invalid, old, not-yet-valid or revoked certificates.
*/
public static void verifyCrl(
X509Certificate rootCert, @Nullable X509CRL oldCrl, @Tainted X509CRL newCrl, DateTime now)
X509Certificate rootCert, @Nullable X509CRL oldCrl, @Tainted X509CRL newCrl, Instant now)
throws GeneralSecurityException {
if (oldCrl != null
&& DateTimeComparator.getInstance().compare(newCrl.getThisUpdate(), oldCrl.getThisUpdate())
< 0) {
&& newCrl.getThisUpdate().toInstant().isBefore(oldCrl.getThisUpdate().toInstant())) {
throw new CRLException(
String.format(
"New CRL is more out of date than our current CRL. %s < %s\n%s",
newCrl.getThisUpdate(), oldCrl.getThisUpdate(), newCrl));
}
if (DateTimeComparator.getInstance().compare(new DateTime(newCrl.getNextUpdate()), now) < 0) {
if (newCrl.getNextUpdate().toInstant().isBefore(now)) {
throw new CRLException("CRL has expired.\n" + newCrl);
}
newCrl.verify(rootCert.getPublicKey());
@@ -16,100 +16,77 @@ package google.registry.util;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.util.DateTimeUtils.END_INSTANT;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.START_INSTANT;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.earliestDateTimeOf;
import static google.registry.util.DateTimeUtils.earliestOf;
import static google.registry.util.DateTimeUtils.formatInstant;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import static google.registry.util.DateTimeUtils.latestDateTimeOf;
import static google.registry.util.DateTimeUtils.latestOf;
import static google.registry.util.DateTimeUtils.minusDays;
import static google.registry.util.DateTimeUtils.minusHours;
import static google.registry.util.DateTimeUtils.minusMinutes;
import static google.registry.util.DateTimeUtils.minusMonths;
import static google.registry.util.DateTimeUtils.minusWeeks;
import static google.registry.util.DateTimeUtils.minusYears;
import static google.registry.util.DateTimeUtils.parseInstant;
import static google.registry.util.DateTimeUtils.plusDays;
import static google.registry.util.DateTimeUtils.plusHours;
import static google.registry.util.DateTimeUtils.plusMinutes;
import static google.registry.util.DateTimeUtils.plusMonths;
import static google.registry.util.DateTimeUtils.plusWeeks;
import static google.registry.util.DateTimeUtils.plusYears;
import static google.registry.util.DateTimeUtils.toDateTime;
import static google.registry.util.DateTimeUtils.toInstant;
import static google.registry.util.DateTimeUtils.toJodaInstant;
import static google.registry.util.DateTimeUtils.toLocalDate;
import static google.registry.util.DateTimeUtils.toSqlDate;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import java.sql.Date;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link DateTimeUtils}. */
class DateTimeUtilsTest {
private ImmutableList<DateTime> sampleDates =
ImmutableList.of(START_OF_TIME, START_OF_TIME.plusDays(1), END_OF_TIME, END_OF_TIME);
@Test
void testSuccess_earliestOf() {
assertThat(earliestOf(START_OF_TIME, END_OF_TIME)).isEqualTo(START_OF_TIME);
assertThat(earliestDateTimeOf(sampleDates)).isEqualTo(START_OF_TIME);
}
private ImmutableList<Instant> sampleInstants =
ImmutableList.of(
START_INSTANT, START_INSTANT.plus(1, ChronoUnit.DAYS), END_INSTANT, END_INSTANT);
@Test
void testSuccess_earliestOf_instant() {
assertThat(earliestOf(START_INSTANT, END_INSTANT)).isEqualTo(START_INSTANT);
assertThat(earliestOf(ImmutableList.of(START_INSTANT, END_INSTANT))).isEqualTo(START_INSTANT);
}
@Test
void testSuccess_latestOf() {
assertThat(latestOf(START_OF_TIME, END_OF_TIME)).isEqualTo(END_OF_TIME);
assertThat(latestDateTimeOf(sampleDates)).isEqualTo(END_OF_TIME);
assertThat(earliestOf(sampleInstants)).isEqualTo(START_INSTANT);
}
@Test
void testSuccess_latestOf_instant() {
assertThat(latestOf(START_INSTANT, END_INSTANT)).isEqualTo(END_INSTANT);
assertThat(latestOf(ImmutableList.of(START_INSTANT, END_INSTANT))).isEqualTo(END_INSTANT);
assertThat(latestOf(sampleInstants)).isEqualTo(END_INSTANT);
}
@Test
void testSuccess_isBeforeOrAt() {
assertThat(isBeforeOrAt(START_OF_TIME, START_OF_TIME.plusDays(1))).isTrue();
assertThat(isBeforeOrAt(START_OF_TIME, START_OF_TIME)).isTrue();
assertThat(isBeforeOrAt(START_OF_TIME.plusDays(1), START_OF_TIME)).isFalse();
assertThat(isBeforeOrAt(START_INSTANT, START_INSTANT.plus(1, ChronoUnit.DAYS))).isTrue();
assertThat(isBeforeOrAt(START_INSTANT, START_INSTANT)).isTrue();
assertThat(isBeforeOrAt(START_INSTANT.plus(1, ChronoUnit.DAYS), START_INSTANT)).isFalse();
}
@Test
void testSuccess_isAtOrAfter() {
assertThat(isAtOrAfter(START_OF_TIME, START_OF_TIME.plusDays(1))).isFalse();
assertThat(isAtOrAfter(START_OF_TIME, START_OF_TIME)).isTrue();
assertThat(isAtOrAfter(START_OF_TIME.plusDays(1), START_OF_TIME)).isTrue();
assertThat(isAtOrAfter(START_INSTANT, START_INSTANT.plus(1, ChronoUnit.DAYS))).isFalse();
assertThat(isAtOrAfter(START_INSTANT, START_INSTANT)).isTrue();
assertThat(isAtOrAfter(START_INSTANT.plus(1, ChronoUnit.DAYS), START_INSTANT)).isTrue();
}
@Test
void testSuccess_plusYears() {
DateTime startDate = DateTime.parse("2012-02-29T00:00:00Z");
assertThat(startDate.plusYears(4)).isEqualTo(DateTime.parse("2016-02-29T00:00:00Z"));
assertThat(plusYears(startDate, 4)).isEqualTo(DateTime.parse("2016-02-28T00:00:00Z"));
}
@Test
void test_plusYears_worksWithInstants() {
Instant startDate = Instant.parse("2012-02-29T00:00:00Z");
assertThat(plusYears(startDate, 4)).isEqualTo(Instant.parse("2016-02-28T00:00:00Z"));
}
@Test
void testSuccess_minusYears() {
DateTime startDate = DateTime.parse("2012-02-29T00:00:00Z");
assertThat(startDate.minusYears(4)).isEqualTo(DateTime.parse("2008-02-29T00:00:00Z"));
assertThat(minusYears(startDate, 4)).isEqualTo(DateTime.parse("2008-02-28T00:00:00Z"));
}
@Test
void test_minusYears_worksWithInstants() {
Instant startDate = Instant.parse("2012-02-29T00:00:00Z");
assertThat(minusYears(startDate, 4)).isEqualTo(Instant.parse("2008-02-28T00:00:00Z"));
}
@@ -140,40 +117,36 @@ class DateTimeUtilsTest {
@Test
void testFailure_earliestOfEmpty() {
assertThrows(IllegalArgumentException.class, () -> earliestDateTimeOf(ImmutableList.of()));
assertThrows(IllegalArgumentException.class, () -> earliestOf(ImmutableList.of()));
}
@Test
void testFailure_latestOfEmpty() {
assertThrows(IllegalArgumentException.class, () -> latestDateTimeOf(ImmutableList.of()));
assertThrows(IllegalArgumentException.class, () -> latestOf(ImmutableList.of()));
}
@Test
void testSuccess_toSqlDate() {
LocalDate localDate = LocalDate.parse("2020-02-29");
assertThat(toSqlDate(localDate)).isEqualTo(Date.valueOf("2020-02-29"));
void test_formatInstant() {
assertThat(formatInstant(Instant.parse("2024-03-27T10:15:30.105Z")))
.isEqualTo("2024-03-27T10:15:30.105Z");
assertThat(formatInstant(Instant.parse("2024-03-27T10:15:30Z")))
.isEqualTo("2024-03-27T10:15:30.000Z");
}
@Test
void testSuccess_toLocalDate() {
Date date = Date.valueOf("2020-02-29");
assertThat(toLocalDate(date)).isEqualTo(LocalDate.parse("2020-02-29"));
}
void test_plusMinusWeeksDaysHoursMinutes() {
Instant time = Instant.parse("2024-03-27T10:15:30.000Z");
assertThat(plusWeeks(time, 2)).isEqualTo(Instant.parse("2024-04-10T10:15:30.000Z"));
assertThat(minusWeeks(time, 2)).isEqualTo(Instant.parse("2024-03-13T10:15:30.000Z"));
@Test
void test_startOfTimeConstants_areTheSame() {
assertThat(toInstant(START_OF_TIME)).isEqualTo(START_INSTANT);
assertThat(toDateTime(START_INSTANT)).isEqualTo(START_OF_TIME);
assertThat(toInstant(toDateTime(START_INSTANT))).isEqualTo(START_INSTANT);
assertThat(toDateTime(toInstant(START_OF_TIME))).isEqualTo(START_OF_TIME);
}
assertThat(plusDays(time, 2)).isEqualTo(Instant.parse("2024-03-29T10:15:30.000Z"));
assertThat(minusDays(time, 2)).isEqualTo(Instant.parse("2024-03-25T10:15:30.000Z"));
@Test
void test_endOfTimeConstants_areTheSame() {
assertThat(toInstant(END_OF_TIME)).isEqualTo(END_INSTANT);
assertThat(toDateTime(END_INSTANT)).isEqualTo(END_OF_TIME);
assertThat(toInstant(toDateTime(END_INSTANT))).isEqualTo(END_INSTANT);
assertThat(toDateTime(toInstant(END_OF_TIME))).isEqualTo(END_OF_TIME);
assertThat(plusHours(time, 2)).isEqualTo(Instant.parse("2024-03-27T12:15:30.000Z"));
assertThat(minusHours(time, 2)).isEqualTo(Instant.parse("2024-03-27T08:15:30.000Z"));
assertThat(plusMinutes(time, 2)).isEqualTo(Instant.parse("2024-03-27T10:17:30.000Z"));
assertThat(minusMinutes(time, 2)).isEqualTo(Instant.parse("2024-03-27T10:13:30.000Z"));
}
@Test