Skip to content

Commit 6f49d5b

Browse files
committed
Migrate core temporal models and related entities to java.time.Instant
This comprehensive refactor continues the migration from Joda-Time to java.time (Instant), focusing on core timestamp models, transition properties, and their integration across the codebase. Key changes: - Migrated CreateAutoTimestamp and UpdateAutoTimestamp to use Instant internally, providing Joda-Time bridge methods for backward compatibility. - Updated TimedTransitionProperty to handle Instant-based transition maps and updated corresponding Hibernate UserTypes (TimedTransitionBaseUserType). - Migrated GracePeriod, BillingBase, BillingEvent, PollMessage, and PendingActionNotificationResponse fields (e.g., expirationTime, eventTime) to Instant. - Updated Tld and FeatureFlag models to use Instant for claimsPeriodEnd, bsaEnrollStartTime, and status transitions. - Enhanced CLI tools and parameters (TransitionListParameter, InstantParameter) to support Instant-based input and output. - Updated EntityYamlUtils with custom Instant serializers/deserializers to maintain format consistency (e.g., .SSSZ precision) required for YAML-based tests. - Implemented UtcInstantAdapter to ensure JAXB XML serialization maintains millisecond accuracy, matching legacy Joda-Time behavior. - Resolved Hibernate 6 type mismatches in JPQL and Native queries by ensuring consistent use of Instant for comparisons. - Updated GEMINI.md with project-specific engineering standards, including the 'one commit per PR' mandate, full-build validation requirement, and commit message style rules. - Added necessary @JsonIgnore and @JsonProperty annotations to ensure consistent Jackson serialization across models. - Introduced RegistryTruth with assertAt() helper to simplify cross-type temporal assertions in tests. - Refactored DateTimeUtils to use strongly-typed toInstant overloads (DateTime, Timestamp, Date, Instant), eliminating the generic Object-based method. - Cleaned up fully qualified calls to toDateTime and toInstant by adding static imports across 20+ core model and flow files.
1 parent 49f14b5 commit 6f49d5b

126 files changed

Lines changed: 2696 additions & 612 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

GEMINI.md

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ This document outlines foundational mandates, architectural patterns, and projec
88
- **Addition:** When adding new symbols, ensure the corresponding import is added.
99
- **Removal:** When removing the last usage of a class or symbol from a file (e.g., removing a `@Inject Clock clock;` field), **immediately remove the associated import**. Do not wait for a build failure to identify unused imports.
1010
- **Checkstyle:** Proactively fix common checkstyle errors (line length > 100, formatting, unused imports) during the initial code write. Do not wait for CI/build failures to address these, as iterative fixes are inefficient.
11-
- **Verification:** Before finalizing any change, scan the imports section for redundancy.
11+
- **Verification**: Before finalizing any change, scan the imports section for redundancy.
12+
- **License Headers**: When creating new files, ensure the license header uses the current year (e.g., 2026). Existing files should retain their original year.
13+
14+
## 2. Time and Precision Handling (java.time Migration)
1215

