Complete Joda-Time to java.time migration (#3039)

This completes the exhaustive refactoring of foundational temporal types from Joda-Time to the native java.time API across the entire codebase.

- Replaced org.joda.time.DateTime, Instant, LocalDate, and Duration with java.time equivalents.
- Audited and updated Clock implementations (FakeClock, SystemClock). Added nowMillis(), nowDate(), and nowDateTime() to eliminate repetitive conversions and maintain parallel naming.
- Replaced ZonedDateTime with OffsetDateTime globally per go/avoid-zdt. OffsetDateTime is a better fit as we use a hardcoded ZoneOffset.UTC throughout the system, making geographical time zone rules (like daylight saving time) irrelevant and preventing serialization ambiguities. Added a presubmit check.
- Completely removed all transitional bridge methods from DateTimeUtils and deleted obsolete converters (e.g., DateTimeConverter).
- Updated testing infrastructure, Apache Beam pipelines, custom JCommander parameters, and networking modules to solely rely on java.time primitives.
- Retained the lone necessary org.joda.time.Instant usage in SafeBrowsingTransforms required by the Apache Beam API.
- Cleared Gradle lockfiles and removed the joda-time dependency entirely from the build configuration.
This commit is contained in:
Ben McIlwain
2026-05-13 16:07:19 +00:00
committed by GitHub
parent b33c2f4874
commit 56fe588b56
218 changed files with 589 additions and 1390 deletions
@@ -1,28 +0,0 @@
// Copyright 2023 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 org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
/** GSON type adapter for Joda {@link DateTime} objects. */
public class DateTimeTypeAdapter extends StringBaseTypeAdapter<DateTime> {
@Override
protected DateTime fromString(String stringValue) throws IOException {
return ISODateTimeFormat.dateTime().withZoneUTC().parseDateTime(stringValue);
}
}
@@ -17,7 +17,7 @@ package google.registry.util;
import java.io.IOException;
import java.time.Duration;
/** GSON type adapter for {@link java.time.Duration} objects. */
/** GSON type adapter for {@link Duration} objects. */
public class DurationTypeAdapter extends StringBaseTypeAdapter<Duration> {
@Override
@@ -19,13 +19,11 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.joda.time.DateTimeConstants.MILLIS_PER_SECOND;
import static org.joda.time.DateTimeZone.UTC;
import java.time.Instant;
import java.util.Arrays;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.joda.time.DateTime;
/**
* POSIX Tar Header.
@@ -171,10 +169,14 @@ public final class PosixTarHeader {
return Integer.parseInt(extractField(124, 12).trim(), 8);
}
/** Returns the modified time as a UTC {@link DateTime} object. */
/** Returns the modified time as a UTC {@link Instant} object. */
@SuppressWarnings("JodaDateTimeConstants")
public DateTime getMtime() {
return new DateTime(Long.parseLong(extractField(136, 12).trim(), 8) * MILLIS_PER_SECOND, UTC);
public Instant getMtime() {
try {
return Instant.ofEpochMilli(Long.parseLong(extractField(136, 12).trim(), 8) * 1000L);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(e);
}
}
/** Returns the checksum value stored in the . */
@@ -411,13 +413,13 @@ public final class PosixTarHeader {
* constructed.
*
* <p>The modified time is always stored as a UNIX timestamp which is seconds since the UNIX
* epoch in UTC time. Because {@link DateTime} has millisecond precision, it gets rounded down
* epoch in UTC time. Because {@link Instant} has millisecond precision, it gets rounded down
* (floor) to the second.
*/
@SuppressWarnings("JodaDateTimeConstants")
public Builder setMtime(DateTime mtime) {
public Builder setMtime(Instant mtime) {
checkNotNull(mtime, "mtime");
setField("mtime", 136, 12, String.format("%011o", mtime.getMillis() / MILLIS_PER_SECOND));
setField("mtime", 136, 12, String.format("%011o", mtime.getEpochSecond()));
hasMtime = true;
return this;
}
@@ -14,7 +14,6 @@
package google.registry.util;
import com.google.common.base.Ascii;
import com.google.common.base.CharMatcher;
@@ -43,8 +43,8 @@ import java.util.Map;
* including third-party dependencies. (As a side note, this class does not use allow lists
* for Nomulus or third-party classes because it is infeasible in practice. Super classes of
* the instance being deserialized must be resolved, and therefore must be on the allow list;
* same for the field types of the instance. The allow list for the Joda {@code DateTime}
* class alone would have more than 10 classes. Generated classes, e.g., by AutoValue, present
* same for the field types of the instance. The allow list for the Joda {@code Instant} class
* alone would have more than 10 classes. Generated classes, e.g., by AutoValue, present
* another problem: their real names are not meant to be a concern to the user).
* <li>CPU-targeting denial-of-service attacks. Containers and arrays may be used to construct
* object graphs that require enormous amount of computation during deserialization and/or
@@ -14,7 +14,6 @@
package google.registry.util;
import com.google.common.flogger.FluentLogger;
import java.util.Map;
import java.util.Optional;
@@ -28,22 +28,17 @@ 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 org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableList;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link DateTimeUtils}. */
@@ -79,18 +74,6 @@ class DateTimeUtilsTest {
assertThat(isAtOrAfter(START_INSTANT.plus(1, ChronoUnit.DAYS), START_INSTANT)).isTrue();
}
@Test
void testSuccess_plusYears() {
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() {
Instant startDate = Instant.parse("2012-02-29T00:00:00Z");
assertThat(minusYears(startDate, 4)).isEqualTo(Instant.parse("2008-02-28T00:00:00Z"));
}
@Test
void test_plusMonths_worksWithInstants() {
Instant startDate = Instant.parse("2012-02-29T00:00:00Z");
@@ -109,10 +92,23 @@ class DateTimeUtilsTest {
assertThat(minusMonths(startLeapYear, 1)).isEqualTo(Instant.parse("2012-02-29T00:00:00Z"));
}
@Test
void testSuccess_plusYears() {
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() {
Instant startDate = Instant.parse("2012-02-29T00:00:00Z");
assertThat(minusYears(startDate, 4)).isEqualTo(Instant.parse("2008-02-28T00:00:00Z"));
}
@Test
void testSuccess_minusYears_zeroYears() {
DateTime leapDay = DateTime.parse("2012-02-29T00:00:00Z");
assertThat(leapDay.minusYears(0)).isEqualTo(leapDay);
Instant leapDay = Instant.parse("2012-02-29T00:00:00Z");
assertThat(minusYears(leapDay, 0)).isEqualTo(leapDay);
assertThat(plusYears(leapDay, 0)).isEqualTo(leapDay);
}
@Test
@@ -136,6 +132,7 @@ class DateTimeUtilsTest {
@Test
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"));
@@ -149,17 +146,4 @@ class DateTimeUtilsTest {
assertThat(minusMinutes(time, 2)).isEqualTo(Instant.parse("2024-03-27T10:13:30.000Z"));
}
@Test
void test_instantConversionMethods_workCorrectly() {
assertThat(toDateTime(Instant.parse("2024-03-27T10:15:30.105Z")))
.isEqualTo(DateTime.parse("2024-03-27T10:15:30.105Z"));
assertThat(toInstant(DateTime.parse("2024-03-27T10:15:30.105Z")))
.isEqualTo(Instant.parse("2024-03-27T10:15:30.105Z"));
assertThat(toJodaInstant(Instant.parse("2024-03-27T10:15:30.105Z")))
.isEqualTo(org.joda.time.Instant.parse("2024-03-27T10:15:30.105Z"));
assertThat(parseInstant("2024-03-27T10:15:30.105Z"))
.isEqualTo(Instant.parse("2024-03-27T10:15:30.105Z"));
assertThat(parseInstant("2024-03-27T10:15:30Z"))
.isEqualTo(Instant.parse("2024-03-27T10:15:30Z"));
}
}
@@ -18,17 +18,15 @@ import static com.google.common.io.BaseEncoding.base64;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.joda.time.DateTimeZone.UTC;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.testing.EqualsTester;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.time.Instant;
import java.util.Arrays;
import java.util.zip.GZIPInputStream;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link PosixTarHeader}. */
@@ -76,7 +74,7 @@ class PosixTarHeaderTest {
.setMode(0640)
// This timestamp should have been midnight but I think GNU tar might not understand
// daylight savings time. Woe is me.
.setMtime(DateTime.parse("2013-08-13T01:50:12Z"))
.setMtime(Instant.parse("2013-08-13T01:50:12Z"))
.setUname("jart")
.setGname("eng")
.setUid(180918) // echo $UID
@@ -125,7 +123,7 @@ class PosixTarHeaderTest {
.setName("(◕‿◕).txt")
.setSize(666)
.setMode(0777)
.setMtime(DateTime.parse("1984-12-18T04:20:00Z"))
.setMtime(Instant.parse("1984-12-18T04:20:00Z"))
.setUname("everything i ever touched")
.setGname("everything i ever had, has died")
.setUid(180918)
@@ -139,7 +137,7 @@ class PosixTarHeaderTest {
assertThat(header.getGname()).isEqualTo("everything i ever had, has died");
assertThat(header.getUid()).isEqualTo(180918);
assertThat(header.getGid()).isEqualTo(5000);
assertThat(header.getMtime()).isEqualTo(DateTime.parse("1984-12-18T04:20:00Z"));
assertThat(header.getMtime()).isEqualTo(Instant.parse("1984-12-18T04:20:00Z"));
assertThat(header.getMagic()).isEqualTo("ustar");
assertThat(header.getVersion()).isEqualTo("00");
}
@@ -152,7 +150,7 @@ class PosixTarHeaderTest {
.setName("Black lung full of fumes, choke on memories")
.setSize(1024 * 1024 * 1024)
.setMode(31337)
.setMtime(DateTime.parse("2020-12-18T04:20:00Z"))
.setMtime(Instant.parse("2020-12-18T04:20:00Z"))
.setUname("every street i ever walked")
.setGname("every home i ever had, is lost")
.setUid(0)
@@ -166,7 +164,7 @@ class PosixTarHeaderTest {
assertThat(header.getGname()).isEqualTo("every home i ever had, is lost");
assertThat(header.getUid()).isEqualTo(0);
assertThat(header.getGid()).isEqualTo(31337);
assertThat(header.getMtime()).isEqualTo(DateTime.parse("2020-12-18T04:20:00Z"));
assertThat(header.getMtime()).isEqualTo(Instant.parse("2020-12-18T04:20:00Z"));
}
@Test
@@ -177,7 +175,7 @@ class PosixTarHeaderTest {
.setName("(◕‿◕).txt")
.setSize(31337)
.setMode(0777)
.setMtime(DateTime.parse("1984-12-18T04:20:00Z"))
.setMtime(Instant.parse("1984-12-18T04:20:00Z"))
.setUname("everything i ever touched")
.setGname("everything i ever had, has died")
.setUid(180918)
@@ -192,7 +190,7 @@ class PosixTarHeaderTest {
assertThat(header.getGname()).isEqualTo("everything i ever had, has died");
assertThat(header.getUid()).isEqualTo(180918);
assertThat(header.getGid()).isEqualTo(5000);
assertThat(header.getMtime()).isEqualTo(DateTime.parse("1984-12-18T04:20:00Z"));
assertThat(header.getMtime()).isEqualTo(Instant.parse("1984-12-18T04:20:00Z"));
assertThat(header.getMagic()).isEqualTo("ustar");
assertThat(header.getVersion()).isEqualTo("00");
}
@@ -203,7 +201,7 @@ class PosixTarHeaderTest {
new PosixTarHeader.Builder()
.setName("(◕‿◕).txt")
.setSize(31337)
.setMtime(DateTime.now(UTC))
.setMtime(Instant.now())
.build();
byte[] bytes = header.getBytes();
bytes[150] = '0';
@@ -220,7 +218,7 @@ class PosixTarHeaderTest {
new PosixTarHeader.Builder()
.setName("(◕‿◕).txt")
.setSize(123)
.setMtime(DateTime.parse("1984-12-18TZ"))
.setMtime(Instant.parse("1984-12-18T00:00:00Z"))
.setUid(1234)
.setGid(456)
.setUname("jart")
@@ -229,7 +227,7 @@ class PosixTarHeaderTest {
new PosixTarHeader.Builder()
.setName("(◕‿◕).txt")
.setSize(123)
.setMtime(DateTime.parse("1984-12-18TZ"))
.setMtime(Instant.parse("1984-12-18T00:00:00Z"))
.setUid(1234)
.setGid(456)
.setUname("jart")
@@ -239,7 +237,7 @@ class PosixTarHeaderTest {
new PosixTarHeader.Builder()
.setName("(•︵•).txt") // Awwwww! It looks so sad...
.setSize(123)
.setMtime(DateTime.now(UTC))
.setMtime(Instant.now())
.build())
.testEquals();
}
@@ -277,7 +275,7 @@ class PosixTarHeaderTest {
assertThat(header.getGname()).isEqualTo("wheel");
assertThat(header.getUid()).isEqualTo(180918);
assertThat(header.getGid()).isEqualTo(0);
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
assertThat(header.getMtime().toString().substring(0, 10)).isEqualTo("2013-08-16");
assertThat(input.read(block)).isEqualTo(512);
assertThat(new String(block, 0, likeTears.length(), UTF_8)).isEqualTo(likeTears);
@@ -291,7 +289,7 @@ class PosixTarHeaderTest {
assertThat(header.getGname()).isEqualTo("wheel");
assertThat(header.getUid()).isEqualTo(180918);
assertThat(header.getGid()).isEqualTo(0);
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
assertThat(header.getMtime().toString().substring(0, 10)).isEqualTo("2013-08-16");
assertThat(input.read(block)).isEqualTo(512);
assertThat(new String(block, 0, inRain.length(), UTF_8)).isEqualTo(inRain);
@@ -335,7 +333,7 @@ class PosixTarHeaderTest {
assertThat(header.getGname()).isEqualTo("wheel");
assertThat(header.getUid()).isEqualTo(180918);
assertThat(header.getGid()).isEqualTo(0);
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
assertThat(header.getMtime().toString().substring(0, 10)).isEqualTo("2013-08-16");
assertThat(input.read(block)).isEqualTo(512);
assertThat(new String(block, 0, likeTears.length(), UTF_8)).isEqualTo(likeTears);
@@ -349,7 +347,7 @@ class PosixTarHeaderTest {
assertThat(header.getGname()).isEqualTo("wheel");
assertThat(header.getUid()).isEqualTo(180918);
assertThat(header.getGid()).isEqualTo(0);
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
assertThat(header.getMtime().toString().substring(0, 10)).isEqualTo("2013-08-16");
assertThat(input.read(block)).isEqualTo(512);
assertThat(new String(block, 0, inRain.length(), UTF_8)).isEqualTo(inRain);
@@ -393,7 +391,7 @@ class PosixTarHeaderTest {
assertThat(header.getGname()).isEqualTo("eng");
assertThat(header.getUid()).isEqualTo(180918);
assertThat(header.getGid()).isEqualTo(5000);
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
assertThat(header.getMtime().toString().substring(0, 10)).isEqualTo("2013-08-16");
assertThat(input.read(block)).isEqualTo(512);
assertThat(new String(block, 0, likeTears.length(), UTF_8)).isEqualTo(likeTears);
@@ -407,7 +405,7 @@ class PosixTarHeaderTest {
assertThat(header.getGname()).isEqualTo("eng");
assertThat(header.getUid()).isEqualTo(180918);
assertThat(header.getGid()).isEqualTo(5000);
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
assertThat(header.getMtime().toString().substring(0, 10)).isEqualTo("2013-08-16");
assertThat(input.read(block)).isEqualTo(512);
assertThat(new String(block, 0, inRain.length(), UTF_8)).isEqualTo(inRain);
@@ -452,7 +450,7 @@ class PosixTarHeaderTest {
assertThat(header.getGname()).isEqualTo("eng");
assertThat(header.getUid()).isEqualTo(180918);
assertThat(header.getGid()).isEqualTo(5000);
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
assertThat(header.getMtime().toString().substring(0, 10)).isEqualTo("2013-08-16");
assertThat(input.read(block)).isEqualTo(512);
assertThat(new String(block, 0, likeTears.length(), UTF_8)).isEqualTo(likeTears);
@@ -467,7 +465,7 @@ class PosixTarHeaderTest {
assertThat(header.getGname()).isEqualTo("eng");
assertThat(header.getUid()).isEqualTo(180918);
assertThat(header.getGid()).isEqualTo(5000);
assertThat(header.getMtime().toString(ISODateTimeFormat.date())).isEqualTo("2013-08-16");
assertThat(header.getMtime().toString().substring(0, 10)).isEqualTo("2013-08-16");
assertThat(input.read(block)).isEqualTo(512);
assertThat(new String(block, 0, inRain.length(), UTF_8)).isEqualTo(inRain);
@@ -24,9 +24,9 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import java.io.ByteArrayInputStream;
import java.io.Serializable;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import org.joda.time.Duration;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link SafeObjectInputStream}. */
@@ -72,7 +72,7 @@ public class SafeObjectInputStreamTest {
@Test
void nonJavaNonNomulusUnitaryFailure() throws Exception {
Serializable orig = Duration.millis(1);
Serializable orig = Duration.ofMillis(1);
try (SafeObjectInputStream sois =
new SafeObjectInputStream(new ByteArrayInputStream(serialize(orig)))) {
assertThrows(ClassNotFoundException.class, () -> sois.readObject());