diff --git a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapElasticFullTextSearchService.java b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapElasticFullTextSearchService.java index 12a11bd52c..1f4e8f2b28 100644 --- a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapElasticFullTextSearchService.java +++ b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapElasticFullTextSearchService.java @@ -54,7 +54,8 @@ public RdapElasticFullTextSearchService(@Qualifier("jdbcRpslObjectSlaveDao") fin } @Override - public List performSearch(final String[] fields, final String term, final String clientIp, final Source source) throws IOException { + public List performSearch(final String[] fields, final String term, final String clientIp, + final Source source, final boolean matchExact) throws IOException { try { return new ElasticSearchAccountingCallback>(accessControlListManager, clientIp, null, source) { @@ -63,7 +64,7 @@ public List performSearch(final String[] fields, final String term, protected List doSearch() throws IOException { final SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); - sourceBuilder.query(getQueryBuilder(fields, term)); + sourceBuilder.query(getQueryBuilder(fields, term, matchExact)); sourceBuilder.size(maxResultSize); sourceBuilder.sort(SORT_BUILDERS); @@ -90,13 +91,30 @@ protected List doSearch() throws IOException { return results; } - private QueryBuilder getQueryBuilder(final String[] fields, final String term) { - if (term.indexOf('*') == -1 && term.indexOf('?') == -1) { - return new MultiMatchQueryBuilder(term, fields) - .type(MultiMatchQueryBuilder.Type.PHRASE_PREFIX) - .operator(Operator.AND); + private QueryBuilder getQueryBuilder(final String[] fields, final String term, final boolean matchExact) { + if (hasWildCard()) { + return createWildCardQuery(); } + return matchExact ? createExactMatchQuery() : + new MultiMatchQueryBuilder(term, fields) + .type(MultiMatchQueryBuilder.Type.PHRASE_PREFIX) + .operator(Operator.AND); + } + + private boolean hasWildCard(){ + return term.indexOf('*') != -1 || term.indexOf('?') != -1; + } + + private BoolQueryBuilder createExactMatchQuery(){ + final BoolQueryBuilder exactMatch = QueryBuilders.boolQuery(); + for (String field : fields) { + exactMatch.should(QueryBuilders.termQuery(String.format("%s.lowercase", field), term.toLowerCase())); + } + return exactMatch; + } + + private BoolQueryBuilder createWildCardQuery(){ final BoolQueryBuilder wildCardBuilder = QueryBuilders.boolQuery(); for (String field : fields) { wildCardBuilder.should(QueryBuilders.wildcardQuery(String.format("%s.lowercase", field), term.toLowerCase())); diff --git a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapFullTextSearch.java b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapFullTextSearch.java index 3a3f4ac3d7..4283db2c99 100644 --- a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapFullTextSearch.java +++ b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapFullTextSearch.java @@ -8,5 +8,6 @@ public interface RdapFullTextSearch { - List performSearch(final String[] fields, final String term, final String remoteAddr, final Source source) throws IOException; + List performSearch(final String[] fields, final String term, final String remoteAddr, + final Source source, final boolean matchExact) throws IOException; } diff --git a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapService.java b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapService.java index 4ecef0d38a..348e99cdb4 100644 --- a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapService.java +++ b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapService.java @@ -187,8 +187,12 @@ public Response searchIps( LOGGER.debug("Request: {}", RestServiceHelper.getRequestURI(request)); - if (name != null && handle == null || name == null && handle != null) { - return handleSearch(new String[]{"netname"}, name != null ? name : handle, request); + if (name != null && handle == null) { + return handleSearch(new String[]{"netname"}, name, request, true); + } + + if (name == null && handle != null) { + return handleSearch(new String[]{"inetnum", "inet6num"}, handle, request, true); } throw new RdapException("400 Bad Request", "Either name or handle is a required parameter, but never both", HttpStatus.BAD_REQUEST_400); @@ -205,11 +209,11 @@ public Response searchAutnums( LOGGER.debug("Request: {}", RestServiceHelper.getRequestURI(request)); if (name != null && handle == null) { - return handleSearch(new String[]{"as-name"}, name, request); + return handleSearch(new String[]{"as-name"}, name, request, true); } if (name == null && handle != null) { - return handleSearch(new String[]{"aut-num"}, handle, request); + return handleSearch(new String[]{"aut-num"}, handle, request, true); } throw new RdapException("400 Bad Request", "Either name or handle is a required parameter, but never both", HttpStatus.BAD_REQUEST_400); @@ -487,6 +491,11 @@ private String objectTypesToString(final Collection objectTypes) { } private Response handleSearch(final String[] fields, final String term, final HttpServletRequest request) { + return handleSearch(fields, term, request, false); + } + + private Response handleSearch(final String[] fields, final String term, final HttpServletRequest request, + final boolean matchExact) { LOGGER.debug("Search {} for {}", fields, term); if (StringUtils.isEmpty(term)) { @@ -494,11 +503,8 @@ private Response handleSearch(final String[] fields, final String term, final Ht } try { - final List objects = rdapFullTextSearch.performSearch(fields, term, request.getRemoteAddr(), source); - - if (objects.isEmpty()) { - throw new RdapException("404 Not Found", "Requested object not found: " + term, HttpStatus.NOT_FOUND_404); - } + final List objects = rdapFullTextSearch.performSearch(fields, term, request.getRemoteAddr(), + source, matchExact); return Response.ok(rdapObjectMapper.mapSearch( getRequestUrl(request), diff --git a/whois-api/src/test/java/net/ripe/db/whois/api/elasticsearch/RdapElasticServiceTestIntegration.java b/whois-api/src/test/java/net/ripe/db/whois/api/elasticsearch/RdapElasticServiceTestIntegration.java index 074dbfcede..18bceeab7d 100644 --- a/whois-api/src/test/java/net/ripe/db/whois/api/elasticsearch/RdapElasticServiceTestIntegration.java +++ b/whois-api/src/test/java/net/ripe/db/whois/api/elasticsearch/RdapElasticServiceTestIntegration.java @@ -48,9 +48,11 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; @@ -333,16 +335,13 @@ public void lookup_forward_domain() { // search - domain @Test - public void search_domain_not_found() { - final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { - rebuildIndex(); - createResource("domains?name=ripe.net") - .request(MediaType.APPLICATION_JSON_TYPE) - .get(Entity.class); - }); - assertErrorStatus(notFoundException, 404); - assertErrorTitle(notFoundException, "404 Not Found"); - assertErrorDescription(notFoundException, "Requested object not found: ripe.net"); + public void search_domain_then_empty() { + rebuildIndex(); + final SearchResult searchResult = createResource("domains?name=ripe.net") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(searchResult.getDomainSearchResults(), is(nullValue())); } @Test @@ -405,14 +404,12 @@ public void search_entity_person_object_deleted_before_index_updated() { rebuildIndex(); databaseHelper.deleteObject(person); - final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { - createResource("entities?fn=Lost%20Person") - .request(MediaType.APPLICATION_JSON_TYPE) - .get(SearchResult.class); - }); - assertErrorStatus(notFoundException, 404); - assertErrorTitle(notFoundException, "404 Not Found"); - assertErrorDescription(notFoundException, "Requested object not found: Lost Person"); + + final SearchResult searchResult = createResource("entities?fn=Lost%20Person") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(searchResult.getEntitySearchResults(), is(nullValue())); } @Test @@ -439,14 +436,11 @@ public void search_entity_person_umlaut_latin1_encoded() { databaseHelper.addObject("person: Tëst Person3\nnic-hdl: TP3-TEST\ncreated: 2022-08-14T11:48:28Z\nlast-modified: 2022-10-25T12:22:39Z\nsource: TEST"); rebuildIndex(); - final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { - createResource("entities?fn=T%EBst%20Person3") - .request(MediaType.APPLICATION_JSON_TYPE) - .get(SearchResult.class); - }); - assertErrorStatus(notFoundException, 404); - assertErrorTitle(notFoundException, "404 Not Found"); - assertErrorDescriptionContains(notFoundException, "st Person3"); + final SearchResult searchResult = createResource("entities?fn=T%EBst%20Person3") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(searchResult.getEntitySearchResults(), is(nullValue())); } @Test @@ -466,27 +460,22 @@ public void search_entity_person_umlaut_substitution() { databaseHelper.addObject("person: Tëst Person3\nnic-hdl: TP3-TEST\ncreated: 2022-08-14T11:48:28Z\nlast-modified: 2022-10-25T12:22:39Z\nsource: TEST"); rebuildIndex(); - final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { - createResource("entities?fn=Test%20Person3") - .request(MediaType.APPLICATION_JSON_TYPE) - .get(SearchResult.class); - }); - assertErrorStatus(notFoundException, 404); - assertErrorTitle(notFoundException, "404 Not Found"); - assertErrorDescription(notFoundException, "Requested object not found: Test Person3"); + + final SearchResult searchResult = createResource("entities?fn=Test%20Person3") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(searchResult.getEntitySearchResults(), is(nullValue())); } @Test public void search_entity_person_by_name_not_found() { - final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { - rebuildIndex(); - createResource("entities?fn=Santa%20Claus") - .request(MediaType.APPLICATION_JSON_TYPE) - .get(Entity.class); - }); - assertErrorStatus(notFoundException, 404); - assertErrorTitle(notFoundException, "404 Not Found"); - assertErrorDescription(notFoundException, "Requested object not found: Santa Claus"); + rebuildIndex(); + final SearchResult searchResult = createResource("entities?fn=Santa%20Claus") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(searchResult.getEntitySearchResults(), is(nullValue())); } @Test @@ -510,16 +499,13 @@ public void search_entity_person_by_handle_is_case_insensitive() { } @Test - public void search_entity_person_by_handle_not_found() { - final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { - createResource("entities?handle=XYZ-TEST") - .request(MediaType.APPLICATION_JSON_TYPE) - .get(Entity.class); - fail(); - }); - assertErrorStatus(notFoundException, 404); - assertErrorTitle(notFoundException, "404 Not Found"); - assertErrorDescription(notFoundException, "Requested object not found: XYZ-TEST"); + public void search_entity_person_by_handle_then_empty() { + + final SearchResult searchResult = createResource("entities?handle=XYZ-TEST") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(searchResult.getEntitySearchResults(), is(nullValue())); } // search - entities - role @@ -780,14 +766,42 @@ public void search_wildcard_is_case_insensitive() { @Test public void search_ips_inetnum_by_handle() { - final SearchResult response = createResource("ips?handle=IANA-BLK-IPV4") + final SearchResult response = createResource("ips?handle=0.0.0.0%20-%20255.255.255.255") .request(MediaType.APPLICATION_JSON_TYPE) .get(SearchResult.class); assertThat(response.getIpSearchResults().size(), is(1)); - assertThat(response.getIpSearchResults().get(0).getName(), equalTo("IANA-BLK-IPV4")); + assertThat(response.getIpSearchResults().getFirst().getHandle(), equalTo("0.0.0.0 - 255.255.255.255")); } + @Test + public void search_more_specific_inetnum_by_handle() { + databaseHelper.addObject(""" + inetnum: 192.12.12.0 - 192.12.12.255 + netname: RIPE-BLK-IPV4 + descr: The whole IPv4 address space + country: NL + tech-c: TP1-TEST + admin-c: TP1-TEST + status: OTHER + mnt-by: OWNER-MNT + created: 2022-08-14T11:48:28Z + last-modified: 2022-10-25T12:22:39Z + source: TEST + """); + + ipTreeUpdater.rebuild(); + rebuildIndex(); + + final SearchResult response = createResource("ips?handle=192.12.12.0%20-%20192.12.12.255") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(response.getIpSearchResults().size(), is(1)); + assertThat(response.getIpSearchResults().getFirst().getHandle(), equalTo("192.12.12.0 - 192.12.12.255")); + } + + @Test public void search_ips_inetnum_by_name() { final SearchResult response = createResource("ips?name=IANA-*-IPV4") @@ -795,17 +809,48 @@ public void search_ips_inetnum_by_name() { .get(SearchResult.class); assertThat(response.getIpSearchResults().size(), is(1)); - assertThat(response.getIpSearchResults().get(0).getName(), equalTo("IANA-BLK-IPV4")); + assertThat(response.getIpSearchResults().getFirst().getName(), equalTo("IANA-BLK-IPV4")); + } + + @Test + public void search_ips_inetnum_by_exact_name() { + final SearchResult response = createResource("ips?name=IANA-BLK-IPV4") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(response.getIpSearchResults().size(), is(1)); + assertThat(response.getIpSearchResults().getFirst().getName(), equalTo("IANA-BLK-IPV4")); + } + + @Test + public void search_ips_inetnum_by_exact_name_is_case_insensitive() { + final SearchResult uppercaseResponse = createResource("ips?name=IANA-BLK-IPV4") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final SearchResult lowercaseResponse = createResource("ips?name=iana-blk-ipv4") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final SearchResult mixedCaseResponse = createResource("ips?name=Iana-BLK-IPv4") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(uppercaseResponse.getIpSearchResults().getFirst().getHandle(), + is(lowercaseResponse.getIpSearchResults().getFirst().getHandle())); + + assertThat(lowercaseResponse.getIpSearchResults().getFirst().getHandle(), + is(mixedCaseResponse.getIpSearchResults().getFirst().getHandle())); } @Test public void search_ips_inet6num_by_handle() { - final SearchResult response = createResource("ips?handle=IANA-BLK-IPV6") + final SearchResult response = createResource("ips?handle=::/0") .request(MediaType.APPLICATION_JSON_TYPE) .get(SearchResult.class); assertThat(response.getIpSearchResults().size(), is(1)); - assertThat(response.getIpSearchResults().get(0).getName(), equalTo("IANA-BLK-IPV6")); + assertThat(response.getIpSearchResults().getFirst().getHandle(), equalTo("::/0")); } @Test @@ -814,7 +859,7 @@ public void search_ips_inet6num_by_name() { .request(MediaType.APPLICATION_JSON_TYPE) .get(SearchResult.class); - assertThat(response.getIpSearchResults().get(0).getName(), equalTo("IANA-BLK-IPV6")); + assertThat(response.getIpSearchResults().getFirst().getName(), equalTo("IANA-BLK-IPV6")); } @Test @@ -854,15 +899,57 @@ public void search_ips_with_both_parameters_then_error() { } @Test - public void search_non_existing_ip_then_error() { - final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { - createResource("ips?handle=NOT_FOUND") + public void search_non_existing_ip_then_empty() { + final SearchResult searchResult = createResource("ips?handle=NOT_FOUND") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(searchResult.getIpSearchResults(), is(nullValue())); + } + + @Test + public void search_non_full_existing_name_then_empty() { + + final SearchResult searchResult = createResource("ips?name=IANA-BLK") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(searchResult.getIpSearchResults(), is(nullValue())); + } + + @Test + public void search_non_full_existing_inetnum_then_empty() { + final SearchResult searchResult = createResource("ips?handle=0.0.0") .request(MediaType.APPLICATION_JSON_TYPE) .get(SearchResult.class); - }); - assertErrorStatus(notFoundException, HttpStatus.NOT_FOUND_404); - assertErrorTitle(notFoundException, "404 Not Found"); - assertErrorDescription(notFoundException, "Requested object not found: NOT_FOUND"); + + assertThat(searchResult.getIpSearchResults(), is(nullValue())); + } + + @Test + public void search_not_full_more_specific_inetnum_then_empty() { + databaseHelper.addObject(""" + inetnum: 192.12.12.0 - 192.12.12.255 + netname: RIPE-BLK-IPV4 + descr: The whole IPv4 address space + country: NL + tech-c: TP1-TEST + admin-c: TP1-TEST + status: OTHER + mnt-by: OWNER-MNT + created: 2022-08-14T11:48:28Z + last-modified: 2022-10-25T12:22:39Z + source: TEST + """); + + ipTreeUpdater.rebuild(); + rebuildIndex(); + + final SearchResult response = createResource("ips?handle=192.12.12.0%20-%20192.12.12") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(response.getIpSearchResults(), is(nullValue())); } @@ -875,7 +962,7 @@ public void search_autnums_by_name() { .get(SearchResult.class); assertThat(response.getAutnumSearchResults().size(), is(1)); - assertThat(response.getAutnumSearchResults().get(0).getName(), equalTo("AS-TEST")); + assertThat(response.getAutnumSearchResults().getFirst().getName(), equalTo("AS-TEST")); } @Test @@ -885,7 +972,7 @@ public void search_autnums_by_handle() { .get(SearchResult.class); assertThat(response.getAutnumSearchResults().size(), is(1)); - assertThat(response.getAutnumSearchResults().get(0).getHandle(), equalTo("AS102")); + assertThat(response.getAutnumSearchResults().getFirst().getHandle(), equalTo("AS102")); } @Test @@ -926,18 +1013,25 @@ public void search_autnums_with_both_parameters_then_error() { @Test - public void search_non_existing_autnum_then_error() { - final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { - createResource("autnums?handle=NOT_FOUND") - .request(MediaType.APPLICATION_JSON_TYPE) - .get(SearchResult.class); - }); - assertErrorStatus(notFoundException, HttpStatus.NOT_FOUND_404); - assertErrorTitle(notFoundException, "404 Not Found"); - assertErrorDescription(notFoundException, "Requested object not found: NOT_FOUND"); + public void search_non_existing_autnum_then_empty() { + final SearchResult searchResult = createResource("autnums?handle=NOT_FOUND") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(searchResult.getAutnumSearchResults(), is(nullValue())); } + @Test + public void search_non_full_autnums_by_handle() { + + final SearchResult searchResult = createResource("autnums?handle=AS10") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + assertThat(searchResult.getAutnumSearchResults(), is(nullValue())); + } + // Test redactions @Test