> results;
if (fnParam.isPresent()) {
// syntax: /rdap/entities?fn=Bobby%20Joe*
- // TODO(b/25973399): implement entity name search, and move the comment below to that routine
- // As per Gustavo Lozano of ICANN, registrar name search should be by registrar name only, not
- // by registrar contact name:
- //
- // The search is by registrar name only. The profile is supporting the functionality defined
- // in the Base Registry Agreement (see 1.6 of Section 4 of the Base Registry Agreement,
- // https://newgtlds.icann.org/sites/default/files/agreements/
- // agreement-approved-09jan14-en.htm).
- throw new NotImplementedException("Entity name search not implemented");
+ // The name is the contact name or registrar name (not registrar contact name).
+ results = searchByName(RdapSearchPattern.create(fnParam.get(), false), now);
} else {
// syntax: /rdap/entities?handle=12345-*
// The handle is either the contact roid or the registrar clientId.
@@ -111,6 +102,57 @@ public class RdapEntitySearchAction extends RdapActionBase {
return builder.build();
}
+ /**
+ * Searches for entities by name, returning a JSON array of entity info maps.
+ *
+ * As per Gustavo Lozano of ICANN, registrar name search should be by registrar name only, not
+ * by registrar contact name:
+ *
+ *
The search is by registrar name only. The profile is supporting the functionality defined
+ * in the Base Registry Agreement (see 1.6 of Section 4 of the Base Registry Agreement,
+ * https://newgtlds.icann.org/sites/default/files/agreements/
+ * agreement-approved-09jan14-en.htm).
+ *
+ *
According to RFC 7482 section 6.1, punycode is only used for domain name labels, so we can
+ * assume that entity names are regular unicode.
+ */
+ private ImmutableList>
+ searchByName(final RdapSearchPattern partialStringQuery, DateTime now) throws HttpException {
+ // Handle queries without a wildcard -- load by name, which may not be unique.
+ if (!partialStringQuery.getHasWildcard()) {
+ Registrar registrar = Registrar.loadByName(partialStringQuery.getInitialString());
+ return makeSearchResults(
+ ofy().load()
+ .type(ContactResource.class)
+ .filter("searchName", partialStringQuery.getInitialString())
+ .filter("deletionTime", END_OF_TIME)
+ .limit(rdapResultSetMaxSize),
+ (registrar == null)
+ ? ImmutableList.of() : ImmutableList.of(registrar),
+ now);
+ // Handle queries with a wildcard, but no suffix. For contact resources, the deletion time will
+ // always be END_OF_TIME for non-deleted records; unlike domain resources, we don't need to
+ // worry about deletion times in the future. That allows us to use an equality query for the
+ // deletion time.
+ } else if (partialStringQuery.getSuffix() == null) {
+ return makeSearchResults(
+ ofy().load()
+ .type(ContactResource.class)
+ .filter("searchName >=", partialStringQuery.getInitialString())
+ .filter("searchName <", partialStringQuery.getNextInitialString())
+ .filter("deletionTime", END_OF_TIME)
+ .limit(rdapResultSetMaxSize),
+ Registrar.loadByNameRange(
+ partialStringQuery.getInitialString(),
+ partialStringQuery.getNextInitialString(),
+ rdapResultSetMaxSize),
+ now);
+ // Don't allow suffixes in entity name search queries.
+ } else {
+ throw new UnprocessableEntityException("Suffixes not allowed in entity name searches");
+ }
+ }
+
/** Searches for entities by handle, returning a JSON array of entity info maps. */
private ImmutableList> searchByHandle(
final RdapSearchPattern partialStringQuery, DateTime now) throws HttpException {
@@ -121,63 +163,62 @@ public class RdapEntitySearchAction extends RdapActionBase {
.id(partialStringQuery.getInitialString())
.now();
Registrar registrar = Registrar.loadByClientId(partialStringQuery.getInitialString());
- ImmutableList.Builder> builder = new ImmutableList.Builder<>();
- if ((contactResource != null) && contactResource.getDeletionTime().isEqual(END_OF_TIME)) {
- // As per Andy Newton on the regext mailing list, contacts by themselves have no role, since
- // they are global, and might have different roles for different domains.
- builder.add(RdapJsonFormatter.makeRdapJsonForContact(
- contactResource,
- false,
- Optional.absent(),
- rdapLinkBase,
- rdapWhoisServer,
- now));
- }
- if ((registrar != null) && registrar.isActiveAndPubliclyVisible()) {
- builder.add(RdapJsonFormatter.makeRdapJsonForRegistrar(
- registrar, false, rdapLinkBase, rdapWhoisServer, now));
- }
- return builder.build();
+ return makeSearchResults(
+ ((contactResource == null) || !contactResource.getDeletionTime().isEqual(END_OF_TIME))
+ ? ImmutableList.of() : ImmutableList.of(contactResource),
+ (registrar == null)
+ ? ImmutableList.of() : ImmutableList.of(registrar),
+ now);
// Handle queries with a wildcard, but no suffix. For contact resources, the deletion time will
// always be END_OF_TIME for non-deleted records; unlike domain resources, we don't need to
// worry about deletion times in the future. That allows us to use an equality query for the
// deletion time.
} else if (partialStringQuery.getSuffix() == null) {
- ImmutableList.Builder> builder = new ImmutableList.Builder<>();
- Query query = ofy().load()
- .type(ContactResource.class)
- .filterKey(">=", Key.create(ContactResource.class, partialStringQuery.getInitialString()))
- .filterKey(
- "<", Key.create(ContactResource.class, partialStringQuery.getNextInitialString()))
- .filter("deletionTime", END_OF_TIME)
- .limit(rdapResultSetMaxSize);
- for (ContactResource contactResource : query) {
- builder.add(RdapJsonFormatter.makeRdapJsonForContact(
- contactResource,
- false,
- Optional.absent(),
- rdapLinkBase,
- rdapWhoisServer,
- now));
- }
- for (Registrar registrar
- : Registrar.loadByClientIdRange(
+ return makeSearchResults(
+ ofy().load()
+ .type(ContactResource.class)
+ .filterKey(
+ ">=", Key.create(ContactResource.class, partialStringQuery.getInitialString()))
+ .filterKey(
+ "<", Key.create(ContactResource.class, partialStringQuery.getNextInitialString()))
+ .filter("deletionTime", END_OF_TIME)
+ .limit(rdapResultSetMaxSize),
+ Registrar.loadByClientIdRange(
partialStringQuery.getInitialString(),
partialStringQuery.getNextInitialString(),
- rdapResultSetMaxSize)) {
- if (registrar.isActiveAndPubliclyVisible()) {
- builder.add(RdapJsonFormatter.makeRdapJsonForRegistrar(
- registrar, false, rdapLinkBase, rdapWhoisServer, now));
- }
- }
- // In theory, there could be more results than our max size, so limit the size.
- ImmutableList> resultSet = builder.build();
- return (resultSet.size() <= rdapResultSetMaxSize)
- ? resultSet
- : resultSet.subList(0, rdapResultSetMaxSize);
+ rdapResultSetMaxSize),
+ now);
// Don't allow suffixes in entity handle search queries.
} else {
throw new UnprocessableEntityException("Suffixes not allowed in entity handle searches");
}
}
+
+ /** Builds a JSON array of entity info maps based on the specified contacts and registrars. */
+ private ImmutableList> makeSearchResults(
+ Iterable contactResources, Iterable registrars, DateTime now)
+ throws HttpException {
+ ImmutableList.Builder> builder = new ImmutableList.Builder<>();
+ for (ContactResource contact : contactResources) {
+ // As per Andy Newton on the regext mailing list, contacts by themselves have no role, since
+ // they are global, and might have different roles for different domains.
+ builder.add(RdapJsonFormatter.makeRdapJsonForContact(
+ contact,
+ false,
+ Optional.absent(),
+ rdapLinkBase,
+ rdapWhoisServer, now));
+ }
+ for (Registrar registrar : registrars) {
+ if (registrar.isActiveAndPubliclyVisible()) {
+ builder.add(RdapJsonFormatter.makeRdapJsonForRegistrar(
+ registrar, false, rdapLinkBase, rdapWhoisServer, now));
+ }
+ }
+ // In theory, there could be more results than our max size, so limit the size.
+ ImmutableList> resultSet = builder.build();
+ return (resultSet.size() <= rdapResultSetMaxSize)
+ ? resultSet
+ : resultSet.subList(0, rdapResultSetMaxSize);
+ }
}
diff --git a/javatests/google/registry/model/contact/ContactResourceTest.java b/javatests/google/registry/model/contact/ContactResourceTest.java
index 1fdfe5b68..f8b13218b 100644
--- a/javatests/google/registry/model/contact/ContactResourceTest.java
+++ b/javatests/google/registry/model/contact/ContactResourceTest.java
@@ -129,7 +129,8 @@ public class ContactResourceTest extends EntityTestCase {
verifyIndexing(
contactResource,
"deletionTime",
- "currentSponsorClientId");
+ "currentSponsorClientId",
+ "searchName");
}
@Test
diff --git a/javatests/google/registry/model/schema.txt b/javatests/google/registry/model/schema.txt
index ffc39ec02..6ef63ec90 100644
--- a/javatests/google/registry/model/schema.txt
+++ b/javatests/google/registry/model/schema.txt
@@ -157,6 +157,7 @@ class google.registry.model.contact.ContactResource {
java.lang.String currentSponsorClientId;
java.lang.String email;
java.lang.String lastEppUpdateClientId;
+ java.lang.String searchName;
java.util.Set status;
org.joda.time.DateTime deletionTime;
org.joda.time.DateTime lastEppUpdateTime;
diff --git a/javatests/google/registry/rdap/RdapEntitySearchActionTest.java b/javatests/google/registry/rdap/RdapEntitySearchActionTest.java
index 13fbb6d26..fb8252c0c 100644
--- a/javatests/google/registry/rdap/RdapEntitySearchActionTest.java
+++ b/javatests/google/registry/rdap/RdapEntitySearchActionTest.java
@@ -85,6 +85,13 @@ public class RdapEntitySearchActionTest {
ImmutableList.of("123 Blinky St", "Blinkyland"),
clock.nowUtc());
+ makeAndPersistContactResource(
+ "blindly",
+ "Blindly",
+ "blindly@b.tld",
+ ImmutableList.of("123 Blindly St", "Blindlyland"),
+ clock.nowUtc());
+
// deleted
persistResource(
makeContactResource("clyde", "Clyde (愚図た)", "clyde@c.tld")
@@ -188,7 +195,15 @@ public class RdapEntitySearchActionTest {
}
@Test
- public void testSuffix_rejected() throws Exception {
+ public void testNameMatch_suffixRejected() throws Exception {
+ assertThat(generateActualJsonWithFullName("exam*ple"))
+ .isEqualTo(
+ generateExpectedJson("Suffix not allowed after wildcard", "rdap_error_422.json"));
+ assertThat(response.getStatus()).isEqualTo(422);
+ }
+
+ @Test
+ public void testHandleMatch_suffixRejected() throws Exception {
assertThat(generateActualJsonWithHandle("exam*ple"))
.isEqualTo(
generateExpectedJson("Suffix not allowed after wildcard", "rdap_error_422.json"));
@@ -212,11 +227,51 @@ public class RdapEntitySearchActionTest {
}
@Test
- public void testNameMatch_notImplemented() throws Exception {
- assertThat(generateActualJsonWithFullName("hello"))
+ public void testNameMatch_contactFound() throws Exception {
+ assertThat(generateActualJsonWithFullName("Blinky (赤ベイ)"))
.isEqualTo(
- generateExpectedJson("Entity name search not implemented", "rdap_error_501.json"));
- assertThat(response.getStatus()).isEqualTo(501);
+ generateExpectedJsonForEntity(
+ "2-ROID",
+ "Blinky (赤ベイ)",
+ "blinky@b.tld",
+ "\"123 Blinky St\", \"Blinkyland\"",
+ "rdap_contact.json"));
+ assertThat(response.getStatus()).isEqualTo(200);
+ }
+
+ @Test
+ public void testNameMatch_contactWildcardFound() throws Exception {
+ assertThat(generateActualJsonWithFullName("Blinky*"))
+ .isEqualTo(
+ generateExpectedJsonForEntity(
+ "2-ROID",
+ "Blinky (赤ベイ)",
+ "blinky@b.tld",
+ "\"123 Blinky St\", \"Blinkyland\"",
+ "rdap_contact.json"));
+ assertThat(response.getStatus()).isEqualTo(200);
+ }
+
+ @Test
+ public void testNameMatch_contactWildcardFoundBoth() throws Exception {
+ assertThat(generateActualJsonWithFullName("Blin*"))
+ .isEqualTo(generateExpectedJson("rdap_multiple_contacts2.json"));
+ assertThat(response.getStatus()).isEqualTo(200);
+ }
+
+ @Test
+ public void testNameMatch_deletedContactNotFound() throws Exception {
+ generateActualJsonWithFullName("Cl*");
+ assertThat(response.getStatus()).isEqualTo(404);
+ }
+
+ @Test
+ public void testNameMatch_registrarFound() throws Exception {
+ assertThat(generateActualJsonWithFullName("Yes Virginia