13-
### 2. Time and Precision Handling (java.time Migration)
1416
- **Millisecond Precision:** Always truncate `Instant.now()` to milliseconds (using `.truncatedTo(ChronoUnit.MILLIS)`) to maintain consistency with Joda `DateTime` and the PostgreSQL schema (which enforces millisecond precision via JPA converters).
1517
- **Clock Injection:**
1618
- Avoid direct calls to `Instant.now()`, `DateTime.now()`, `ZonedDateTime.now()`, or `System.currentTimeMillis()`.
@@ -50,3 +52,51 @@ This document outlines foundational mandates, architectural patterns, and projec
5052
## Performance and Efficiency
5153
- **Turn Minimization:** Aim for "perfect" code in the first iteration. Iterative fixes for checkstyle or compilation errors consume significant context and time.
5254
- **Context Management:** Use sub-agents for batch refactoring or high-volume output tasks to keep the main session history lean and efficient.
55+
56+
---
57+
58+
# Gemini Engineering Guide: Nomulus Codebase
59+
60+
This document captures high-level architectural patterns, lessons learned from large-scale refactorings (like the Joda-Time to `java.time` migration), and specific instructions to avoid common pitfalls in this environment.
61+
62+
## 🏛 Architecture Overview
63+
64+
- **Transaction Management:** The codebase uses a custom wrapper around JPA. Always use `tm()` (from `TransactionManagerFactory`) to interact with the database.
65+
- **Dependency Injection:** Dagger 2 is used extensively. If you see "cannot find symbol" errors for classes starting with `Dagger...`, the project is in a state where annotation processing failed. Fix compilation in core models first to restore generated code.
66+
- **Value Types:** AutoValue and "ImmutableObject" patterns are dominant. Most models follow a `Buildable` pattern with a nested `Builder`.
67+
- **Temporal Logic:** The project is migrating from Joda-Time to `java.time`.
68+
- Core boundaries: `DateTimeUtils.START_OF_TIME_INSTANT` (Unix Epoch) and `END_OF_TIME_INSTANT` (Long.MAX_VALUE / 1000).
69+
- Year Arithmetic: Use `DateTimeUtils.leapSafeAddYears()` and `leapSafeSubtractYears()` to handle February 29th logic correctly.
70+
71+
## Source Control
72+
- **One Commit Per PR:** All changes for a single PR must be squashed into a single commit before merging.
73+
- **Default to Amend:** Once an initial commit is created for a PR, all subsequent functional changes should be amended into that same commit by default (`git commit --amend --no-edit`). This ensures the PR remains a single, clean unit of work throughout the development lifecycle.
74+
- **Commit Message Style:** Follow standard Git commit best practices. The subject line (first line) should be concise, capitalized, and **must not end with punctuation** (e.g., a period).
75+
- **Final Validation:** Always run `git status` as the final step before declaring a task complete to ensure all changes are committed and the working directory is clean.
76+
- **Commit Verification:** After any commit or amendment, explicitly verify the success of the operation (e.g., using `git status` and reviewing the diff). Never report a Git operation as "done" without having first successfully executed the command and confirmed the repository state.
77+
- **Diff Review:** Before finalizing a task, review the full diff (e.g., `git diff HEAD^`) to ensure all changes are functional and relevant. Identify and revert any formatting-only changes in files that do not contain functional updates to keep the commit focused.
78+
79+
## Refactoring & Migration Guardrails
80+
81+
82+
### 1. Compiler Warnings are Errors (`-Werror`)
83+
This project treats Error Prone warnings as errors.
84+
- **`@InlineMeSuggester`**: When creating deprecated Joda-Time bridge methods (e.g., `getTimestamp() -> return toDateTime(getTimestampInstant())`), you **MUST** immediately add `@SuppressWarnings("InlineMeSuggester")`. If you don't, the build will fail.
85+
- **Repeatable Annotations**: `@SuppressWarnings` is **NOT** repeatable in this environment. If a method or class already has a suppression (e.g., `@SuppressWarnings("unchecked")`), you must merge them:
86+
-`@SuppressWarnings("unchecked") @SuppressWarnings("InlineMeSuggester")`
87+
-`@SuppressWarnings({"unchecked", "InlineMeSuggester"})`
88+
89+
### 2. Resolving Ambiguity
90+
- **Null Overloads**: Adding an `Instant` overload to a method that previously took `DateTime` will break all `create(null)` calls. You must cast them: `create((Instant) null)`.
91+
- **Type Erasure**: Methods taking `Optional<DateTime>` and `Optional<Instant>` will clash due to erasure. Use distinct names, e.g., `setAutorenewEndTimeInstant(Optional<Instant> time)`.
92+
93+
### 3. Build Strategy
94+
- **Surgical Changes**: In large-scale migrations, focus on "leaf" nodes first (Utilities -> Models -> Flows -> Actions).
95+
- **PR Size**: Minimize PR size by retaining Joda-Time bridge methods for high-level "Action" and "Flow" classes unless a full migration is requested. Reverting changes to DNS and Reporting logic while updating the underlying models is a valid strategy to keep PRs reviewable.
96+
- **Validation**: Always run `./gradlew build -x test` before attempting to run unit tests. Unit tests will not run if there are compilation errors in any part of the `core` module. Before finalizing a PR, run the entire build using `./gradlew build` to ensure all checks (including tests, checkstyle, and presubmits) pass.
97+
98+
## 🚫 Common Pitfalls to Avoid
99+
100+
- **Do not go in circles with the build:** If you see an `InlineMeSuggester` error, apply the suppression to **ALL** similar methods in that file and related files in one turn. Do not fix them one by one.
101+
- **Dagger/AutoValue corruption:** If you modify a builder or a component incorrectly, Dagger will fail to generate code, leading to hundreds of "cannot find symbol" errors. If this happens, `git checkout` the last working state of the specific file and re-apply changes more surgically.
102+
- **`replace` tool context**: When using `replace` on large files (like `Tld.java` or `DomainBase.java`), provide significant surrounding context. These files have many similar method signatures (getters/setters) that can lead to incorrect replacements.

