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