diff --git a/java/google/registry/config/RdapNoticeDescriptor.java b/java/google/registry/config/RdapNoticeDescriptor.java deleted file mode 100644 index 1686d0f7e..000000000 --- a/java/google/registry/config/RdapNoticeDescriptor.java +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2017 The Nomulus Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package google.registry.config; - -import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; -import javax.annotation.Nullable; - -/** - * AutoValue class describing an RDAP Notice object. - * - *

This is used for injecting RDAP help pages. - */ -@AutoValue -public abstract class RdapNoticeDescriptor { - - public static Builder builder() { - return new AutoValue_RdapNoticeDescriptor.Builder(); - } - - @Nullable public abstract String getTitle(); - public abstract ImmutableList getDescription(); - @Nullable public abstract String getTypeString(); - @Nullable public abstract String getLinkValueSuffix(); - @Nullable public abstract String getLinkHrefUrlString(); - - /** Builder class for {@link RdapNoticeDescriptor}. */ - @AutoValue.Builder - public abstract static class Builder { - - public abstract Builder setTitle(@Nullable String title); - public abstract Builder setDescription(Iterable description); - public abstract Builder setTypeString(@Nullable String typeString); - public abstract Builder setLinkValueSuffix(@Nullable String linkValueSuffix); - public abstract Builder setLinkHrefUrlString(@Nullable String linkHrefUrlString); - - public abstract RdapNoticeDescriptor build(); - } -} - diff --git a/java/google/registry/config/RegistryConfig.java b/java/google/registry/config/RegistryConfig.java index 641f1dc5d..edac2b415 100644 --- a/java/google/registry/config/RegistryConfig.java +++ b/java/google/registry/config/RegistryConfig.java @@ -26,7 +26,6 @@ import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import dagger.Module; import dagger.Provides; @@ -1267,19 +1266,6 @@ public final class RegistryConfig { return ImmutableList.copyOf(config.credentialOAuth.localCredentialOauthScopes); } - /** - * Returns the help path for the RDAP terms of service. - * - *

Make sure that this path is equal to the key of the entry in the RDAP help map containing - * the terms of service. The ICANN operational profile requires that the TOS be included in all - * responses, and this string is used to find the TOS in the help map. - */ - @Provides - @Config("rdapTosPath") - public static String provideRdapTosPath() { - return "/tos"; - } - /** OAuth client ID used by the nomulus tool. */ @Provides @Config("toolsClientId") @@ -1311,51 +1297,6 @@ public final class RegistryConfig { public static String provideRdapTosStaticUrl(RegistryConfigSettings config) { return config.registryPolicy.rdapTosStaticUrl; } - - /** - * Returns the help text to be used by RDAP. - * - *

Make sure that the map entry for the terms of service use the same key as specified in - * rdapTosPath above. - */ - @Singleton - @Provides - @Config("rdapHelpMap") - public static ImmutableMap provideRdapHelpMap( - @Config("rdapTos") ImmutableList rdapTos, - @Config("rdapTosStaticUrl") @Nullable String rdapTosStaticUrl) { - return new ImmutableMap.Builder() - .put( - "/", - RdapNoticeDescriptor.builder() - .setTitle("RDAP Help") - .setDescription( - ImmutableList.of( - "domain/XXXX", - "nameserver/XXXX", - "entity/XXXX", - "domains?name=XXXX", - "domains?nsLdhName=XXXX", - "domains?nsIp=XXXX", - "nameservers?name=XXXX", - "nameservers?ip=XXXX", - "entities?fn=XXXX", - "entities?handle=XXXX", - "help/XXXX")) - .setLinkValueSuffix("help/") - .setLinkHrefUrlString( - "https://github.com/google/nomulus/blob/master/docs/rdap.md") - .build()) - .put( - "/tos", - RdapNoticeDescriptor.builder() - .setTitle("RDAP Terms of Service") - .setDescription(rdapTos) - .setLinkValueSuffix("help/tos") - .setLinkHrefUrlString(rdapTosStaticUrl) - .build()) - .build(); - } } /** diff --git a/java/google/registry/rdap/AbstractJsonableObject.java b/java/google/registry/rdap/AbstractJsonableObject.java index b1bf3722a..44bbb769e 100644 --- a/java/google/registry/rdap/AbstractJsonableObject.java +++ b/java/google/registry/rdap/AbstractJsonableObject.java @@ -360,8 +360,6 @@ abstract class AbstractJsonableObject implements Jsonable { * *

If not empty - the resulting list is the allowed names. If the name ends with [], it means * the class is an element in a array with this name. - * - *

A name of "*" means this is allowed to merge. */ static Optional> getNameRestriction(Class clazz) { // Find the first superclass that has an RestrictJsonNames annotation. diff --git a/java/google/registry/rdap/RdapActionBase.java b/java/google/registry/rdap/RdapActionBase.java index cccc5a654..8028543b0 100644 --- a/java/google/registry/rdap/RdapActionBase.java +++ b/java/google/registry/rdap/RdapActionBase.java @@ -25,11 +25,11 @@ import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_OK; -import com.google.api.client.json.jackson2.JacksonFactory; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.flogger.FluentLogger; import com.google.common.net.MediaType; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.re2j.Pattern; import com.googlecode.objectify.Key; import com.googlecode.objectify.cmd.Query; @@ -38,9 +38,12 @@ import google.registry.model.EppResource; import google.registry.model.registrar.Registrar; import google.registry.rdap.RdapMetrics.EndpointType; import google.registry.rdap.RdapMetrics.WildcardType; +import google.registry.rdap.RdapObjectClasses.ErrorResponse; +import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase; +import google.registry.rdap.RdapObjectClasses.TopLevelReplyObject; +import google.registry.rdap.RdapSearchResults.BaseSearchResponse; import google.registry.rdap.RdapSearchResults.IncompletenessWarningType; import google.registry.request.Action; -import google.registry.request.FullServletPath; import google.registry.request.HttpException; import google.registry.request.HttpException.UnprocessableEntityException; import google.registry.request.Parameter; @@ -51,7 +54,6 @@ import google.registry.request.auth.AuthResult; import google.registry.request.auth.AuthenticatedRegistrarAccessor; import google.registry.request.auth.UserAuthInfo; import google.registry.util.Clock; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -60,7 +62,6 @@ import java.util.Optional; import javax.annotation.Nullable; import javax.inject.Inject; import org.joda.time.DateTime; -import org.json.simple.JSONValue; /** * Base RDAP (new WHOIS) action for all requests. @@ -92,7 +93,6 @@ public abstract class RdapActionBase implements Runnable { @Inject Clock clock; @Inject @RequestMethod Action.Method requestMethod; @Inject @RequestPath String requestPath; - @Inject @FullServletPath String fullServletPath; @Inject AuthResult authResult; @Inject AuthenticatedRegistrarAccessor registrarAccessor; @Inject RdapJsonFormatter rdapJsonFormatter; @@ -138,7 +138,7 @@ public abstract class RdapActionBase implements Runnable { * expensive task required to create the map which will never result in a request failure. * @return A map (probably containing nested maps and lists) with the final JSON response data. */ - abstract ImmutableMap getJsonObjectForResource( + abstract ReplyPayloadBase getJsonObjectForResource( String pathSearchString, boolean isHeadRequest); @Override @@ -159,12 +159,16 @@ public abstract class RdapActionBase implements Runnable { checkArgument( pathProper.startsWith(getActionPath()), "%s doesn't start with %s", pathProper, getActionPath()); - ImmutableMap rdapJson = + ReplyPayloadBase replyObject = getJsonObjectForResource( pathProper.substring(getActionPath().length()), requestMethod == Action.Method.HEAD); + if (replyObject instanceof BaseSearchResponse) { + metricInformationBuilder.setIncompletenessWarningType( + ((BaseSearchResponse) replyObject).incompletenessWarningType()); + } response.setStatus(SC_OK); response.setContentType(RESPONSE_MEDIA_TYPE); - setPayload(rdapJson); + setPayload(replyObject); metricInformationBuilder.setStatusCode(SC_OK); } catch (HttpException e) { setError(e.getResponseCode(), e.getResponseCodeString(), e.getMessage()); @@ -182,26 +186,29 @@ public abstract class RdapActionBase implements Runnable { response.setStatus(status); response.setContentType(RESPONSE_MEDIA_TYPE); try { - setPayload(rdapJsonFormatter.makeError(status, title, description)); + setPayload(ErrorResponse.create(status, title, description)); } catch (Exception ex) { + logger.atSevere().withCause(ex).log("Failed to create an error response."); response.setPayload(""); } } - void setPayload(ImmutableMap rdapJson) { + void setPayload(ReplyPayloadBase replyObject) { if (requestMethod == Action.Method.HEAD) { return; } + + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.disableHtmlEscaping(); if (formatOutputParam.orElse(false)) { - try { - response.setPayload(new JacksonFactory().toPrettyString(rdapJson)); - return; - } catch (IOException e) { - logger.atWarning().withCause(e).log( - "Unable to pretty-print RDAP JSON response; falling back to unformatted output."); - } + gsonBuilder.setPrettyPrinting(); } - response.setPayload(JSONValue.toJSONString(rdapJson)); + Gson gson = gsonBuilder.create(); + + TopLevelReplyObject topLevelObject = + TopLevelReplyObject.create(replyObject, rdapJsonFormatter.createTosNotice()); + + response.setPayload(gson.toJson(topLevelObject.toJson())); } RdapAuthorization getAuthorization() { diff --git a/java/google/registry/rdap/RdapAutnumAction.java b/java/google/registry/rdap/RdapAutnumAction.java index c56c6615f..e55b6b730 100644 --- a/java/google/registry/rdap/RdapAutnumAction.java +++ b/java/google/registry/rdap/RdapAutnumAction.java @@ -17,8 +17,8 @@ package google.registry.rdap; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; -import com.google.common.collect.ImmutableMap; import google.registry.rdap.RdapMetrics.EndpointType; +import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase; import google.registry.request.Action; import google.registry.request.HttpException.NotImplementedException; import google.registry.request.auth.Auth; @@ -43,8 +43,7 @@ public class RdapAutnumAction extends RdapActionBase { } @Override - public ImmutableMap getJsonObjectForResource( - String pathSearchString, boolean isHeadRequest) { + public ReplyPayloadBase getJsonObjectForResource(String pathSearchString, boolean isHeadRequest) { throw new NotImplementedException("Domain Name Registry information only"); } } diff --git a/java/google/registry/rdap/RdapDataStructures.java b/java/google/registry/rdap/RdapDataStructures.java new file mode 100644 index 000000000..e0eada897 --- /dev/null +++ b/java/google/registry/rdap/RdapDataStructures.java @@ -0,0 +1,487 @@ +// Copyright 2019 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.rdap; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.gson.JsonArray; +import com.google.gson.JsonPrimitive; +import google.registry.rdap.AbstractJsonableObject.RestrictJsonNames; +import java.util.Optional; +import org.joda.time.DateTime; + +/** + * Data Structures defined in RFC7483 section 4. + */ +final class RdapDataStructures { + + private RdapDataStructures() {} + + /** + * RDAP conformance defined in 4.1 of RFC7483. + */ + @RestrictJsonNames("rdapConformance") + static final class RdapConformance implements Jsonable { + + static final RdapConformance INSTANCE = new RdapConformance(); + + private RdapConformance() {} + + @Override + public JsonArray toJson() { + JsonArray jsonArray = new JsonArray(); + // Conformance to RFC7483 + // TODO(b/127490882) check if we need to Add back the rdap_level_0 string, as I think that + // just means we conform to the RFC, which we do + // jsonArray.add("rdap_level_0"); + + // Conformance to the RDAP Response Profile V2.1 + // (see section 1.3) + jsonArray.add("icann_rdap_response_profile_0"); + return jsonArray; + } + } + + /** + * Links defined in 4.2 of RFC7483. + */ + @RestrictJsonNames("links[]") + @AutoValue + abstract static class Link extends AbstractJsonableObject { + @JsonableElement abstract String href(); + + @JsonableElement abstract Optional rel(); + @JsonableElement abstract Optional hreflang(); + @JsonableElement abstract Optional title(); + @JsonableElement abstract Optional media(); + @JsonableElement abstract Optional type(); + @JsonableElement abstract Optional value(); + + static Builder builder() { + return new AutoValue_RdapDataStructures_Link.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setHref(String href); + abstract Builder setRel(String rel); + abstract Builder setHreflang(String hrefLang); + abstract Builder setTitle(String title); + abstract Builder setMedia(String media); + abstract Builder setType(String type); + abstract Builder setValue(String value); + + abstract Link build(); + } + } + + /** + * Notices and Remarks defined in 4.3 of RFC7483. + * + *

Each has an optional "type" denoting a registered type string defined in 10.2.1. The type is + * defined as common to both Notices and Remarks, but each item is only appropriate to one of + * them. So we will divide all the "types" from the RFC to two enums - one for Notices and one for + * Remarks. + */ + private abstract static class NoticeOrRemark extends AbstractJsonableObject { + @JsonableElement abstract Optional title(); + @JsonableElement abstract ImmutableList description(); + @JsonableElement abstract ImmutableList links(); + + abstract static class Builder> { + abstract B setTitle(String title); + abstract B setDescription(ImmutableList description); + abstract B setDescription(String... description); + abstract ImmutableList.Builder linksBuilder(); + + @SuppressWarnings("unchecked") + B addLink(Link link) { + linksBuilder().add(link); + return (B) this; + } + } + } + + /** + * Notices defined in 4.3 of RFC7483. + * + *

A notice denotes information about the service itself or the entire response, and hence will + * only be in the top-most object. + */ + @AutoValue + @RestrictJsonNames("notices[]") + abstract static class Notice extends NoticeOrRemark { + + /** + * Notice and Remark Type are defined in 10.2.1 of RFC7483. + * + *

We only keep the "service or entire response" values for Notice.Type. + */ + @RestrictJsonNames("type") + enum Type implements Jsonable { + RESULT_TRUNCATED_AUTHORIZATION("result set truncated due to authorization"), + RESULT_TRUNCATED_LOAD("result set truncated due to excessive load"), + RESULT_TRUNCATED_UNEXPLAINABLE("result set truncated due to unexplainable reasons"); + + + private final String rfc7483String; + + Type(String rfc7483String) { + this.rfc7483String = rfc7483String; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(rfc7483String); + } + } + + @JsonableElement + abstract Optional type(); + + static Builder builder() { + return new AutoValue_RdapDataStructures_Notice.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends NoticeOrRemark.Builder { + abstract Builder setType(Notice.Type type); + + abstract Notice build(); + } + } + + /** + * Remarks defined in 4.3 of RFC7483. + * + *

A remark denotes information about the specific object, and hence each object has its own + * "remarks" array. + */ + @AutoValue + @RestrictJsonNames("remarks[]") + abstract static class Remark extends NoticeOrRemark { + + /** + * Notice and Remark Type are defined in 10.2.1 of RFC7483. + * + *

We only keep the "specific object" values for Remark.Type. + */ + @RestrictJsonNames("type") + enum Type implements Jsonable { + OBJECT_TRUNCATED_AUTHORIZATION("object truncated due to authorization"), + OBJECT_TRUNCATED_LOAD("object truncated due to excessive load"), + OBJECT_TRUNCATED_UNEXPLAINABLE("object truncated due to unexplainable reasons"), + // This one isn't in the "RDAP JSON Values registry", but it's in the RDAP Response Profile, + // so I'm adding it here, but we have to ask them about it... + OBJECT_REDACTED_AUTHORIZATION("object redacted due to authorization"); + + private final String rfc7483String; + + Type(String rfc7483String) { + this.rfc7483String = rfc7483String; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(rfc7483String); + } + } + + @JsonableElement + abstract Optional type(); + + static Builder builder() { + return new AutoValue_RdapDataStructures_Remark.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends NoticeOrRemark.Builder { + abstract Builder setType(Remark.Type type); + + abstract Remark build(); + } + } + + /** + * Language Identifier defined in 4.4 of RFC7483. + * + * The allowed values are described in RFC5646. + */ + @RestrictJsonNames("lang") + enum LanguageIdentifier implements Jsonable { + EN("en"); + + private final String languageIdentifier; + + LanguageIdentifier(String languageIdentifier) { + this.languageIdentifier = languageIdentifier; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(languageIdentifier); + } + } + + /** + * Events defined in 4.5 of RFC7483. + * + *

There's a type of Event that must not have the "eventActor" (see 5.1), so we create 2 + * versions - one with and one without. + */ + private abstract static class EventBase extends AbstractJsonableObject { + @JsonableElement abstract EventAction eventAction(); + @JsonableElement abstract DateTime eventDate(); + @JsonableElement abstract ImmutableList links(); + + + abstract static class Builder> { + abstract B setEventAction(EventAction eventAction); + abstract B setEventDate(DateTime eventDate); + abstract ImmutableList.Builder linksBuilder(); + + @SuppressWarnings("unchecked") + B addLink(Link link) { + linksBuilder().add(link); + return (B) this; + } + } + } + + /** Status values for events specified in RFC 7483 § 10.2.3. */ + enum EventAction implements Jsonable { + REGISTRATION("registration"), + REREGISTRATION("reregistration"), + LAST_CHANGED("last changed"), + EXPIRATION("expiration"), + DELETION("deletion"), + REINSTANTIATION("reinstantiation"), + TRANSFER("transfer"), + LOCKED("locked"), + UNLOCKED("unlocked"), + LAST_UPDATE_OF_RDAP_DATABASE("last update of RDAP database"); + + /** Value as it appears in RDAP messages. */ + private final String rfc7483String; + + EventAction(String rfc7483String) { + this.rfc7483String = rfc7483String; + } + + String getDisplayName() { + return rfc7483String; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(rfc7483String); + } + } + + + /** + * Events defined in 4.5 of RFC7483. + * + *

There's a type of Event that MUST NOT have the "eventActor" (see 5.1), so we have this + * object to enforce that. + */ + @RestrictJsonNames("asEventActor[]") + @AutoValue + abstract static class EventWithoutActor extends EventBase { + + static Builder builder() { + return new AutoValue_RdapDataStructures_EventWithoutActor.Builder(); + } + + + @AutoValue.Builder + abstract static class Builder extends EventBase.Builder { + abstract EventWithoutActor build(); + } + } + + /** + * Events defined in 4.5 of RFC7483. + */ + @RestrictJsonNames("events[]") + @AutoValue + abstract static class Event extends EventBase { + @JsonableElement abstract Optional eventActor(); + + static Builder builder() { + return new AutoValue_RdapDataStructures_Event.Builder(); + } + + + @AutoValue.Builder + abstract static class Builder extends EventBase.Builder { + abstract Builder setEventActor(String eventActor); + abstract Event build(); + } + } + + /** + * Status defined in 4.6 of RFC7483. + * + *

This indicates the state of the registered object. + * + *

The allowed values are in section 10.2.2. + */ + @RestrictJsonNames("status[]") + enum RdapStatus implements Jsonable { + + // Status values specified in RFC 7483 § 10.2.2. + VALIDATED("validated"), + RENEW_PROHIBITED("renew prohibited"), + UPDATE_PROHIBITED("update prohibited"), + TRANSFER_PROHIBITED("transfer prohibited"), + DELETE_PROHIBITED("delete prohibited"), + PROXY("proxy"), + PRIVATE("private"), + REMOVED("removed"), + OBSCURED("obscured"), + ASSOCIATED("associated"), + ACTIVE("active"), + INACTIVE("inactive"), + LOCKED("locked"), + PENDING_CREATE("pending create"), + PENDING_RENEW("pending renew"), + PENDING_TRANSFER("pending transfer"), + PENDING_UPDATE("pending update"), + PENDING_DELETE("pending delete"), + + // Additional status values defined in + // https://tools.ietf.org/html/draft-ietf-regext-epp-rdap-status-mapping-01. + ADD_PERIOD("add period"), + AUTO_RENEW_PERIOD("auto renew period"), + CLIENT_DELETE_PROHIBITED("client delete prohibited"), + CLIENT_HOLD("client hold"), + CLIENT_RENEW_PROHIBITED("client renew prohibited"), + CLIENT_TRANSFER_PROHIBITED("client transfer prohibited"), + CLIENT_UPDATE_PROHIBITED("client update prohibited"), + PENDING_RESTORE("pending restore"), + REDEMPTION_PERIOD("redemption period"), + RENEW_PERIOD("renew period"), + SERVER_DELETE_PROHIBITED("server deleted prohibited"), + SERVER_RENEW_PROHIBITED("server renew prohibited"), + SERVER_TRANSFER_PROHIBITED("server transfer prohibited"), + SERVER_UPDATE_PROHIBITED("server update prohibited"), + SERVER_HOLD("server hold"), + TRANSFER_PERIOD("transfer period"); + + /** Value as it appears in RDAP messages. */ + private final String rfc7483String; + + RdapStatus(String rfc7483String) { + this.rfc7483String = rfc7483String; + } + + String getDisplayName() { + return rfc7483String; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(rfc7483String); + } + } + + /** + * Port 43 WHOIS Server defined in 4.7 of RFC7483. + * + *

This contains the fully qualifies host name of IP address of the WHOIS RFC3912 server where + * the containing object instance may be found. + */ + @RestrictJsonNames("port43") + @AutoValue + abstract static class Port43WhoisServer implements Jsonable { + abstract String port43(); + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(port43()); + } + + static Port43WhoisServer create(String port43) { + return new AutoValue_RdapDataStructures_Port43WhoisServer(port43); + } + } + + /** + * Public IDs defined in 4.8 of RFC7483. + * + *

Maps a public identifier to an object class. + */ + @RestrictJsonNames("publicIds[]") + @AutoValue + abstract static class PublicId extends AbstractJsonableObject { + @RestrictJsonNames("type") + enum Type implements Jsonable { + IANA_REGISTRAR_ID("IANA Registrar ID"); + + private final String rfc7483String; + + Type(String rfc7483String) { + this.rfc7483String = rfc7483String; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(rfc7483String); + } + } + + @JsonableElement + abstract PublicId.Type type(); + + @JsonableElement abstract String identifier(); + + static PublicId create(PublicId.Type type, String identifier) { + return new AutoValue_RdapDataStructures_PublicId(type, identifier); + } + } + + /** + * Object Class Name defined in 4.7 of RFC7483. + * + *

Identifies the type of the object being processed. Is REQUIRED in all RDAP response objects, + * but not so for internal objects whose type can be inferred by their key name in the enclosing + * object. + */ + @RestrictJsonNames("objectClassName") + enum ObjectClassName implements Jsonable { + /** Defined in 5.1 of RFC7483. */ + ENTITY("entity"), + /** Defined in 5.2 of RFC7483. */ + NAMESERVER("nameserver"), + /** Defined in 5.3 of RFC7483. */ + DOMAIN("domain"), + /** Defined in 5.4 of RFC7483. Only relevant for Registrars, so isn't implemented here. */ + IP_NETWORK("ip network"), + /** Defined in 5.5 of RFC7483. Only relevant for Registrars, so isn't implemented here. */ + AUTONOMUS_SYSTEM("autnum"); + + private final String className; + + ObjectClassName(String className) { + this.className = className; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(className); + } + } +} diff --git a/java/google/registry/rdap/RdapDomainAction.java b/java/google/registry/rdap/RdapDomainAction.java index f5d9f1ff7..81297858f 100644 --- a/java/google/registry/rdap/RdapDomainAction.java +++ b/java/google/registry/rdap/RdapDomainAction.java @@ -20,11 +20,11 @@ import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; import static google.registry.util.DateTimeUtils.START_OF_TIME; -import com.google.common.collect.ImmutableMap; import google.registry.flows.EppException; import google.registry.model.domain.DomainBase; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.rdap.RdapMetrics.EndpointType; +import google.registry.rdap.RdapObjectClasses.RdapDomain; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.NotFoundException; @@ -47,8 +47,7 @@ public class RdapDomainAction extends RdapActionBase { } @Override - public ImmutableMap getJsonObjectForResource( - String pathSearchString, boolean isHeadRequest) { + public RdapDomain getJsonObjectForResource(String pathSearchString, boolean isHeadRequest) { DateTime now = clock.nowUtc(); pathSearchString = canonicalizeName(pathSearchString); try { @@ -68,8 +67,6 @@ public class RdapDomainAction extends RdapActionBase { } return rdapJsonFormatter.makeRdapJsonForDomain( domainBase.get(), - true, - fullServletPath, rdapWhoisServer, now, OutputDataType.FULL, diff --git a/java/google/registry/rdap/RdapDomainSearchAction.java b/java/google/registry/rdap/RdapDomainSearchAction.java index 1a74a7899..9fc5ccea9 100644 --- a/java/google/registry/rdap/RdapDomainSearchAction.java +++ b/java/google/registry/rdap/RdapDomainSearchAction.java @@ -22,7 +22,6 @@ import static google.registry.request.Action.Method.HEAD; import static google.registry.util.DateTimeUtils.START_OF_TIME; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.collect.Streams; @@ -33,11 +32,11 @@ import com.googlecode.objectify.Key; import com.googlecode.objectify.cmd.Query; import google.registry.model.domain.DomainBase; import google.registry.model.host.HostResource; -import google.registry.rdap.RdapJsonFormatter.BoilerplateType; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.rdap.RdapMetrics.EndpointType; import google.registry.rdap.RdapMetrics.SearchType; import google.registry.rdap.RdapMetrics.WildcardType; +import google.registry.rdap.RdapSearchResults.DomainSearchResponse; import google.registry.rdap.RdapSearchResults.IncompletenessWarningType; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; @@ -48,7 +47,6 @@ import google.registry.request.auth.Auth; import google.registry.util.Idn; import google.registry.util.NonFinalForTesting; import java.net.InetAddress; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -93,7 +91,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { *

The RDAP spec allows for domain search by domain name, nameserver name or nameserver IP. */ @Override - public ImmutableMap getJsonObjectForResource( + public DomainSearchResponse getJsonObjectForResource( String pathSearchString, boolean isHeadRequest) { DateTime now = clock.nowUtc(); // RDAP syntax example: /rdap/domains?name=exam*.com. @@ -107,7 +105,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { "You must specify either name=XXXX, nsLdhName=YYYY or nsIp=ZZZZ"); } decodeCursorToken(); - RdapSearchResults results; + DomainSearchResponse results; if (nameParam.isPresent()) { metricInformationBuilder.setSearchType(SearchType.BY_DOMAIN_NAME); // syntax: /rdap/domains?name=exam*.com @@ -142,18 +140,10 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { } results = searchByNameserverIp(inetAddress, now); } - if (results.jsonList().isEmpty()) { + if (results.domainSearchResults().isEmpty()) { throw new NotFoundException("No domains found"); } - ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); - builder.put("domainSearchResults", results.jsonList()); - rdapJsonFormatter.addTopLevelEntries( - builder, - BoilerplateType.DOMAIN, - getNotices(results), - ImmutableList.of(), - fullServletPath); - return builder.build(); + return results; } /** @@ -168,7 +158,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { *

Searches which include deleted entries are effectively treated as if they have a wildcard, * since the same name can return multiple results. */ - private RdapSearchResults searchByDomainName( + private DomainSearchResponse searchByDomainName( final RdapSearchPattern partialStringQuery, final DateTime now) { // Handle queries without a wildcard -- just load by foreign key. We can't do this if deleted // entries are included, because there may be multiple nameservers with the same name. @@ -199,7 +189,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { /** * Searches for domains by domain name without a wildcard or interest in deleted entries. */ - private RdapSearchResults searchByDomainNameWithoutWildcard( + private DomainSearchResponse searchByDomainNameWithoutWildcard( final RdapSearchPattern partialStringQuery, final DateTime now) { Optional domainBase = loadByForeignKey(DomainBase.class, partialStringQuery.getInitialString(), now); @@ -211,7 +201,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { } /** Searches for domains by domain name with an initial string, wildcard and possible suffix. */ - private RdapSearchResults searchByDomainNameWithInitialString( + private DomainSearchResponse searchByDomainNameWithInitialString( final RdapSearchPattern partialStringQuery, final DateTime now) { // We can't query for undeleted domains as part of the query itself; that would require an // inequality query on deletion time, and we are already using inequality queries on @@ -241,7 +231,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { } /** Searches for domains by domain name with a TLD suffix. */ - private RdapSearchResults searchByDomainNameByTld(String tld, DateTime now) { + private DomainSearchResponse searchByDomainNameByTld(String tld, DateTime now) { // Even though we are not searching on fullyQualifiedDomainName, we want the results to come // back ordered by name, so we are still in the same boat as // searchByDomainNameWithInitialString, unable to perform an inequality query on deletion time. @@ -268,7 +258,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { *

The includeDeleted parameter does NOT cause deleted nameservers to be searched, only deleted * domains which used to be connected to an undeleted nameserver. */ - private RdapSearchResults searchByNameserverLdhName( + private DomainSearchResponse searchByNameserverLdhName( final RdapSearchPattern partialStringQuery, final DateTime now) { Iterable> hostKeys = getNameserverRefsByLdhName(partialStringQuery, now); if (Iterables.isEmpty(hostKeys)) { @@ -407,7 +397,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { *

The includeDeleted parameter does NOT cause deleted nameservers to be searched, only deleted * domains which used to be connected to an undeleted nameserver. */ - private RdapSearchResults searchByNameserverIp( + private DomainSearchResponse searchByNameserverIp( final InetAddress inetAddress, final DateTime now) { Query query = queryItems( @@ -431,7 +421,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { *

This method is called by {@link #searchByNameserverLdhName} and {@link * #searchByNameserverIp} after they assemble the relevant host keys. */ - private RdapSearchResults searchByNameserverRefs( + private DomainSearchResponse searchByNameserverRefs( final Iterable> hostKeys, final DateTime now) { // We must break the query up into chunks, because the in operator is limited to 30 subqueries. // Since it is possible for the same domain to show up more than once in our result list (if @@ -466,34 +456,26 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { } List domains = domainSetBuilder.build().asList(); metricInformationBuilder.setNumHostsRetrieved(numHostKeysSearched); - if (domains.size() > rdapResultSetMaxSize) { - return makeSearchResults( - domains.subList(0, rdapResultSetMaxSize), - IncompletenessWarningType.TRUNCATED, - Optional.of((long) domains.size()), - now); - } else { - // If everything that we found will fit in the result, check whether there might have been - // more results that got dropped because the first stage limit on number of nameservers. If - // so, indicate the result might be incomplete. - return makeSearchResults( - domains, - (numHostKeysSearched >= maxNameserversInFirstStage) - ? IncompletenessWarningType.MIGHT_BE_INCOMPLETE - : IncompletenessWarningType.COMPLETE, - (numHostKeysSearched > 0) ? Optional.of((long) domains.size()) : Optional.empty(), - now); - } + // If everything that we found will fit in the result, check whether there might have been + // more results that got dropped because the first stage limit on number of nameservers. If + // so, indicate the result might be incomplete. + return makeSearchResults( + domains, + (numHostKeysSearched >= maxNameserversInFirstStage) + ? IncompletenessWarningType.MIGHT_BE_INCOMPLETE + : IncompletenessWarningType.COMPLETE, + (numHostKeysSearched > 0) ? Optional.of((long) domains.size()) : Optional.empty(), + now); } /** Output JSON for a list of domains, with no incompleteness warnings. */ - private RdapSearchResults makeSearchResults(List domains, DateTime now) { + private DomainSearchResponse makeSearchResults(List domains, DateTime now) { return makeSearchResults( domains, IncompletenessWarningType.COMPLETE, Optional.of((long) domains.size()), now); } /** Output JSON from data in an {@link RdapResultSet} object. */ - private RdapSearchResults makeSearchResults( + private DomainSearchResponse makeSearchResults( RdapResultSet resultSet, DateTime now) { return makeSearchResults( resultSet.resources(), @@ -509,7 +491,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { * than are in the list, or MIGHT_BE_INCOMPLETE if a search for domains by nameserver returned the * maximum number of nameservers in the first stage query. */ - private RdapSearchResults makeSearchResults( + private DomainSearchResponse makeSearchResults( List domains, IncompletenessWarningType incompletenessWarningType, Optional numDomainsRetrieved, @@ -517,28 +499,21 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { numDomainsRetrieved.ifPresent(metricInformationBuilder::setNumDomainsRetrieved); OutputDataType outputDataType = (domains.size() > 1) ? OutputDataType.SUMMARY : OutputDataType.FULL; + DomainSearchResponse.Builder builder = + DomainSearchResponse.builder() + .setIncompletenessWarningType(incompletenessWarningType); RdapAuthorization authorization = getAuthorization(); - List> jsonList = new ArrayList<>(); Optional newCursor = Optional.empty(); - for (DomainBase domain : domains) { + for (DomainBase domain : Iterables.limit(domains, rdapResultSetMaxSize)) { newCursor = Optional.of(domain.getFullyQualifiedDomainName()); - jsonList.add( + builder.domainSearchResultsBuilder().add( rdapJsonFormatter.makeRdapJsonForDomain( - domain, false, fullServletPath, rdapWhoisServer, now, outputDataType, authorization)); - if (jsonList.size() >= rdapResultSetMaxSize) { - break; - } + domain, rdapWhoisServer, now, outputDataType, authorization)); } - IncompletenessWarningType finalIncompletenessWarningType = - (jsonList.size() < domains.size()) - ? IncompletenessWarningType.TRUNCATED - : incompletenessWarningType; - metricInformationBuilder.setIncompletenessWarningType(finalIncompletenessWarningType); - return RdapSearchResults.create( - ImmutableList.copyOf(jsonList), - finalIncompletenessWarningType, - (finalIncompletenessWarningType == IncompletenessWarningType.TRUNCATED) - ? newCursor - : Optional.empty()); + if (rdapResultSetMaxSize < domains.size()) { + builder.setNextPageUri(createNavigationUri(newCursor.get())); + builder.setIncompletenessWarningType(IncompletenessWarningType.TRUNCATED); + } + return builder.build(); } } diff --git a/java/google/registry/rdap/RdapEntityAction.java b/java/google/registry/rdap/RdapEntityAction.java index 05aae8474..b0606afeb 100644 --- a/java/google/registry/rdap/RdapEntityAction.java +++ b/java/google/registry/rdap/RdapEntityAction.java @@ -19,7 +19,6 @@ import static google.registry.rdap.RdapUtils.getRegistrarByIanaIdentifier; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; -import com.google.common.collect.ImmutableMap; import com.google.common.primitives.Longs; import com.google.re2j.Pattern; import com.googlecode.objectify.Key; @@ -27,6 +26,7 @@ import google.registry.model.contact.ContactResource; import google.registry.model.registrar.Registrar; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.rdap.RdapMetrics.EndpointType; +import google.registry.rdap.RdapObjectClasses.RdapEntity; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.NotFoundException; @@ -60,7 +60,7 @@ public class RdapEntityAction extends RdapActionBase { } @Override - public ImmutableMap getJsonObjectForResource( + public RdapEntity getJsonObjectForResource( String pathSearchString, boolean isHeadRequest) { DateTime now = clock.nowUtc(); // The query string is not used; the RDAP syntax is /rdap/entity/handle (the handle is the roid @@ -76,9 +76,7 @@ public class RdapEntityAction extends RdapActionBase { if ((contactResource != null) && shouldBeVisible(contactResource, now)) { return rdapJsonFormatter.makeRdapJsonForContact( contactResource, - true, Optional.empty(), - fullServletPath, rdapWhoisServer, now, OutputDataType.FULL, @@ -91,7 +89,7 @@ public class RdapEntityAction extends RdapActionBase { Optional registrar = getRegistrarByIanaIdentifier(ianaIdentifier); if (registrar.isPresent() && shouldBeVisible(registrar.get())) { return rdapJsonFormatter.makeRdapJsonForRegistrar( - registrar.get(), true, fullServletPath, rdapWhoisServer, now, OutputDataType.FULL); + registrar.get(), rdapWhoisServer, now, OutputDataType.FULL); } } // At this point, we have failed to find either a contact or a registrar. diff --git a/java/google/registry/rdap/RdapEntitySearchAction.java b/java/google/registry/rdap/RdapEntitySearchAction.java index 3fe0e1daa..387eeb8c7 100644 --- a/java/google/registry/rdap/RdapEntitySearchAction.java +++ b/java/google/registry/rdap/RdapEntitySearchAction.java @@ -21,17 +21,17 @@ import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import com.google.common.collect.Streams; import com.google.common.primitives.Booleans; import com.google.common.primitives.Longs; import com.googlecode.objectify.cmd.Query; import google.registry.model.contact.ContactResource; import google.registry.model.registrar.Registrar; -import google.registry.rdap.RdapJsonFormatter.BoilerplateType; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.rdap.RdapMetrics.EndpointType; import google.registry.rdap.RdapMetrics.SearchType; +import google.registry.rdap.RdapSearchResults.EntitySearchResponse; import google.registry.rdap.RdapSearchResults.IncompletenessWarningType; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; @@ -39,7 +39,6 @@ import google.registry.request.HttpException.NotFoundException; import google.registry.request.HttpException.UnprocessableEntityException; import google.registry.request.Parameter; import google.registry.request.auth.Auth; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -110,7 +109,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { /** Parses the parameters and calls the appropriate search function. */ @Override - public ImmutableMap getJsonObjectForResource( + public EntitySearchResponse getJsonObjectForResource( String pathSearchString, boolean isHeadRequest) { DateTime now = clock.nowUtc(); @@ -157,7 +156,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { } // Search by name. - RdapSearchResults results; + EntitySearchResponse results; if (fnParam.isPresent()) { metricInformationBuilder.setSearchType(SearchType.BY_FULL_NAME); // syntax: /rdap/entities?fn=Bobby%20Joe* @@ -185,18 +184,10 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { } // Build the result object and return it. - if (results.jsonList().isEmpty()) { + if (results.entitySearchResults().isEmpty()) { throw new NotFoundException("No entities found"); } - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("entitySearchResults", results.jsonList()); - rdapJsonFormatter.addTopLevelEntries( - jsonBuilder, - BoilerplateType.ENTITY, - getNotices(results), - ImmutableList.of(), - fullServletPath); - return jsonBuilder.build(); + return results; } /** @@ -223,7 +214,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { * href="https://newgtlds.icann.org/sites/default/files/agreements/agreement-approved-09jan14-en.htm">1.6 * of Section 4 of the Base Registry Agreement */ - private RdapSearchResults searchByName( + private EntitySearchResponse searchByName( final RdapSearchPattern partialStringQuery, CursorType cursorType, Optional cursorQueryString, @@ -308,7 +299,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { * In both cases, the suffix can be turned into an additional query filter field. For contacts, * there is no equivalent string suffix that can be used as a query filter, so we disallow use. */ - private RdapSearchResults searchByHandle( + private EntitySearchResponse searchByHandle( final RdapSearchPattern partialStringQuery, CursorType cursorType, Optional cursorQueryString, @@ -424,7 +415,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { *

This is a convenience wrapper for the four-argument makeSearchResults; it unpacks the * properties of the {@link RdapResultSet} structure and passes them as separate arguments. */ - private RdapSearchResults makeSearchResults( + private EntitySearchResponse makeSearchResults( RdapResultSet resultSet, List registrars, QueryType queryType, @@ -454,7 +445,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { * @param now the current date and time * @return an {@link RdapSearchResults} object */ - private RdapSearchResults makeSearchResults( + private EntitySearchResponse makeSearchResults( List contacts, IncompletenessWarningType incompletenessWarningType, int numContactsRetrieved, @@ -473,25 +464,19 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { // (contacts and registrars), and partially because we try to fetch one more than the max size, // so we can tell whether to display the truncation notification. RdapAuthorization authorization = getAuthorization(); - List> jsonOutputList = new ArrayList<>(); // Each time we add a contact or registrar to the output data set, remember what the appropriate // cursor would be if it were the last item returned. When we stop adding items, the last cursor // value we remembered will be the right one to pass back. + EntitySearchResponse.Builder builder = + EntitySearchResponse.builder() + .setIncompletenessWarningType(incompletenessWarningType); Optional newCursor = Optional.empty(); - for (ContactResource contact : contacts) { - if (jsonOutputList.size() >= rdapResultSetMaxSize) { - return RdapSearchResults.create( - ImmutableList.copyOf(jsonOutputList), - IncompletenessWarningType.TRUNCATED, - newCursor); - } + for (ContactResource contact : Iterables.limit(contacts, rdapResultSetMaxSize)) { // As per Andy Newton on the regext mailing list, contacts by themselves have no role, since // they are global, and might have different roles for different domains. - jsonOutputList.add(rdapJsonFormatter.makeRdapJsonForContact( + builder.entitySearchResultsBuilder().add(rdapJsonFormatter.makeRdapJsonForContact( contact, - false, Optional.empty(), - fullServletPath, rdapWhoisServer, now, outputDataType, @@ -503,22 +488,22 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { ? contact.getSearchName() : contact.getRepoId())); } - for (Registrar registrar : registrars) { - if (jsonOutputList.size() >= rdapResultSetMaxSize) { - return RdapSearchResults.create( - ImmutableList.copyOf(jsonOutputList), - IncompletenessWarningType.TRUNCATED, - newCursor); + if (rdapResultSetMaxSize > contacts.size()) { + for (Registrar registrar : + Iterables.limit(registrars, rdapResultSetMaxSize - contacts.size())) { + builder + .entitySearchResultsBuilder() + .add( + rdapJsonFormatter.makeRdapJsonForRegistrar( + registrar, rdapWhoisServer, now, outputDataType)); + newCursor = Optional.of(REGISTRAR_CURSOR_PREFIX + registrar.getRegistrarName()); } - jsonOutputList.add(rdapJsonFormatter.makeRdapJsonForRegistrar( - registrar, false, fullServletPath, rdapWhoisServer, now, outputDataType)); - newCursor = Optional.of(REGISTRAR_CURSOR_PREFIX + registrar.getRegistrarName()); } - return RdapSearchResults.create( - ImmutableList.copyOf(jsonOutputList), - (jsonOutputList.size() < rdapResultSetMaxSize) - ? incompletenessWarningType - : IncompletenessWarningType.COMPLETE, - Optional.empty()); + if (rdapResultSetMaxSize < contacts.size() + registrars.size()) { + builder.setNextPageUri(createNavigationUri(newCursor.get())); + builder.setIncompletenessWarningType(IncompletenessWarningType.TRUNCATED); + return builder.build(); + } + return builder.build(); } } diff --git a/java/google/registry/rdap/RdapHelpAction.java b/java/google/registry/rdap/RdapHelpAction.java index 215dca192..1d813c5d9 100644 --- a/java/google/registry/rdap/RdapHelpAction.java +++ b/java/google/registry/rdap/RdapHelpAction.java @@ -17,12 +17,14 @@ package google.registry.rdap; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import google.registry.rdap.RdapJsonFormatter.BoilerplateType; +import google.registry.rdap.RdapDataStructures.Link; +import google.registry.rdap.RdapDataStructures.Notice; import google.registry.rdap.RdapMetrics.EndpointType; +import google.registry.rdap.RdapObjectClasses.HelpResponse; import google.registry.request.Action; +import google.registry.request.HttpException.NotFoundException; import google.registry.request.auth.Auth; +import java.util.Optional; import javax.inject.Inject; /** RDAP (new WHOIS) action for help requests. */ @@ -34,22 +36,53 @@ import javax.inject.Inject; auth = Auth.AUTH_PUBLIC_ANONYMOUS) public class RdapHelpAction extends RdapActionBase { + /** The help path for the RDAP terms of service. */ + public static final String TOS_PATH = "/tos"; + + private static final String RDAP_HELP_LINK = + "https://github.com/google/nomulus/blob/master/docs/rdap.md"; + @Inject public RdapHelpAction() { super("help", EndpointType.HELP); } + private Notice createHelpNotice() { + String linkValue = rdapJsonFormatter.makeRdapServletRelativeUrl("help"); + Link.Builder linkBuilder = + Link.builder() + .setValue(linkValue) + .setRel("alternate") + .setHref(RDAP_HELP_LINK) + .setType("text/html"); + return Notice.builder() + .setTitle("RDAP Help") + .setDescription( + "domain/XXXX", + "nameserver/XXXX", + "entity/XXXX", + "domains?name=XXXX", + "domains?nsLdhName=XXXX", + "domains?nsIp=XXXX", + "nameservers?name=XXXX", + "nameservers?ip=XXXX", + "entities?fn=XXXX", + "entities?handle=XXXX", + "help/XXXX") + .addLink(linkBuilder.build()) + .build(); + } + @Override - public ImmutableMap getJsonObjectForResource( + public HelpResponse getJsonObjectForResource( String pathSearchString, boolean isHeadRequest) { - // We rely on addTopLevelEntries to notice if we are sending the TOS notice, and not add a - // duplicate boilerplate entry. - ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); - rdapJsonFormatter.addTopLevelEntries( - builder, - BoilerplateType.OTHER, - ImmutableList.of(rdapJsonFormatter.getJsonHelpNotice(pathSearchString, fullServletPath)), - ImmutableList.of(), - fullServletPath); - return builder.build(); + if (pathSearchString.isEmpty() || pathSearchString.equals("/")) { + return HelpResponse.create(Optional.of(createHelpNotice())); + } + if (pathSearchString.equals(TOS_PATH)) { + // A TOS notice is added to every reply automatically, so we don't want to add another one + // here + return HelpResponse.create(Optional.empty()); + } + throw new NotFoundException("no help found for " + pathSearchString); } } diff --git a/java/google/registry/rdap/RdapIcannStandardInformation.java b/java/google/registry/rdap/RdapIcannStandardInformation.java index d0fc7d873..6d59586e8 100644 --- a/java/google/registry/rdap/RdapIcannStandardInformation.java +++ b/java/google/registry/rdap/RdapIcannStandardInformation.java @@ -15,145 +15,143 @@ package google.registry.rdap; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import google.registry.rdap.RdapDataStructures.Link; +import google.registry.rdap.RdapDataStructures.Notice; +import google.registry.rdap.RdapDataStructures.Remark; /** * This file contains boilerplate required by the ICANN RDAP Profile. * - * @see RDAP Operational Profile for gTLD Registries and Registrars + * @see RDAP + * Operational Profile for gTLD Registries and Registrars */ - public class RdapIcannStandardInformation { /** Required by ICANN RDAP Profile section 1.4.10. */ - private static final ImmutableMap CONFORMANCE_NOTICE = - ImmutableMap.of( - "description", - ImmutableList.of( + private static final Notice CONFORMANCE_NOTICE = + Notice.builder() + .setDescription( "This response conforms to the RDAP Operational Profile for gTLD Registries and" - + " Registrars version 1.0")); + + " Registrars version 1.0") + .build(); /** Required by ICANN RDAP Profile section 1.5.18. */ - private static final ImmutableMap DOMAIN_STATUS_CODES_NOTICE = - ImmutableMap.of( - "title", - "Status Codes", - "description", - ImmutableList.of( - "For more information on domain status codes, please visit https://icann.org/epp"), - "links", - ImmutableList.of( - ImmutableMap.of( - "value", "https://icann.org/epp", - "rel", "alternate", - "href", "https://icann.org/epp", - "type", "text/html"))); + private static final Notice DOMAIN_STATUS_CODES_NOTICE = + Notice.builder() + .setTitle("Status Codes") + .setDescription( + "For more information on domain status codes, please visit" + + " https://icann.org/epp") + .addLink( + Link.builder() + .setValue("https://icann.org/epp") + .setRel("alternate") + .setHref("https://icann.org/epp") + .setType("text/html") + .build()) + .build(); /** Required by ICANN RDAP Profile section 1.5.20. */ - private static final ImmutableMap INACCURACY_COMPLAINT_FORM_NOTICE = - ImmutableMap.of( - "description", - ImmutableList.of( - "URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf"), - "links", - ImmutableList.of( - ImmutableMap.of( - "value", "https://www.icann.org/wicf", - "rel", "alternate", - "href", "https://www.icann.org/wicf", - "type", "text/html"))); + private static final Notice INACCURACY_COMPLAINT_FORM_NOTICE = + Notice.builder() + .setDescription( + "URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf") + .addLink( + Link.builder() + .setValue("https://www.icann.org/wicf") + .setRel("alternate") + .setHref("https://www.icann.org/wicf") + .setType("text/html") + .build()) + .build(); /** Boilerplate notices required by domain responses. */ - static final ImmutableList> domainBoilerplateNotices = + static final ImmutableList domainBoilerplateNotices = ImmutableList.of( - CONFORMANCE_NOTICE, DOMAIN_STATUS_CODES_NOTICE, INACCURACY_COMPLAINT_FORM_NOTICE); + CONFORMANCE_NOTICE, + // RDAP Response Profile 2.6.3 + DOMAIN_STATUS_CODES_NOTICE, + INACCURACY_COMPLAINT_FORM_NOTICE); /** Boilerplate remarks required by nameserver and entity responses. */ - static final ImmutableList> nameserverAndEntityBoilerplateNotices = + static final ImmutableList nameserverAndEntityBoilerplateNotices = ImmutableList.of(CONFORMANCE_NOTICE); /** * Required by ICANN RDAP Profile section 1.4.9, as corrected by Gustavo Lozano of ICANN. * - * @see Questions about the ICANN RDAP Profile + * @see Questions about + * the ICANN RDAP Profile */ - static final ImmutableMap SUMMARY_DATA_REMARK = - ImmutableMap.of( - "title", - "Incomplete Data", - "description", - ImmutableList.of( - "Summary data only. For complete data, send a specific query for the object."), - "type", - "object truncated due to unexplainable reasons"); + static final Remark SUMMARY_DATA_REMARK = + Remark.builder() + .setTitle("Incomplete Data") + .setDescription( + "Summary data only. For complete data, send a specific query for the object.") + .setType(Remark.Type.OBJECT_TRUNCATED_UNEXPLAINABLE) + .build(); /** * Required by ICANN RDAP Profile section 1.4.8, as corrected by Gustavo Lozano of ICANN. * - * @see Questions about the ICANN RDAP Profile + * @see Questions about + * the ICANN RDAP Profile */ - static final ImmutableMap TRUNCATED_RESULT_SET_NOTICE = - ImmutableMap.of( - "title", - "Search Policy", - "description", - ImmutableList.of("Search results per query are limited."), - "type", - "result set truncated due to unexplainable reasons"); + static final Notice TRUNCATED_RESULT_SET_NOTICE = + Notice.builder() + .setTitle("Search Policy") + .setDescription("Search results per query are limited.") + .setType(Notice.Type.RESULT_TRUNCATED_UNEXPLAINABLE) + .build(); /** Truncation notice as a singleton list, for easy use. */ - static final ImmutableList> TRUNCATION_NOTICES = + static final ImmutableList TRUNCATION_NOTICES = ImmutableList.of(TRUNCATED_RESULT_SET_NOTICE); /** * Used when a search for domains by nameserver may have returned incomplete information because * there were too many nameservers in the first stage results. */ - static final ImmutableMap POSSIBLY_INCOMPLETE_RESULT_SET_NOTICE = - ImmutableMap.of( - "title", - "Search Policy", - "description", - ImmutableList.of( - "Search results may contain incomplete information due to first-stage query limits."), - "type", - "result set truncated due to unexplainable reasons"); + static final Notice POSSIBLY_INCOMPLETE_RESULT_SET_NOTICE = + Notice.builder() + .setTitle("Search Policy") + .setDescription( + "Search results may contain incomplete information due to first-stage query" + + " limits.") + .setType(Notice.Type.RESULT_TRUNCATED_UNEXPLAINABLE) + .build(); /** Possibly incomplete notice as a singleton list, for easy use. */ - static final ImmutableList> POSSIBLY_INCOMPLETE_NOTICES = + static final ImmutableList POSSIBLY_INCOMPLETE_NOTICES = ImmutableList.of(POSSIBLY_INCOMPLETE_RESULT_SET_NOTICE); /** Included when the requester is not logged in as the owner of the domain being returned. */ - static final ImmutableMap DOMAIN_CONTACTS_HIDDEN_DATA_REMARK = - ImmutableMap.of( - "title", - "Contacts Hidden", - "description", - ImmutableList.of("Domain contacts are visible only to the owning registrar."), - "type", - "object truncated due to unexplainable reasons"); + static final Remark DOMAIN_CONTACTS_HIDDEN_DATA_REMARK = + Remark.builder() + .setTitle("Contacts Hidden") + .setDescription("Domain contacts are visible only to the owning registrar.") + .setType(Remark.Type.OBJECT_TRUNCATED_UNEXPLAINABLE) + .build(); /** * Included when requester is not logged in as the owner of the contact being returned. Format - * required by ICANN RDAP Pilot Profile draft section 1.4.11. + * required by ICANN RDAP Response Profile 15feb19 section 2.7.4.3. */ - static final ImmutableMap CONTACT_PERSONAL_DATA_HIDDEN_DATA_REMARK = - ImmutableMap.of( - "title", - "Data Policy", - "description", - ImmutableList.of( + static final Remark CONTACT_PERSONAL_DATA_HIDDEN_DATA_REMARK = + Remark.builder() + .setTitle("Redacted for Privacy") + .setDescription( "Some of the data in this object has been removed.", - "Contact personal data is visible only to the owning registrar."), - "type", - "object truncated due to authorization", - "links", - ImmutableList.of( - ImmutableMap.of( - "value", - "https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication", - "rel", "alternate", - "href", - "https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication", - "type", "text/html"))); + "Contact personal data is visible only to the owning registrar.") + .setType(Remark.Type.OBJECT_REDACTED_AUTHORIZATION) + .addLink( + Link.builder() + .setValue( + "https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication") + .setRel("alternate") + .setHref( + "https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication") + .setType("text/html") + .build()) + .build(); } diff --git a/java/google/registry/rdap/RdapIpAction.java b/java/google/registry/rdap/RdapIpAction.java index 15ce2b3b0..590fedd76 100644 --- a/java/google/registry/rdap/RdapIpAction.java +++ b/java/google/registry/rdap/RdapIpAction.java @@ -17,8 +17,8 @@ package google.registry.rdap; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; -import com.google.common.collect.ImmutableMap; import google.registry.rdap.RdapMetrics.EndpointType; +import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase; import google.registry.request.Action; import google.registry.request.HttpException.NotImplementedException; import google.registry.request.auth.Auth; @@ -43,8 +43,7 @@ public class RdapIpAction extends RdapActionBase { } @Override - public ImmutableMap getJsonObjectForResource( - String pathSearchString, boolean isHeadRequest) { + public ReplyPayloadBase getJsonObjectForResource(String pathSearchString, boolean isHeadRequest) { throw new NotImplementedException("Domain Name Registry information only"); } } diff --git a/java/google/registry/rdap/RdapJsonFormatter.java b/java/google/registry/rdap/RdapJsonFormatter.java index 921876f90..a59b1b649 100644 --- a/java/google/registry/rdap/RdapJsonFormatter.java +++ b/java/google/registry/rdap/RdapJsonFormatter.java @@ -17,11 +17,9 @@ package google.registry.rdap; import static com.google.common.base.Predicates.not; import static com.google.common.base.Strings.nullToEmpty; import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet; import static google.registry.model.EppResourceUtils.isLinked; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.util.CollectionUtils.union; -import static google.registry.util.DomainNameUtils.ACE_PREFIX; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -30,8 +28,8 @@ import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.common.collect.Streams; import com.google.common.net.InetAddresses; +import com.google.gson.JsonArray; import com.googlecode.objectify.Key; -import google.registry.config.RdapNoticeDescriptor; import google.registry.config.RegistryConfig.Config; import google.registry.model.EppResource; import google.registry.model.contact.ContactPhoneNumber; @@ -47,22 +45,32 @@ import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; import google.registry.model.registrar.RegistrarContact; import google.registry.model.reporting.HistoryEntry; +import google.registry.rdap.RdapDataStructures.Event; +import google.registry.rdap.RdapDataStructures.EventAction; +import google.registry.rdap.RdapDataStructures.Link; +import google.registry.rdap.RdapDataStructures.Notice; +import google.registry.rdap.RdapDataStructures.Port43WhoisServer; +import google.registry.rdap.RdapDataStructures.PublicId; +import google.registry.rdap.RdapDataStructures.RdapStatus; +import google.registry.rdap.RdapObjectClasses.RdapDomain; +import google.registry.rdap.RdapObjectClasses.RdapEntity; +import google.registry.rdap.RdapObjectClasses.RdapNameserver; +import google.registry.rdap.RdapObjectClasses.Vcard; +import google.registry.rdap.RdapObjectClasses.VcardArray; +import google.registry.request.FullServletPath; import google.registry.request.HttpException.InternalServerErrorException; -import google.registry.request.HttpException.NotFoundException; -import google.registry.util.Idn; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.URI; +import java.nio.file.Paths; import java.util.HashMap; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.stream.Stream; import javax.annotation.Nullable; import javax.inject.Inject; -import javax.inject.Singleton; import org.joda.time.DateTime; import org.joda.time.DateTimeComparator; @@ -77,11 +85,12 @@ import org.joda.time.DateTimeComparator; * @see * RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP) */ -@Singleton public class RdapJsonFormatter { - @Inject @Config("rdapTosPath") String rdapTosPath; - @Inject @Config("rdapHelpMap") ImmutableMap rdapHelpMap; + + @Inject @Config("rdapTos") ImmutableList rdapTos; + @Inject @Config("rdapTosStaticUrl") @Nullable String rdapTosStaticUrl; + @Inject @FullServletPath String fullServletPath; @Inject RdapJsonFormatter() {} /** @@ -100,78 +109,6 @@ public class RdapJsonFormatter { SUMMARY } - /** - * Indication of what type of boilerplate notices are required for the RDAP JSON messages. The - * ICANN RDAP Profile specifies that, for instance, domain name responses should include a remark - * about domain status codes. So we need to know when to include such boilerplate. On the other - * hand, remarks are not allowed except in domain, nameserver and entity objects, so we need to - * suppress them for other types of responses (e.g. help). - */ - public enum BoilerplateType { - DOMAIN, - NAMESERVER, - ENTITY, - OTHER - } - - private static final String RDAP_CONFORMANCE_LEVEL = "icann_rdap_response_profile_0"; - private static final String VCARD_VERSION_NUMBER = "4.0"; - static final String NOTICES = "notices"; - private static final String REMARKS = "remarks"; - - private enum RdapStatus { - - // Status values specified in RFC 7483 § 10.2.2. - VALIDATED("validated"), - RENEW_PROHIBITED("renew prohibited"), - UPDATE_PROHIBITED("update prohibited"), - TRANSFER_PROHIBITED("transfer prohibited"), - DELETE_PROHIBITED("delete prohibited"), - PROXY("proxy"), - PRIVATE("private"), - REMOVED("removed"), - OBSCURED("obscured"), - ASSOCIATED("associated"), - ACTIVE("active"), - INACTIVE("inactive"), - LOCKED("locked"), - PENDING_CREATE("pending create"), - PENDING_RENEW("pending renew"), - PENDING_TRANSFER("pending transfer"), - PENDING_UPDATE("pending update"), - PENDING_DELETE("pending delete"), - - // Additional status values defined in - // https://tools.ietf.org/html/draft-ietf-regext-epp-rdap-status-mapping-01. - ADD_PERIOD("add period"), - AUTO_RENEW_PERIOD("auto renew period"), - CLIENT_DELETE_PROHIBITED("client delete prohibited"), - CLIENT_HOLD("client hold"), - CLIENT_RENEW_PROHIBITED("client renew prohibited"), - CLIENT_TRANSFER_PROHIBITED("client transfer prohibited"), - CLIENT_UPDATE_PROHIBITED("client update prohibited"), - PENDING_RESTORE("pending restore"), - REDEMPTION_PERIOD("redemption period"), - RENEW_PERIOD("renew period"), - SERVER_DELETE_PROHIBITED("server deleted prohibited"), - SERVER_RENEW_PROHIBITED("server renew prohibited"), - SERVER_TRANSFER_PROHIBITED("server transfer prohibited"), - SERVER_UPDATE_PROHIBITED("server update prohibited"), - SERVER_HOLD("server hold"), - TRANSFER_PERIOD("transfer period"); - - /** Value as it appears in RDAP messages. */ - private final String rfc7483String; - - RdapStatus(String rfc7483String) { - this.rfc7483String = rfc7483String; - } - - public String getDisplayName() { - return rfc7483String; - } - } - /** Map of EPP status values to the RDAP equivalents. */ private static final ImmutableMap STATUS_TO_RDAP_STATUS_MAP = new ImmutableMap.Builder() @@ -201,83 +138,48 @@ public class RdapJsonFormatter { // RdapStatus.TRANSFER_PERIOD not defined in our system .build(); - /** Role values specified in RFC 7483 § 10.2.4. */ - private enum RdapEntityRole { - REGISTRANT("registrant"), - TECH("technical"), - ADMIN("administrative"), - ABUSE("abuse"), - BILLING("billing"), - REGISTRAR("registrar"), - RESELLER("reseller"), - SPONSOR("sponsor"), - PROXY("proxy"), - NOTIFICATIONS("notifications"), - NOC("noc"); - - /** Value as it appears in RDAP messages. */ - final String rfc7483String; - - RdapEntityRole(String rfc7483String) { - this.rfc7483String = rfc7483String; - } - } - - /** Status values specified in RFC 7483 § 10.2.2. */ - private enum RdapEventAction { - REGISTRATION("registration"), - REREGISTRATION("reregistration"), - LAST_CHANGED("last changed"), - EXPIRATION("expiration"), - DELETION("deletion"), - REINSTANTIATION("reinstantiation"), - TRANSFER("transfer"), - LOCKED("locked"), - UNLOCKED("unlocked"), - LAST_UPDATE_OF_RDAP_DATABASE("last update of RDAP database"); - - /** Value as it appears in RDAP messages. */ - private final String rfc7483String; - - RdapEventAction(String rfc7483String) { - this.rfc7483String = rfc7483String; - } - - public String getDisplayName() { - return rfc7483String; - } - } - - /** Map of EPP event values to the RDAP equivalents. */ - private static final ImmutableMap + /** + * Map of EPP event values to the RDAP equivalents. + * + *

Only has entries for the events we care about, according to the RDAP Response Profile + * 15feb19. + * + * There are additional events that don't have HistoryEntry equivalent and are created + * differently. They will be in different locations in the code. These values are: EXPIRATION, + * LAST_CHANGED, LAST_UPDATE_OF_RDAP_DATABASE. + */ + private static final ImmutableMap HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP = - new ImmutableMap.Builder() - .put(HistoryEntry.Type.CONTACT_CREATE, RdapEventAction.REGISTRATION) - .put(HistoryEntry.Type.CONTACT_DELETE, RdapEventAction.DELETION) - .put(HistoryEntry.Type.CONTACT_TRANSFER_APPROVE, RdapEventAction.TRANSFER) - .put(HistoryEntry.Type.DOMAIN_AUTORENEW, RdapEventAction.REREGISTRATION) - .put(HistoryEntry.Type.DOMAIN_CREATE, RdapEventAction.REGISTRATION) - .put(HistoryEntry.Type.DOMAIN_DELETE, RdapEventAction.DELETION) - .put(HistoryEntry.Type.DOMAIN_RENEW, RdapEventAction.REREGISTRATION) - .put(HistoryEntry.Type.DOMAIN_RESTORE, RdapEventAction.REINSTANTIATION) - .put(HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE, RdapEventAction.TRANSFER) - .put(HistoryEntry.Type.HOST_CREATE, RdapEventAction.REGISTRATION) - .put(HistoryEntry.Type.HOST_DELETE, RdapEventAction.DELETION) + new ImmutableMap.Builder() + .put(HistoryEntry.Type.CONTACT_CREATE, EventAction.REGISTRATION) + .put(HistoryEntry.Type.CONTACT_DELETE, EventAction.DELETION) + .put(HistoryEntry.Type.CONTACT_TRANSFER_APPROVE, EventAction.TRANSFER) + + /** Not in the Response Profile. */ + .put(HistoryEntry.Type.DOMAIN_AUTORENEW, EventAction.REREGISTRATION) + /** Section 2.3.1.1, obligatory. */ + .put(HistoryEntry.Type.DOMAIN_CREATE, EventAction.REGISTRATION) + /** Not in the Response Profile. */ + .put(HistoryEntry.Type.DOMAIN_DELETE, EventAction.DELETION) + /** Not in the Response Profile. */ + .put(HistoryEntry.Type.DOMAIN_RENEW, EventAction.REREGISTRATION) + /** Not in the Response Profile. */ + .put(HistoryEntry.Type.DOMAIN_RESTORE, EventAction.REINSTANTIATION) + /** Section 2.3.2.3, optional. */ + .put(HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE, EventAction.TRANSFER) + + .put(HistoryEntry.Type.HOST_CREATE, EventAction.REGISTRATION) + .put(HistoryEntry.Type.HOST_DELETE, EventAction.DELETION) .build(); - private static final ImmutableList CONFORMANCE_LIST = - ImmutableList.of(RDAP_CONFORMANCE_LEVEL); - - private static final ImmutableList STATUS_LIST_ACTIVE = - ImmutableList.of(RdapStatus.ACTIVE.rfc7483String); - private static final ImmutableList STATUS_LIST_INACTIVE = - ImmutableList.of(RdapStatus.INACTIVE.rfc7483String); + private static final ImmutableList STATUS_LIST_ACTIVE = + ImmutableList.of(RdapStatus.ACTIVE); + private static final ImmutableList STATUS_LIST_INACTIVE = + ImmutableList.of(RdapStatus.INACTIVE); private static final ImmutableMap> PHONE_TYPE_VOICE = ImmutableMap.of("type", ImmutableList.of("voice")); private static final ImmutableMap> PHONE_TYPE_FAX = ImmutableMap.of("type", ImmutableList.of("fax")); - private static final ImmutableList VCARD_ENTRY_VERSION = - ImmutableList.of("version", ImmutableMap.of(), "text", VCARD_VERSION_NUMBER); /** Sets the ordering for hosts; just use the fully qualified host name. */ private static final Ordering HOST_RESOURCE_ORDERING = @@ -287,182 +189,29 @@ public class RdapJsonFormatter { private static final Ordering DESIGNATED_CONTACT_ORDERING = Ordering.natural().onResultOf(DesignatedContact::getType); - ImmutableMap getJsonTosNotice(String rdapLinkBase) { - return getJsonHelpNotice(rdapTosPath, rdapLinkBase); - } - - ImmutableMap getJsonHelpNotice( - String pathSearchString, String rdapLinkBase) { - if (pathSearchString.isEmpty()) { - pathSearchString = "/"; - } - if (!rdapHelpMap.containsKey(pathSearchString)) { - throw new NotFoundException("no help found for " + pathSearchString); - } - try { - return RdapJsonFormatter.makeRdapJsonNotice(rdapHelpMap.get(pathSearchString), rdapLinkBase); - } catch (Exception e) { - throw new InternalServerErrorException( - String.format("Error reading RDAP help file: %s", pathSearchString), e); - } - } - - /** - * Adds the required top-level boilerplate. RFC 7483 specifies that the top-level object should - * include an entry indicating the conformance level. The ICANN RDAP Profile document (dated 3 - * December 2015) mandates several additional entries, in sections 1.4.4, 1.4.10, 1.5.18 and - * 1.5.20. Note that this method will only work if there are no object-specific remarks already in - * the JSON object being built. If there are, the boilerplate must be merged in. - * - * @param jsonBuilder a builder for a JSON map object - * @param boilerplateType type of boilerplate to be added; the ICANN RDAP Profile document - * mandates extra boilerplate for domain objects - * @param notices a list of notices to be inserted before the boilerplate notices. If the TOS - * notice is in this list, the method avoids adding a second copy. - * @param remarks a list of remarks to be inserted. - * @param rdapLinkBase the base for link URLs - */ - void addTopLevelEntries( - ImmutableMap.Builder jsonBuilder, - BoilerplateType boilerplateType, - List> notices, - List> remarks, - String rdapLinkBase) { - jsonBuilder.put("rdapConformance", CONFORMANCE_LIST); - ImmutableList.Builder> noticesBuilder = - new ImmutableList.Builder<>(); - ImmutableMap tosNotice = getJsonTosNotice(rdapLinkBase); - boolean tosNoticeFound = false; - if (!notices.isEmpty()) { - noticesBuilder.addAll(notices); - for (ImmutableMap notice : notices) { - if (notice.equals(tosNotice)) { - tosNoticeFound = true; - break; - } - } - } - if (!tosNoticeFound) { - noticesBuilder.add(tosNotice); - } - switch (boilerplateType) { - case DOMAIN: - noticesBuilder.addAll(RdapIcannStandardInformation.domainBoilerplateNotices); - break; - case NAMESERVER: - case ENTITY: - noticesBuilder.addAll(RdapIcannStandardInformation.nameserverAndEntityBoilerplateNotices); - break; - default: // things other than domains, nameservers and entities do not yet have boilerplate - break; - } - jsonBuilder.put(NOTICES, noticesBuilder.build()); - if (!remarks.isEmpty()) { - jsonBuilder.put(REMARKS, remarks); - } - } - - /** - * Creates a JSON object containing a notice or remark object, as defined by RFC 7483 § 4.3. - * The object should then be inserted into a notices or remarks array. The builder fields are: - * - *

title: the title of the notice; if null, the notice will have no title - * - *

description: objects which will be converted to strings to form the description of the - * notice (this is the only required field; all others are optional) - * - *

typeString: the notice or remark type as defined in § 10.2.1; if null, no type - * - *

linkValueSuffix: the path at the end of the URL used in the value field of the link, - * without any initial slash (e.g. a suffix of help/toc equates to a URL of - * http://example.net/help/toc); if null, no link is created; if it is not null, a single link is - * created; this method never creates more than one link) - * - *

htmlUrlString: the path, if any, to be used in the href value of the link; if the URL is - * absolute, it is used as is; if it is relative, starting with a slash, it is appended to the - * protocol and host of the link base; if it is relative, not starting with a slash, it is - * appended to the complete link base; if null, a self link is generated instead, using the link - * link value - * - *

linkBase: the base for the link value and href; if null, it is assumed to be the empty - * string - * - * @see - * RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP) - */ - static ImmutableMap makeRdapJsonNotice( - RdapNoticeDescriptor parameters, @Nullable String linkBase) { - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - if (parameters.getTitle() != null) { - jsonBuilder.put("title", parameters.getTitle()); - } - ImmutableList.Builder descriptionBuilder = new ImmutableList.Builder<>(); - for (String line : parameters.getDescription()) { - descriptionBuilder.add(nullToEmpty(line)); - } - jsonBuilder.put("description", descriptionBuilder.build()); - if (parameters.getTypeString() != null) { - jsonBuilder.put("typeString", parameters.getTypeString()); - } - String linkBaseNotNull = nullToEmpty(linkBase); - String linkValueSuffixNotNull = nullToEmpty(parameters.getLinkValueSuffix()); - String linkValueString = - String.format( - "%s%s%s", - linkBaseNotNull, - (linkBaseNotNull.endsWith("/") || linkValueSuffixNotNull.startsWith("/")) ? "" : "/", - linkValueSuffixNotNull); - if (parameters.getLinkHrefUrlString() == null) { - jsonBuilder.put("links", ImmutableList.of(ImmutableMap.of( - "value", linkValueString, - "rel", "self", - "href", linkValueString, - "type", "application/rdap+json"))); + /** Creates the TOS notice that is added to every reply. */ + Notice createTosNotice() { + String linkValue = makeRdapServletRelativeUrl("help", RdapHelpAction.TOS_PATH); + Link.Builder linkBuilder = Link.builder() + .setValue(linkValue); + if (rdapTosStaticUrl == null) { + linkBuilder.setRel("self").setHref(linkValue).setType("application/rdap+json"); } else { - URI htmlBaseURI = URI.create(nullToEmpty(linkBase)); - URI htmlUri = htmlBaseURI.resolve(parameters.getLinkHrefUrlString()); - jsonBuilder.put("links", ImmutableList.of(ImmutableMap.of( - "value", linkValueString, - "rel", "alternate", - "href", htmlUri.toString(), - "type", "text/html"))); + URI htmlBaseURI = URI.create(fullServletPath); + URI htmlUri = htmlBaseURI.resolve(rdapTosStaticUrl); + linkBuilder.setRel("alternate").setHref(htmlUri.toString()).setType("text/html"); } - return jsonBuilder.build(); - } - - /** - * Creates a JSON object containing a notice with a next page navigation link, which can then be - * inserted into a notices array. - * - *

At the moment, only a next page link is supported. Other types of links (e.g. previous page) - * could be added in the future, but it's not clear how to generate such links, given the way we - * are querying the database. - * - * @param nextPageUrl URL string used to navigate to next page, or empty if there is no next - */ - static ImmutableMap makeRdapJsonNavigationLinkNotice( - Optional nextPageUrl) { - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("title", "Navigation Links"); - jsonBuilder.put("description", ImmutableList.of("Links to related pages.")); - if (nextPageUrl.isPresent()) { - jsonBuilder.put( - "links", - ImmutableList.of( - ImmutableMap.of( - "rel", "next", - "href", nextPageUrl.get(), - "type", "application/rdap+json"))); - } - return jsonBuilder.build(); + return Notice.builder() + .setTitle("RDAP Terms of Service") + .setDescription(rdapTos) + .addLink(linkBuilder.build()) + .build(); } /** * Creates a JSON object for a {@link DomainBase}. * * @param domainBase the domain resource object from which the JSON object should be created - * @param isTopLevel if true, the top-level boilerplate will be added - * @param linkBase the URL base to be used when creating links * @param whoisServer the fully-qualified domain name of the WHOIS server to be listed in the * port43 field; if null, port43 is not added to the object * @param now the as-date @@ -470,174 +219,123 @@ public class RdapJsonFormatter { * @param authorization the authorization level of the request; if not authorized for the * registrar owning the domain, no contact information is included */ - ImmutableMap makeRdapJsonForDomain( + RdapDomain makeRdapJsonForDomain( DomainBase domainBase, - boolean isTopLevel, - @Nullable String linkBase, @Nullable String whoisServer, DateTime now, OutputDataType outputDataType, RdapAuthorization authorization) { - // Start with the domain-level information. - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("objectClassName", "domain"); - jsonBuilder.put("handle", domainBase.getRepoId()); - jsonBuilder.put("ldhName", domainBase.getFullyQualifiedDomainName()); - // Only include the unicodeName field if there are unicode characters. - if (hasUnicodeComponents(domainBase.getFullyQualifiedDomainName())) { - jsonBuilder.put("unicodeName", Idn.toUnicode(domainBase.getFullyQualifiedDomainName())); - } - jsonBuilder.put( - "status", + RdapDomain.Builder builder = RdapDomain.builder(); + // RDAP Response Profile 15feb19 section 2.2: + // The domain handle MUST be the ROID + builder.setHandle(domainBase.getRepoId()); + builder.setLdhName(domainBase.getFullyQualifiedDomainName()); + builder.statusBuilder().addAll( makeStatusValueList( domainBase.getStatusValues(), false, // isRedacted domainBase.getDeletionTime().isBefore(now))); - jsonBuilder.put("links", ImmutableList.of( - makeLink("domain", domainBase.getFullyQualifiedDomainName(), linkBase))); + builder.linksBuilder().add( + makeSelfLink("domain", domainBase.getFullyQualifiedDomainName())); boolean displayContacts = authorization.isAuthorizedForClientId(domainBase.getCurrentSponsorClientId()); // If we are outputting all data (not just summary data), also add information about hosts, // contacts and events (history entries). If we are outputting summary data, instead add a // remark indicating that fact. - List> remarks; if (outputDataType == OutputDataType.SUMMARY) { - remarks = ImmutableList.of(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); + builder.remarksBuilder().add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); } else { - remarks = displayContacts - ? ImmutableList.of() - : ImmutableList.of(RdapIcannStandardInformation.DOMAIN_CONTACTS_HIDDEN_DATA_REMARK); - ImmutableList events = makeEvents(domainBase, now); - if (!events.isEmpty()) { - jsonBuilder.put("events", events); - } + ImmutableList events = makeEvents(domainBase, now); + builder.eventsBuilder().addAll(events); // Kick off the database loads of the nameservers that we will need, so it can load // asynchronously while we load and process the contacts. Map, HostResource> loadedHosts = ofy().load().keys(domainBase.getNameservers()); // Load the registrant and other contacts and add them to the data. - ImmutableList> entities; if (!displayContacts) { - entities = ImmutableList.of(); + builder + .remarksBuilder() + .add(RdapIcannStandardInformation.DOMAIN_CONTACTS_HIDDEN_DATA_REMARK); } else { Map, ContactResource> loadedContacts = ofy().load().keys(domainBase.getReferencedContacts()); - entities = - Streams.concat( - domainBase.getContacts().stream(), - Stream.of( - DesignatedContact.create(Type.REGISTRANT, domainBase.getRegistrant()))) - .sorted(DESIGNATED_CONTACT_ORDERING) - .map( - designatedContact -> - makeRdapJsonForContact( - loadedContacts.get(designatedContact.getContactKey()), - false, - Optional.of(designatedContact.getType()), - linkBase, - null, - now, - outputDataType, - authorization)) - .collect(toImmutableList()); - } - entities = - addRegistrarEntity( - entities, domainBase.getCurrentSponsorClientId(), linkBase, whoisServer, now); - if (!entities.isEmpty()) { - jsonBuilder.put("entities", entities); + Streams.concat( + domainBase.getContacts().stream(), + Stream.of( + DesignatedContact.create(Type.REGISTRANT, domainBase.getRegistrant()))) + .sorted(DESIGNATED_CONTACT_ORDERING) + .map( + designatedContact -> + makeRdapJsonForContact( + loadedContacts.get(designatedContact.getContactKey()), + Optional.of(designatedContact.getType()), + null, + now, + outputDataType, + authorization)) + .forEach(builder.entitiesBuilder()::add); } + builder + .entitiesBuilder() + .add( + createInternalRegistrarEntity( + domainBase.getCurrentSponsorClientId(), whoisServer, now)); // Add the nameservers to the data; the load was kicked off above for efficiency. - ImmutableList.Builder nsBuilder = new ImmutableList.Builder<>(); for (HostResource hostResource : HOST_RESOURCE_ORDERING.immutableSortedCopy(loadedHosts.values())) { - nsBuilder.add(makeRdapJsonForHost( - hostResource, false, linkBase, null, now, outputDataType)); - } - ImmutableList ns = nsBuilder.build(); - if (!ns.isEmpty()) { - jsonBuilder.put("nameservers", ns); + builder.nameserversBuilder().add(makeRdapJsonForHost( + hostResource, null, now, outputDataType)); } } if (whoisServer != null) { - jsonBuilder.put("port43", whoisServer); + builder.setPort43(Port43WhoisServer.create(whoisServer)); } - if (isTopLevel) { - addTopLevelEntries( - jsonBuilder, - BoilerplateType.DOMAIN, - remarks, - ImmutableList.of(), linkBase); - } else if (!remarks.isEmpty()) { - jsonBuilder.put(REMARKS, remarks); - } - return jsonBuilder.build(); + return builder.build(); } /** - * Adds a JSON object for the desired registrar to an existing list of JSON objects. + * Creates a JSON object for the desired registrar to an existing list of JSON objects. * - * @param entities list of entities to which the desired registrar should be added * @param clientId the registrar client ID - * @param linkBase the URL base to be used when creating links * @param whoisServer the fully-qualified domain name of the WHOIS server to be listed in the * port43 field; if null, port43 is not added to the object * @param now the as-date */ - ImmutableList> addRegistrarEntity( - ImmutableList> entities, - @Nullable String clientId, - @Nullable String linkBase, + RdapEntity createInternalRegistrarEntity( + String clientId, @Nullable String whoisServer, DateTime now) { - if (clientId == null) { - return entities; - } Optional registrar = Registrar.loadByClientIdCached(clientId); if (!registrar.isPresent()) { - return entities; + throw new InternalServerErrorException( + String.format("Coudn't find registrar '%s'", clientId)); } - ImmutableList.Builder> builder = new ImmutableList.Builder<>(); - builder.addAll(entities); // TODO(b/130150723): we need to display the ABUSE contact for registrar object inside of Domain - // responses. Currently, we use summary for any "internal" registrar. - builder.add( - makeRdapJsonForRegistrar( - registrar.get(), - false /* isTopLevel */, - linkBase, - whoisServer, - now, - OutputDataType.SUMMARY)); - return builder.build(); + // responses. Currently, we use summary for any "internal" registrar. + return makeRdapJsonForRegistrar( + registrar.get(), + whoisServer, + now, + OutputDataType.SUMMARY); } /** * Creates a JSON object for a {@link HostResource}. * * @param hostResource the host resource object from which the JSON object should be created - * @param isTopLevel if true, the top-level boilerplate will be added - * @param linkBase the URL base to be used when creating links * @param whoisServer the fully-qualified domain name of the WHOIS server to be listed in the * port43 field; if null, port43 is not added to the object * @param now the as-date * @param outputDataType whether to generate full or summary data */ - ImmutableMap makeRdapJsonForHost( + RdapNameserver makeRdapJsonForHost( HostResource hostResource, - boolean isTopLevel, - @Nullable String linkBase, @Nullable String whoisServer, DateTime now, OutputDataType outputDataType) { - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("objectClassName", "nameserver"); - jsonBuilder.put("handle", hostResource.getRepoId()); - jsonBuilder.put("ldhName", hostResource.getFullyQualifiedHostName()); - // Only include the unicodeName field if there are unicode characters. - if (hasUnicodeComponents(hostResource.getFullyQualifiedHostName())) { - jsonBuilder.put("unicodeName", Idn.toUnicode(hostResource.getFullyQualifiedHostName())); - } + RdapNameserver.Builder builder = RdapNameserver.builder() + .setHandle(hostResource.getRepoId()) + .setLdhName(hostResource.getFullyQualifiedHostName()); ImmutableSet.Builder statuses = new ImmutableSet.Builder<>(); statuses.addAll(hostResource.getStatusValues()); @@ -650,84 +348,48 @@ public class RdapJsonFormatter { .contains(StatusValue.PENDING_TRANSFER)) { statuses.add(StatusValue.PENDING_TRANSFER); } - jsonBuilder.put( - "status", - makeStatusValueList( - statuses.build(), - false, // isRedacted - hostResource.getDeletionTime().isBefore(now))); - jsonBuilder.put("links", ImmutableList.of( - makeLink("nameserver", hostResource.getFullyQualifiedHostName(), linkBase))); - List> remarks; + builder + .statusBuilder() + .addAll( + makeStatusValueList( + statuses.build(), + false, // isRedacted + hostResource.getDeletionTime().isBefore(now))); + builder + .linksBuilder() + .add(makeSelfLink("nameserver", hostResource.getFullyQualifiedHostName())); + // If we are outputting all data (not just summary data), also add events taken from the history // entries. If we are outputting summary data, instead add a remark indicating that fact. if (outputDataType == OutputDataType.SUMMARY) { - remarks = ImmutableList.of(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); + builder.remarksBuilder().add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); } else { - remarks = ImmutableList.of(); - ImmutableList events = makeEvents(hostResource, now); - if (!events.isEmpty()) { - jsonBuilder.put("events", events); + builder.eventsBuilder().addAll(makeEvents(hostResource, now)); + } + + // We MUST have the ip addresses: RDAP Response Profile 4.2. + for (InetAddress inetAddress : hostResource.getInetAddresses()) { + if (inetAddress instanceof Inet4Address) { + builder.ipv4Builder().add(InetAddresses.toAddrString(inetAddress)); + } else if (inetAddress instanceof Inet6Address) { + builder.ipv6Builder().add(InetAddresses.toAddrString(inetAddress)); } } - ImmutableSet inetAddresses = hostResource.getInetAddresses(); - if (!inetAddresses.isEmpty()) { - ImmutableList.Builder v4AddressesBuilder = new ImmutableList.Builder<>(); - ImmutableList.Builder v6AddressesBuilder = new ImmutableList.Builder<>(); - for (InetAddress inetAddress : inetAddresses) { - if (inetAddress instanceof Inet4Address) { - v4AddressesBuilder.add(InetAddresses.toAddrString(inetAddress)); - } else if (inetAddress instanceof Inet6Address) { - v6AddressesBuilder.add(InetAddresses.toAddrString(inetAddress)); - } - } - ImmutableMap.Builder> ipAddressesBuilder = - new ImmutableMap.Builder<>(); - ImmutableList v4Addresses = v4AddressesBuilder.build(); - if (!v4Addresses.isEmpty()) { - ipAddressesBuilder.put("v4", Ordering.natural().immutableSortedCopy(v4Addresses)); - } - ImmutableList v6Addresses = v6AddressesBuilder.build(); - if (!v6Addresses.isEmpty()) { - ipAddressesBuilder.put("v6", Ordering.natural().immutableSortedCopy(v6Addresses)); - } - ImmutableMap> ipAddresses = ipAddressesBuilder.build(); - if (!ipAddresses.isEmpty()) { - jsonBuilder.put("ipAddresses", ipAddressesBuilder.build()); - } - } - ImmutableList> entities = - addRegistrarEntity( - ImmutableList.of(), - hostResource.getPersistedCurrentSponsorClientId(), - linkBase, - whoisServer, - now); - if (!entities.isEmpty()) { - jsonBuilder.put("entities", entities); - } + builder.entitiesBuilder().add(createInternalRegistrarEntity( + hostResource.getPersistedCurrentSponsorClientId(), + whoisServer, + now)); if (whoisServer != null) { - jsonBuilder.put("port43", whoisServer); + builder.setPort43(Port43WhoisServer.create(whoisServer)); } - if (isTopLevel) { - addTopLevelEntries( - jsonBuilder, - BoilerplateType.NAMESERVER, - remarks, - ImmutableList.of(), linkBase); - } else if (!remarks.isEmpty()) { - jsonBuilder.put(REMARKS, remarks); - } - return jsonBuilder.build(); + return builder.build(); } /** * Creates a JSON object for a {@link ContactResource} and associated contact type. * * @param contactResource the contact resource object from which the JSON object should be created - * @param isTopLevel if true, the top-level boilerplate will be added * @param contactType the contact type to map to an RDAP role; if absent, no role is listed - * @param linkBase the URL base to be used when creating links * @param whoisServer the fully-qualified domain name of the WHOIS server to be listed in the * port43 field; if null, port43 is not added to the object * @param now the as-date @@ -735,53 +397,47 @@ public class RdapJsonFormatter { * @param authorization the authorization level of the request; personal contact data is only * shown if the contact is owned by a registrar for which the request is authorized */ - ImmutableMap makeRdapJsonForContact( + RdapEntity makeRdapJsonForContact( ContactResource contactResource, - boolean isTopLevel, Optional contactType, - @Nullable String linkBase, @Nullable String whoisServer, DateTime now, OutputDataType outputDataType, RdapAuthorization authorization) { boolean isAuthorized = authorization.isAuthorizedForClientId(contactResource.getCurrentSponsorClientId()); - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - ImmutableList.Builder> remarksBuilder - = new ImmutableList.Builder<>(); - jsonBuilder.put("objectClassName", "entity"); - jsonBuilder.put("handle", contactResource.getRepoId()); - jsonBuilder.put( - "status", - makeStatusValueList( - isLinked(Key.create(contactResource), now) - ? union(contactResource.getStatusValues(), StatusValue.LINKED) - : contactResource.getStatusValues(), - !isAuthorized, - contactResource.getDeletionTime().isBefore(now))); + + RdapEntity.Builder entityBuilder = + RdapEntity.builder() + .setHandle(contactResource.getRepoId()); + entityBuilder + .statusBuilder() + .addAll( + makeStatusValueList( + isLinked(Key.create(contactResource), now) + ? union(contactResource.getStatusValues(), StatusValue.LINKED) + : contactResource.getStatusValues(), + !isAuthorized, + contactResource.getDeletionTime().isBefore(now))); + contactType.ifPresent( - type -> jsonBuilder.put("roles", ImmutableList.of(convertContactTypeToRdapRole(type)))); - jsonBuilder.put("links", - ImmutableList.of(makeLink("entity", contactResource.getRepoId(), linkBase))); + type -> entityBuilder.rolesBuilder().add(convertContactTypeToRdapRole(type))); + entityBuilder.linksBuilder().add(makeSelfLink("entity", contactResource.getRepoId())); // If we are logged in as the owner of this contact, create the vCard. if (isAuthorized) { - ImmutableList.Builder vcardBuilder = new ImmutableList.Builder<>(); - vcardBuilder.add(VCARD_ENTRY_VERSION); + VcardArray.Builder vcardBuilder = VcardArray.builder(); PostalInfo postalInfo = contactResource.getInternationalizedPostalInfo(); if (postalInfo == null) { postalInfo = contactResource.getLocalizedPostalInfo(); } if (postalInfo != null) { if (postalInfo.getName() != null) { - vcardBuilder.add(ImmutableList.of("fn", ImmutableMap.of(), "text", postalInfo.getName())); + vcardBuilder.add(Vcard.create("fn", "text", postalInfo.getName())); } if (postalInfo.getOrg() != null) { - vcardBuilder.add(ImmutableList.of("org", ImmutableMap.of(), "text", postalInfo.getOrg())); - } - ImmutableList addressEntry = makeVCardAddressEntry(postalInfo.getAddress()); - if (addressEntry != null) { - vcardBuilder.add(addressEntry); + vcardBuilder.add(Vcard.create("org", "text", postalInfo.getOrg())); } + addVCardAddressEntry(vcardBuilder, postalInfo.getAddress()); } ContactPhoneNumber voicePhoneNumber = contactResource.getVoiceNumber(); if (voicePhoneNumber != null) { @@ -793,91 +449,66 @@ public class RdapJsonFormatter { } String emailAddress = contactResource.getEmailAddress(); if (emailAddress != null) { - vcardBuilder.add(ImmutableList.of("email", ImmutableMap.of(), "text", emailAddress)); + vcardBuilder.add(Vcard.create("email", "text", emailAddress)); } - jsonBuilder.put("vcardArray", ImmutableList.of("vcard", vcardBuilder.build())); + entityBuilder.setVcardArray(vcardBuilder.build()); } else { - remarksBuilder.add(RdapIcannStandardInformation.CONTACT_PERSONAL_DATA_HIDDEN_DATA_REMARK); + entityBuilder + .remarksBuilder() + .add(RdapIcannStandardInformation.CONTACT_PERSONAL_DATA_HIDDEN_DATA_REMARK); } // If we are outputting all data (not just summary data), also add events taken from the history // entries. If we are outputting summary data, instead add a remark indicating that fact. if (outputDataType == OutputDataType.SUMMARY) { - remarksBuilder.add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); + entityBuilder.remarksBuilder().add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); } else { - ImmutableList events = makeEvents(contactResource, now); - if (!events.isEmpty()) { - jsonBuilder.put("events", events); - } + entityBuilder.eventsBuilder().addAll(makeEvents(contactResource, now)); } if (whoisServer != null) { - jsonBuilder.put("port43", whoisServer); + entityBuilder.setPort43(Port43WhoisServer.create(whoisServer)); } - if (isTopLevel) { - addTopLevelEntries( - jsonBuilder, - BoilerplateType.ENTITY, - remarksBuilder.build(), - ImmutableList.of(), - linkBase); - } else { - ImmutableList> remarks = remarksBuilder.build(); - if (!remarks.isEmpty()) { - jsonBuilder.put(REMARKS, remarks); - } - } - return jsonBuilder.build(); + return entityBuilder.build(); } /** * Creates a JSON object for a {@link Registrar}. * * @param registrar the registrar object from which the JSON object should be created - * @param isTopLevel if true, the top-level boilerplate will be added - * @param linkBase the URL base to be used when creating links * @param whoisServer the fully-qualified domain name of the WHOIS server to be listed in the * port43 field; if null, port43 is not added to the object * @param now the as-date * @param outputDataType whether to generate full or summary data */ - ImmutableMap makeRdapJsonForRegistrar( + RdapEntity makeRdapJsonForRegistrar( Registrar registrar, - boolean isTopLevel, - @Nullable String linkBase, @Nullable String whoisServer, DateTime now, OutputDataType outputDataType) { - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("objectClassName", "entity"); + RdapEntity.Builder builder = RdapEntity.builder(); Long ianaIdentifier = registrar.getIanaIdentifier(); - jsonBuilder.put("handle", (ianaIdentifier == null) ? "(none)" : ianaIdentifier.toString()); - jsonBuilder.put("status", registrar.isLive() ? STATUS_LIST_ACTIVE : STATUS_LIST_INACTIVE); - jsonBuilder.put("roles", ImmutableList.of(RdapEntityRole.REGISTRAR.rfc7483String)); + // the handle MUST be the ianaIdentifier, RDAP Response Profile 2.4.2. + builder.setHandle((ianaIdentifier == null) ? "(none)" : ianaIdentifier.toString()); + builder.statusBuilder().addAll(registrar.isLive() ? STATUS_LIST_ACTIVE : STATUS_LIST_INACTIVE); + builder.rolesBuilder().add(RdapEntity.Role.REGISTRAR); if (ianaIdentifier != null) { - jsonBuilder.put("links", - ImmutableList.of(makeLink("entity", ianaIdentifier.toString(), linkBase))); - jsonBuilder.put( - "publicIds", - ImmutableList.of( - ImmutableMap.of( - "type", "IANA Registrar ID", "identifier", ianaIdentifier.toString()))); + builder.linksBuilder().add(makeSelfLink("entity", ianaIdentifier.toString())); + // We MUST have a publicId with the ianaIdentifier, RDAP Response Profile 2.4.3, 4.3 + builder + .publicIdsBuilder() + .add(PublicId.create(PublicId.Type.IANA_REGISTRAR_ID, ianaIdentifier.toString())); } // Create the vCard. - ImmutableList.Builder vcardBuilder = new ImmutableList.Builder<>(); - vcardBuilder.add(VCARD_ENTRY_VERSION); + VcardArray.Builder vcardBuilder = VcardArray.builder(); String registrarName = registrar.getRegistrarName(); if (registrarName != null) { - vcardBuilder.add(ImmutableList.of("fn", ImmutableMap.of(), "text", registrarName)); + // A valid fn member MUST be present: RDAP Response Profile 2.4.1. + vcardBuilder.add(Vcard.create("fn", "text", registrarName)); } RegistrarAddress address = registrar.getInternationalizedAddress(); if (address == null) { address = registrar.getLocalizedAddress(); } - if (address != null) { - ImmutableList addressEntry = makeVCardAddressEntry(address); - if (addressEntry != null) { - vcardBuilder.add(addressEntry); - } - } + addVCardAddressEntry(vcardBuilder, address); String voicePhoneNumber = registrar.getPhoneNumber(); if (voicePhoneNumber != null) { vcardBuilder.add(makePhoneEntry(PHONE_TYPE_VOICE, "tel:" + voicePhoneNumber)); @@ -888,77 +519,55 @@ public class RdapJsonFormatter { } String emailAddress = registrar.getEmailAddress(); if (emailAddress != null) { - vcardBuilder.add(ImmutableList.of("email", ImmutableMap.of(), "text", emailAddress)); + vcardBuilder.add(Vcard.create("email", "text", emailAddress)); } - jsonBuilder.put("vcardArray", ImmutableList.of("vcard", vcardBuilder.build())); + builder.setVcardArray(vcardBuilder.build()); // If we are outputting all data (not just summary data), also add registrar contacts. If we are // outputting summary data, instead add a remark indicating that fact. - List> remarks; if (outputDataType == OutputDataType.SUMMARY) { - remarks = ImmutableList.of(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); + builder.remarksBuilder().add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); } else { - remarks = ImmutableList.of(); - ImmutableList events = makeEvents(registrar, now); - if (!events.isEmpty()) { - jsonBuilder.put("events", events); - } + builder.eventsBuilder().addAll(makeEvents(registrar, now)); // include the registrar contacts as subentities - ImmutableList> registrarContacts = + ImmutableList registrarContacts = registrar.getContacts().stream() - .map(registrarContact -> makeRdapJsonForRegistrarContact(registrarContact, null)) - .filter(entity -> !entity.isEmpty()) - .collect(toImmutableList()); + .map(registrarContact -> makeRdapJsonForRegistrarContact(registrarContact, null)) + .filter(optional -> optional.isPresent()) + .map(optional -> optional.get()) + .collect(toImmutableList()); // TODO(b/117242274): add a warning (severe?) log if registrar has no ABUSE contact, as having // one is required by the RDAP response profile - if (!registrarContacts.isEmpty()) { - jsonBuilder.put("entities", registrarContacts); - } + builder.entitiesBuilder().addAll(registrarContacts); } if (whoisServer != null) { - jsonBuilder.put("port43", whoisServer); + builder.setPort43(Port43WhoisServer.create(whoisServer)); } - if (isTopLevel) { - addTopLevelEntries( - jsonBuilder, - BoilerplateType.ENTITY, - remarks, - ImmutableList.of(), - linkBase); - } else if (!remarks.isEmpty()) { - jsonBuilder.put(REMARKS, remarks); - } - return jsonBuilder.build(); + return builder.build(); } /** * Creates a JSON object for a {@link RegistrarContact}. * - *

Returns an empty object if this contact shouldn't be visible (doesn't have a role). + *

Returns empty if this contact shouldn't be visible (doesn't have a role). * * @param registrarContact the registrar contact for which the JSON object should be created * @param whoisServer the fully-qualified domain name of the WHOIS server to be listed in the * port43 field; if null, port43 is not added to the object */ - static ImmutableMap makeRdapJsonForRegistrarContact( + static Optional makeRdapJsonForRegistrarContact( RegistrarContact registrarContact, @Nullable String whoisServer) { - ImmutableList roles = makeRdapRoleList(registrarContact); + ImmutableList roles = makeRdapRoleList(registrarContact); if (roles.isEmpty()) { - return ImmutableMap.of(); + return Optional.empty(); } - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("objectClassName", "entity"); - String gaeUserId = registrarContact.getGaeUserId(); - if (gaeUserId != null) { - jsonBuilder.put("handle", registrarContact.getGaeUserId()); - } - jsonBuilder.put("status", STATUS_LIST_ACTIVE); - jsonBuilder.put("roles", roles); + RdapEntity.Builder builder = RdapEntity.builder(); + builder.statusBuilder().addAll(STATUS_LIST_ACTIVE); + builder.rolesBuilder().addAll(roles); // Create the vCard. - ImmutableList.Builder vcardBuilder = new ImmutableList.Builder<>(); - vcardBuilder.add(VCARD_ENTRY_VERSION); + VcardArray.Builder vcardBuilder = VcardArray.builder(); String name = registrarContact.getName(); if (name != null) { - vcardBuilder.add(ImmutableList.of("fn", ImmutableMap.of(), "text", name)); + vcardBuilder.add(Vcard.create("fn", "text", name)); } String voicePhoneNumber = registrarContact.getPhoneNumber(); if (voicePhoneNumber != null) { @@ -970,26 +579,26 @@ public class RdapJsonFormatter { } String emailAddress = registrarContact.getEmailAddress(); if (emailAddress != null) { - vcardBuilder.add(ImmutableList.of("email", ImmutableMap.of(), "text", emailAddress)); + vcardBuilder.add(Vcard.create("email", "text", emailAddress)); } - jsonBuilder.put("vcardArray", ImmutableList.of("vcard", vcardBuilder.build())); + builder.setVcardArray(vcardBuilder.build()); if (whoisServer != null) { - jsonBuilder.put("port43", whoisServer); + builder.setPort43(Port43WhoisServer.create(whoisServer)); } - return jsonBuilder.build(); + return Optional.of(builder.build()); } /** Converts a domain registry contact type into a role as defined by RFC 7483. */ - private static String convertContactTypeToRdapRole(DesignatedContact.Type contactType) { + private static RdapEntity.Role convertContactTypeToRdapRole(DesignatedContact.Type contactType) { switch (contactType) { case REGISTRANT: - return RdapEntityRole.REGISTRANT.rfc7483String; + return RdapEntity.Role.REGISTRANT; case TECH: - return RdapEntityRole.TECH.rfc7483String; + return RdapEntity.Role.TECH; case BILLING: - return RdapEntityRole.BILLING.rfc7483String; + return RdapEntity.Role.BILLING; case ADMIN: - return RdapEntityRole.ADMIN.rfc7483String; + return RdapEntity.Role.ADMIN; default: throw new AssertionError(); } @@ -1006,16 +615,17 @@ public class RdapJsonFormatter { * *abuse* role within the registrar *entity* which MUST include *tel* and *email*, and MAY * include other members */ - private static ImmutableList makeRdapRoleList(RegistrarContact registrarContact) { - ImmutableList.Builder rolesBuilder = new ImmutableList.Builder<>(); + private static ImmutableList makeRdapRoleList( + RegistrarContact registrarContact) { + ImmutableList.Builder rolesBuilder = new ImmutableList.Builder<>(); if (registrarContact.getVisibleInWhoisAsAdmin()) { - rolesBuilder.add(RdapEntityRole.ADMIN.rfc7483String); + rolesBuilder.add(RdapEntity.Role.ADMIN); } if (registrarContact.getVisibleInWhoisAsTech()) { - rolesBuilder.add(RdapEntityRole.TECH.rfc7483String); + rolesBuilder.add(RdapEntity.Role.TECH); } if (registrarContact.getVisibleInDomainWhoisAsAbuse()) { - rolesBuilder.add(RdapEntityRole.ABUSE.rfc7483String); + rolesBuilder.add(RdapEntity.Role.ABUSE); } return rolesBuilder.build(); } @@ -1023,8 +633,8 @@ public class RdapJsonFormatter { /** * Creates an event list for a domain, host or contact resource. */ - private static ImmutableList makeEvents(EppResource resource, DateTime now) { - HashMap lastEntryOfType = Maps.newHashMap(); + private static ImmutableList makeEvents(EppResource resource, DateTime now) { + HashMap lastEntryOfType = Maps.newHashMap(); // Events (such as transfer, but also create) can appear multiple times. We only want the last // time they appeared. // @@ -1038,7 +648,7 @@ public class RdapJsonFormatter { // domain name has not been transferred since it was created. for (HistoryEntry historyEntry : ofy().load().type(HistoryEntry.class).ancestor(resource).order("modificationTime")) { - RdapEventAction rdapEventAction = + EventAction rdapEventAction = HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP.get(historyEntry.getType()); // Only save the historyEntries if this is a type we care about. if (rdapEventAction == null) { @@ -1046,7 +656,7 @@ public class RdapJsonFormatter { } lastEntryOfType.put(rdapEventAction, historyEntry); } - ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); + ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); // There are 2 possibly conflicting values for the creation time - either the // resource.getCreationTime, or the REGISTRATION event created from a HistoryEntry // @@ -1056,15 +666,15 @@ public class RdapJsonFormatter { // This is mostly an issue in the tests as in "reality" these two values should be the same. // DateTime creationTime = - Optional.ofNullable(lastEntryOfType.get(RdapEventAction.REGISTRATION)) + Optional.ofNullable(lastEntryOfType.get(EventAction.REGISTRATION)) .map(historyEntry -> historyEntry.getModificationTime()) .orElse(resource.getCreationTime()); // TODO(b/129849684) remove this and use the events List defined above once we have Event // objects ImmutableList.Builder changeTimesBuilder = new ImmutableList.Builder<>(); // The order of the elements is stable - it's the order in which the enum elements are defined - // in RdapEventAction - for (RdapEventAction rdapEventAction : RdapEventAction.values()) { + // in EventAction + for (EventAction rdapEventAction : EventAction.values()) { HistoryEntry historyEntry = lastEntryOfType.get(rdapEventAction); // Check if there was any entry of this type if (historyEntry == null) { @@ -1077,13 +687,22 @@ public class RdapJsonFormatter { if (modificationTime.isBefore(creationTime)) { continue; } - eventsBuilder.add(makeEvent(rdapEventAction, historyEntry.getClientId(), modificationTime)); + eventsBuilder.add( + Event.builder() + .setEventAction(rdapEventAction) + .setEventActor(historyEntry.getClientId()) + .setEventDate(modificationTime) + .build()); changeTimesBuilder.add(modificationTime); } if (resource instanceof DomainBase) { DateTime expirationTime = ((DomainBase) resource).getRegistrationExpirationTime(); if (expirationTime != null) { - eventsBuilder.add(makeEvent(RdapEventAction.EXPIRATION, null, expirationTime)); + eventsBuilder.add( + Event.builder() + .setEventAction(EventAction.EXPIRATION) + .setEventDate(expirationTime) + .build()); changeTimesBuilder.add(expirationTime); } } @@ -1098,9 +717,9 @@ public class RdapJsonFormatter { .max(DateTimeComparator.getInstance()) .orElse(null); if (lastChangeTime != null && lastChangeTime.isAfter(creationTime)) { - eventsBuilder.add(makeEvent(RdapEventAction.LAST_CHANGED, null, lastChangeTime)); + eventsBuilder.add(makeEvent(EventAction.LAST_CHANGED, null, lastChangeTime)); } - eventsBuilder.add(makeEvent(RdapEventAction.LAST_UPDATE_OF_RDAP_DATABASE, null, now)); + eventsBuilder.add(makeEvent(EventAction.LAST_UPDATE_OF_RDAP_DATABASE, null, now)); // TODO(b/129849684): sort events by their time once we return a list of Events instead of JSON // objects. return eventsBuilder.build(); @@ -1109,34 +728,36 @@ public class RdapJsonFormatter { /** * Creates an event list for a {@link Registrar}. */ - private static ImmutableList makeEvents(Registrar registrar, DateTime now) { - ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); + private static ImmutableList makeEvents(Registrar registrar, DateTime now) { + ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); Long ianaIdentifier = registrar.getIanaIdentifier(); eventsBuilder.add(makeEvent( - RdapEventAction.REGISTRATION, + EventAction.REGISTRATION, (ianaIdentifier == null) ? "(none)" : ianaIdentifier.toString(), registrar.getCreationTime())); if ((registrar.getLastUpdateTime() != null) && registrar.getLastUpdateTime().isAfter(registrar.getCreationTime())) { eventsBuilder.add(makeEvent( - RdapEventAction.LAST_CHANGED, null, registrar.getLastUpdateTime())); + EventAction.LAST_CHANGED, null, registrar.getLastUpdateTime())); } - eventsBuilder.add(makeEvent(RdapEventAction.LAST_UPDATE_OF_RDAP_DATABASE, null, now)); + eventsBuilder.add(makeEvent(EventAction.LAST_UPDATE_OF_RDAP_DATABASE, null, now)); return eventsBuilder.build(); } /** * Creates an RDAP event object as defined by RFC 7483. */ - private static ImmutableMap makeEvent( - RdapEventAction eventAction, @Nullable String eventActor, DateTime eventDate) { - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("eventAction", eventAction.getDisplayName()); + private static Event makeEvent( + EventAction eventAction, + @Nullable String eventActor, + DateTime eventDate) { + Event.Builder builder = Event.builder() + .setEventAction(eventAction) + .setEventDate(eventDate); if (eventActor != null) { - jsonBuilder.put("eventActor", eventActor); + builder.setEventActor(eventActor); } - jsonBuilder.put("eventDate", eventDate.toString()); - return jsonBuilder.build(); + return builder.build(); } /** @@ -1145,13 +766,13 @@ public class RdapJsonFormatter { * @see * RFC 7095: jCard: The JSON Format for vCard */ - private static ImmutableList makeVCardAddressEntry(Address address) { + private static void addVCardAddressEntry(VcardArray.Builder vcardArrayBuilder, Address address) { if (address == null) { - return null; + return; } - ImmutableList.Builder jsonBuilder = new ImmutableList.Builder<>(); - jsonBuilder.add(""); // PO box - jsonBuilder.add(""); // extended address + JsonArray addressArray = new JsonArray(); + addressArray.add(""); // PO box + addressArray.add(""); // extended address // The vCard spec allows several different ways to handle multiline street addresses. Per // Gustavo Lozano of ICANN, the one we should use is an embedded array of street address lines @@ -1181,27 +802,30 @@ public class RdapJsonFormatter { // according to RFC 7095. ImmutableList street = address.getStreet(); if (street.isEmpty()) { - jsonBuilder.add(""); + addressArray.add(""); } else if (street.size() == 1) { - jsonBuilder.add(street.get(0)); + addressArray.add(street.get(0)); } else { - jsonBuilder.add(street); + JsonArray streetArray = new JsonArray(); + street.forEach(streetArray::add); + addressArray.add(streetArray); } - jsonBuilder.add(nullToEmpty(address.getCity())); - jsonBuilder.add(nullToEmpty(address.getState())); - jsonBuilder.add(nullToEmpty(address.getZip())); - jsonBuilder.add(new Locale("en", address.getCountryCode()).getDisplayCountry(new Locale("en"))); - return ImmutableList.of( + addressArray.add(nullToEmpty(address.getCity())); + addressArray.add(nullToEmpty(address.getState())); + addressArray.add(nullToEmpty(address.getZip())); + addressArray.add( + new Locale("en", address.getCountryCode()).getDisplayCountry(new Locale("en"))); + vcardArrayBuilder.add(Vcard.create( "adr", - ImmutableMap.of(), "text", - jsonBuilder.build()); + addressArray)); } /** Creates a vCard phone number entry. */ - private static ImmutableList makePhoneEntry( + private static Vcard makePhoneEntry( ImmutableMap> type, String phoneNumber) { - return ImmutableList.of("tel", type, "uri", phoneNumber); + + return Vcard.create("tel", type, "uri", phoneNumber); } /** Creates a phone string in URI format, as per the vCard spec. */ @@ -1220,7 +844,7 @@ public class RdapJsonFormatter { * indicate deleted objects, and as directed by the profile, the "removed" status to indicate * redacted objects. */ - private static ImmutableList makeStatusValueList( + private static ImmutableList makeStatusValueList( ImmutableSet statusValues, boolean isRedacted, boolean isDeleted) { Stream stream = statusValues @@ -1236,51 +860,34 @@ public class RdapJsonFormatter { Stream.of(RdapStatus.INACTIVE)); } return stream - .map(RdapStatus::getDisplayName) - .collect(toImmutableSortedSet(Ordering.natural())) - .asList(); + .sorted(Ordering.natural().onResultOf(RdapStatus::getDisplayName)) + .collect(toImmutableList()); + } + + /** + * Create a link relative to the RDAP server endpoint. + */ + String makeRdapServletRelativeUrl(String part, String... moreParts) { + String relativePath = Paths.get(part, moreParts).toString(); + if (fullServletPath.endsWith("/")) { + return fullServletPath + relativePath; + } + return fullServletPath + "/" + relativePath; } /** * Creates a self link as directed by the spec. * - * @see - * RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP) + * @see RFC 7483: JSON Responses for the + * Registration Data Access Protocol (RDAP) */ - private static ImmutableMap makeLink( - String type, String name, @Nullable String linkBase) { - String url; - if (linkBase == null) { - url = type + '/' + name; - } else if (linkBase.endsWith("/")) { - url = linkBase + type + '/' + name; - } else { - url = linkBase + '/' + type + '/' + name; - } - return ImmutableMap.of( - "value", url, - "rel", "self", - "href", url, - "type", "application/rdap+json"); - } - - /** - * Creates a JSON error indication. - * - * @see - * RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP) - */ - ImmutableMap makeError(int status, String title, String description) { - return ImmutableMap.of( - "rdapConformance", CONFORMANCE_LIST, - "lang", "en", - "errorCode", (long) status, - "title", title, - "description", ImmutableList.of(description)); - } - - private static boolean hasUnicodeComponents(String fullyQualifiedName) { - return fullyQualifiedName.startsWith(ACE_PREFIX) - || fullyQualifiedName.contains("." + ACE_PREFIX); + private Link makeSelfLink(String type, String name) { + String url = makeRdapServletRelativeUrl(type, name); + return Link.builder() + .setValue(url) + .setRel("self") + .setHref(url) + .setType("application/rdap+json") + .build(); } } diff --git a/java/google/registry/rdap/RdapNameserverAction.java b/java/google/registry/rdap/RdapNameserverAction.java index 148945cd6..95592e133 100644 --- a/java/google/registry/rdap/RdapNameserverAction.java +++ b/java/google/registry/rdap/RdapNameserverAction.java @@ -20,11 +20,11 @@ import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; import static google.registry.util.DateTimeUtils.START_OF_TIME; -import com.google.common.collect.ImmutableMap; import google.registry.flows.EppException; import google.registry.model.host.HostResource; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.rdap.RdapMetrics.EndpointType; +import google.registry.rdap.RdapObjectClasses.RdapNameserver; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.NotFoundException; @@ -47,8 +47,7 @@ public class RdapNameserverAction extends RdapActionBase { } @Override - public ImmutableMap getJsonObjectForResource( - String pathSearchString, boolean isHeadRequest) { + public RdapNameserver getJsonObjectForResource(String pathSearchString, boolean isHeadRequest) { DateTime now = clock.nowUtc(); pathSearchString = canonicalizeName(pathSearchString); // The RDAP syntax is /rdap/nameserver/ns1.mydomain.com. @@ -69,6 +68,6 @@ public class RdapNameserverAction extends RdapActionBase { throw new NotFoundException(pathSearchString + " not found"); } return rdapJsonFormatter.makeRdapJsonForHost( - hostResource.get(), true, fullServletPath, rdapWhoisServer, now, OutputDataType.FULL); + hostResource.get(), rdapWhoisServer, now, OutputDataType.FULL); } } diff --git a/java/google/registry/rdap/RdapNameserverSearchAction.java b/java/google/registry/rdap/RdapNameserverSearchAction.java index 5f4f47602..96116fc08 100644 --- a/java/google/registry/rdap/RdapNameserverSearchAction.java +++ b/java/google/registry/rdap/RdapNameserverSearchAction.java @@ -18,8 +18,6 @@ import static google.registry.model.EppResourceUtils.loadByForeignKey; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.net.InetAddresses; @@ -27,11 +25,11 @@ import com.google.common.primitives.Booleans; import com.googlecode.objectify.cmd.Query; import google.registry.model.domain.DomainBase; import google.registry.model.host.HostResource; -import google.registry.rdap.RdapJsonFormatter.BoilerplateType; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.rdap.RdapMetrics.EndpointType; import google.registry.rdap.RdapMetrics.SearchType; import google.registry.rdap.RdapSearchResults.IncompletenessWarningType; +import google.registry.rdap.RdapSearchResults.NameserverSearchResponse; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.NotFoundException; @@ -82,7 +80,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { *

The RDAP spec allows nameserver search by either name or IP address. */ @Override - public ImmutableMap getJsonObjectForResource( + public NameserverSearchResponse getJsonObjectForResource( String pathSearchString, boolean isHeadRequest) { DateTime now = clock.nowUtc(); // RDAP syntax example: /rdap/nameservers?name=ns*.example.com. @@ -94,7 +92,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { throw new BadRequestException("You must specify either name=XXXX or ip=YYYY"); } decodeCursorToken(); - RdapSearchResults results; + NameserverSearchResponse results; if (nameParam.isPresent()) { // syntax: /rdap/nameservers?name=exam*.com metricInformationBuilder.setSearchType(SearchType.BY_NAMESERVER_NAME); @@ -118,19 +116,10 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { } results = searchByIp(inetAddress, now); } - if (results.jsonList().isEmpty()) { + if (results.nameserverSearchResults().isEmpty()) { throw new NotFoundException("No nameservers found"); } - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("nameserverSearchResults", results.jsonList()); - - rdapJsonFormatter.addTopLevelEntries( - jsonBuilder, - BoilerplateType.NAMESERVER, - getNotices(results), - ImmutableList.of(), - fullServletPath); - return jsonBuilder.build(); + return results; } /** @@ -139,7 +128,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { *

When deleted nameservers are included in the search, the search is treated as if it has a * wildcard, because multiple results can be returned. */ - private RdapSearchResults searchByName( + private NameserverSearchResponse searchByName( final RdapSearchPattern partialStringQuery, final DateTime now) { // Handle queries without a wildcard -- just load by foreign key. We can't do this if deleted // nameservers are desired, because there may be multiple nameservers with the same name. @@ -168,7 +157,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { * *

In this case, we can load by foreign key. */ - private RdapSearchResults searchByNameUsingForeignKey( + private NameserverSearchResponse searchByNameUsingForeignKey( final RdapSearchPattern partialStringQuery, final DateTime now) { Optional hostResource = loadByForeignKey(HostResource.class, partialStringQuery.getInitialString(), now); @@ -177,19 +166,21 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { throw new NotFoundException("No nameservers found"); } metricInformationBuilder.setNumHostsRetrieved(1); - return RdapSearchResults.create( - ImmutableList.of( + + NameserverSearchResponse.Builder builder = + NameserverSearchResponse.builder() + .setIncompletenessWarningType(IncompletenessWarningType.COMPLETE); + builder.nameserverSearchResultsBuilder().add( rdapJsonFormatter.makeRdapJsonForHost( hostResource.get(), - false, - fullServletPath, rdapWhoisServer, now, - OutputDataType.FULL))); + OutputDataType.FULL)); + return builder.build(); } /** Searches for nameservers by name using the superordinate domain as a suffix. */ - private RdapSearchResults searchByNameUsingSuperordinateDomain( + private NameserverSearchResponse searchByNameUsingSuperordinateDomain( final RdapSearchPattern partialStringQuery, final DateTime now) { Optional domainBase = loadByForeignKey(DomainBase.class, partialStringQuery.getSuffix(), now); @@ -232,7 +223,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { * *

There are no pending deletes for hosts, so we can call {@link RdapActionBase#queryItems}. */ - private RdapSearchResults searchByNameUsingPrefix( + private NameserverSearchResponse searchByNameUsingPrefix( final RdapSearchPattern partialStringQuery, final DateTime now) { // Add 1 so we can detect truncation. int querySizeLimit = getStandardQuerySizeLimit(); @@ -251,7 +242,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { } /** Searches for nameservers by IP address, returning a JSON array of nameserver info maps. */ - private RdapSearchResults searchByIp(final InetAddress inetAddress, DateTime now) { + private NameserverSearchResponse searchByIp(final InetAddress inetAddress, DateTime now) { // Add 1 so we can detect truncation. int querySizeLimit = getStandardQuerySizeLimit(); Query query = @@ -270,7 +261,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { } /** Output JSON for a lists of hosts contained in an {@link RdapResultSet}. */ - private RdapSearchResults makeSearchResults( + private NameserverSearchResponse makeSearchResults( RdapResultSet resultSet, CursorType cursorType, DateTime now) { return makeSearchResults( resultSet.resources(), @@ -281,7 +272,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { } /** Output JSON for a list of hosts. */ - private RdapSearchResults makeSearchResults( + private NameserverSearchResponse makeSearchResults( List hosts, IncompletenessWarningType incompletenessWarningType, int numHostsRetrieved, @@ -290,8 +281,8 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { metricInformationBuilder.setNumHostsRetrieved(numHostsRetrieved); OutputDataType outputDataType = (hosts.size() > 1) ? OutputDataType.SUMMARY : OutputDataType.FULL; - ImmutableList.Builder> jsonListBuilder = - new ImmutableList.Builder<>(); + NameserverSearchResponse.Builder builder = + NameserverSearchResponse.builder().setIncompletenessWarningType(incompletenessWarningType); Optional newCursor = Optional.empty(); for (HostResource host : Iterables.limit(hosts, rdapResultSetMaxSize)) { newCursor = @@ -299,15 +290,14 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { (cursorType == CursorType.NAME) ? host.getFullyQualifiedHostName() : host.getRepoId()); - jsonListBuilder.add( + builder.nameserverSearchResultsBuilder().add( rdapJsonFormatter.makeRdapJsonForHost( - host, false, fullServletPath, rdapWhoisServer, now, outputDataType)); + host, rdapWhoisServer, now, outputDataType)); } - ImmutableList> jsonList = jsonListBuilder.build(); - if (jsonList.size() < hosts.size()) { - return RdapSearchResults.create(jsonList, IncompletenessWarningType.TRUNCATED, newCursor); - } else { - return RdapSearchResults.create(jsonList, incompletenessWarningType, Optional.empty()); + if (rdapResultSetMaxSize < hosts.size()) { + builder.setNextPageUri(createNavigationUri(newCursor.get())); + builder.setIncompletenessWarningType(IncompletenessWarningType.TRUNCATED); } + return builder.build(); } } diff --git a/java/google/registry/rdap/RdapObjectClasses.java b/java/google/registry/rdap/RdapObjectClasses.java new file mode 100644 index 000000000..fa10a5416 --- /dev/null +++ b/java/google/registry/rdap/RdapObjectClasses.java @@ -0,0 +1,442 @@ +// Copyright 2019 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.rdap; + +import static google.registry.util.DomainNameUtils.ACE_PREFIX; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Ordering; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import google.registry.rdap.AbstractJsonableObject.RestrictJsonNames; +import google.registry.rdap.RdapDataStructures.Event; +import google.registry.rdap.RdapDataStructures.EventWithoutActor; +import google.registry.rdap.RdapDataStructures.LanguageIdentifier; +import google.registry.rdap.RdapDataStructures.Link; +import google.registry.rdap.RdapDataStructures.Notice; +import google.registry.rdap.RdapDataStructures.ObjectClassName; +import google.registry.rdap.RdapDataStructures.Port43WhoisServer; +import google.registry.rdap.RdapDataStructures.PublicId; +import google.registry.rdap.RdapDataStructures.RdapConformance; +import google.registry.rdap.RdapDataStructures.RdapStatus; +import google.registry.rdap.RdapDataStructures.Remark; +import google.registry.util.Idn; +import java.util.Optional; + +/** + * Object Classes defined in RFC7483 section 5. + */ +final class RdapObjectClasses { + + /** + * Temporary implementation of VCards. + * + * Will create a better implementation soon. + */ + @RestrictJsonNames({}) + @AutoValue + abstract static class Vcard implements Jsonable { + abstract String property(); + abstract ImmutableMap> parameters(); + abstract String valueType(); + abstract JsonElement value(); + + static Vcard create( + String property, + ImmutableMap> parameters, + String valueType, + JsonElement value) { + return new AutoValue_RdapObjectClasses_Vcard(property, parameters, valueType, value); + } + + static Vcard create( + String property, + ImmutableMap> parameters, + String valueType, + String value) { + return create(property, parameters, valueType, new JsonPrimitive(value)); + } + + static Vcard create(String property, String valueType, JsonElement value) { + return create(property, ImmutableMap.of(), valueType, value); + } + + static Vcard create(String property, String valueType, String value) { + return create(property, valueType, new JsonPrimitive(value)); + } + + @Override + public JsonArray toJson() { + JsonArray jsonArray = new JsonArray(); + jsonArray.add(property()); + jsonArray.add(new Gson().toJsonTree(parameters())); + jsonArray.add(valueType()); + jsonArray.add(value()); + return jsonArray; + } + } + + @RestrictJsonNames("vcardArray") + @AutoValue + abstract static class VcardArray implements Jsonable { + + private static final String VCARD_VERSION_NUMBER = "4.0"; + private static final Vcard VCARD_ENTRY_VERSION = + Vcard.create("version", "text", VCARD_VERSION_NUMBER); + + abstract ImmutableList vcards(); + + @Override + public JsonArray toJson() { + JsonArray jsonArray = new JsonArray(); + jsonArray.add("vcard"); + JsonArray jsonVcardsArray = new JsonArray(); + jsonVcardsArray.add(VCARD_ENTRY_VERSION.toJson()); + vcards().forEach(vcard -> jsonVcardsArray.add(vcard.toJson())); + jsonArray.add(jsonVcardsArray); + return jsonArray; + } + + static Builder builder() { + return new AutoValue_RdapObjectClasses_VcardArray.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract ImmutableList.Builder vcardsBuilder(); + Builder add(Vcard vcard) { + vcardsBuilder().add(vcard); + return this; + } + + abstract VcardArray build(); + } + } + + /** + * Indication of what type of boilerplate notices are required for the RDAP JSON messages. The + * ICANN RDAP Profile specifies that, for instance, domain name responses should include a remark + * about domain status codes. So we need to know when to include such boilerplate. On the other + * hand, remarks are not allowed except in domain, nameserver and entity objects, so we need to + * suppress them for other types of responses (e.g. help). + */ + public enum BoilerplateType { + DOMAIN, + NAMESERVER, + ENTITY, + OTHER + } + + /** + * An object that can be used to create a TopLevelReply. + * + * All Actions need to return an object of this type. + */ + @RestrictJsonNames("*") + abstract static class ReplyPayloadBase extends AbstractJsonableObject { + final BoilerplateType boilerplateType; + + ReplyPayloadBase(BoilerplateType boilerplateType) { + this.boilerplateType = boilerplateType; + } + } + + /** + * The Top Level JSON reply, Adds the required top-level boilerplate to a ReplyPayloadBase. + * + *

RFC 7483 specifies that the top-level object should include an entry indicating the + * conformance level. ICANN RDAP spec for 15feb19 mandates several additional entries, in sections + * 2.6.3, 2.11 of the Response Profile and 3.3.2, 3.5, of the Technical Implementation Guide. + */ + @AutoValue + @RestrictJsonNames({}) + abstract static class TopLevelReplyObject extends AbstractJsonableObject { + @JsonableElement("rdapConformance") + static final RdapConformance RDAP_CONFORMANCE = RdapConformance.INSTANCE; + + @JsonableElement("*") abstract ReplyPayloadBase aAreplyObject(); + @JsonableElement("notices[]") abstract Notice aTosNotice(); + + @JsonableElement("notices") ImmutableList boilerplateNotices() { + switch (aAreplyObject().boilerplateType) { + case DOMAIN: + return RdapIcannStandardInformation.domainBoilerplateNotices; + case NAMESERVER: + case ENTITY: + return RdapIcannStandardInformation.nameserverAndEntityBoilerplateNotices; + default: // things other than domains, nameservers and entities do not yet have boilerplate + return ImmutableList.of(); + } + } + + static TopLevelReplyObject create(ReplyPayloadBase replyObject, Notice tosNotice) { + return new AutoValue_RdapObjectClasses_TopLevelReplyObject(replyObject, tosNotice); + } + } + + /** + * A base object shared by Entity, Nameserver, and Domain. + * + *

Not part of the spec, but seems convenient. + */ + private abstract static class RdapObjectBase extends ReplyPayloadBase { + @JsonableElement final ObjectClassName objectClassName; + + @JsonableElement abstract Optional handle(); + @JsonableElement abstract ImmutableList publicIds(); + @JsonableElement abstract ImmutableList entities(); + @JsonableElement abstract ImmutableList status(); + @JsonableElement abstract ImmutableList remarks(); + @JsonableElement abstract ImmutableList links(); + @JsonableElement abstract Optional port43(); + @JsonableElement abstract ImmutableList events(); + + RdapObjectBase(BoilerplateType boilerplateType, ObjectClassName objectClassName) { + super(boilerplateType); + this.objectClassName = objectClassName; + } + + + abstract static class Builder> { + abstract B setHandle(String handle); + abstract ImmutableList.Builder publicIdsBuilder(); + abstract ImmutableList.Builder entitiesBuilder(); + abstract ImmutableList.Builder statusBuilder(); + abstract ImmutableList.Builder remarksBuilder(); + abstract ImmutableList.Builder linksBuilder(); + abstract B setPort43(Port43WhoisServer port43); + abstract ImmutableList.Builder eventsBuilder(); + } + } + + /** + * The Entity Object Class defined in 5.1 of RFC7483. + * + *

We're missing the "autnums" and "networks" fields + */ + @RestrictJsonNames({"entities[]", "entitySearchResults[]"}) + @AutoValue + abstract static class RdapEntity extends RdapObjectBase { + + /** Role values specified in RFC 7483 § 10.2.4. */ + @RestrictJsonNames("roles[]") + enum Role implements Jsonable { + REGISTRANT("registrant"), + TECH("technical"), + ADMIN("administrative"), + ABUSE("abuse"), + BILLING("billing"), + REGISTRAR("registrar"), + RESELLER("reseller"), + SPONSOR("sponsor"), + PROXY("proxy"), + NOTIFICATIONS("notifications"), + NOC("noc"); + + /** Value as it appears in RDAP messages. */ + final String rfc7483String; + + Role(String rfc7483String) { + this.rfc7483String = rfc7483String; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(rfc7483String); + } + } + + RdapEntity() { + super(BoilerplateType.ENTITY, ObjectClassName.ENTITY); + } + + @JsonableElement abstract Optional vcardArray(); + @JsonableElement abstract ImmutableSet roles(); + @JsonableElement abstract ImmutableList asEventActor(); + + static Builder builder() { + return new AutoValue_RdapObjectClasses_RdapEntity.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends RdapObjectBase.Builder { + abstract Builder setVcardArray(VcardArray vcardArray); + abstract ImmutableSet.Builder rolesBuilder(); + abstract ImmutableList.Builder asEventActorBuilder(); + + abstract RdapEntity build(); + } + } + + /** + * A base object shared by Nameserver, and Domain. + * + *

Takes care of the name and unicode field. + * + *

See RDAP Response Profile 15feb19 sections 2.1 and 4.1. + * + *

Not part of the spec, but seems convenient. + */ + private abstract static class RdapNamedObjectBase extends RdapObjectBase { + + @JsonableElement abstract String ldhName(); + + @JsonableElement final Optional unicodeName() { + // Only include the unicodeName field if there are unicode characters. + // + // TODO(b/127490882) Consider removing the condition (i.e. always having the unicodeName + // field) + if (!hasUnicodeComponents(ldhName())) { + return Optional.empty(); + } + return Optional.of(Idn.toUnicode(ldhName())); + } + + private static boolean hasUnicodeComponents(String fullyQualifiedName) { + return fullyQualifiedName.startsWith(ACE_PREFIX) + || fullyQualifiedName.contains("." + ACE_PREFIX); + } + + abstract static class Builder> extends RdapObjectBase.Builder { + abstract B setLdhName(String ldhName); + } + + RdapNamedObjectBase(BoilerplateType boilerplateType, ObjectClassName objectClassName) { + super(boilerplateType, objectClassName); + } + } + + /** + * The Nameserver Object Class defined in 5.2 of RFC7483. + */ + @RestrictJsonNames({"nameservers[]", "nameserverSearchResults[]"}) + @AutoValue + abstract static class RdapNameserver extends RdapNamedObjectBase { + + @JsonableElement Optional ipAddresses() { + if (ipv6().isEmpty() && ipv4().isEmpty()) { + return Optional.empty(); + } + return Optional.of(new IpAddresses()); + } + + abstract ImmutableList ipv6(); + abstract ImmutableList ipv4(); + + class IpAddresses extends AbstractJsonableObject { + @JsonableElement ImmutableList v6() { + return Ordering.natural().immutableSortedCopy(ipv6()); + } + + @JsonableElement ImmutableList v4() { + return Ordering.natural().immutableSortedCopy(ipv4()); + } + } + + RdapNameserver() { + super(BoilerplateType.NAMESERVER, ObjectClassName.NAMESERVER); + } + + static Builder builder() { + return new AutoValue_RdapObjectClasses_RdapNameserver.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends RdapNamedObjectBase.Builder { + abstract ImmutableList.Builder ipv6Builder(); + abstract ImmutableList.Builder ipv4Builder(); + + abstract RdapNameserver build(); + } + + } + + /** + * The Domain Object Class defined in 5.3 of RFC7483. + * + * We're missing the "variants", "secureDNS", "network" fields + */ + @RestrictJsonNames("domainSearchResults[]") + @AutoValue + abstract static class RdapDomain extends RdapNamedObjectBase { + + @JsonableElement abstract ImmutableList nameservers(); + + RdapDomain() { + super(BoilerplateType.DOMAIN, ObjectClassName.DOMAIN); + } + + static Builder builder() { + return new AutoValue_RdapObjectClasses_RdapDomain.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends RdapNamedObjectBase.Builder { + abstract ImmutableList.Builder nameserversBuilder(); + + abstract RdapDomain build(); + } + } + + /** + * Error Response Body defined in 6 of RFC7483. + */ + @RestrictJsonNames({}) + @AutoValue + abstract static class ErrorResponse extends ReplyPayloadBase { + + @JsonableElement final LanguageIdentifier lang = LanguageIdentifier.EN; + + @JsonableElement abstract int errorCode(); + @JsonableElement abstract String title(); + @JsonableElement abstract ImmutableList description(); + + ErrorResponse() { + super(BoilerplateType.OTHER); + } + + static ErrorResponse create(int status, String title, String description) { + return new AutoValue_RdapObjectClasses_ErrorResponse( + status, title, ImmutableList.of(description)); + } + } + + /** + * Help Response defined in 7 of RFC7483. + * + *

The helpNotice field is optional, because if the user requests the TOS - that's already + * given by the boilerplate of TopLevelReplyObject so we don't want to give it again. + */ + @RestrictJsonNames({}) + @AutoValue + abstract static class HelpResponse extends ReplyPayloadBase { + @JsonableElement("notices[]") abstract Optional helpNotice(); + + HelpResponse() { + super(BoilerplateType.OTHER); + } + + static HelpResponse create(Optional helpNotice) { + return new AutoValue_RdapObjectClasses_HelpResponse(helpNotice); + } + } + + private RdapObjectClasses() {} +} diff --git a/java/google/registry/rdap/RdapSearchActionBase.java b/java/google/registry/rdap/RdapSearchActionBase.java index 4b8d46547..aa5d2e4d3 100644 --- a/java/google/registry/rdap/RdapSearchActionBase.java +++ b/java/google/registry/rdap/RdapSearchActionBase.java @@ -18,12 +18,12 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.ImmutableMap; import google.registry.rdap.RdapMetrics.EndpointType; import google.registry.request.Parameter; import google.registry.request.ParameterMap; import google.registry.request.RequestUrl; import java.io.UnsupportedEncodingException; +import java.net.URI; import java.net.URLEncoder; import java.util.Base64; import java.util.List; @@ -119,19 +119,8 @@ public abstract class RdapSearchActionBase extends RdapActionBase { } } - ImmutableList> getNotices(RdapSearchResults results) { - ImmutableList> notices = results.getIncompletenessWarnings(); - if (results.nextCursor().isPresent()) { - ImmutableList.Builder> noticesBuilder = - new ImmutableList.Builder<>(); - noticesBuilder.addAll(notices); - noticesBuilder.add( - RdapJsonFormatter.makeRdapJsonNavigationLinkNotice( - Optional.of( - getRequestUrlWithExtraParameter( - "cursor", encodeCursorToken(results.nextCursor().get()))))); - notices = noticesBuilder.build(); - } - return notices; + /** Creates the URL for this same search with a different starting point cursor. */ + URI createNavigationUri(String cursor) { + return URI.create(getRequestUrlWithExtraParameter("cursor", encodeCursorToken(cursor))); } } diff --git a/java/google/registry/rdap/RdapSearchResults.java b/java/google/registry/rdap/RdapSearchResults.java index b2c4cac05..5d55adb87 100644 --- a/java/google/registry/rdap/RdapSearchResults.java +++ b/java/google/registry/rdap/RdapSearchResults.java @@ -20,6 +20,14 @@ import static google.registry.rdap.RdapIcannStandardInformation.TRUNCATION_NOTIC import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import google.registry.rdap.RdapDataStructures.Link; +import google.registry.rdap.RdapDataStructures.Notice; +import google.registry.rdap.RdapObjectClasses.BoilerplateType; +import google.registry.rdap.RdapObjectClasses.RdapDomain; +import google.registry.rdap.RdapObjectClasses.RdapEntity; +import google.registry.rdap.RdapObjectClasses.RdapNameserver; +import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase; +import java.net.URI; import java.util.Optional; /** @@ -31,6 +39,130 @@ import java.util.Optional; @AutoValue abstract class RdapSearchResults { + /** + * Responding To Searches defined in 8 of RFC7483. + */ + abstract static class BaseSearchResponse extends ReplyPayloadBase { + abstract IncompletenessWarningType incompletenessWarningType(); + abstract ImmutableMap navigationLinks(); + + @JsonableElement("notices") ImmutableList getIncompletenessWarnings() { + switch (incompletenessWarningType()) { + case TRUNCATED: + return TRUNCATION_NOTICES; + case MIGHT_BE_INCOMPLETE: + return POSSIBLY_INCOMPLETE_NOTICES; + case COMPLETE: + break; + } + return ImmutableList.of(); + } + + /** + * Creates a JSON object containing a notice with page navigation links. + * + *

At the moment, only a next page link is supported. Other types of links (e.g. previous + * page) could be added in the future, but it's not clear how to generate such links, given the + * way we are querying the database. + * + *

This isn't part of the spec. + */ + @JsonableElement("notices[]") + Optional getNavigationNotice() { + if (navigationLinks().isEmpty()) { + return Optional.empty(); + } + Notice.Builder builder = + Notice.builder().setTitle("Navigation Links").setDescription("Links to related pages."); + navigationLinks().forEach((name, uri) -> + builder.linksBuilder() + .add(Link.builder() + .setRel(name) + .setHref(uri.toString()) + .setType("application/rdap+json") + .build())); + return Optional.of(builder.build()); + } + + BaseSearchResponse(BoilerplateType boilerplateType) { + super(boilerplateType); + } + + abstract static class Builder> { + abstract ImmutableMap.Builder navigationLinksBuilder(); + abstract B setIncompletenessWarningType(IncompletenessWarningType type); + + @SuppressWarnings("unchecked") + B setNextPageUri(URI uri) { + navigationLinksBuilder().put("next", uri); + return (B) this; + } + } + } + + @AutoValue + abstract static class DomainSearchResponse extends BaseSearchResponse { + + @JsonableElement abstract ImmutableList domainSearchResults(); + + DomainSearchResponse() { + super(BoilerplateType.DOMAIN); + } + + static Builder builder() { + return new AutoValue_RdapSearchResults_DomainSearchResponse.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends BaseSearchResponse.Builder { + abstract ImmutableList.Builder domainSearchResultsBuilder(); + + abstract DomainSearchResponse build(); + } + } + + @AutoValue + abstract static class EntitySearchResponse extends BaseSearchResponse { + + @JsonableElement public abstract ImmutableList entitySearchResults(); + + EntitySearchResponse() { + super(BoilerplateType.ENTITY); + } + + static Builder builder() { + return new AutoValue_RdapSearchResults_EntitySearchResponse.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends BaseSearchResponse.Builder { + abstract ImmutableList.Builder entitySearchResultsBuilder(); + + abstract EntitySearchResponse build(); + } + } + + @AutoValue + abstract static class NameserverSearchResponse extends BaseSearchResponse { + + @JsonableElement public abstract ImmutableList nameserverSearchResults(); + + NameserverSearchResponse() { + super(BoilerplateType.NAMESERVER); + } + + static Builder builder() { + return new AutoValue_RdapSearchResults_NameserverSearchResponse.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends BaseSearchResponse.Builder { + abstract ImmutableList.Builder nameserverSearchResultsBuilder(); + + abstract NameserverSearchResponse build(); + } + } + enum IncompletenessWarningType { /** Result set is complete. */ @@ -45,35 +177,4 @@ abstract class RdapSearchResults { */ MIGHT_BE_INCOMPLETE } - - static RdapSearchResults create(ImmutableList> jsonList) { - return create(jsonList, IncompletenessWarningType.COMPLETE, Optional.empty()); - } - - static RdapSearchResults create( - ImmutableList> jsonList, - IncompletenessWarningType incompletenessWarningType, - Optional nextCursor) { - return new AutoValue_RdapSearchResults(jsonList, incompletenessWarningType, nextCursor); - } - - /** List of JSON result object representations. */ - abstract ImmutableList> jsonList(); - - /** Type of warning to display regarding possible incomplete data. */ - abstract IncompletenessWarningType incompletenessWarningType(); - - /** Cursor for fetching the next page of results, or empty() if there are no more. */ - abstract Optional nextCursor(); - - /** Convenience method to get the appropriate warnings for the incompleteness warning type. */ - ImmutableList> getIncompletenessWarnings() { - if (incompletenessWarningType() == IncompletenessWarningType.TRUNCATED) { - return TRUNCATION_NOTICES; - } - if (incompletenessWarningType() == IncompletenessWarningType.MIGHT_BE_INCOMPLETE) { - return POSSIBLY_INCOMPLETE_NOTICES; - } - return ImmutableList.of(); - } } diff --git a/java/google/registry/request/RequestModule.java b/java/google/registry/request/RequestModule.java index dcc632f7a..f2b380d41 100644 --- a/java/google/registry/request/RequestModule.java +++ b/java/google/registry/request/RequestModule.java @@ -96,6 +96,12 @@ public final class RequestModule { return req.getRequestURI(); } + /** + * Returns the part of this request's URL that calls the servlet. + * + *

This includes the path to the servlet, but does not include any extra path information or a + * query string. + */ @Provides @FullServletPath static String provideFullServletPath(HttpServletRequest req) { diff --git a/javatests/google/registry/rdap/RdapActionBaseTest.java b/javatests/google/registry/rdap/RdapActionBaseTest.java index 13efb4951..878124718 100644 --- a/javatests/google/registry/rdap/RdapActionBaseTest.java +++ b/javatests/google/registry/rdap/RdapActionBaseTest.java @@ -19,15 +19,13 @@ import static com.google.common.truth.Truth.assertThat; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; import static google.registry.testing.DatastoreHelper.createTld; -import static google.registry.testing.TestDataHelper.loadFile; import static org.mockito.Mockito.verify; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import google.registry.rdap.RdapJsonFormatter.BoilerplateType; import google.registry.rdap.RdapMetrics.EndpointType; import google.registry.rdap.RdapMetrics.SearchType; import google.registry.rdap.RdapMetrics.WildcardType; +import google.registry.rdap.RdapObjectClasses.BoilerplateType; +import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase; import google.registry.rdap.RdapSearchResults.IncompletenessWarningType; import google.registry.request.Action; import google.registry.request.auth.Auth; @@ -59,7 +57,7 @@ public class RdapActionBaseTest extends RdapActionBaseTestCase getJsonObjectForResource( + public ReplyPayloadBase getJsonObjectForResource( String pathSearchString, boolean isHeadRequest) { if (pathSearchString.equals("IllegalArgumentException")) { throw new IllegalArgumentException(); @@ -67,46 +65,36 @@ public class RdapActionBaseTest extends RdapActionBaseTestCase builder = new ImmutableMap.Builder<>(); - builder.put("key", "value"); - rdapJsonFormatter.addTopLevelEntries( - builder, - BoilerplateType.OTHER, - ImmutableList.of(), - ImmutableList.of(), - "http://myserver.example.com/"); - return builder.build(); + return new ReplyPayloadBase(BoilerplateType.OTHER) { + @JsonableElement public String key = "value"; + }; } } @Before public void setUp() { createTld("thing"); - action.fullServletPath = "http://myserver.example.com" + actionPath; + action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); } @Test public void testIllegalValue_showsReadableTypeName() { - assertThat(generateActualJson("IllegalArgumentException")).isEqualTo(JSONValue.parse( - "{\"lang\":\"en\", \"errorCode\":400, \"title\":\"Bad Request\"," - + "\"rdapConformance\":[\"icann_rdap_response_profile_0\"]," - + "\"description\":[\"Not a valid human-readable string\"]}")); + assertThat(generateActualJson("IllegalArgumentException")).isEqualTo(generateExpectedJsonError( + "Not a valid human-readable string", + 400)); assertThat(response.getStatus()).isEqualTo(400); } @Test public void testRuntimeException_returns500Error() { - assertThat(generateActualJson("RuntimeException")).isEqualTo(JSONValue.parse( - "{\"lang\":\"en\", \"errorCode\":500, \"title\":\"Internal Server Error\"," - + "\"rdapConformance\":[\"icann_rdap_response_profile_0\"]," - + "\"description\":[\"An error was encountered\"]}")); + assertThat(generateActualJson("RuntimeException")) + .isEqualTo(generateExpectedJsonError("An error was encountered", 500)); assertThat(response.getStatus()).isEqualTo(500); } @Test public void testValidName_works() { - assertThat(generateActualJson("no.thing")).isEqualTo(JSONValue.parse( - loadFile(this.getClass(), "rdapjson_toplevel.json"))); + assertThat(generateActualJson("no.thing")).isEqualTo(loadJsonFile("rdapjson_toplevel.json")); assertThat(response.getStatus()).isEqualTo(200); } @@ -173,18 +161,14 @@ public class RdapActionBaseTest extends RdapActionBaseTestCase { action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); action.rdapMetrics = rdapMetrics; action.requestMethod = Action.Method.GET; - action.fullServletPath = "https://example.tld/rdap"; action.rdapWhoisServer = null; logout(); } @@ -126,18 +130,80 @@ public class RdapActionBaseTestCase { metricRole = ADMINISTRATOR; } - protected Object generateActualJson(String domainName) { + protected JSONObject generateActualJson(String domainName) { action.requestPath = actionPath + domainName; action.requestMethod = GET; action.run(); - return JSONValue.parse(response.getPayload()); + return (JSONObject) JSONValue.parse(response.getPayload()); } protected String generateHeadPayload(String domainName) { action.requestPath = actionPath + domainName; - action.fullServletPath = "http://myserver.example.com" + actionPath; action.requestMethod = HEAD; action.run(); return response.getPayload(); } + + /** + * Loads a resource testdata JSON file, and applies substitutions. + * + *

{@code loadJsonFile("filename.json", "NANE", "something", "ID", "other")} is the same as + * {@code loadJsonFile("filename.json", ImmutableMap.of("NANE", "something", "ID", "other"))}. + * + * @param filename the name of the file from the testdata directory + * @param keysAndValues alternating substitution key and value. The substitutions are applied to + * the file before parsing it to JSON. + */ + protected JSONObject loadJsonFile(String filename, String... keysAndValues) { + checkArgument(keysAndValues.length % 2 == 0); + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + for (int i = 0; i < keysAndValues.length; i += 2) { + if (keysAndValues[i + 1] != null) { + builder.put(keysAndValues[i], keysAndValues[i + 1]); + } + } + return loadJsonFile(filename, builder.build()); + } + + /** + * Loads a resource testdata JSON file, and applies substitutions. + * + * @param filename the name of the file from the testdata directory + * @param substitutions map of substitutions to apply to the file. The substitutions are applied + * to the file before parsing it to JSON. + */ + protected JSONObject loadJsonFile(String filename, Map substitutions) { + return (JSONObject) JSONValue.parse(loadFile(this.getClass(), filename, substitutions)); + } + + protected JSONObject generateExpectedJsonError( + String description, + int code) { + String title; + switch (code) { + case 404: + title = "Not Found"; + break; + case 500: + title = "Internal Server Error"; + break; + case 501: + title = "Not Implemented"; + break; + case 400: + title = "Bad Request"; + break; + case 422: + title = "Unprocessable Entity"; + break; + default: + title = "ERR"; + break; + } + return loadJsonFile( + "rdap_error.json", + "DESCRIPTION", description, + "TITLE", title, + "CODE", String.valueOf(code)); + } } diff --git a/javatests/google/registry/rdap/RdapDataStructuresTest.java b/javatests/google/registry/rdap/RdapDataStructuresTest.java new file mode 100644 index 000000000..4bc8ffff5 --- /dev/null +++ b/javatests/google/registry/rdap/RdapDataStructuresTest.java @@ -0,0 +1,177 @@ +// Copyright 2019 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google.registry.rdap; +import static com.google.common.truth.Truth.assertThat; +import static google.registry.rdap.RdapTestHelper.createJson; + +import com.google.common.collect.ImmutableSet; +import google.registry.rdap.RdapDataStructures.Event; +import google.registry.rdap.RdapDataStructures.EventAction; +import google.registry.rdap.RdapDataStructures.EventWithoutActor; +import google.registry.rdap.RdapDataStructures.LanguageIdentifier; +import google.registry.rdap.RdapDataStructures.Link; +import google.registry.rdap.RdapDataStructures.Notice; +import google.registry.rdap.RdapDataStructures.ObjectClassName; +import google.registry.rdap.RdapDataStructures.Port43WhoisServer; +import google.registry.rdap.RdapDataStructures.PublicId; +import google.registry.rdap.RdapDataStructures.RdapConformance; +import google.registry.rdap.RdapDataStructures.RdapStatus; +import google.registry.rdap.RdapDataStructures.Remark; +import org.joda.time.DateTime; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class RdapDataStructuresTest { + + private void assertRestrictedNames(Object object, String... names) { + assertThat(AbstractJsonableObject.getNameRestriction(object.getClass()).get()) + .containsExactlyElementsIn(ImmutableSet.copyOf(names)); + } + + @Test + public void testRdapConformance() { + assertThat(RdapConformance.INSTANCE.toJson()) + .isEqualTo(createJson("['icann_rdap_response_profile_0']")); + } + + @Test + public void testLink() { + Link link = + Link.builder() + .setHref("myHref") + .setRel("myRel") + .setTitle("myTitle") + .setType("myType") + .build(); + assertThat(link.toJson()) + .isEqualTo(createJson("{'href':'myHref','rel':'myRel','title':'myTitle','type':'myType'}")); + assertRestrictedNames(link, "links[]"); + } + + @Test + public void testNotice() { + Notice notice = Notice.builder() + .setDescription("AAA", "BBB") + .setTitle("myTitle") + .addLink(Link.builder().setHref("myHref").setTitle("myLink").build()) + .setType(Notice.Type.RESULT_TRUNCATED_AUTHORIZATION) + .build(); + assertThat(notice.toJson()) + .isEqualTo( + createJson( + "{", + " 'title':'myTitle',", + " 'type':'result set truncated due to authorization',", + " 'description':['AAA','BBB'],", + " 'links':[{'href':'myHref','title':'myLink'}]", + "}")); + assertRestrictedNames(notice, "notices[]"); + } + + @Test + public void testRemark() { + Remark remark = Remark.builder() + .setDescription("AAA", "BBB") + .setTitle("myTitle") + .addLink(Link.builder().setHref("myHref").setTitle("myLink").build()) + .setType(Remark.Type.OBJECT_TRUNCATED_AUTHORIZATION) + .build(); + assertThat(remark.toJson()) + .isEqualTo( + createJson( + "{", + " 'title':'myTitle',", + " 'type':'object truncated due to authorization',", + " 'description':['AAA','BBB'],", + " 'links':[{'href':'myHref','title':'myLink'}]", + "}")); + assertRestrictedNames(remark, "remarks[]"); + } + + @Test + public void testLanguage() { + assertThat(LanguageIdentifier.EN.toJson()).isEqualTo(createJson("'en'")); + assertRestrictedNames(LanguageIdentifier.EN, "lang"); + } + + @Test + public void testEvent() { + Event event = + Event.builder() + .setEventAction(EventAction.REGISTRATION) + .setEventActor("Event Actor") + .setEventDate(DateTime.parse("2012-04-03T14:54Z")) + .addLink(Link.builder().setHref("myHref").build()) + .build(); + assertThat(event.toJson()) + .isEqualTo( + createJson( + "{", + " 'eventAction':'registration',", + " 'eventActor':'Event Actor',", + " 'eventDate':'2012-04-03T14:54:00.000Z',", + " 'links':[{'href':'myHref'}]", + "}")); + assertRestrictedNames(event, "events[]"); + } + + @Test + public void testEventWithoutActor() { + EventWithoutActor event = + EventWithoutActor.builder() + .setEventAction(EventAction.REGISTRATION) + .setEventDate(DateTime.parse("2012-04-03T14:54Z")) + .addLink(Link.builder().setHref("myHref").build()) + .build(); + assertThat(event.toJson()) + .isEqualTo( + createJson( + "{", + " 'eventAction':'registration',", + " 'eventDate':'2012-04-03T14:54:00.000Z',", + " 'links':[{'href':'myHref'}]", + "}")); + assertRestrictedNames(event, "asEventActor[]"); + } + + @Test + public void testRdapStatus() { + assertThat(RdapStatus.ACTIVE.toJson()).isEqualTo(createJson("'active'")); + assertRestrictedNames(RdapStatus.ACTIVE, "status[]"); + } + + @Test + public void testPort43() { + Port43WhoisServer port43 = Port43WhoisServer.create("myServer"); + assertThat(port43.toJson()).isEqualTo(createJson("'myServer'")); + assertRestrictedNames(port43, "port43"); + } + + @Test + public void testPublicId() { + PublicId publicId = PublicId.create(PublicId.Type.IANA_REGISTRAR_ID, "myId"); + assertThat(publicId.toJson()) + .isEqualTo(createJson("{'identifier':'myId','type':'IANA Registrar ID'}")); + assertRestrictedNames(publicId, "publicIds[]"); + } + + @Test + public void testObjectClassName() { + assertThat(ObjectClassName.DOMAIN.toJson()).isEqualTo(createJson("'domain'")); + assertRestrictedNames(ObjectClassName.DOMAIN, "objectClassName"); + } +} diff --git a/javatests/google/registry/rdap/RdapDomainActionTest.java b/javatests/google/registry/rdap/RdapDomainActionTest.java index 90ee442e7..d2e98d0d9 100644 --- a/javatests/google/registry/rdap/RdapDomainActionTest.java +++ b/javatests/google/registry/rdap/RdapDomainActionTest.java @@ -24,7 +24,6 @@ import static google.registry.testing.FullFieldsTestEntityHelper.makeDomainBase; 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; import static org.mockito.Mockito.verify; import com.google.common.collect.ImmutableList; @@ -47,7 +46,6 @@ import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; import org.json.simple.JSONObject; -import org.json.simple.JSONValue; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -230,24 +228,15 @@ public class RdapDomainActionTest extends RdapActionBaseTestCase contactRoids, @Nullable List nameserverRoids, @Nullable List nameserverNames, - @Nullable String registrarName, - String expectedOutputFile) { + @Nullable String registrarName) { ImmutableMap.Builder substitutionsBuilder = new ImmutableMap.Builder<>(); substitutionsBuilder.put("NAME", name); substitutionsBuilder.put("PUNYCODENAME", (punycodeName == null) ? name : punycodeName); @@ -282,11 +271,10 @@ public class RdapDomainActionTest extends RdapActionBaseTestCase nameserverNames, @Nullable String registrarName, String expectedOutputFile) { - Object obj = + JSONObject obj = generateExpectedJson( + expectedOutputFile, name, punycodeName, handle, contactRoids, nameserverRoids, nameserverNames, - registrarName, - expectedOutputFile); - if (obj instanceof Map) { + registrarName); @SuppressWarnings("unchecked") Map map = (Map) obj; ImmutableMap.Builder builder = - RdapTestHelper.getBuilderExcluding( - map, ImmutableSet.of("rdapConformance", "notices", "remarks")); - builder.put("rdapConformance", ImmutableList.of("icann_rdap_response_profile_0")); + RdapTestHelper.getBuilderExcluding(map, ImmutableSet.of("notices")); RdapTestHelper.addDomainBoilerplateNotices( - builder, - false, - RdapTestHelper.createNotices( - "https://example.tld/rdap/", - (contactRoids == null) - ? RdapTestHelper.ContactNoticeType.DOMAIN - : RdapTestHelper.ContactNoticeType.NONE, - map.get("notices"))); + builder, RdapTestHelper.createNotices("https://example.tld/rdap/", map.get("notices"))); obj = new JSONObject(builder.build()); - } return obj; } @@ -361,9 +338,7 @@ public class RdapDomainActionTest extends RdapActionBaseTestCase", expectedOutputFile)); @@ -374,12 +349,10 @@ public class RdapDomainActionTest extends RdapActionBaseTestCase() - .put("TYPE", "domain name") - .put("DOMAINPUNYCODENAME1", domain1Name) - .put("DOMAINNAME1", IDN.toUnicode(domain1Name)) - .put("DOMAINHANDLE1", domain1Handle) - .put("DOMAINPUNYCODENAME2", domain2Name) - .put("DOMAINNAME2", IDN.toUnicode(domain2Name)) - .put("DOMAINHANDLE2", domain2Handle) - .put("DOMAINPUNYCODENAME3", domain3Name) - .put("DOMAINNAME3", IDN.toUnicode(domain3Name)) - .put("DOMAINHANDLE3", domain3Handle) - .put("DOMAINPUNYCODENAME4", domain4Name) - .put("DOMAINNAME4", IDN.toUnicode(domain4Name)) - .put("DOMAINHANDLE4", domain4Handle) - .put("NEXT_QUERY", nextQuery) - .build())); + return loadJsonFile( + expectedOutputFile, + "TYPE", "domain name", + "DOMAINPUNYCODENAME1", domain1Name, + "DOMAINNAME1", IDN.toUnicode(domain1Name), + "DOMAINHANDLE1", domain1Handle, + "DOMAINPUNYCODENAME2", domain2Name, + "DOMAINNAME2", IDN.toUnicode(domain2Name), + "DOMAINHANDLE2", domain2Handle, + "DOMAINPUNYCODENAME3", domain3Name, + "DOMAINNAME3", IDN.toUnicode(domain3Name), + "DOMAINHANDLE3", domain3Handle, + "DOMAINPUNYCODENAME4", domain4Name, + "DOMAINNAME4", IDN.toUnicode(domain4Name), + "DOMAINHANDLE4", domain4Handle, + "NEXT_QUERY", nextQuery); } - private Object generateExpectedJson(String name, String expectedOutputFile) { - return generateExpectedJson(name, null, null, null, null, null, expectedOutputFile); - } - - private Object generateExpectedJson( + private JSONObject generateExpectedJson( String name, String punycodeName, String handle, @@ -476,11 +465,10 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase nameservers, @Nullable String registrarName, String expectedOutputFile) { - Object obj = + JSONObject obj = generateExpectedJson( name, punycodeName, @@ -497,6 +485,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase builder = new ImmutableMap.Builder<>(); builder.put("domainSearchResults", ImmutableList.of(obj)); builder.put("rdapConformance", ImmutableList.of("icann_rdap_response_profile_0")); @@ -560,7 +549,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase() - .put("DOMAINNAME1", domainName1) - .put("DOMAINHANDLE1", domainHandle1) - .put("DOMAINNAME2", domainName2) - .put("DOMAINHANDLE2", domainHandle2) - .put("DOMAINNAME3", domainName3) - .put("DOMAINHANDLE3", domainHandle3) - .put("DOMAINNAME4", domainName4) - .put("DOMAINHANDLE4", domainHandle4) - .put("NEXT_QUERY", nextQuery) - .build())); + "DOMAINNAME1", domainName1, + "DOMAINHANDLE1", domainHandle1, + "DOMAINNAME2", domainName2, + "DOMAINHANDLE2", domainHandle2, + "DOMAINNAME3", domainName3, + "DOMAINHANDLE3", domainHandle3, + "DOMAINNAME4", domainName4, + "DOMAINHANDLE4", domainHandle4, + "NEXT_QUERY", nextQuery); } - private void checkNumberOfDomainsInResult(Object obj, int expected) { + private void checkNumberOfDomainsInResult(JSONObject obj, int expected) { assertThat(obj).isInstanceOf(Map.class); @SuppressWarnings("unchecked") - Map map = (Map) obj; - - @SuppressWarnings("unchecked") - List domains = (List) map.get("domainSearchResults"); + List domains = (List) obj.get("domainSearchResults"); assertThat(domains).hasSize(expected); } @@ -717,7 +700,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase() - .put("NAME", handle) - .put("FULLNAME", fullName) - .put("ADDRESS", (address == null) ? "\"1 Smiley Row\", \"Suite みんな\"" : address) - .put("EMAIL", "lol@cat.みんな") - .put("TYPE", "entity") - .put("STATUS", status) - .build())); + return loadJsonFile( + expectedOutputFile, + "NAME", handle, + "FULLNAME", fullName, + "ADDRESS", (address == null) ? "\"1 Smiley Row\", \"Suite みんな\"" : address, + "EMAIL", "lol@cat.みんな", + "TYPE", "entity", + "STATUS", status); } private Object generateExpectedJsonWithTopLevelEntries( String handle, String expectedOutputFile) { return generateExpectedJsonWithTopLevelEntries( - handle, "(◕‿◕)", "active", null, false, expectedOutputFile); + handle, "(◕‿◕)", "active", null, expectedOutputFile); } private Object generateExpectedJsonWithTopLevelEntries( @@ -184,35 +159,26 @@ public class RdapEntityActionTest extends RdapActionBaseTestCase map = (Map) obj; ImmutableMap.Builder builder = - RdapTestHelper.getBuilderExcluding( - map, ImmutableSet.of("rdapConformance", "notices", "remarks")); - builder.put("rdapConformance", ImmutableList.of("icann_rdap_response_profile_0")); + RdapTestHelper.getBuilderExcluding(map, ImmutableSet.of("notices")); RdapTestHelper.addNonDomainBoilerplateNotices( - builder, - RdapTestHelper.createNotices( - "https://example.tld/rdap/", - addNoPersonalDataRemark - ? RdapTestHelper.ContactNoticeType.CONTACT - : RdapTestHelper.ContactNoticeType.NONE, - map.get("notices"))); - obj = builder.build(); + builder, RdapTestHelper.createNotices("https://example.tld/rdap/", map.get("notices"))); + obj = new JSONObject(builder.build()); } return obj; } private void runSuccessfulTest(String queryString, String fileName) { - runSuccessfulTest(queryString, "(◕‿◕)", "active", null, false, fileName); + runSuccessfulTest(queryString, "(◕‿◕)", "active", null, fileName); } private void runSuccessfulTest(String queryString, String fullName, String fileName) { - runSuccessfulTest(queryString, fullName, "active", null, false, fileName); + runSuccessfulTest(queryString, fullName, "active", null, fileName); } private void runSuccessfulTest( @@ -220,27 +186,26 @@ public class RdapEntityActionTest extends RdapActionBaseTestCase", "inactive", null, false, "rdap_registrar.json"); + "104", "Yes Virginia