common/src/main/java/google/registry/util/DateTimeUtils.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.sql.Date;
2323
import java.time.Instant;
2424
import java.time.ZoneOffset;
25+
import java.time.format.DateTimeFormatter;
2526
import javax.annotation.Nullable;
2627
import org.joda.time.DateTime;
2728
import org.joda.time.DateTimeZone;
@@ -55,6 +56,34 @@ public abstract class DateTimeUtils {
5556
*/
5657
public static final Instant END_INSTANT = Instant.ofEpochMilli(Long.MAX_VALUE / 1000);
5758

59+
/**
60+
* Standard ISO 8601 formatter with millisecond precision in UTC.
61+
*
62+
* <p>Example: {@code 2024-03-27T10:15:30.105Z}
63+
*
64+
* <p>Uses the 'u' symbol for year to support large/negative years in a way compatible with ISO
65+
* 8601.
66+
*/
67+
public static final DateTimeFormatter ISO_8601_FORMATTER =
68+
DateTimeFormatter.ofPattern("u-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC);
69+
70+
/**
71+
* Parses an ISO-8601 string to an {@link Instant}.
72+
*
73+
* <p>This method is lenient and supports both strings with and without millisecond precision
74+
* (e.g. {@code 2024-03-27T10:15:30Z} and {@code 2024-03-27T10:15:30.105Z}). It also supports
75+
* large years (e.g. {@code 294247-01-10T04:00:54.775Z}).
76+
*/
77+
public static Instant parseInstant(String timestamp) {
78+
try {
79+
// Try the standard millisecond precision format first.
80+
return Instant.from(ISO_8601_FORMATTER.parse(timestamp));
81+
} catch (java.time.format.DateTimeParseException e) {
82+
// Fall back to the standard ISO instant parser which handles varied precision.
83+
return Instant.parse(timestamp);
84+
}
85+
}
86+
5887
/** Returns the earliest of a number of given {@link DateTime} instances. */
5988
public static DateTime earliestOf(DateTime first, DateTime... rest) {
6089
return earliestDateTimeOf(Lists.asList(first, rest));
@@ -79,15 +108,26 @@ public static Instant earliestOf(Iterable<Instant> instants) {
79108

80109
/** Returns the latest of a number of given {@link DateTime} instances. */
81110
public static DateTime latestOf(DateTime first, DateTime... rest) {
111+
return latestDateTimeOf(Lists.asList(first, rest));
112+
}
113+
114+
/** Returns the latest of a number of given {@link Instant} instances. */
115+
public static Instant latestOf(Instant first, Instant... rest) {
82116
return latestOf(Lists.asList(first, rest));
83117
}
84118

85119
/** Returns the latest element in a {@link DateTime} iterable. */
86-
public static DateTime latestOf(Iterable<DateTime> dates) {
120+
public static DateTime latestDateTimeOf(Iterable<DateTime> dates) {
87121
checkArgument(!Iterables.isEmpty(dates));
88122
return Ordering.<DateTime>natural().max(dates);
89123
}
90124

125+
/** Returns the latest element in a {@link Instant} iterable. */
126+
public static Instant latestOf(Iterable<Instant> instants) {
127+
checkArgument(!Iterables.isEmpty(instants));
128+
return Ordering.<Instant>natural().max(instants);
129+
}
130+
91131
/** Returns whether the first {@link DateTime} is equal to or earlier than the second. */
92132
public static boolean isBeforeOrAt(DateTime timeToCheck, DateTime timeToCompareTo) {
93133
return !timeToCheck.isAfter(timeToCompareTo);
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2026 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package google.registry.testing.truth;
16+
17+
import static com.google.common.truth.Truth.assertAbout;
18+
import static google.registry.util.DateTimeUtils.toDateTime;
19+
20+
import com.google.common.truth.ComparableSubject;
21+
import com.google.common.truth.FailureMetadata;
22+
import java.time.Instant;
23+
import javax.annotation.Nullable;
24+
import org.joda.time.DateTime;
25+
26+
/**
27+
* Truth subject for {@link DateTime}.
28+
*
29+
* <p>This class facilitates easier temporal testing during the migration from Joda-Time to {@link
30+
* java.time}. It provides overloads for comparing {@link DateTime} instances against {@link
31+
* Instant} instances, reducing the need for manual conversions in test code.
32+
*/
33+
public final class DateTimeSubject extends ComparableSubject<DateTime> {
34+
35+
private final DateTime actual;
36+
37+
private DateTimeSubject(FailureMetadata failureMetadata, @Nullable DateTime actual) {
38+
super(failureMetadata, actual);
39+
this.actual = actual;
40+
}
41+
42+
/** Asserts that the actual {@link DateTime} is equal to the expected {@link DateTime}. */
43+
public void isAt(@Nullable DateTime expected) {
44+
isEqualTo(expected);
45+
}
46+
47+
/**
48+
* Asserts that the actual {@link DateTime} is equal to the expected {@link Instant}.
49+
*
50+
* <p>Conversion is handled via {@link google.registry.util.DateTimeUtils#toDateTime(Instant)}.
51+
*/
52+
public void isAt(@Nullable Instant expected) {
53+
isEqualTo(toDateTime(expected));
54+
}
55+
56+
/** Asserts that the actual {@link DateTime} is strictly before the expected {@link Instant}. */
57+
public void isBefore(@Nullable Instant expected) {
58+
isLessThan(toDateTime(expected));
59+
}
60+
61+
/** Asserts that the actual {@link DateTime} is at or before the expected {@link DateTime}. */
62+
public void isAtOrBefore(@Nullable DateTime expected) {
63+
isAtMost(expected);
64+
}
65+
66+
/** Asserts that the actual {@link DateTime} is at or before the expected {@link Instant}. */
67+
public void isAtOrBefore(@Nullable Instant expected) {
68+
isAtMost(toDateTime(expected));
69+
}
70+
71+
/** Asserts that the actual {@link DateTime} is strictly after the expected {@link Instant}. */
72+
public void isAfter(@Nullable Instant expected) {
73+
isGreaterThan(toDateTime(expected));
74+
}
75+
76+
/** Asserts that the actual {@link DateTime} is at or after the expected {@link DateTime}. */
77+
public void isAtOrAfter(@Nullable DateTime expected) {
78+
isAtLeast(expected);
79+
}
80+
81+
/** Asserts that the actual {@link DateTime} is at or after the expected {@link Instant}. */
82+
public void isAtOrAfter(@Nullable Instant expected) {
83+
isAtLeast(toDateTime(expected));
84+
}
85+
86+
/** Static entry point for creating a {@link DateTimeSubject} for the given actual value. */
87+
public static DateTimeSubject assertThat(@Nullable DateTime actual) {
88+
return assertAbout(DateTimeSubject::new).that(actual);
89+
}
90+
91+
/** Internal factory for Truth. */
92+
static Factory<DateTimeSubject, DateTime> dateTimes() {
93+
return DateTimeSubject::new;
94+
}
95+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2026 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package google.registry.testing.truth;
16+
17+
import static com.google.common.truth.Truth.assertAbout;
18+
import static google.registry.util.DateTimeUtils.toInstant;
19+
20+
import com.google.common.truth.ComparableSubject;
21+
import com.google.common.truth.FailureMetadata;
22+
import java.time.Instant;
23+
import javax.annotation.Nullable;
24+
import org.joda.time.DateTime;
25+
26+
/**
27+
* Truth subject for {@link Instant}.
28+
*
29+
* <p>This class facilitates easier temporal testing during the migration from Joda-Time to {@link
30+
* java.time}. It provides overloads for comparing {@link Instant} instances against {@link
31+
* DateTime} instances, reducing the need for manual conversions in test code.
32+
*/
33+
public final class InstantSubject extends ComparableSubject<Instant> {
34+
35+
private final Instant actual;
36+
37+
private InstantSubject(FailureMetadata failureMetadata, @Nullable Instant actual) {
38+
super(failureMetadata, actual);
39+
this.actual = actual;
40+
}
41+
42+
/** Asserts that the actual {@link Instant} is equal to the expected {@link Instant}. */
43+
public void isAt(@Nullable Instant expected) {
44+
isEqualTo(expected);
45+
}
46+
47+
/**
48+
* Asserts that the actual {@link Instant} is equal to the expected {@link DateTime}.
49+
*
50+
* <p>Conversion is handled via {@link google.registry.util.DateTimeUtils#toInstant(DateTime)}.
51+
*/
52+
public void isAt(@Nullable DateTime expected) {
53+
isEqualTo(toInstant(expected));
54+
}
55+
56+
/** Asserts that the actual {@link Instant} is strictly before the expected {@link DateTime}. */
57+
public void isBefore(@Nullable DateTime expected) {
58+
isLessThan(toInstant(expected));
59+
}
60+
61+
/** Asserts that the actual {@link Instant} is at or before the expected {@link Instant}. */
62+
public void isAtOrBefore(@Nullable Instant expected) {
63+
isAtMost(expected);
64+
}
65+
66+
/** Asserts that the actual {@link Instant} is at or before the expected {@link DateTime}. */
67+
public void isAtOrBefore(@Nullable DateTime expected) {
68+
isAtMost(toInstant(expected));
69+
}
70+
71+
/** Asserts that the actual {@link Instant} is strictly after the expected {@link DateTime}. */
72+
public void isAfter(@Nullable DateTime expected) {
73+
isGreaterThan(toInstant(expected));
74+
}
75+
76+
/** Asserts that the actual {@link Instant} is at or after the expected {@link Instant}. */
77+
public void isAtOrAfter(@Nullable Instant expected) {
78+
isAtLeast(expected);
79+
}
80+
81+
/** Asserts that the actual {@link Instant} is at or after the expected {@link DateTime}. */
82+
public void isAtOrAfter(@Nullable DateTime expected) {
83+
isAtLeast(toInstant(expected));
84+
}
85+
86+
/** Static entry point for creating an {@link InstantSubject} for the given actual value. */
87+
public static InstantSubject assertThat(@Nullable Instant actual) {
88+
return assertAbout(InstantSubject::new).that(actual);
89+
}
90+
91+
/** Internal factory for Truth. */
92+
static Factory<InstantSubject, Instant> instants() {
93+
return InstantSubject::new;
94+
}
95+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2026 The Nomulus Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package google.registry.testing.truth;
16+
17+
import java.time.Instant;
18+
import javax.annotation.Nullable;
19+
import org.joda.time.DateTime;
20+
21+
/**
22+
* Central entry point for Nomulus-specific Truth subjects.
23+
*
24+
* <p>This class provides convenient methods for performing temporal assertions in tests, especially
25+
* during the migration from Joda-Time to {@link java.time}.
26+
*
27+
* <p>Usage: {@code import static google.registry.testing.truth.RegistryTruth.assertAt;}
28+
*/
29+
public final class RegistryTruth {
30+
31+
/** Returns a {@link InstantSubject} for the given {@link Instant}. */
32+
public static InstantSubject assertAt(@Nullable Instant actual) {
33+
return InstantSubject.assertThat(actual);
34+
}
35+
36+
/** Returns a {@link DateTimeSubject} for the given {@link DateTime}. */
37+
public static DateTimeSubject assertAt(@Nullable DateTime actual) {
38+
return DateTimeSubject.assertThat(actual);
39+
}
40+
41+
private RegistryTruth() {}
42+
}

config/presubmits.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def fails(self, file):
153153
PresubmitCheck(
154154
r".*java\.util\.Date.*",
155155
"java",
156-
{"/node_modules/", "JpaTransactionManagerImpl.java"},
156+
{"/node_modules/", "JpaTransactionManagerImpl.java", "DateTimeUtils.java"},
157157
):
158158
"Do not use java.util.Date. Use classes in java.time package instead.",
159159
PresubmitCheck(

0 commit comments

Comments
 (0)