diff --git a/java/google/registry/rdap/RdapJsonFormatter.java b/java/google/registry/rdap/RdapJsonFormatter.java index cf730a49d..7984856af 100644 --- a/java/google/registry/rdap/RdapJsonFormatter.java +++ b/java/google/registry/rdap/RdapJsonFormatter.java @@ -54,6 +54,7 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.URI; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -63,6 +64,7 @@ import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; import org.joda.time.DateTime; +import org.joda.time.DateTimeComparator; /** * Helper class to create RDAP JSON objects for various registry entities and objects. @@ -1008,32 +1010,85 @@ public class RdapJsonFormatter { * Creates an event list for a domain, host or contact resource. */ private static ImmutableList makeEvents(EppResource resource, DateTime now) { - ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); - for (HistoryEntry historyEntry : ofy().load() - .type(HistoryEntry.class) - .ancestor(resource) - .order("modificationTime")) { - // Only create an event if this is a type we care about. - if (!historyEntryTypeToRdapEventActionMap.containsKey(historyEntry.getType())) { + HashMap lastEntryOfType = Maps.newHashMap(); + // Events (such as transfer, but also create) can appear multiple times. We only want the last + // time they appeared. + // + // We can have multiple create historyEntries if a domain was deleted, and then someone new + // bought it. + // + // From RDAP response profile + // 2.3.2 The domain object in the RDAP response MAY contain the following events: + // 2.3.2.3 An event of *eventAction* type *transfer*, with the last date and time that the + // domain was transferred. The event of *eventAction* type *transfer* MUST be omitted if the + // domain name has not been transferred since it was created. + for (HistoryEntry historyEntry : + ofy().load().type(HistoryEntry.class).ancestor(resource).order("modificationTime")) { + RdapEventAction rdapEventAction = + historyEntryTypeToRdapEventActionMap.get(historyEntry.getType()); + // Only save the historyEntries if this is a type we care about. + if (rdapEventAction == null) { continue; } - RdapEventAction eventAction = - historyEntryTypeToRdapEventActionMap.get(historyEntry.getType()); - eventsBuilder.add(makeEvent( - eventAction, historyEntry.getClientId(), historyEntry.getModificationTime())); + lastEntryOfType.put(rdapEventAction, historyEntry); + } + ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); + // There are 2 possibly conflicting values for the creation time - either the + // resource.getCreationTime, or the REGISTRATION event created from a HistoryEntry + // + // We favor the HistoryEntry if it exists, since we show that value as REGISTRATION time in the + // reply, so the reply will be self-consistent. + // + // This is mostly an issue in the tests as in "reality" these two values should be the same. + // + DateTime creationTime = + Optional.ofNullable(lastEntryOfType.get(RdapEventAction.REGISTRATION)) + .map(historyEntry -> historyEntry.getModificationTime()) + .orElse(resource.getCreationTime()); + // TODO(b/129849684) remove this and use the events List defined above once we have Event + // objects + ImmutableList.Builder changeTimesBuilder = new ImmutableList.Builder<>(); + // The order of the elements is stable - it's the order in which the enum elements are defined + // in RdapEventAction + for (RdapEventAction rdapEventAction : RdapEventAction.values()) { + HistoryEntry historyEntry = lastEntryOfType.get(rdapEventAction); + // Check if there was any entry of this type + if (historyEntry == null) { + continue; + } + DateTime modificationTime = historyEntry.getModificationTime(); + // We will ignore all events that happened before the "creation time", since these events are + // from a "previous incarnation of the domain" (for a domain that was owned by someone, + // deleted, and then bought by someone else) + if (modificationTime.isBefore(creationTime)) { + continue; + } + eventsBuilder.add(makeEvent(rdapEventAction, historyEntry.getClientId(), modificationTime)); + changeTimesBuilder.add(modificationTime); } if (resource instanceof DomainBase) { DateTime expirationTime = ((DomainBase) resource).getRegistrationExpirationTime(); if (expirationTime != null) { eventsBuilder.add(makeEvent(RdapEventAction.EXPIRATION, null, expirationTime)); + changeTimesBuilder.add(expirationTime); } } - if ((resource.getLastEppUpdateTime() != null) - && resource.getLastEppUpdateTime().isAfter(resource.getCreationTime())) { - eventsBuilder.add(makeEvent( - RdapEventAction.LAST_CHANGED, null, resource.getLastEppUpdateTime())); + if (resource.getLastEppUpdateTime() != null) { + changeTimesBuilder.add(resource.getLastEppUpdateTime()); + } + // The last change time might not be the lastEppUpdateTime, since some changes happen without + // any EPP update (for example, by the passage of time). + DateTime lastChangeTime = + changeTimesBuilder.build().stream() + .filter(changeTime -> changeTime.isBefore(now)) + .max(DateTimeComparator.getInstance()) + .orElse(null); + if (lastChangeTime != null && lastChangeTime.isAfter(creationTime)) { + eventsBuilder.add(makeEvent(RdapEventAction.LAST_CHANGED, null, lastChangeTime)); } eventsBuilder.add(makeEvent(RdapEventAction.LAST_UPDATE_OF_RDAP_DATABASE, null, now)); + // TODO(b/129849684): sort events by their time once we return a list of Events instead of JSON + // objects. return eventsBuilder.build(); } diff --git a/javatests/google/registry/rdap/RdapJsonFormatterTest.java b/javatests/google/registry/rdap/RdapJsonFormatterTest.java index 501c22b7c..7fac6fd1e 100644 --- a/javatests/google/registry/rdap/RdapJsonFormatterTest.java +++ b/javatests/google/registry/rdap/RdapJsonFormatterTest.java @@ -72,7 +72,7 @@ public class RdapJsonFormatterTest { private Registrar registrar; private DomainBase domainBaseFull; - private DomainBase domainBaseNoNameservers; + private DomainBase domainBaseNoNameserversNoTransfers; private HostResource hostResourceIpv4; private HostResource hostResourceIpv6; private HostResource hostResourceBoth; @@ -195,7 +195,7 @@ public class RdapJsonFormatterTest { hostResourceIpv4, hostResourceIpv6, registrar)); - domainBaseNoNameservers = persistResource( + domainBaseNoNameserversNoTransfers = persistResource( makeDomainBase( "fish.みんな", contactResourceRegistrant, @@ -224,14 +224,45 @@ public class RdapJsonFormatterTest { HistoryEntry.Type.DOMAIN_CREATE, Period.create(1, Period.Unit.YEARS), "created", - clock.nowUtc())); + clock.nowUtc().minusMonths(4))); persistResource( makeHistoryEntry( - domainBaseNoNameservers, + domainBaseNoNameserversNoTransfers, HistoryEntry.Type.DOMAIN_CREATE, Period.create(1, Period.Unit.YEARS), "created", clock.nowUtc())); + // We create 3 "transfer approved" entries, to make sure we only save the last one + persistResource( + makeHistoryEntry( + domainBaseFull, + HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE, + null, + null, + clock.nowUtc().minusMonths(3))); + persistResource( + makeHistoryEntry( + domainBaseFull, + HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE, + null, + null, + clock.nowUtc().minusMonths(1))); + persistResource( + makeHistoryEntry( + domainBaseFull, + HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE, + null, + null, + clock.nowUtc().minusMonths(2))); + // We create a "transfer approved" entry for domainBaseNoNameserversNoTransfers that happened + // before the domain was created, to make sure we don't show it + persistResource( + makeHistoryEntry( + domainBaseNoNameserversNoTransfers, + HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE, + null, + null, + clock.nowUtc().minusMonths(3))); } public static ImmutableList makeMoreRegistrarContacts(Registrar registrar) { @@ -553,9 +584,9 @@ public class RdapJsonFormatterTest { } @Test - public void testDomain_noNameservers() { + public void testDomain_noNameserversNoTransfers() { assertThat(rdapJsonFormatter.makeRdapJsonForDomain( - domainBaseNoNameservers, + domainBaseNoNameserversNoTransfers, false, LINK_BASE, WHOIS_SERVER, diff --git a/javatests/google/registry/rdap/testdata/rdap_contact_deleted.json b/javatests/google/registry/rdap/testdata/rdap_contact_deleted.json index 3dba0de55..a477eaea3 100644 --- a/javatests/google/registry/rdap/testdata/rdap_contact_deleted.json +++ b/javatests/google/registry/rdap/testdata/rdap_contact_deleted.json @@ -23,6 +23,10 @@ "eventActor": "foo", "eventDate": "1999-07-01T00:00:00.000Z" }, + { + "eventAction": "last changed", + "eventDate": "1999-07-01T00:00:00.000Z" + }, { "eventAction": "last update of RDAP database", "eventDate": "2000-01-01T00:00:00.000Z" diff --git a/javatests/google/registry/rdap/testdata/rdap_domain.json b/javatests/google/registry/rdap/testdata/rdap_domain.json index f2aa4b6cc..b1310deac 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain.json @@ -24,10 +24,6 @@ "eventAction": "expiration", "eventDate": "2110-10-08T00:44:59.000Z" }, - { - "eventAction": "last changed", - "eventDate": "2009-05-29T20:13:00.000Z" - }, { "eventAction": "last update of RDAP database", "eventDate": "2000-01-01T00:00:00.000Z" diff --git a/javatests/google/registry/rdap/testdata/rdap_domain_cat2.json b/javatests/google/registry/rdap/testdata/rdap_domain_cat2.json index 5999d4bc0..3bfa1e8fe 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain_cat2.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain_cat2.json @@ -24,10 +24,6 @@ "eventAction": "expiration", "eventDate": "2110-10-08T00:44:59.000Z" }, - { - "eventAction": "last changed", - "eventDate": "2009-05-29T20:13:00.000Z" - }, { "eventAction": "last update of RDAP database", "eventDate": "2000-01-01T00:00:00.000Z" diff --git a/javatests/google/registry/rdap/testdata/rdap_domain_deleted.json b/javatests/google/registry/rdap/testdata/rdap_domain_deleted.json index 0575d830c..0eec0690c 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain_deleted.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain_deleted.json @@ -32,7 +32,7 @@ }, { "eventAction": "last changed", - "eventDate": "2009-05-29T20:13:00.000Z" + "eventDate": "1999-07-01T00:00:00.000Z" }, { "eventAction": "last update of RDAP database", diff --git a/javatests/google/registry/rdap/testdata/rdap_domain_no_contacts.json b/javatests/google/registry/rdap/testdata/rdap_domain_no_contacts.json index cfb1346c0..00c0164a4 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain_no_contacts.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain_no_contacts.json @@ -26,10 +26,6 @@ "eventAction": "expiration", "eventDate": "2110-10-08T00:44:59.000Z" }, - { - "eventAction": "last changed", - "eventDate": "2009-05-29T20:13:00.000Z" - }, { "eventAction": "last update of RDAP database", "eventDate": "2000-01-01T00:00:00.000Z" diff --git a/javatests/google/registry/rdap/testdata/rdap_domain_no_contacts_with_remark.json b/javatests/google/registry/rdap/testdata/rdap_domain_no_contacts_with_remark.json index e9cffacab..5a8018592 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain_no_contacts_with_remark.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain_no_contacts_with_remark.json @@ -26,10 +26,6 @@ "eventAction": "expiration", "eventDate": "2110-10-08T00:44:59.000Z" }, - { - "eventAction": "last changed", - "eventDate": "2009-05-29T20:13:00.000Z" - }, { "eventAction": "last update of RDAP database", "eventDate": "2000-01-01T00:00:00.000Z" diff --git a/javatests/google/registry/rdap/testdata/rdap_domain_unicode.json b/javatests/google/registry/rdap/testdata/rdap_domain_unicode.json index 1f228a31f..9e796f416 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain_unicode.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain_unicode.json @@ -25,10 +25,6 @@ "eventAction": "expiration", "eventDate": "2110-10-08T00:44:59.000Z" }, - { - "eventAction": "last changed", - "eventDate": "2009-05-29T20:13:00.000Z" - }, { "eventAction": "last update of RDAP database", "eventDate": "2000-01-01T00:00:00.000Z" diff --git a/javatests/google/registry/rdap/testdata/rdap_domain_unicode_no_contacts_with_remark.json b/javatests/google/registry/rdap/testdata/rdap_domain_unicode_no_contacts_with_remark.json index 188eb77a1..9cc874db4 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain_unicode_no_contacts_with_remark.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain_unicode_no_contacts_with_remark.json @@ -27,10 +27,6 @@ "eventAction": "expiration", "eventDate": "2110-10-08T00:44:59.000Z" }, - { - "eventAction": "last changed", - "eventDate": "2009-05-29T20:13:00.000Z" - }, { "eventAction": "last update of RDAP database", "eventDate": "2000-01-01T00:00:00.000Z" diff --git a/javatests/google/registry/rdap/testdata/rdapjson_domain_full.json b/javatests/google/registry/rdap/testdata/rdapjson_domain_full.json index 2671ecc6e..c36611577 100644 --- a/javatests/google/registry/rdap/testdata/rdapjson_domain_full.json +++ b/javatests/google/registry/rdap/testdata/rdapjson_domain_full.json @@ -23,7 +23,12 @@ { "eventAction": "registration", "eventActor": "foo", - "eventDate": "2000-01-01T00:00:00.000Z" + "eventDate": "1999-09-01T00:00:00.000Z" + }, + { + "eventAction": "transfer", + "eventActor": "foo", + "eventDate": "1999-12-01T00:00:00.000Z" }, { "eventAction": "expiration", @@ -31,7 +36,7 @@ }, { "eventAction": "last changed", - "eventDate": "2009-05-29T20:13:00.000Z" + "eventDate": "1999-12-01T00:00:00.000Z" }, { "eventAction": "last update of RDAP database", diff --git a/javatests/google/registry/rdap/testdata/rdapjson_domain_logged_out.json b/javatests/google/registry/rdap/testdata/rdapjson_domain_logged_out.json index 1070326ee..af948acf4 100644 --- a/javatests/google/registry/rdap/testdata/rdapjson_domain_logged_out.json +++ b/javatests/google/registry/rdap/testdata/rdapjson_domain_logged_out.json @@ -23,7 +23,12 @@ { "eventAction": "registration", "eventActor": "foo", - "eventDate": "2000-01-01T00:00:00.000Z" + "eventDate": "1999-09-01T00:00:00.000Z" + }, + { + "eventAction": "transfer", + "eventActor": "foo", + "eventDate": "1999-12-01T00:00:00.000Z" }, { "eventAction": "expiration", @@ -31,7 +36,7 @@ }, { "eventAction": "last changed", - "eventDate": "2009-05-29T20:13:00.000Z" + "eventDate": "1999-12-01T00:00:00.000Z" }, { "eventAction": "last update of RDAP database", diff --git a/javatests/google/registry/rdap/testdata/rdapjson_domain_no_nameservers.json b/javatests/google/registry/rdap/testdata/rdapjson_domain_no_nameservers.json index 00651e034..e08e1fd97 100644 --- a/javatests/google/registry/rdap/testdata/rdapjson_domain_no_nameservers.json +++ b/javatests/google/registry/rdap/testdata/rdapjson_domain_no_nameservers.json @@ -30,10 +30,6 @@ "eventAction": "expiration", "eventDate": "2110-10-08T00:44:59.000Z" }, - { - "eventAction": "last changed", - "eventDate": "2009-05-29T20:13:00.000Z" - }, { "eventAction": "last update of RDAP database", "eventDate": "2000-01-01T00:00:00.000Z"