diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleDomainListAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleDomainListAction.java index 4ec4a2206..f6031f9fc 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleDomainListAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleDomainListAction.java @@ -19,6 +19,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory. import com.google.api.client.http.HttpStatusCodes; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Ascii; import com.google.gson.Gson; import com.google.gson.annotations.Expose; import google.registry.model.CreateAutoTimestamp; @@ -33,6 +34,7 @@ import google.registry.ui.server.registrar.JsonGetAction; import java.util.List; import java.util.Optional; import javax.inject.Inject; +import javax.persistence.TypedQuery; import org.joda.time.DateTime; /** Returns a (paginated) list of domains for a particular registrar. */ @@ -49,6 +51,8 @@ public class ConsoleDomainListAction implements JsonGetAction { private static final String DOMAIN_QUERY_TEMPLATE = "FROM Domain WHERE currentSponsorRegistrarId = :registrarId AND deletionTime >" + " :deletedAfterTime AND creationTime <= :createdBeforeTime"; + private static final String SEARCH_TERM_QUERY = " AND LOWER(domainName) LIKE :searchTerm"; + private static final String ORDER_BY_STATEMENT = " ORDER BY creationTime DESC"; private final AuthResult authResult; private final Response response; @@ -58,6 +62,7 @@ public class ConsoleDomainListAction implements JsonGetAction { private final int pageNumber; private final int resultsPerPage; private final Optional totalResults; + private final Optional searchTerm; @Inject public ConsoleDomainListAction( @@ -68,7 +73,8 @@ public class ConsoleDomainListAction implements JsonGetAction { @Parameter("checkpointTime") Optional checkpointTime, @Parameter("pageNumber") Optional pageNumber, @Parameter("resultsPerPage") Optional resultsPerPage, - @Parameter("totalResults") Optional totalResults) { + @Parameter("totalResults") Optional totalResults, + @Parameter("searchTerm") Optional searchTerm) { this.authResult = authResult; this.response = response; this.gson = gson; @@ -77,6 +83,7 @@ public class ConsoleDomainListAction implements JsonGetAction { this.pageNumber = pageNumber.orElse(0); this.resultsPerPage = resultsPerPage.orElse(DEFAULT_RESULTS_PER_PAGE); this.totalResults = totalResults; + this.searchTerm = searchTerm; } @Override @@ -110,13 +117,13 @@ public class ConsoleDomainListAction implements JsonGetAction { long actualTotalResults = totalResults.orElseGet( () -> - tm().query("SELECT COUNT(*) " + DOMAIN_QUERY_TEMPLATE, Long.class) + createCountQuery() .setParameter("registrarId", registrarId) .setParameter("createdBeforeTime", checkpointTimestamp) .setParameter("deletedAfterTime", checkpoint) .getSingleResult()); List domains = - tm().query(DOMAIN_QUERY_TEMPLATE + " ORDER BY creationTime DESC", Domain.class) + createDomainQuery() .setParameter("registrarId", registrarId) .setParameter("createdBeforeTime", checkpointTimestamp) .setParameter("deletedAfterTime", checkpoint) @@ -127,6 +134,26 @@ public class ConsoleDomainListAction implements JsonGetAction { response.setStatus(HttpStatusCodes.STATUS_CODE_OK); } + /** Creates the query to get the total number of matching domains, interpolating as necessary. */ + private TypedQuery createCountQuery() { + String queryString = "SELECT COUNT(*) " + DOMAIN_QUERY_TEMPLATE; + if (searchTerm.isPresent() && !searchTerm.get().isEmpty()) { + return tm().query(queryString + SEARCH_TERM_QUERY, Long.class) + .setParameter("searchTerm", String.format("%%%s%%", Ascii.toLowerCase(searchTerm.get()))); + } + return tm().query(queryString, Long.class); + } + + /** Creates the query to retrieve the matching domains themselves, interpolating as necessary. */ + private TypedQuery createDomainQuery() { + if (searchTerm.isPresent() && !searchTerm.get().isEmpty()) { + return tm().query( + DOMAIN_QUERY_TEMPLATE + SEARCH_TERM_QUERY + ORDER_BY_STATEMENT, Domain.class) + .setParameter("searchTerm", String.format("%%%s%%", Ascii.toLowerCase(searchTerm.get()))); + } + return tm().query(DOMAIN_QUERY_TEMPLATE + ORDER_BY_STATEMENT, Domain.class); + } + private void writeBadRequest(String message) { response.setPayload(message); response.setStatus(HttpStatusCodes.STATUS_CODE_BAD_REQUEST); diff --git a/core/src/main/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java b/core/src/main/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java index 05852f1d2..f4215224b 100644 --- a/core/src/main/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java +++ b/core/src/main/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java @@ -226,4 +226,10 @@ public final class RegistrarConsoleModule { public static Optional provideTotalResults(HttpServletRequest req) { return extractOptionalParameter(req, "totalResults").map(Long::valueOf); } + + @Provides + @Parameter("searchTerm") + public static Optional provideSearchTerm(HttpServletRequest req) { + return extractOptionalParameter(req, "searchTerm"); + } } diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainListActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainListActionTest.java index 3cfa9d845..b6c6297b2 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainListActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainListActionTest.java @@ -21,6 +21,7 @@ import static google.registry.testing.DatabaseHelper.persistActiveDomain; import static google.registry.testing.DatabaseHelper.persistDomainAsDeleted; import com.google.api.client.http.HttpStatusCodes; +import com.google.common.collect.Iterables; import com.google.gson.Gson; import google.registry.model.EppResourceUtils; import google.registry.model.console.GlobalRole; @@ -90,7 +91,7 @@ public class ConsoleDomainListActionTest { @Test void testSuccess_pages() { // Two pages of results should go in reverse chronological order - ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null); + ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null, null); action.run(); DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains.stream().map(Domain::getDomainName).collect(toImmutableList())) @@ -98,7 +99,7 @@ public class ConsoleDomainListActionTest { assertThat(result.totalResults).isEqualTo(10); // Now do the second page - action = createAction("TheRegistrar", result.checkpointTime, 1, 5, 10L); + action = createAction("TheRegistrar", result.checkpointTime, 1, 5, 10L, null); action.run(); result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains.stream().map(Domain::getDomainName).collect(toImmutableList())) @@ -107,7 +108,7 @@ public class ConsoleDomainListActionTest { @Test void testSuccess_partialPage() { - ConsoleDomainListAction action = createAction("TheRegistrar", null, 1, 8, null); + ConsoleDomainListAction action = createAction("TheRegistrar", null, 1, 8, null, null); action.run(); DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains.stream().map(Domain::getDomainName).collect(toImmutableList())) @@ -116,7 +117,7 @@ public class ConsoleDomainListActionTest { @Test void testSuccess_checkpointTime_createdBefore() { - ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 10, null); + ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 10, null, null); action.run(); DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); @@ -127,7 +128,7 @@ public class ConsoleDomainListActionTest { persistActiveDomain("newdomain.tld", clock.nowUtc()); // Even though we persisted a new domain, the old checkpoint should return no more results - action = createAction("TheRegistrar", result.checkpointTime, 1, 10, null); + action = createAction("TheRegistrar", result.checkpointTime, 1, 10, null, null); action.run(); result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains).isEmpty(); @@ -136,7 +137,7 @@ public class ConsoleDomainListActionTest { @Test void testSuccess_checkpointTime_deletion() { - ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null); + ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null, null); action.run(); DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); @@ -146,16 +147,50 @@ public class ConsoleDomainListActionTest { persistDomainAsDeleted(toDelete, clock.nowUtc()); // Second page should include the domain that is now deleted due to the checkpoint time - action = createAction("TheRegistrar", result.checkpointTime, 1, 5, null); + action = createAction("TheRegistrar", result.checkpointTime, 1, 5, null, null); action.run(); result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains.stream().map(Domain::getDomainName).collect(toImmutableList())) .containsExactly("4exists.tld", "3exists.tld", "2exists.tld", "1exists.tld", "0exists.tld"); } + @Test + void testSuccess_searchTerm_oneMatch() { + ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null, "0"); + action.run(); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); + assertThat(Iterables.getOnlyElement(result.domains).getDomainName()).isEqualTo("0exists.tld"); + } + + @Test + void testSuccess_searchTerm_returnsNone() { + ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null, "deleted"); + action.run(); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); + assertThat(result.domains).isEmpty(); + } + + @Test + void testSuccess_searchTerm_caseInsensitive() { + ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null, "eXiStS"); + action.run(); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); + assertThat(result.domains).hasSize(5); + assertThat(result.totalResults).isEqualTo(10); + } + + @Test + void testSuccess_searchTerm_tld() { + ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 5, null, "tld"); + action.run(); + DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); + assertThat(result.domains).hasSize(5); + assertThat(result.totalResults).isEqualTo(10); + } + @Test void testPartialSuccess_pastEnd() { - ConsoleDomainListAction action = createAction("TheRegistrar", null, 5, 5, null); + ConsoleDomainListAction action = createAction("TheRegistrar", null, 5, 5, null, null); action.run(); DomainListResult result = GSON.fromJson(response.getPayload(), DomainListResult.class); assertThat(result.domains).isEmpty(); @@ -163,13 +198,13 @@ public class ConsoleDomainListActionTest { @Test void testFailure_invalidResultsPerPage() { - ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 0, null); + ConsoleDomainListAction action = createAction("TheRegistrar", null, 0, 0, null, null); action.run(); assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST); assertThat(response.getPayload()) .isEqualTo("Results per page must be between 1 and 500 inclusive"); - action = createAction("TheRegistrar", null, 0, 501, null); + action = createAction("TheRegistrar", null, 0, 501, null, null); action.run(); assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST); assertThat(response.getPayload()) @@ -178,14 +213,14 @@ public class ConsoleDomainListActionTest { @Test void testFailure_invalidPageNumber() { - ConsoleDomainListAction action = createAction("TheRegistrar", null, -1, 10, null); + ConsoleDomainListAction action = createAction("TheRegistrar", null, -1, 10, null, null); action.run(); assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_BAD_REQUEST); assertThat(response.getPayload()).isEqualTo("Page number must be non-negative"); } private ConsoleDomainListAction createAction(String registrarId) { - return createAction(registrarId, null, null, null, null); + return createAction(registrarId, null, null, null, null, null); } private ConsoleDomainListAction createAction( @@ -193,7 +228,8 @@ public class ConsoleDomainListActionTest { @Nullable DateTime checkpointTime, @Nullable Integer pageNumber, @Nullable Integer resultsPerPage, - @Nullable Long totalResults) { + @Nullable Long totalResults, + @Nullable String searchTerm) { response = new FakeResponse(); AuthResult authResult = AuthResult.createUser( @@ -210,6 +246,7 @@ public class ConsoleDomainListActionTest { Optional.ofNullable(checkpointTime), Optional.ofNullable(pageNumber), Optional.ofNullable(resultsPerPage), - Optional.ofNullable(totalResults)); + Optional.ofNullable(totalResults), + Optional.ofNullable(searchTerm)); } }