diff --git a/README.RDAP.md b/README.RDAP.md index 11c9f81844..183832e100 100644 --- a/README.RDAP.md +++ b/README.RDAP.md @@ -116,3 +116,8 @@ then a 404 is returned. An object with "administrative" status is never returned Currently, IANA allocations are not present in the RIPE database, but just out-of-region placeholders. Refer to [NRO RDAP](https://bitbucket.org/nroecg/nro-rdap-profile/raw/v1/nro-rdap-profile.txt) Profile section 4.5. "Status" + +Relation Searches doesn't consider the "status" query parameter +----------------------------------------------------------------- +This is related to the previous point. An object with "administrative" status is never returned. +Therefore, only "active" (non-administrative) objects are taking into account when using relation searches. diff --git a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapRelationService.java b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapRelationService.java new file mode 100644 index 0000000000..ac8d42aebe --- /dev/null +++ b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapRelationService.java @@ -0,0 +1,202 @@ +package net.ripe.db.whois.api.rdap; + +import com.google.common.collect.Sets; +import net.ripe.db.whois.api.rdap.domain.RelationType; +import net.ripe.db.whois.common.dao.RpslObjectDao; +import net.ripe.db.whois.common.domain.CIString; +import net.ripe.db.whois.common.ip.IpInterval; +import net.ripe.db.whois.common.ip.Ipv4Resource; +import net.ripe.db.whois.common.ip.Ipv6Resource; +import net.ripe.db.whois.common.iptree.IpEntry; +import net.ripe.db.whois.common.iptree.IpTree; +import net.ripe.db.whois.common.iptree.Ipv4DomainTree; +import net.ripe.db.whois.common.iptree.Ipv4Tree; +import net.ripe.db.whois.common.iptree.Ipv6DomainTree; +import net.ripe.db.whois.common.iptree.Ipv6Tree; +import net.ripe.db.whois.common.rpsl.AttributeType; +import net.ripe.db.whois.common.rpsl.ObjectType; +import net.ripe.db.whois.common.rpsl.RpslObject; +import net.ripe.db.whois.common.rpsl.attrs.Domain; +import net.ripe.db.whois.common.rpsl.attrs.Inet6numStatus; +import net.ripe.db.whois.common.rpsl.attrs.InetnumStatus; +import org.eclipse.jetty.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static net.ripe.db.whois.common.rpsl.attrs.Inet6numStatus.ALLOCATED_BY_RIR; +import static net.ripe.db.whois.common.rpsl.attrs.InetnumStatus.ALLOCATED_UNSPECIFIED; + + +@Service +public class RdapRelationService { + + private static final Logger LOGGER = LoggerFactory.getLogger(RdapRelationService.class); + + private final Ipv4Tree ip4Tree; + private final Ipv6Tree ip6Tree; + private final Ipv4DomainTree ipv4DomainTree; + private final Ipv6DomainTree ipv6DomainTree; + private final RpslObjectDao rpslObjectDao; + + public RdapRelationService(final Ipv4Tree ip4Tree, final Ipv6Tree ip6Tree, + final Ipv4DomainTree ipv4DomainTree, final Ipv6DomainTree ipv6DomainTree, + final RpslObjectDao rpslObjectDao) { + this.ip4Tree = ip4Tree; + this.ip6Tree = ip6Tree; + this.ipv4DomainTree = ipv4DomainTree; + this.ipv6DomainTree = ipv6DomainTree; + this.rpslObjectDao = rpslObjectDao; + } + + public List getDomainRelationPkeys(final String pkey, final RelationType relationType){ + final Domain domain = Domain.parse(pkey); + final IpInterval reverseIp = domain.getReverseIp(); + final List ipEntries = getIpEntries(getIpDomainTree(reverseIp), relationType, reverseIp); + + return ipEntries + .stream() + .map(ipEntry -> rpslObjectDao.getById(ipEntry.getObjectId()).getKey().toString()) + .toList(); + } + + public List getInetnumRelationPkeys(final String pkey, final RelationType relationType){ + final IpInterval ip = IpInterval.parse(pkey); + final List ipEntries = getIpEntries(getIpTree(ip), relationType, ip); + return ipEntries + .stream() + .map(ipEntry -> ipEntry.getKey().toString()) + .toList(); + } + + private List getIpEntries(final IpTree ipTree, final RelationType relationType, + final IpInterval reverseIp) { + return switch (relationType) { + case UP -> List.of(searchFirstLessSpecific(ipTree, reverseIp)); + case TOP -> searchTopLevel(ipTree, reverseIp); + case DOWN -> ipTree.findFirstMoreSpecific(reverseIp); + case BOTTOM -> searchMostSpecificFillingOverlaps(ipTree, reverseIp); + }; + } + + private List searchMostSpecificFillingOverlaps(final IpTree ipTree, final IpInterval reverseIp){ + final List mostSpecificValues = ipTree.findMostSpecific(reverseIp); + final List> ipResources = mostSpecificValues.stream().map(ip -> IpInterval.parse(ip.getKey().toString())).toList(); + final Set mostSpecificFillingOverlaps = Sets.newConcurrentHashSet(); + ipResources.forEach(ipResource -> processChildren(ipTree, reverseIp, ipResource, mostSpecificFillingOverlaps)); + return mostSpecificFillingOverlaps.stream().toList(); + } + + private static void processChildren(final IpTree ipTree, final IpInterval reverseIp, + final IpInterval mostSpecificResource, + final Set mostSpecificFillingOverlaps) { + + final List siblingsAndExact = findSiblingsAndExact(ipTree, mostSpecificResource); + + final IpEntry firstResourceIpEntry = siblingsAndExact.stream() + .filter(sibling -> sibling.getKey().toString().equals(mostSpecificResource.toString())) + .findFirst().orElse(null); + + mostSpecificFillingOverlaps.add(firstResourceIpEntry); + + final IpInterval firstSibling = (IpInterval)siblingsAndExact.getFirst().getKey(); + final IpInterval lastSibling = (IpInterval)siblingsAndExact.getLast().getKey(); + + final IpEntry parent = (IpEntry) ipTree.findFirstLessSpecific(IpInterval.parse(mostSpecificResource.toString())).getFirst(); + final IpInterval parentInterval = (IpInterval) parent.getKey(); + + if (!parentInterval.equals(reverseIp) && + !childrenCoverParentRange(firstSibling, lastSibling, parentInterval)){ + processChildren(ipTree, reverseIp, parentInterval, mostSpecificFillingOverlaps); + } + } + + private static boolean childrenCoverParentRange(final IpInterval firstResource, final IpInterval lastResource, final IpInterval parent){ + if (firstResource instanceof Ipv4Resource ipv4Resource){ + final Ipv4Resource lastIpv4Resource = (Ipv4Resource)lastResource; + final Ipv4Resource parentIpv4Resource = (Ipv4Resource)parent; + return ipv4Resource.begin() == parentIpv4Resource.begin() && lastIpv4Resource.end() == parentIpv4Resource.end(); + } + + final Ipv6Resource ipv6Resource = (Ipv6Resource)firstResource; + final Ipv6Resource lastIpv6Resource = (Ipv6Resource)lastResource; + final Ipv6Resource parentIpv6Resource = (Ipv6Resource)parent; + return Objects.equals(ipv6Resource.begin(), parentIpv6Resource.begin()) && Objects.equals(lastIpv6Resource.end(), parentIpv6Resource.end()); + } + + private static List findSiblingsAndExact(final IpTree ipTree, final IpInterval parentResource) { + final List parentList = ipTree.findFirstLessSpecific(parentResource); + if (parentList.isEmpty()){ + return ipTree.findExact(parentResource); + } + return ipTree.findFirstMoreSpecific(IpInterval.parse(parentList.getFirst().getKey().toString())); + } + + private IpEntry searchFirstLessSpecific(final IpTree ipTree, final IpInterval reverseIp){ + final List parentList = ipTree.findFirstLessSpecific(reverseIp); + if (parentList.isEmpty() || !resourceExist(parentList.getFirst())){ + throw new RdapException("404 Not Found", "No up level object has been found for " + reverseIp.toString(), HttpStatus.NOT_FOUND_404); + } + return parentList.getFirst(); + } + + private List searchTopLevel(final IpTree ipTree, final IpInterval reverseIp) { + for (final Object parentEntry : ipTree.findAllLessSpecific(reverseIp)) { + final IpEntry ipEntry = (IpEntry) parentEntry; + if (resourceExist(ipEntry)){ + return List.of(ipEntry); + } + } + throw new RdapException("404 Not Found", "No top level object has been found for " + reverseIp.toString(), HttpStatus.NOT_FOUND_404); + } + + private boolean resourceExist(final IpEntry firstLessSpecific){ + final RpslObject rpslObject = getResourceByKey(firstLessSpecific.getKey().toString()); + if (rpslObject == null || isAdministrativeResource(rpslObject)) { + LOGGER.error("INET(6)NUM {} does not exist in RIPE Database ", firstLessSpecific.getKey().toString()); + return false; + } + return true; + } + + private static boolean isAdministrativeResource(final RpslObject rpslObject) { + final CIString statusAttributeValue = rpslObject.getValueForAttribute(AttributeType.STATUS); + return (rpslObject.getType().equals(ObjectType.INETNUM) && InetnumStatus.getStatusFor(statusAttributeValue).equals(ALLOCATED_UNSPECIFIED)) + || (rpslObject.getType().equals(ObjectType.INET6NUM) && Inet6numStatus.getStatusFor(statusAttributeValue).equals(ALLOCATED_BY_RIR)); + } + + + @Nullable + private RpslObject getResourceByKey(final String key){ + final RpslObject rpslObject = rpslObjectDao.getByKeyOrNull(ObjectType.INET6NUM, key); + if (rpslObject == null){ + return rpslObjectDao.getByKeyOrNull(ObjectType.INETNUM, key); + } + return rpslObject; + } + + private IpTree getIpTree(final IpInterval reverseIp) { + if (reverseIp instanceof Ipv4Resource) { + return ip4Tree; + } else if (reverseIp instanceof Ipv6Resource) { + return ip6Tree; + } + + throw new IllegalArgumentException("Unexpected reverse ip: " + reverseIp); + } + + private IpTree getIpDomainTree(final IpInterval reverseIp) { + if (reverseIp instanceof Ipv4Resource) { + return ipv4DomainTree; + } else if (reverseIp instanceof Ipv6Resource) { + return ipv6DomainTree; + } + + throw new IllegalArgumentException("Unexpected reverse ip: " + reverseIp); + } +} diff --git a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapRelationTypeConverter.java b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapRelationTypeConverter.java new file mode 100644 index 0000000000..521c24954d --- /dev/null +++ b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapRelationTypeConverter.java @@ -0,0 +1,34 @@ +package net.ripe.db.whois.api.rdap; + +import jakarta.ws.rs.ext.ParamConverter; +import jakarta.ws.rs.ext.ParamConverterProvider; +import jakarta.ws.rs.ext.Provider; +import net.ripe.db.whois.api.rdap.domain.RelationType; +import org.springframework.stereotype.Component; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +@Component +@Provider +public class RdapRelationTypeConverter implements ParamConverterProvider { + + @Override + public ParamConverter getConverter(Class rawType, Type type, Annotation[] annotations) { + if(!rawType.equals(RelationType.class)) { + return null; + } + + return (ParamConverter) new ParamConverter() { + @Override + public RelationType fromString(String relationType){ + return RelationType.valueOf(relationType.toUpperCase()); + } + + @Override + public String toString(RelationType relationType){ + return relationType.toString(); + } + }; + } +} 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..5a99b45090 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 @@ -5,6 +5,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; @@ -15,6 +16,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import net.ripe.db.whois.api.rdap.domain.RdapRequestType; +import net.ripe.db.whois.api.rdap.domain.RelationType; import net.ripe.db.whois.api.rest.RestServiceHelper; import net.ripe.db.whois.common.dao.RpslObjectInfo; import net.ripe.db.whois.common.dao.RpslObjectUpdateDao; @@ -73,6 +75,7 @@ public class RdapService { private final RdapRequestValidator rdapRequestValidator; private final RpslObjectUpdateDao rpslObjectUpdateDao; private final SourceContext sourceContext; + private final RdapRelationService rdapRelationService; /** * @@ -99,7 +102,8 @@ public RdapService(final RdapQueryHandler rdapQueryHandler, @Value("${rdap.public.baseUrl:}") final String baseUrl, final RdapRequestValidator rdapRequestValidator, @Value("${rdap.search.max.results:100}") final int maxResultSize, - @Value("${rdap.entity.max.results:100}") final int maxEntityResultSize) { + @Value("${rdap.entity.max.results:100}") final int maxEntityResultSize, + final RdapRelationService rdapRelationService) { this.sourceContext = sourceContext; this.rdapQueryHandler = rdapQueryHandler; this.abuseCFinder = abuseCFinder; @@ -112,6 +116,7 @@ public RdapService(final RdapQueryHandler rdapQueryHandler, this.maxResultSize = maxResultSize; this.maxEntityResultSize = maxEntityResultSize; this.rpslObjectUpdateDao = rpslObjectUpdateDao; + this.rdapRelationService = rdapRelationService; } @GET @@ -245,6 +250,38 @@ public Response help(@Context final HttpServletRequest request) { .build(); } + @GET + @Produces({MediaType.APPLICATION_JSON, CONTENT_TYPE_RDAP_JSON}) + @Path("/{objectType}/rirSearch1/{relation}/{key:.*}") + public Response relationSearch( + @Context final HttpServletRequest request, + @PathParam("objectType") RdapRequestType requestType, + @PathParam("relation") RelationType relationType, + @PathParam("key") final String key, + @QueryParam("status") String status) { + + //TODO: [MH] Status is being ignored until administrative resources are included in RDAP. If status is not + // given or status is inactive...include administrative resources in the output. However, if status is active + // return just non administrative resources, as we are doing now. + if (status != null && (relationType.equals(RelationType.DOWN) || relationType.equals(RelationType.BOTTOM))){ + throw new RdapException("501 Not Implemented", "Status is not implement in down and bottom relation", HttpStatus.NOT_IMPLEMENTED_501); + } + + final Set objectTypes = requestType.getWhoisObjectTypes(key); + if (isRedirect(Iterables.getOnlyElement(objectTypes), key)) { + return redirect(getRequestPath(request), getQueryObject(objectTypes, key)); + } + + final List rpslObjects = handleRelationQuery(request, requestType, relationType, key); + + return Response.ok(rdapObjectMapper.mapSearch( + getRequestUrl(request), + rpslObjects, + maxResultSize)) + .header(CONTENT_TYPE, CONTENT_TYPE_RDAP_JSON) + .build(); + } + private Response lookupWithRedirectUrl(final HttpServletRequest request, final Set objectTypes, final String key) { if (isRedirect(Iterables.getOnlyElement(objectTypes), key)) { return redirect(getRequestPath(request), getQueryObject(objectTypes, key)); @@ -300,7 +337,10 @@ protected Response lookupForDomain(final HttpServletRequest request, final Strin final Stream inetnumResult = rdapQueryHandler.handleQueryStream(getQueryObject(ImmutableSet.of(INETNUM, INET6NUM), domain.getReverseIp().toString()), request); - return getDomainResponse(request, domainResult, inetnumResult); + return Response.ok( + getDomainEntity(request, domainResult, inetnumResult)) + .header(CONTENT_TYPE, CONTENT_TYPE_RDAP_JSON) + .build(); } protected Response lookupObject(final HttpServletRequest request, final Set objectTypes, final String key) { @@ -374,7 +414,8 @@ private Response getOrganisationResponse(final HttpServletRequest request, .build(); } - private Response getDomainResponse(final HttpServletRequest request, final Stream domainResult, final Stream inetnumResult) { + private Object getDomainEntity(final HttpServletRequest request, final Stream domainResult, + final Stream inetnumResult) { final Iterator domainIterator = domainResult.iterator(); final Iterator inetnumIterator = inetnumResult.iterator(); if (!domainIterator.hasNext()) { @@ -387,10 +428,7 @@ private Response getDomainResponse(final HttpServletRequest request, final Strea throw new RdapException("500 Internal Error", "Unexpected result size: " + Iterators.size(domainIterator), HttpStatus.INTERNAL_SERVER_ERROR_500); } - return Response.ok( - rdapObjectMapper.mapDomainEntity(getRequestUrl(request), domainObject, inetnumObject)) - .header(CONTENT_TYPE, CONTENT_TYPE_RDAP_JSON) - .build(); + return rdapObjectMapper.mapDomainEntity(getRequestUrl(request), domainObject, inetnumObject); } private Response getResponse(final HttpServletRequest request, final Iterable result) { @@ -486,6 +524,34 @@ private String objectTypesToString(final Collection objectTypes) { return COMMA_JOINER.join(objectTypes.stream().map(ObjectType::getName).toList()); } + private List handleRelationQuery(final HttpServletRequest request, final RdapRequestType requestType, + final RelationType relationType, final String key) { + final List rpslObjects; + switch (requestType) { + case AUTNUMS -> throw new RdapException("400 Bad Request", "Relation queries not allowed for autnum", HttpStatus.BAD_REQUEST_400); + case DOMAINS -> { + rdapRequestValidator.validateDomain(key); + final List relatedPkeys = rdapRelationService.getDomainRelationPkeys(key, relationType); + + rpslObjects = relatedPkeys + .stream() + .flatMap(relatedPkey -> rdapQueryHandler.handleQueryStream(getQueryObject(ImmutableSet.of(DOMAIN), relatedPkey), request)) + .toList(); + } + case IPS -> { + rdapRequestValidator.validateIp(request.getRequestURI(), key); + final List relatedPkeys = rdapRelationService.getInetnumRelationPkeys(key, relationType); + + rpslObjects = relatedPkeys + .stream() + .flatMap(relatedPkey -> rdapQueryHandler.handleQueryStream(getQueryObject(ImmutableSet.of(INETNUM, INET6NUM), relatedPkey), request)) + .toList(); + } + default -> throw new RdapException("400 Bad Request", "Invalid or unknown type " + requestType.toString().toLowerCase(), HttpStatus.BAD_REQUEST_400); + } + return rpslObjects; + } + private Response handleSearch(final String[] fields, final String term, final HttpServletRequest request) { LOGGER.debug("Search {} for {}", fields, term); diff --git a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapServletDeployer.java b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapServletDeployer.java index 5d47b9185c..5922373bad 100644 --- a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapServletDeployer.java +++ b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/RdapServletDeployer.java @@ -20,12 +20,17 @@ public class RdapServletDeployer implements ServletDeployer { private final RdapService rdapService; private final RdapExceptionMapper rdapExceptionMapper; private final RdapRequestTypeConverter rdapRequestTypeConverter; + private final RdapRelationTypeConverter rdapRelationTypeConverter; @Autowired - public RdapServletDeployer(final RdapService rdapService, final RdapExceptionMapper rdapExceptionMapper, final RdapRequestTypeConverter rdapRequestTypeConverter) { + public RdapServletDeployer(final RdapService rdapService, + final RdapExceptionMapper rdapExceptionMapper, + final RdapRequestTypeConverter rdapRequestTypeConverter, + final RdapRelationTypeConverter rdapRelationTypeConverter) { this.rdapService = rdapService; this.rdapExceptionMapper = rdapExceptionMapper; this.rdapRequestTypeConverter = rdapRequestTypeConverter; + this.rdapRelationTypeConverter = rdapRelationTypeConverter; } @Override @@ -43,6 +48,7 @@ public void deploy(final WebAppContext context) { resourceConfig.register(rdapService); resourceConfig.register(rdapRequestTypeConverter); resourceConfig.register(rdapExceptionMapper); + resourceConfig.register(rdapRelationTypeConverter); resourceConfig.register(rdapJsonProvider); context.addServlet(new ServletHolder("Whois RDAP REST API", new ServletContainer(resourceConfig)), "/rdap/*"); } diff --git a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/domain/RdapRequestType.java b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/domain/RdapRequestType.java index 36e54bcb0f..7ea08972a6 100644 --- a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/domain/RdapRequestType.java +++ b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/domain/RdapRequestType.java @@ -23,18 +23,36 @@ public Set getWhoisObjectTypes(final String key) { } }, + AUTNUMS { + public Set getWhoisObjectTypes(final String key) { + return ImmutableSet.of(AUT_NUM); + } + }, + DOMAIN { public Set getWhoisObjectTypes(final String key) { return ImmutableSet.of(ObjectType.DOMAIN); } }, + DOMAINS { + public Set getWhoisObjectTypes(final String key) { + return ImmutableSet.of(ObjectType.DOMAIN); + } + }, + IP { public Set getWhoisObjectTypes(final String key) { return key.contains(":") ? ImmutableSet.of(INET6NUM) : ImmutableSet.of(INETNUM); } }, + IPS { + public Set getWhoisObjectTypes(final String key) { + return key.contains(":") ? ImmutableSet.of(INET6NUM) : ImmutableSet.of(INETNUM); + } + }, + ENTITY { public Set getWhoisObjectTypes(final String key) { return ImmutableSet.of(PERSON, ROLE, MNTNER); diff --git a/whois-api/src/main/java/net/ripe/db/whois/api/rdap/domain/RelationType.java b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/domain/RelationType.java new file mode 100644 index 0000000000..6dfb8a5127 --- /dev/null +++ b/whois-api/src/main/java/net/ripe/db/whois/api/rdap/domain/RelationType.java @@ -0,0 +1,18 @@ +package net.ripe.db.whois.api.rdap.domain; + +public enum RelationType { + UP("up"), + TOP("top"), + DOWN("down"), + BOTTOM("bottom"); + + private final String value; + + RelationType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/whois-api/src/test/java/net/ripe/db/whois/api/rdap/RdapServiceTestIntegration.java b/whois-api/src/test/java/net/ripe/db/whois/api/rdap/RdapServiceTestIntegration.java index f80966fd39..349d691f03 100644 --- a/whois-api/src/test/java/net/ripe/db/whois/api/rdap/RdapServiceTestIntegration.java +++ b/whois-api/src/test/java/net/ripe/db/whois/api/rdap/RdapServiceTestIntegration.java @@ -24,6 +24,7 @@ import net.ripe.db.whois.api.rdap.domain.Redaction; import net.ripe.db.whois.api.rdap.domain.Remark; import net.ripe.db.whois.api.rdap.domain.Role; +import net.ripe.db.whois.api.rdap.domain.SearchResult; import net.ripe.db.whois.common.rpsl.AttributeType; import net.ripe.db.whois.common.rpsl.RpslObject; import net.ripe.db.whois.query.support.TestWhoisLog; @@ -172,7 +173,7 @@ public void setup() { "country: NL\n" + "tech-c: TP1-TEST\n" + "admin-c: TP1-TEST\n" + - "status: OTHER\n" + + "status: ALLOCATED UNSPECIFIED\n" + "mnt-by: OWNER-MNT\n" + "created: 2022-08-14T11:48:28Z\n" + "last-modified: 2022-10-25T12:22:39Z\n" + @@ -184,7 +185,7 @@ public void setup() { "country: NL\n" + "tech-c: TP1-TEST\n" + "admin-c: TP1-TEST\n" + - "status: OTHER\n" + + "status: ALLOCATED-BY-RIR\n" + "mnt-by: OWNER-MNT\n" + "created: 2022-08-14T11:48:28Z\n" + "last-modified: 2022-10-25T12:22:39Z\n" + @@ -3127,6 +3128,422 @@ public void get_help_response() { assertCopyrightLink(help.getLinks(), "https://rdap.db.ripe.net/help"); } + /*RIR Search*/ + + @Test + public void get_invalid_relation_autnum_then_400(){ + final BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> { + createResource("autnums/rirSearch1/upper/AS123") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + }); + + assertErrorTitle(badRequestException, "400 Bad Request"); + assertErrorStatus(badRequestException, HttpStatus.BAD_REQUEST_400); + assertErrorDescription(badRequestException, "unknown relation"); + } + + + //up + @Test + public void get_up_autnum_then_400(){ + final BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> { + createResource("autnums/rirSearch1/up/AS123") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + }); + + assertErrorTitle(badRequestException, "400 Bad Request"); + assertErrorStatus(badRequestException, HttpStatus.BAD_REQUEST_400); + assertErrorDescription(badRequestException, "Relation queries not allowed for autnum"); + } + + @Test + public void get_up_then_parent(){ + loadIpv4RelationTreeExample(); + + final SearchResult searchResult = createResource("ips/rirSearch1/up/192.0.2.0/28") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List ipResults = searchResult.getIpSearchResults(); + assertThat(ipResults.size(), is(1)); + assertThat(ipResults.getFirst().getHandle(), is("192.0.2.0 - 192.0.2.127")); // /26 + } + + @Test + public void get_non_existing_up_then_404(){ + loadIpv4RelationTreeExample(); + + final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { + createResource("ips/rirSearch1/up/192.0.2.0/24") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + }); + + assertErrorTitle(notFoundException, "404 Not Found"); + assertErrorStatus(notFoundException, HttpStatus.NOT_FOUND_404); + assertErrorDescription(notFoundException, "No up level object has been found for 192.0.2.0/24"); + } + + + @Test + public void get_ipv6_up_then_parent(){ + loadIpv6RelationTreeExample(); + + final SearchResult searchResult = createResource("ips/rirSearch1/up/2001:db8::/32") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List ipResults = searchResult.getIpSearchResults(); + assertThat(ipResults.size(), is(1)); + assertThat(ipResults.getFirst().getHandle(), is("2001::/16")); + } + + + @Test + public void get_domain_up_then_parent(){ + loadIpv4RelationTreeExample(); + loadIpv4RelationDomainExample(); + + final SearchResult searchResult = createResource("domains/rirSearch1/up/1.2.0.192.in-addr.arpa") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List domainResults = searchResult.getDomainSearchResults(); + assertThat(domainResults.size(), is(1)); + assertThat(domainResults.getFirst().getHandle(), is("2.0.192.in-addr.arpa")); + } + + + @Test + public void get_non_existing_domain_up_then_404(){ + loadIpv4RelationTreeExample(); + loadIpv4RelationDomainExample(); + + final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { + createResource("domains/rirSearch1/up/0.192.in-addr.arpa") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + }); + + assertErrorTitle(notFoundException, "404 Not Found"); + assertErrorStatus(notFoundException, HttpStatus.NOT_FOUND_404); + assertErrorDescription(notFoundException, "No up level object has been found for 192.0.0.0/16"); + } + + + // Top + @Test + public void get_top_then_less_specific_allocated_assigned_first_parent(){ + loadIpv4RelationTreeExample(); + + final SearchResult searchResult = createResource("ips/rirSearch1/top/192.0.2.0/28") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List ipResults = searchResult.getIpSearchResults(); + assertThat(ipResults.size(), is(1)); + assertThat(ipResults.getFirst().getHandle(), is("192.0.2.0 - 192.0.2.255")); // /24 + } + + @Test + public void get_ipv6_top_then_less_specific_allocated_assigned_first_parent(){ + loadIpv6RelationTreeExample(); + + final SearchResult searchResult = createResource("ips/rirSearch1/top/2001:db8::/32") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List ipResults = searchResult.getIpSearchResults(); + assertThat(ipResults.size(), is(1)); + assertThat(ipResults.getFirst().getHandle(), is("2000::/3")); + } + + @Test + public void get_domain_top_then_less_specific_allocated_assigned_first_parent(){ + loadIpv4RelationTreeExample(); + loadIpv4RelationDomainExample(); + + final SearchResult searchResult = createResource("domains/rirSearch1/top/1.2.0.192.in-addr.arpa") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List domainResults = searchResult.getDomainSearchResults(); + assertThat(domainResults.size(), is(1)); + assertThat(domainResults.getFirst().getHandle(), is("0.192.in-addr.arpa")); + } + + @Test + public void get_non_existing_domain_top_then_404(){ + loadIpv4RelationTreeExample(); + loadIpv4RelationDomainExample(); + + final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { + createResource("domains/rirSearch1/top/0.192.in-addr.arpa") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + }); + + assertErrorTitle(notFoundException, "404 Not Found"); + assertErrorStatus(notFoundException, HttpStatus.NOT_FOUND_404); + assertErrorDescription(notFoundException, "No top level object has been found for 192.0.0.0/16"); + } + + @Test + public void get_ipv6_top_not_found(){ + loadIpv6RelationTreeExample(); + + final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { + createResource("ips/rirSearch1/top/2000::/3") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + }); + + assertErrorTitle(notFoundException, "404 Not Found"); + assertErrorStatus(notFoundException, HttpStatus.NOT_FOUND_404); + assertErrorDescription(notFoundException, "No top level object has been found for 2000::/3"); + } + + + @Test + public void get_non_existing_top_then_404(){ + loadIpv4RelationTreeExample(); + + final NotFoundException notFoundException = assertThrows(NotFoundException.class, () -> { + createResource("ips/rirSearch1/top/192.0.2.0/24") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + }); + + assertErrorTitle(notFoundException, "404 Not Found"); + assertErrorStatus(notFoundException, HttpStatus.NOT_FOUND_404); + assertErrorDescription(notFoundException, "No top level object has been found for 192.0.2.0/24"); + } + + + // Bottom + @Test + public void get_bottom_then_bottom(){ + loadIpv4RelationTreeExample(); + + final SearchResult searchResult = createResource("ips/rirSearch1/bottom/192.0.2.0/24") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List ipResults = searchResult.getIpSearchResults(); + assertThat(ipResults.size(), is(5)); + assertThat(ipResults.getFirst().getHandle(), is("192.0.2.0 - 192.0.2.0")); //32 + assertThat(ipResults.get(1).getHandle(), is("192.0.2.0 - 192.0.2.15")); //28 + assertThat(ipResults.get(2).getHandle(), is("192.0.2.0 - 192.0.2.127")); //25 + assertThat(ipResults.get(3).getHandle(), is("192.0.2.128 - 192.0.2.191")); //26 + assertThat(ipResults.get(4).getHandle(), is("192.0.2.192 - 192.0.2.255")); //26 + } + + @Test + public void get_bottom_ipv6_then_bottom(){ + loadIpv6RelationTreeExample(); + + final SearchResult searchResult = createResource("ips/rirSearch1/bottom/2000::/3") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List ipResults = searchResult.getIpSearchResults(); + assertThat(ipResults.size(), is(6)); + assertThat(ipResults.getFirst().getHandle(), is("2001::/16")); + assertThat(ipResults.get(1).getHandle(), is("2001::/23")); + assertThat(ipResults.get(2).getHandle(), is("2001:db8::/32")); + assertThat(ipResults.get(3).getHandle(), is("2400::/12")); + assertThat(ipResults.get(4).getHandle(), is("2600::/12")); + assertThat(ipResults.get(5).getHandle(), is("2800::/12")); + } + + @Test + public void get_bottom_ipv6_cover_parent_then_bottom(){ + loadIpv6RelationTreeExample(); + + final SearchResult searchResult = createResource("ips/rirSearch1/bottom/FC00::/7") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List ipResults = searchResult.getIpSearchResults(); + assertThat(ipResults.size(), is(2)); + assertThat(ipResults.getFirst().getHandle(), is("FC00::/8")); + assertThat(ipResults.get(1).getHandle(), is("FD00::/8")); + } + + @Test + public void get_bottom_from_root_parent_then_bottom(){ + loadIpv6RelationTreeExample(); + + final SearchResult searchResult = createResource("ips/rirSearch1/bottom/::/0") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List ipResults = searchResult.getIpSearchResults(); + assertThat(ipResults.size(), is(9)); + assertThat(ipResults.getFirst().getHandle(), is("2001::/16")); + assertThat(ipResults.get(1).getHandle(), is("2001::/23")); + assertThat(ipResults.get(2).getHandle(), is("2000::/3")); + assertThat(ipResults.get(3).getHandle(), is("2001:db8::/32")); + assertThat(ipResults.get(4).getHandle(), is("2400::/12")); + assertThat(ipResults.get(5).getHandle(), is("2600::/12")); + assertThat(ipResults.get(6).getHandle(), is("2800::/12")); + assertThat(ipResults.get(7).getHandle(), is("FC00::/8")); + assertThat(ipResults.get(8).getHandle(), is("FD00::/8")); + } + + @Test + public void get_domain_bottom_then_bottom(){ + loadIpv4RelationTreeExample(); + loadIpv4RelationDomainExample(); + + final SearchResult searchResult = createResource("domains/rirSearch1/bottom/0.192.in-addr.arpa") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List domainResults = searchResult.getDomainSearchResults(); + assertThat(domainResults.size(), is(2)); + assertThat(domainResults.getFirst().getHandle(), is("1.2.0.192.in-addr.arpa")); + assertThat(domainResults.get(1).getHandle(), is("2.0.192.in-addr.arpa")); + } + + @Test + public void get_non_existing_domain_bottom_then_empty(){ + loadIpv4RelationTreeExample(); + loadIpv4RelationDomainExample(); + + final SearchResult searchResult = createResource("domains/rirSearch1/bottom/1.2.0.192.in-addr.arpa") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List domainResults = searchResult.getDomainSearchResults(); + assertThat(domainResults, is(nullValue())); + } + + @Test + public void get_non_existing_bottom_then_empty_response(){ + loadIpv4RelationTreeExample(); + + final SearchResult searchResult = createResource("ips/rirSearch1/bottom/192.0.2.0/32") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List ipResults = searchResult.getIpSearchResults(); + assertThat(ipResults, is(nullValue())); + } + + @Test + public void get_bottom_wrong_type_then_400(){ + + final BadRequestException badRequestException = assertThrows(BadRequestException.class, () -> { + createResource("ip/rirSearch1/bottom/192.0.2.0/24") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + }); + assertErrorTitle(badRequestException, "400 Bad Request"); + assertErrorStatus(badRequestException, HttpStatus.BAD_REQUEST_400); + assertErrorDescription(badRequestException, "Invalid or unknown type ip"); + } + + @Test + public void bottom_with_status_then_501(){ + + final ServerErrorException notImplementedException = assertThrows(ServerErrorException.class, () -> { + createResource("ip/rirSearch1/bottom/192.0.2.0/24?status=inactive") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + }); + assertErrorTitle(notImplementedException, "501 Not Implemented"); + assertErrorStatus(notImplementedException, HttpStatus.NOT_IMPLEMENTED_501); + assertErrorDescription(notImplementedException, "Status is not implement in down and bottom relation"); + } + + // Down + @Test + public void get_down_then_immediate_child(){ + loadIpv4RelationTreeExample(); + + final SearchResult searchResult = createResource("ips/rirSearch1/down/192.0.2.0/24") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List ipResults = searchResult.getIpSearchResults(); + assertThat(ipResults.size(), is(2)); + assertThat(ipResults.getFirst().getHandle(), is("192.0.2.0 - 192.0.2.127")); + assertThat(ipResults.get(1).getHandle(), is("192.0.2.128 - 192.0.2.255")); + } + + @Test + public void get_down_ipv6_then_immediate_child(){ + loadIpv6RelationTreeExample(); + + final SearchResult searchResult = createResource("ips/rirSearch1/down/2000::/3") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List ipResults = searchResult.getIpSearchResults(); + assertThat(ipResults.size(), is(4)); + assertThat(ipResults.getFirst().getHandle(), is("2001::/16")); + assertThat(ipResults.get(1).getHandle(), is("2400::/12")); + assertThat(ipResults.get(2).getHandle(), is("2600::/12")); + assertThat(ipResults.get(3).getHandle(), is("2800::/12")); + } + + @Test + public void get_down_domain_then_immediate_child(){ + loadIpv4RelationTreeExample(); + loadIpv4RelationDomainExample(); + + final SearchResult searchResult = createResource("domains/rirSearch1/down/0.192.in-addr.arpa") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List domainResults = searchResult.getDomainSearchResults(); + assertThat(domainResults.size(), is(1)); + assertThat(domainResults.getFirst().getHandle(), is("2.0.192.in-addr.arpa")); + } + + @Test + public void get_down_domain_no_child_then_empty_response(){ + loadIpv4RelationTreeExample(); + loadIpv4RelationDomainExample(); + + final SearchResult searchResult = createResource("domains/rirSearch1/down/1.2.0.192.in-addr.arpa") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List domainResults = searchResult.getDomainSearchResults(); + assertThat(domainResults, is(nullValue())); + } + + @Test + public void get_down_when_no_child_then_empty_response(){ + loadIpv4RelationTreeExample(); + + final SearchResult searchResult = createResource("ips/rirSearch1/down/192.0.2.0/32") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + + final List ipResults = searchResult.getIpSearchResults(); + assertThat(ipResults, is(nullValue())); + } + + @Test + public void down_with_status_then_501(){ + + final ServerErrorException notImplementedException = assertThrows(ServerErrorException.class, () -> { + createResource("ip/rirSearch1/down/192.0.2.0/24?status=inactive") + .request(MediaType.APPLICATION_JSON_TYPE) + .get(SearchResult.class); + }); + assertErrorTitle(notImplementedException, "501 Not Implemented"); + assertErrorStatus(notImplementedException, HttpStatus.NOT_IMPLEMENTED_501); + assertErrorDescription(notImplementedException, "Status is not implement in down and bottom relation"); + } + + /* Helper methods*/ + private void assertCommon(RdapObject object) { assertThat(object.getPort43(), is("whois.ripe.net")); assertThat(object.getRdapConformance(), hasSize(4)); @@ -3209,4 +3626,350 @@ private void createEntityRedactionObjects() { "source: TEST"); } + private void loadIpv4RelationTreeExample(){ + /* + +--------------+ + | 192.0.2.0/24 | + +--------------+ + / \ + +--------------+ +----------------+ + | 192.0.2.0/25 | | 192.0.2.128/25 | + +--------------+ +----------------+ + / / \ + +--------------+ +----------------+ +----------------+ + | 192.0.2.0/28 | | 192.0.2.128/26 | | 192.0.2.192/26 | + +--------------+ +----------------+ +----------------+ + / + +--------------+ + | 192.0.2.0/32 | + +--------------+ + */ + + databaseHelper.addObject("" + + "inetnum: 192.0.2.0 - 192.0.2.255\n" + // /24 + "netname: TEST-NET-NAME\n" + + "descr: TEST network\n" + + "country: NL\n" + + "language: en\n" + + "tech-c: TP1-TEST\n" + + "status: ALLOCATED PA\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + + // One branch + databaseHelper.addObject("" + + "inetnum: 192.0.2.0 - 192.0.2.127\n" + // /25 + "netname: TEST-NET-NAME\n" + + "descr: TEST network\n" + + "country: NL\n" + + "language: en\n" + + "tech-c: TP1-TEST\n" + + "status: ALLOCATED PA\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + + databaseHelper.addObject("" + + "inetnum: 192.0.2.0 - 192.0.2.0\n" + // /32 + "netname: TEST-NET-NAME\n" + + "descr: TEST network\n" + + "country: NL\n" + + "language: en\n" + + "tech-c: TP1-TEST\n" + + "status: ASSIGNED PA\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + databaseHelper.addObject("" + + "inetnum: 192.0.2.0 - 192.0.2.15\n" + // /28 + "netname: TEST-NET-NAME\n" + + "descr: TEST network\n" + + "country: NL\n" + + "language: en\n" + + "tech-c: TP1-TEST\n" + + "status: ALLOCATED PA\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + + //Another branch + + databaseHelper.addObject("" + + "inetnum: 192.0.2.128 - 192.0.2.255\n" + // /25 + "netname: TEST-NET-NAME\n" + + "descr: TEST network\n" + + "country: NL\n" + + "language: en\n" + + "tech-c: TP1-TEST\n" + + "status: ALLOCATED PA\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + databaseHelper.addObject("" + + "inetnum: 192.0.2.128 - 192.0.2.191\n" + // /26 + "netname: TEST-NET-NAME\n" + + "descr: TEST network\n" + + "country: NL\n" + + "language: en\n" + + "tech-c: TP1-TEST\n" + + "status: ALLOCATED PA\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + databaseHelper.addObject("" + + "inetnum: 192.0.2.192 - 192.0.2.255\n" + // /26 + "netname: TEST-NET-NAME\n" + + "descr: TEST network\n" + + "country: NL\n" + + "language: en\n" + + "tech-c: TP1-TEST\n" + + "status: ALLOCATED PA\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + ipTreeUpdater.rebuild(); + } + + + private void loadIpv4RelationDomainExample(){ + databaseHelper.addObject("" + + "inetnum: 192.0.0.0 - 192.0.255.255\n" + + "netname: TEST-NET-NAME\n" + + "descr: TEST network\n" + + "country: NL\n" + + "language: en\n" + + "tech-c: TP1-TEST\n" + + "status: ALLOCATED PA\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + databaseHelper.addObject("" + + "domain: 0.192.in-addr.arpa\n" + + "descr: Test domain\n" + + "admin-c: TP1-TEST\n" + + "tech-c: TP1-TEST\n" + + "zone-c: TP1-TEST\n" + + "notify: notify@test.net.au\n" + + "nserver: ns1.test.com.au 10.0.0.1\n" + + "nserver: ns2.test.com.au 2001:10::2\n" + + "ds-rdata: 52151 1 1 13ee60f7499a70e5aadaf05828e7fc59e8e70bc1\n" + + "ds-rdata: 17881 5 1 2e58131e5fe28ec965a7b8e4efb52d0a028d7a78\n" + + "ds-rdata: 17881 5 2 8c6265733a73e5588bfac516a4fcfbe1103a544b95f254cb67a21e474079547e\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2011-07-28T00:35:42Z\n" + + "last-modified: 2019-02-28T10:14:46Z\n" + + "source: TEST"); + + databaseHelper.addObject("" + + "domain: 2.0.192.in-addr.arpa\n" + + "descr: Test domain\n" + + "admin-c: TP1-TEST\n" + + "tech-c: TP1-TEST\n" + + "zone-c: TP1-TEST\n" + + "notify: notify@test.net.au\n" + + "nserver: ns1.test.com.au 10.0.0.1\n" + + "nserver: ns2.test.com.au 2001:10::2\n" + + "ds-rdata: 52151 1 1 13ee60f7499a70e5aadaf05828e7fc59e8e70bc1\n" + + "ds-rdata: 17881 5 1 2e58131e5fe28ec965a7b8e4efb52d0a028d7a78\n" + + "ds-rdata: 17881 5 2 8c6265733a73e5588bfac516a4fcfbe1103a544b95f254cb67a21e474079547e\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2011-07-28T00:35:42Z\n" + + "last-modified: 2019-02-28T10:14:46Z\n" + + "source: TEST"); + + + databaseHelper.addObject("" + + "domain: 1.2.0.192.in-addr.arpa\n" + + "descr: Test domain\n" + + "admin-c: TP1-TEST\n" + + "tech-c: TP1-TEST\n" + + "zone-c: TP1-TEST\n" + + "notify: notify@test.net.au\n" + + "nserver: ns1.test.com.au 10.0.0.1\n" + + "nserver: ns2.test.com.au 2001:10::2\n" + + "ds-rdata: 52151 1 1 13ee60f7499a70e5aadaf05828e7fc59e8e70bc1\n" + + "ds-rdata: 17881 5 1 2e58131e5fe28ec965a7b8e4efb52d0a028d7a78\n" + + "ds-rdata: 17881 5 2 8c6265733a73e5588bfac516a4fcfbe1103a544b95f254cb67a21e474079547e\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2011-07-28T00:35:42Z\n" + + "last-modified: 2019-02-28T10:14:46Z\n" + + "source: TEST"); + + ipTreeUpdater.rebuild(); + } + + private void loadIpv6RelationTreeExample(){ + /* + +--------------+ + | /0 | + +--------------+ + / \ + +--------------+ +----------------+ + | FC00::/7 | | 2000::/3 | + +--------------+ +----------------+ + / \ / | | \ + +-----------------+ +-------------------+ +------------+ +-----------+ +-----------+ +-----------+ + | FC00::/8 | | FD00::/8 | | 2001::/16 | | 2400::/12 | | 2600::/12 | | 2800::/12 | + +-----------------+ +-------------------+ +------------+ +-----------+ +-----------+ +-----------+ + / | + +--------------+ +--------------+ + | 2001:db8::/32 | | 2001::/23 | + +--------------+ +--------------+ + */ + + databaseHelper.addObject("" + + "inet6num: 2000::/3\n" + + "netname: TEST\n" + + "descr: The whole IPv6 address space\n" + + "country: NL\n" + + "tech-c: TP1-TEST\n" + + "admin-c: TP1-TEST\n" + + "status: ALLOCATED-BY-LIR\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + + // One branch + databaseHelper.addObject("" + + "inet6num: 2001::/16\n" + + "netname: TEST\n" + + "descr: The whole IPv6 address space\n" + + "country: NL\n" + + "tech-c: TP1-TEST\n" + + "admin-c: TP1-TEST\n" + + "status: ALLOCATED-BY-LIR\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + + databaseHelper.addObject("" + + "inet6num: 2001:db8::/32\n" + + "netname: TEST\n" + + "descr: The whole IPv6 address space\n" + + "country: NL\n" + + "tech-c: TP1-TEST\n" + + "admin-c: TP1-TEST\n" + + "status: ALLOCATED-BY-LIR\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + + databaseHelper.addObject("" + + "inet6num: 2001::/23\n" + + "netname: TEST\n" + + "descr: The whole IPv6 address space\n" + + "country: NL\n" + + "tech-c: TP1-TEST\n" + + "admin-c: TP1-TEST\n" + + "status: ALLOCATED-BY-LIR\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + + databaseHelper.addObject("" + + "inet6num: 2400::/12\n" + + "netname: TEST\n" + + "descr: The whole IPv6 address space\n" + + "country: NL\n" + + "tech-c: TP1-TEST\n" + + "admin-c: TP1-TEST\n" + + "status: ALLOCATED-BY-LIR\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + databaseHelper.addObject("" + + "inet6num: 2600::/12\n" + + "netname: TEST\n" + + "descr: The whole IPv6 address space\n" + + "country: NL\n" + + "tech-c: TP1-TEST\n" + + "admin-c: TP1-TEST\n" + + "status: ALLOCATED-BY-LIR\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + databaseHelper.addObject("" + + "inet6num: 2800::/12\n" + + "netname: TEST\n" + + "descr: The whole IPv6 address space\n" + + "country: NL\n" + + "tech-c: TP1-TEST\n" + + "admin-c: TP1-TEST\n" + + "status: ALLOCATED-BY-LIR\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + + databaseHelper.addObject("" + + "inet6num: FC00::/7\n" + + "netname: TEST\n" + + "descr: The whole IPv6 address space\n" + + "country: NL\n" + + "tech-c: TP1-TEST\n" + + "admin-c: TP1-TEST\n" + + "status: ALLOCATED-BY-LIR\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + databaseHelper.addObject("" + + "inet6num: FC00::/8\n" + + "netname: TEST\n" + + "descr: The whole IPv6 address space\n" + + "country: NL\n" + + "tech-c: TP1-TEST\n" + + "admin-c: TP1-TEST\n" + + "status: ALLOCATED-BY-LIR\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + databaseHelper.addObject("" + + "inet6num: FD00::/8\n" + + "netname: TEST\n" + + "descr: The whole IPv6 address space\n" + + "country: NL\n" + + "tech-c: TP1-TEST\n" + + "admin-c: TP1-TEST\n" + + "status: ALLOCATED-BY-LIR\n" + + "mnt-by: OWNER-MNT\n" + + "created: 2022-08-14T11:48:28Z\n" + + "last-modified: 2022-10-25T12:22:39Z\n" + + "source: TEST"); + + ipTreeUpdater.rebuild(); + } } diff --git a/whois-commons/src/main/java/net/ripe/db/whois/common/etree/IntervalMap.java b/whois-commons/src/main/java/net/ripe/db/whois/common/etree/IntervalMap.java index 66d937eadf..10062cef6e 100644 --- a/whois-commons/src/main/java/net/ripe/db/whois/common/etree/IntervalMap.java +++ b/whois-commons/src/main/java/net/ripe/db/whois/common/etree/IntervalMap.java @@ -161,4 +161,20 @@ public interface IntervalMap, V> { * intervals. */ List findExactAndAllMoreSpecific(K key); + + /** + * Finds all values associated with intervals that are contained within (more specific than) + * and whose prefix is higher, more specific + * key. + *

+ *

+ * The resulting values are the most specific values associated with the + * key. + * + * @param key the key to find the exact and all levels more specific values + * for + * @return the (possibly empty) list of values associated with the matching + * intervals. + */ + List findMostSpecific(K key); } diff --git a/whois-commons/src/main/java/net/ripe/db/whois/common/etree/MultiValueIntervalMap.java b/whois-commons/src/main/java/net/ripe/db/whois/common/etree/MultiValueIntervalMap.java index 994bf327fe..e86a9f5883 100644 --- a/whois-commons/src/main/java/net/ripe/db/whois/common/etree/MultiValueIntervalMap.java +++ b/whois-commons/src/main/java/net/ripe/db/whois/common/etree/MultiValueIntervalMap.java @@ -103,4 +103,9 @@ public List findAllMoreSpecific(K key) { public List findExactAndAllMoreSpecific(K key) { return unroll(wrapped.findExactAndAllMoreSpecific(key)); } + + @Override + public List findMostSpecific(K key) { + return unroll(wrapped.findMostSpecific(key)); + } } diff --git a/whois-commons/src/main/java/net/ripe/db/whois/common/etree/NestedIntervalMap.java b/whois-commons/src/main/java/net/ripe/db/whois/common/etree/NestedIntervalMap.java index 53c6ee8897..763dc932fd 100644 --- a/whois-commons/src/main/java/net/ripe/db/whois/common/etree/NestedIntervalMap.java +++ b/whois-commons/src/main/java/net/ripe/db/whois/common/etree/NestedIntervalMap.java @@ -119,6 +119,12 @@ public List findExactAndAllMoreSpecific(K key) { return mapToValues(internalFindExactAndAllMoreSpecific(key)); } + @Override + public List findMostSpecific(K key) { + Validate.notNull(key); + return mapToValues(internalFindMostSpecific(key)); + } + /** * Clears all values from the map. */ @@ -226,6 +232,17 @@ private List> internalFindFirstMoreSpecific(K range) { return result; } + private List> internalFindMostSpecific(K range){ + final List> result = internalFindAllMoreSpecific(range); + if (result.isEmpty()){ + return Lists.newArrayList(); + } + + return result.parallelStream() + .filter( kvInternalNode -> kvInternalNode.getChildren().isEmpty()) + .toList(); + } + private List> internalFindAllMoreSpecific(K range) { List> result = internalFindExactAndAllMoreSpecific(range); if (!result.isEmpty() && result.get(0).getInterval().equals(range)) { diff --git a/whois-commons/src/main/java/net/ripe/db/whois/common/etree/SynchronizedIntervalMap.java b/whois-commons/src/main/java/net/ripe/db/whois/common/etree/SynchronizedIntervalMap.java index f1d99e0066..07949e0e74 100644 --- a/whois-commons/src/main/java/net/ripe/db/whois/common/etree/SynchronizedIntervalMap.java +++ b/whois-commons/src/main/java/net/ripe/db/whois/common/etree/SynchronizedIntervalMap.java @@ -104,6 +104,13 @@ public List findExactAndAllMoreSpecific(K key) { } } + @Override + public List findMostSpecific(K key) { + synchronized (mutex) { + return wrapped.findMostSpecific(key); + } + } + @Override public void clear() { synchronized (mutex) { diff --git a/whois-commons/src/main/java/net/ripe/db/whois/common/iptree/CachedIpTree.java b/whois-commons/src/main/java/net/ripe/db/whois/common/iptree/CachedIpTree.java index 44e4991c23..69d74c3263 100644 --- a/whois-commons/src/main/java/net/ripe/db/whois/common/iptree/CachedIpTree.java +++ b/whois-commons/src/main/java/net/ripe/db/whois/common/iptree/CachedIpTree.java @@ -59,4 +59,9 @@ public List findFirstMoreSpecific(final K key) { public List findAllMoreSpecific(final K key) { return getIntervalMap().findAllMoreSpecific(key); } + + @Override + public List findMostSpecific(final K key){ + return getIntervalMap().findMostSpecific(key); + } } diff --git a/whois-commons/src/main/java/net/ripe/db/whois/common/iptree/IpTree.java b/whois-commons/src/main/java/net/ripe/db/whois/common/iptree/IpTree.java index 9b82ca6ad2..a53acabb57 100644 --- a/whois-commons/src/main/java/net/ripe/db/whois/common/iptree/IpTree.java +++ b/whois-commons/src/main/java/net/ripe/db/whois/common/iptree/IpTree.java @@ -19,4 +19,6 @@ public interface IpTree, V extends NestedIntervalMap.Key List findFirstMoreSpecific(K key); List findAllMoreSpecific(K key); + + List findMostSpecific(K key); }