diff --git a/docs/rdap.md b/docs/rdap.md
index 9f6c8cc46..1e678b6a7 100644
--- a/docs/rdap.md
+++ b/docs/rdap.md
@@ -341,6 +341,14 @@ formatted version can be requested by adding an extra parameter:
The result is still valid JSON, but with extra whitespace added to align the
data on the page.
+### `subtype` parameter
+
+The subtype parameter is used only for entity searches, to select whether the
+results should include contacts, registrars or both. If specified, the subtype
+should be 'all', 'contacts' or 'registrars'. Setting the subtype to 'all'
+duplicates the normal behavior of returning both. Setting it to 'contacts' or
+'registrars' causes an entity search to return only contacts or only registrars.
+
### Next page links
The number of results returned in a domain, nameserver or entity search is
diff --git a/java/google/registry/rdap/RdapEntitySearchAction.java b/java/google/registry/rdap/RdapEntitySearchAction.java
index bf250c4af..5666a6ae0 100644
--- a/java/google/registry/rdap/RdapEntitySearchAction.java
+++ b/java/google/registry/rdap/RdapEntitySearchAction.java
@@ -86,6 +86,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
@Inject Clock clock;
@Inject @Parameter("fn") Optional fnParam;
@Inject @Parameter("handle") Optional handleParam;
+ @Inject @Parameter("subtype") Optional subtypeParam;
@Inject RdapEntitySearchAction() {}
private enum QueryType {
@@ -93,6 +94,12 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
HANDLE
}
+ private enum Subtype {
+ ALL,
+ CONTACTS,
+ REGISTRARS
+ }
+
private enum CursorType {
NONE,
CONTACT,
@@ -132,6 +139,18 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
throw new BadRequestException("You must specify either fn=XXXX or handle=YYYY");
}
+ // Check the subtype.
+ Subtype subtype;
+ if (!subtypeParam.isPresent() || subtypeParam.get().equalsIgnoreCase("all")) {
+ subtype = Subtype.ALL;
+ } else if (subtypeParam.get().equalsIgnoreCase("contacts")) {
+ subtype = Subtype.CONTACTS;
+ } else if (subtypeParam.get().equalsIgnoreCase("registrars")) {
+ subtype = Subtype.REGISTRARS;
+ } else {
+ throw new BadRequestException("Subtype parameter must specify contacts, registrars or all");
+ }
+
// Decode the cursor token and extract the prefix and string portions.
decodeCursorToken();
CursorType cursorType;
@@ -164,6 +183,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
recordWildcardType(RdapSearchPattern.create(fnParam.get(), false)),
cursorType,
cursorQueryString,
+ subtype,
now);
// Search by handle.
@@ -175,6 +195,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
searchByHandle(
recordWildcardType(RdapSearchPattern.create(handleParam.get(), false)),
cursorQueryString,
+ subtype,
now);
}
@@ -221,6 +242,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
final RdapSearchPattern partialStringQuery,
CursorType cursorType,
Optional cursorQueryString,
+ Subtype subtype,
DateTime now) {
// For wildcard searches, make sure the initial string is long enough, and don't allow suffixes.
if (partialStringQuery.getHasWildcard() && (partialStringQuery.getSuffix() != null)) {
@@ -238,40 +260,50 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
: "Initial search string required when searching for deleted entities");
}
// Get the registrar matches. If we have a registrar cursor, weed out registrars up to and
- // including the one we ended with last time.
- ImmutableList registrars =
- Streams.stream(Registrar.loadAllCached())
- .filter(
- registrar ->
- partialStringQuery.matches(registrar.getRegistrarName())
- && ((cursorType != CursorType.REGISTRAR)
- || (registrar.getRegistrarName().compareTo(cursorQueryString.get())
- > 0))
- && shouldBeVisible(registrar))
- .limit(rdapResultSetMaxSize + 1)
- .collect(toImmutableList());
+ // including the one we ended with last time. We can skip registrars if subtype is CONTACTS.
+ ImmutableList registrars;
+ if (subtype == Subtype.CONTACTS) {
+ registrars = ImmutableList.of();
+ } else {
+ registrars =
+ Streams.stream(Registrar.loadAllCached())
+ .filter(
+ registrar ->
+ partialStringQuery.matches(registrar.getRegistrarName())
+ && ((cursorType != CursorType.REGISTRAR)
+ || (registrar.getRegistrarName().compareTo(cursorQueryString.get())
+ > 0))
+ && shouldBeVisible(registrar))
+ .limit(rdapResultSetMaxSize + 1)
+ .collect(toImmutableList());
+ }
// Get the contact matches and return the results, fetching an additional contact to detect
// truncation. Don't bother searching for contacts by name if the request would not be able to
// see any names anyway. Also, if a registrar cursor is present, we have already moved past the
- // contacts, and don't need to fetch them this time.
+ // contacts, and don't need to fetch them this time. We can skip contacts if subtype is
+ // REGISTRARS.
RdapResultSet resultSet;
- RdapAuthorization authorization = getAuthorization();
- if ((authorization.role() == RdapAuthorization.Role.PUBLIC)
- || (cursorType == CursorType.REGISTRAR)) {
+ if (subtype == Subtype.REGISTRARS) {
resultSet = RdapResultSet.create(ImmutableList.of());
} else {
- Query query =
- queryItems(
- ContactResource.class,
- "searchName",
- partialStringQuery,
- cursorQueryString, // if we get this far, and there's a cursor, it must be a contact
- DeletedItemHandling.EXCLUDE,
- rdapResultSetMaxSize + 1);
- if (authorization.role() != RdapAuthorization.Role.ADMINISTRATOR) {
- query = query.filter("currentSponsorClientId in", authorization.clientIds());
+ RdapAuthorization authorization = getAuthorization();
+ if ((authorization.role() == RdapAuthorization.Role.PUBLIC)
+ || (cursorType == CursorType.REGISTRAR)) {
+ resultSet = RdapResultSet.create(ImmutableList.of());
+ } else {
+ Query query =
+ queryItems(
+ ContactResource.class,
+ "searchName",
+ partialStringQuery,
+ cursorQueryString, // if we get this far, and there's a cursor, it must be a contact
+ DeletedItemHandling.EXCLUDE,
+ rdapResultSetMaxSize + 1);
+ if (authorization.role() != RdapAuthorization.Role.ADMINISTRATOR) {
+ query = query.filter("currentSponsorClientId in", authorization.clientIds());
+ }
+ resultSet = getMatchingResources(query, false, now, rdapResultSetMaxSize + 1);
}
- resultSet = getMatchingResources(query, false, now, rdapResultSetMaxSize + 1);
}
return makeSearchResults(resultSet, registrars, QueryType.FULL_NAME, now);
}
@@ -289,25 +321,39 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
private RdapSearchResults searchByHandle(
final RdapSearchPattern partialStringQuery,
Optional cursorQueryString,
+ Subtype subtype,
DateTime now) {
if (partialStringQuery.getSuffix() != null) {
throw new UnprocessableEntityException("Suffixes not allowed in entity handle searches");
}
// Handle queries without a wildcard (and not including deleted) -- load by ID.
if (!partialStringQuery.getHasWildcard() && !shouldIncludeDeleted()) {
- ContactResource contactResource = ofy().load()
- .type(ContactResource.class)
- .id(partialStringQuery.getInitialString())
- .now();
- ImmutableList contactResourceList =
- ((contactResource != null) && shouldBeVisible(contactResource, now))
- ? ImmutableList.of(contactResource)
- : ImmutableList.of();
+ ImmutableList contactResourceList;
+ if (subtype == Subtype.REGISTRARS) {
+ contactResourceList = ImmutableList.of();
+ } else {
+ ContactResource contactResource =
+ ofy()
+ .load()
+ .type(ContactResource.class)
+ .id(partialStringQuery.getInitialString())
+ .now();
+ contactResourceList =
+ ((contactResource != null) && shouldBeVisible(contactResource, now))
+ ? ImmutableList.of(contactResource)
+ : ImmutableList.of();
+ }
+ ImmutableList registrarList;
+ if (subtype == Subtype.CONTACTS) {
+ registrarList = ImmutableList.of();
+ } else {
+ registrarList = getMatchingRegistrars(partialStringQuery.getInitialString());
+ }
return makeSearchResults(
contactResourceList,
IncompletenessWarningType.COMPLETE,
contactResourceList.size(),
- getMatchingRegistrars(partialStringQuery.getInitialString()),
+ registrarList,
QueryType.HANDLE,
now);
// Handle queries with a wildcard (or including deleted), but no suffix. Because the handle
@@ -316,7 +362,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
// detect result set truncation.
} else {
ImmutableList registrars =
- partialStringQuery.getHasWildcard()
+ ((subtype == Subtype.CONTACTS) || partialStringQuery.getHasWildcard())
? ImmutableList.of()
: getMatchingRegistrars(partialStringQuery.getInitialString());
// Get the contact matches and return the results, fetching an additional contact to detect
@@ -324,15 +370,24 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
// get excluded due to permissioning. Any cursor present must be a contact cursor, because we
// would never return a registrar for this search.
int querySizeLimit = getStandardQuerySizeLimit();
- Query query =
- queryItemsByKey(
- ContactResource.class,
- partialStringQuery,
- cursorQueryString,
- getDeletedItemHandling(),
- querySizeLimit);
+ RdapResultSet contactResultSet;
+ if (subtype == Subtype.REGISTRARS) {
+ contactResultSet = RdapResultSet.create(ImmutableList.of());
+ } else {
+ contactResultSet =
+ getMatchingResources(
+ queryItemsByKey(
+ ContactResource.class,
+ partialStringQuery,
+ cursorQueryString,
+ getDeletedItemHandling(),
+ querySizeLimit),
+ shouldIncludeDeleted(),
+ now,
+ querySizeLimit);
+ }
return makeSearchResults(
- getMatchingResources(query, shouldIncludeDeleted(), now, querySizeLimit),
+ contactResultSet,
registrars,
QueryType.HANDLE,
now);
diff --git a/java/google/registry/rdap/RdapModule.java b/java/google/registry/rdap/RdapModule.java
index b51276974..4cfc1af8f 100644
--- a/java/google/registry/rdap/RdapModule.java
+++ b/java/google/registry/rdap/RdapModule.java
@@ -67,6 +67,12 @@ public final class RdapModule {
return RequestParameters.extractOptionalParameter(req, "registrar");
}
+ @Provides
+ @Parameter("subtype")
+ static Optional provideSubtype(HttpServletRequest req) {
+ return RequestParameters.extractOptionalParameter(req, "subtype");
+ }
+
@Provides
@Parameter("includeDeleted")
static Optional provideIncludeDeleted(HttpServletRequest req) {
diff --git a/javatests/google/registry/rdap/RdapEntitySearchActionTest.java b/javatests/google/registry/rdap/RdapEntitySearchActionTest.java
index 6f486eeb2..c76db2d83 100644
--- a/javatests/google/registry/rdap/RdapEntitySearchActionTest.java
+++ b/javatests/google/registry/rdap/RdapEntitySearchActionTest.java
@@ -26,6 +26,7 @@ import static google.registry.testing.DatastoreHelper.persistSimpleResources;
import static google.registry.testing.FullFieldsTestEntityHelper.makeAndPersistContactResource;
import static google.registry.testing.FullFieldsTestEntityHelper.makeAndPersistDeletedContactResource;
import static google.registry.testing.FullFieldsTestEntityHelper.makeContactResource;
+import static google.registry.testing.FullFieldsTestEntityHelper.makeHistoryEntry;
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrar;
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarContacts;
import static google.registry.testing.TestDataHelper.loadFile;
@@ -40,6 +41,7 @@ import google.registry.model.ImmutableObject;
import google.registry.model.contact.ContactResource;
import google.registry.model.ofy.Ofy;
import google.registry.model.registrar.Registrar;
+import google.registry.model.reporting.HistoryEntry;
import google.registry.rdap.RdapMetrics.EndpointType;
import google.registry.rdap.RdapMetrics.SearchType;
import google.registry.rdap.RdapSearchResults.IncompletenessWarningType;
@@ -192,6 +194,7 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
action.rdapWhoisServer = null;
action.fnParam = Optional.empty();
action.handleParam = Optional.empty();
+ action.subtypeParam = Optional.empty();
action.registrarParam = Optional.empty();
action.includeDeletedParam = Optional.empty();
action.formatOutputParam = Optional.empty();
@@ -281,16 +284,20 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
.asBuilder()
.setRepoId(String.format("%04d-ROID", i))
.build();
+ resourcesBuilder.add(makeHistoryEntry(
+ contact, HistoryEntry.Type.CONTACT_CREATE, null, "created", clock.nowUtc()));
resourcesBuilder.add(contact);
}
persistResources(resourcesBuilder.build());
for (int i = 1; i <= numRegistrars; i++) {
- resourcesBuilder.add(
+ Registrar registrar =
makeRegistrar(
String.format("registrar%d", i),
String.format("Entity %d", i + numContacts),
Registrar.State.ACTIVE,
- 300L + i));
+ 300L + i);
+ resourcesBuilder.add(registrar);
+ resourcesBuilder.addAll(makeRegistrarContacts(registrar));
}
persistResources(resourcesBuilder.build());
}
@@ -537,6 +544,19 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
verifyErrorMetrics(Optional.empty(), 422);
}
+ @Test
+ public void testInvalidSubtype_rejected() throws Exception {
+ action.subtypeParam = Optional.of("Space Aliens");
+ assertThat(generateActualJsonWithFullName("Blinky (赤ベイ)"))
+ .isEqualTo(
+ generateExpectedJson(
+ "Subtype parameter must specify contacts, registrars or all",
+ "rdap_error_400.json"));
+ assertThat(response.getStatus()).isEqualTo(400);
+ metricSearchType = SearchType.NONE; // Error occurs before search type is set.
+ verifyErrorMetrics(Optional.empty(), 400);
+ }
+
@Test
public void testNameMatchContact_found() throws Exception {
login("2-RegistrarTest");
@@ -544,6 +564,30 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
verifyMetrics(1);
}
+ @Test
+ public void testNameMatchContact_found_subtypeAll() throws Exception {
+ login("2-RegistrarTest");
+ action.subtypeParam = Optional.of("aLl");
+ runSuccessfulNameTestWithBlinky("Blinky (赤ベイ)", "rdap_contact.json");
+ verifyMetrics(1);
+ }
+
+ @Test
+ public void testNameMatchContact_found_subtypeContacts() throws Exception {
+ login("2-RegistrarTest");
+ action.subtypeParam = Optional.of("cONTACTS");
+ runSuccessfulNameTestWithBlinky("Blinky (赤ベイ)", "rdap_contact.json");
+ verifyMetrics(1);
+ }
+
+ @Test
+ public void testNameMatchContact_notFound_subtypeRegistrars() throws Exception {
+ login("2-RegistrarTest");
+ action.subtypeParam = Optional.of("Registrars");
+ runNotFoundNameTest("Blinky (赤ベイ)");
+ verifyErrorMetrics(0);
+ }
+
@Test
public void testNameMatchContact_found_specifyingSameRegistrar() throws Exception {
login("2-RegistrarTest");
@@ -653,6 +697,32 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase {
verifyMetrics(0);
}
+ @Test
+ public void testNameMatchRegistrar_found_subtypeAll() throws Exception {
+ login("2-RegistrarTest");
+ action.subtypeParam = Optional.of("all");
+ runSuccessfulNameTest(
+ "Yes Virginia