google.registry.rdap.RdapJsonFormatter.java Source code

Java tutorial

Introduction

Here is the source code for google.registry.rdap.RdapJsonFormatter.java

Source

// Copyright 2016 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.base.Strings.nullToEmpty;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.util.DomainNameUtils.ACE_PREFIX;

import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Optional;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.net.InetAddresses;
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;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.DomainResource;
import google.registry.model.eppcommon.Address;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.HostResource;
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.request.HttpException.InternalServerErrorException;
import google.registry.request.HttpException.NotFoundException;
import google.registry.util.FormattingLogger;
import google.registry.util.Idn;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URI;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.joda.time.DateTime;

/**
 * Helper class to create RDAP JSON objects for various registry entities and objects.
 *
 * <p>The JSON format specifies that entities should be supplied with links indicating how to fetch
 * them via RDAP, which requires the URL to the RDAP server. The linkBase parameter, passed to many
 * of the methods, is used as the first part of the link URL. For instance, if linkBase is
 * "http://rdap.org/dir/", the link URLs will look like "http://rdap.org/dir/domain/XXXX", etc.
 *
 * @see <a href="https://tools.ietf.org/html/rfc7483">
 *        RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP)</a>
 */
@Singleton
public class RdapJsonFormatter {

    @Inject
    @Config("rdapTosPath")
    String rdapTosPath;
    @Inject
    @Config("rdapHelpMap")
    ImmutableMap<String, RdapNoticeDescriptor> rdapHelpMap;

    @Inject
    RdapJsonFormatter() {
    }

    private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();

    /**
     * What type of data to generate. Summary data includes only information about the object itself,
     * while full data includes associated items (e.g. for domains, full data includes the hosts,
     * contacts and history entries connected with the domain). Summary data is appropriate for search
     * queries which return many results, to avoid load on the system. According to the ICANN
     * operational profile, a remark must be attached to the returned object indicating that it
     * includes only summary data.
     */
    public enum OutputDataType {
        FULL, 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 = "rdap_level_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;

        private RdapStatus(String rfc7483String) {
            this.rfc7483String = rfc7483String;
        }

        public String getDisplayName() {
            return rfc7483String;
        }
    }

    /** Map of EPP status values to the RDAP equivalents. */
    private static final ImmutableMap<StatusValue, RdapStatus> statusToRdapStatusMap = Maps
            .immutableEnumMap(new ImmutableMap.Builder<StatusValue, RdapStatus>()
                    // StatusValue.ADD_PERIOD not defined in our system
                    // StatusValue.AUTO_RENEW_PERIOD not defined in our system
                    .put(StatusValue.CLIENT_DELETE_PROHIBITED, RdapStatus.CLIENT_DELETE_PROHIBITED)
                    .put(StatusValue.CLIENT_HOLD, RdapStatus.CLIENT_HOLD)
                    .put(StatusValue.CLIENT_RENEW_PROHIBITED, RdapStatus.CLIENT_RENEW_PROHIBITED)
                    .put(StatusValue.CLIENT_TRANSFER_PROHIBITED, RdapStatus.CLIENT_TRANSFER_PROHIBITED)
                    .put(StatusValue.CLIENT_UPDATE_PROHIBITED, RdapStatus.CLIENT_UPDATE_PROHIBITED)
                    .put(StatusValue.INACTIVE, RdapStatus.INACTIVE).put(StatusValue.LINKED, RdapStatus.ASSOCIATED)
                    .put(StatusValue.OK, RdapStatus.ACTIVE)
                    .put(StatusValue.PENDING_CREATE, RdapStatus.PENDING_CREATE)
                    .put(StatusValue.PENDING_DELETE, RdapStatus.PENDING_DELETE)
                    // StatusValue.PENDING_RENEW not defined in our system
                    // StatusValue.PENDING_RESTORE not defined in our system
                    .put(StatusValue.PENDING_TRANSFER, RdapStatus.PENDING_TRANSFER)
                    .put(StatusValue.PENDING_UPDATE, RdapStatus.PENDING_UPDATE)
                    // StatusValue.REDEMPTION_PERIOD not defined in our system
                    // StatusValue.RENEW_PERIOD not defined in our system
                    .put(StatusValue.SERVER_DELETE_PROHIBITED, RdapStatus.SERVER_DELETE_PROHIBITED)
                    .put(StatusValue.SERVER_HOLD, RdapStatus.SERVER_HOLD)
                    .put(StatusValue.SERVER_RENEW_PROHIBITED, RdapStatus.SERVER_RENEW_PROHIBITED)
                    .put(StatusValue.SERVER_TRANSFER_PROHIBITED, RdapStatus.SERVER_TRANSFER_PROHIBITED)
                    .put(StatusValue.SERVER_UPDATE_PROHIBITED, RdapStatus.SERVER_UPDATE_PROHIBITED)
                    // StatusValue.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;

        private 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;

        private RdapEventAction(String rfc7483String) {
            this.rfc7483String = rfc7483String;
        }

        public String getDisplayName() {
            return rfc7483String;
        }
    }

    /** Map of EPP event values to the RDAP equivalents. */
    private static final ImmutableMap<HistoryEntry.Type, RdapEventAction> historyEntryTypeToRdapEventActionMap = Maps
            .immutableEnumMap(new ImmutableMap.Builder<HistoryEntry.Type, RdapEventAction>()
                    .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_APPLICATION_CREATE, RdapEventAction.REGISTRATION)
                    .put(HistoryEntry.Type.DOMAIN_APPLICATION_DELETE, RdapEventAction.DELETION)
                    .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).build());

    private static final ImmutableList<String> CONFORMANCE_LIST = ImmutableList.of(RDAP_CONFORMANCE_LEVEL);

    private static final ImmutableList<String> STATUS_LIST_ACTIVE = ImmutableList
            .of(RdapStatus.ACTIVE.rfc7483String);
    private static final ImmutableMap<String, ImmutableList<String>> PHONE_TYPE_VOICE = ImmutableMap.of("type",
            ImmutableList.of("voice"));
    private static final ImmutableMap<String, ImmutableList<String>> 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<HostResource> HOST_RESOURCE_ORDERING = Ordering.natural()
            .onResultOf(new Function<HostResource, String>() {
                @Override
                public String apply(HostResource host) {
                    return host.getFullyQualifiedHostName();
                }
            });

    /** Sets the ordering for designated contacts; order them in a fixed order by contact type. */
    private static final Ordering<DesignatedContact> DESIGNATED_CONTACT_ORDERING = Ordering.natural()
            .onResultOf(new Function<DesignatedContact, DesignatedContact.Type>() {
                @Override
                public DesignatedContact.Type apply(DesignatedContact designatedContact) {
                    return designatedContact.getType();
                }
            });

    ImmutableMap<String, Object> getJsonTosNotice(String rdapLinkBase) {
        return getJsonHelpNotice(rdapTosPath, rdapLinkBase);
    }

    ImmutableMap<String, Object> 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) {
            logger.warningfmt(e, "Error reading RDAP help file: %s", pathSearchString);
            throw new InternalServerErrorException("unable to read help for " + pathSearchString);
        }
    }

    /**
     * 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 before the boilerplate notices.
     * @param rdapLinkBase the base for link URLs
     */
    void addTopLevelEntries(ImmutableMap.Builder<String, Object> jsonBuilder, BoilerplateType boilerplateType,
            List<ImmutableMap<String, Object>> notices, List<ImmutableMap<String, Object>> remarks,
            String rdapLinkBase) {
        jsonBuilder.put("rdapConformance", CONFORMANCE_LIST);
        ImmutableList.Builder<ImmutableMap<String, Object>> noticesBuilder = new ImmutableList.Builder<>();
        ImmutableMap<String, Object> tosNotice = getJsonTosNotice(rdapLinkBase);
        boolean tosNoticeFound = false;
        if (!notices.isEmpty()) {
            noticesBuilder.addAll(notices);
            for (ImmutableMap<String, Object> notice : notices) {
                if (notice.equals(tosNotice)) {
                    tosNoticeFound = true;
                    break;
                }
            }
        }
        if (!tosNoticeFound) {
            noticesBuilder.add(tosNotice);
        }
        jsonBuilder.put(NOTICES, noticesBuilder.build());
        ImmutableList.Builder<ImmutableMap<String, Object>> remarksBuilder = new ImmutableList.Builder<>();
        remarksBuilder.addAll(remarks);
        switch (boilerplateType) {
        case DOMAIN:
            remarksBuilder.addAll(RdapIcannStandardInformation.domainBoilerplateRemarks);
            break;
        case NAMESERVER:
        case ENTITY:
            remarksBuilder.addAll(RdapIcannStandardInformation.nameserverAndEntityBoilerplateRemarks);
            break;
        default: // things other than domains, nameservers and entities cannot contain remarks
            break;
        }
        ImmutableList<ImmutableMap<String, Object>> remarksToAdd = remarksBuilder.build();
        if (!remarksToAdd.isEmpty()) {
            jsonBuilder.put(REMARKS, remarksToAdd);
        }
    }

    /**
     * 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:
     *
     * <p>title: the title of the notice; if null, the notice will have no title
     *
     * <p>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)
     *
     * <p>typeString: the notice or remark type as defined in  10.2.1; if null, no type
     *
     * <p>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)
     *
     * <p>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
     *
     * <p>linkBase: the base for the link value and href; if null, it is assumed to be the empty
     * string
     *
     * @see <a href="https://tools.ietf.org/html/rfc7483">
     *     RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP)</a>
     */
    static ImmutableMap<String, Object> makeRdapJsonNotice(RdapNoticeDescriptor parameters,
            @Nullable String linkBase) {
        ImmutableMap.Builder<String, Object> jsonBuilder = new ImmutableMap.Builder<>();
        if (parameters.getTitle() != null) {
            jsonBuilder.put("title", parameters.getTitle());
        }
        ImmutableList.Builder<String> 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 linkValueString = nullToEmpty(linkBase) + nullToEmpty(parameters.getLinkValueSuffix());
        if (parameters.getLinkHrefUrlString() == null) {
            jsonBuilder.put("links", ImmutableList.of(ImmutableMap.of("value", linkValueString, "rel", "self",
                    "href", linkValueString, "type", "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")));
        }
        return jsonBuilder.build();
    }

    /**
     * Creates a JSON object for a {@link DomainResource}.
     *
     * @param domainResource 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
     * @param outputDataType whether to generate full or summary data
     */
    ImmutableMap<String, Object> makeRdapJsonForDomain(DomainResource domainResource, boolean isTopLevel,
            @Nullable String linkBase, @Nullable String whoisServer, DateTime now, OutputDataType outputDataType) {
        // Start with the domain-level information.
        ImmutableMap.Builder<String, Object> jsonBuilder = new ImmutableMap.Builder<>();
        jsonBuilder.put("objectClassName", "domain");
        jsonBuilder.put("handle", domainResource.getRepoId());
        jsonBuilder.put("ldhName", domainResource.getFullyQualifiedDomainName());
        // Only include the unicodeName field if there are unicode characters.
        if (hasUnicodeComponents(domainResource.getFullyQualifiedDomainName())) {
            jsonBuilder.put("unicodeName", Idn.toUnicode(domainResource.getFullyQualifiedDomainName()));
        }
        jsonBuilder.put("status", makeStatusValueList(domainResource.getStatusValues()));
        jsonBuilder.put("links",
                ImmutableList.of(makeLink("domain", domainResource.getFullyQualifiedDomainName(), linkBase)));
        // 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<ImmutableMap<String, Object>> remarks;
        if (outputDataType == OutputDataType.SUMMARY) {
            remarks = ImmutableList.of(RdapIcannStandardInformation.SUMMARY_DATA_REMARK);
        } else {
            remarks = ImmutableList.of();
            ImmutableList<Object> events = makeEvents(domainResource, now);
            if (!events.isEmpty()) {
                jsonBuilder.put("events", events);
            }
            // Kick off the database loads of the nameservers that we will need.
            Map<Key<HostResource>, HostResource> loadedHosts = ofy().load().keys(domainResource.getNameservers());
            // And the registrant and other contacts.
            Map<Key<ContactResource>, ContactResource> loadedContacts = ofy().load()
                    .keys(domainResource.getReferencedContacts());
            // Nameservers
            ImmutableList.Builder<Object> nsBuilder = new ImmutableList.Builder<>();
            for (HostResource hostResource : HOST_RESOURCE_ORDERING.immutableSortedCopy(loadedHosts.values())) {
                nsBuilder.add(makeRdapJsonForHost(hostResource, false, linkBase, null, now, outputDataType));
            }
            ImmutableList<Object> ns = nsBuilder.build();
            if (!ns.isEmpty()) {
                jsonBuilder.put("nameservers", ns);
            }
            // Contacts
            ImmutableList.Builder<Object> entitiesBuilder = new ImmutableList.Builder<>();
            for (DesignatedContact designatedContact : FluentIterable.from(domainResource.getContacts())
                    .append(DesignatedContact.create(Type.REGISTRANT, domainResource.getRegistrant()))
                    .toSortedList(DESIGNATED_CONTACT_ORDERING)) {
                ContactResource loadedContact = loadedContacts.get(designatedContact.getContactKey());
                entitiesBuilder.add(makeRdapJsonForContact(loadedContact, false,
                        Optional.of(designatedContact.getType()), linkBase, null, now, outputDataType));
            }
            ImmutableList<Object> entities = entitiesBuilder.build();
            if (!entities.isEmpty()) {
                jsonBuilder.put("entities", entities);
            }
        }
        if (whoisServer != null) {
            jsonBuilder.put("port43", whoisServer);
        }
        if (isTopLevel) {
            addTopLevelEntries(jsonBuilder, BoilerplateType.DOMAIN, remarks,
                    ImmutableList.<ImmutableMap<String, Object>>of(), linkBase);
        } else if (!remarks.isEmpty()) {
            jsonBuilder.put(REMARKS, remarks);
        }
        return jsonBuilder.build();
    }

    /**
     * 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<String, Object> makeRdapJsonForHost(HostResource hostResource, boolean isTopLevel,
            @Nullable String linkBase, @Nullable String whoisServer, DateTime now, OutputDataType outputDataType) {
        ImmutableMap.Builder<String, Object> 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()));
        }
        jsonBuilder.put("status", makeStatusValueList(hostResource.getStatusValues()));
        jsonBuilder.put("links",
                ImmutableList.of(makeLink("nameserver", hostResource.getFullyQualifiedHostName(), linkBase)));
        List<ImmutableMap<String, Object>> remarks;
        // 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);
        } else {
            remarks = ImmutableList.of();
            ImmutableList<Object> events = makeEvents(hostResource, now);
            if (!events.isEmpty()) {
                jsonBuilder.put("events", events);
            }
        }
        ImmutableSet<InetAddress> inetAddresses = hostResource.getInetAddresses();
        if (!inetAddresses.isEmpty()) {
            ImmutableList.Builder<String> v4AddressesBuilder = new ImmutableList.Builder<>();
            ImmutableList.Builder<String> 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<String, ImmutableList<String>> ipAddressesBuilder = new ImmutableMap.Builder<>();
            ImmutableList<String> v4Addresses = v4AddressesBuilder.build();
            if (!v4Addresses.isEmpty()) {
                ipAddressesBuilder.put("v4", Ordering.natural().immutableSortedCopy(v4Addresses));
            }
            ImmutableList<String> v6Addresses = v6AddressesBuilder.build();
            if (!v6Addresses.isEmpty()) {
                ipAddressesBuilder.put("v6", Ordering.natural().immutableSortedCopy(v6Addresses));
            }
            ImmutableMap<String, ImmutableList<String>> ipAddresses = ipAddressesBuilder.build();
            if (!ipAddresses.isEmpty()) {
                jsonBuilder.put("ipAddresses", ipAddressesBuilder.build());
            }
        }
        if (whoisServer != null) {
            jsonBuilder.put("port43", whoisServer);
        }
        if (isTopLevel) {
            addTopLevelEntries(jsonBuilder, BoilerplateType.NAMESERVER, remarks,
                    ImmutableList.<ImmutableMap<String, Object>>of(), linkBase);
        } else if (!remarks.isEmpty()) {
            jsonBuilder.put(REMARKS, remarks);
        }
        return jsonBuilder.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
     * @param outputDataType whether to generate full or summary data
     */
    ImmutableMap<String, Object> makeRdapJsonForContact(ContactResource contactResource, boolean isTopLevel,
            Optional<DesignatedContact.Type> contactType, @Nullable String linkBase, @Nullable String whoisServer,
            DateTime now, OutputDataType outputDataType) {
        ImmutableMap.Builder<String, Object> jsonBuilder = new ImmutableMap.Builder<>();
        jsonBuilder.put("objectClassName", "entity");
        jsonBuilder.put("handle", contactResource.getRepoId());
        jsonBuilder.put("status", makeStatusValueList(contactResource.getStatusValues()));
        if (contactType.isPresent()) {
            jsonBuilder.put("roles", ImmutableList.of(convertContactTypeToRdapRole(contactType.get())));
        }
        jsonBuilder.put("links", ImmutableList.of(makeLink("entity", contactResource.getRepoId(), linkBase)));
        // Create the vCard.
        ImmutableList.Builder<Object> vcardBuilder = new ImmutableList.Builder<>();
        vcardBuilder.add(VCARD_ENTRY_VERSION);
        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()));
            }
            if (postalInfo.getOrg() != null) {
                vcardBuilder.add(ImmutableList.of("org", ImmutableMap.of(), "text", postalInfo.getOrg()));
            }
            ImmutableList<Object> addressEntry = makeVCardAddressEntry(postalInfo.getAddress());
            if (addressEntry != null) {
                vcardBuilder.add(addressEntry);
            }
        }
        ContactPhoneNumber voicePhoneNumber = contactResource.getVoiceNumber();
        if (voicePhoneNumber != null) {
            vcardBuilder.add(makePhoneEntry(PHONE_TYPE_VOICE, makePhoneString(voicePhoneNumber)));
        }
        ContactPhoneNumber faxPhoneNumber = contactResource.getFaxNumber();
        if (faxPhoneNumber != null) {
            vcardBuilder.add(makePhoneEntry(PHONE_TYPE_FAX, makePhoneString(faxPhoneNumber)));
        }
        String emailAddress = contactResource.getEmailAddress();
        if (emailAddress != null) {
            vcardBuilder.add(ImmutableList.of("email", ImmutableMap.of(), "text", emailAddress));
        }
        jsonBuilder.put("vcardArray", ImmutableList.of("vcard", vcardBuilder.build()));
        // 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.
        List<ImmutableMap<String, Object>> remarks;
        if (outputDataType == OutputDataType.SUMMARY) {
            remarks = ImmutableList.of(RdapIcannStandardInformation.SUMMARY_DATA_REMARK);
        } else {
            remarks = ImmutableList.of();
            ImmutableList<Object> events = makeEvents(contactResource, now);
            if (!events.isEmpty()) {
                jsonBuilder.put("events", events);
            }
        }
        if (whoisServer != null) {
            jsonBuilder.put("port43", whoisServer);
        }
        if (isTopLevel) {
            addTopLevelEntries(jsonBuilder, BoilerplateType.ENTITY, remarks,
                    ImmutableList.<ImmutableMap<String, Object>>of(), linkBase);
        } else if (!remarks.isEmpty()) {
            jsonBuilder.put(REMARKS, remarks);
        }
        return jsonBuilder.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<String, Object> makeRdapJsonForRegistrar(Registrar registrar, boolean isTopLevel,
            @Nullable String linkBase, @Nullable String whoisServer, DateTime now, OutputDataType outputDataType) {
        ImmutableMap.Builder<String, Object> jsonBuilder = new ImmutableMap.Builder<>();
        jsonBuilder.put("objectClassName", "entity");
        jsonBuilder.put("handle", registrar.getIanaIdentifier().toString());
        jsonBuilder.put("status", STATUS_LIST_ACTIVE);
        jsonBuilder.put("roles", ImmutableList.of(RdapEntityRole.REGISTRAR.rfc7483String));
        jsonBuilder.put("links",
                ImmutableList.of(makeLink("entity", registrar.getIanaIdentifier().toString(), linkBase)));
        jsonBuilder.put("publicIds", ImmutableList.of(ImmutableMap.of("type", "IANA Registrar ID", "identifier",
                registrar.getIanaIdentifier().toString())));
        // Create the vCard.
        ImmutableList.Builder<Object> vcardBuilder = new ImmutableList.Builder<>();
        vcardBuilder.add(VCARD_ENTRY_VERSION);
        String registrarName = registrar.getRegistrarName();
        if (registrarName != null) {
            vcardBuilder.add(ImmutableList.of("fn", ImmutableMap.of(), "text", registrarName));
        }
        RegistrarAddress address = registrar.getInternationalizedAddress();
        if (address == null) {
            address = registrar.getLocalizedAddress();
        }
        if (address != null) {
            ImmutableList<Object> addressEntry = makeVCardAddressEntry(address);
            if (addressEntry != null) {
                vcardBuilder.add(addressEntry);
            }
        }
        String voicePhoneNumber = registrar.getPhoneNumber();
        if (voicePhoneNumber != null) {
            vcardBuilder.add(makePhoneEntry(PHONE_TYPE_VOICE, "tel:" + voicePhoneNumber));
        }
        String faxPhoneNumber = registrar.getFaxNumber();
        if (faxPhoneNumber != null) {
            vcardBuilder.add(makePhoneEntry(PHONE_TYPE_FAX, "tel:" + faxPhoneNumber));
        }
        String emailAddress = registrar.getEmailAddress();
        if (emailAddress != null) {
            vcardBuilder.add(ImmutableList.of("email", ImmutableMap.of(), "text", emailAddress));
        }
        jsonBuilder.put("vcardArray", ImmutableList.of("vcard", 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<ImmutableMap<String, Object>> remarks;
        if (outputDataType == OutputDataType.SUMMARY) {
            remarks = ImmutableList.of(RdapIcannStandardInformation.SUMMARY_DATA_REMARK);
        } else {
            remarks = ImmutableList.of();
            ImmutableList<Object> events = makeEvents(registrar, now);
            if (!events.isEmpty()) {
                jsonBuilder.put("events", events);
            }
            // include the registrar contacts as subentities
            ImmutableList.Builder<Map<String, Object>> registrarContactsBuilder = new ImmutableList.Builder<>();
            for (RegistrarContact registrarContact : registrar.getContacts()) {
                if (isVisible(registrarContact)) {
                    registrarContactsBuilder.add(makeRdapJsonForRegistrarContact(registrarContact, null));
                }
            }
            ImmutableList<Map<String, Object>> registrarContacts = registrarContactsBuilder.build();
            if (!registrarContacts.isEmpty()) {
                jsonBuilder.put("entities", registrarContacts);
            }
        }
        if (whoisServer != null) {
            jsonBuilder.put("port43", whoisServer);
        }
        if (isTopLevel) {
            addTopLevelEntries(jsonBuilder, BoilerplateType.ENTITY, remarks,
                    ImmutableList.<ImmutableMap<String, Object>>of(), linkBase);
        } else if (!remarks.isEmpty()) {
            jsonBuilder.put(REMARKS, remarks);
        }
        return jsonBuilder.build();
    }

    /**
     * Creates a JSON object for a {@link RegistrarContact}.
     *
     * @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<String, Object> makeRdapJsonForRegistrarContact(RegistrarContact registrarContact,
            @Nullable String whoisServer) {
        ImmutableMap.Builder<String, Object> 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", makeRdapRoleList(registrarContact));
        // Create the vCard.
        ImmutableList.Builder<Object> vcardBuilder = new ImmutableList.Builder<>();
        vcardBuilder.add(VCARD_ENTRY_VERSION);
        String name = registrarContact.getName();
        if (name != null) {
            vcardBuilder.add(ImmutableList.of("fn", ImmutableMap.of(), "text", name));
        }
        String voicePhoneNumber = registrarContact.getPhoneNumber();
        if (voicePhoneNumber != null) {
            vcardBuilder.add(makePhoneEntry(PHONE_TYPE_VOICE, "tel:" + voicePhoneNumber));
        }
        String faxPhoneNumber = registrarContact.getFaxNumber();
        if (faxPhoneNumber != null) {
            vcardBuilder.add(makePhoneEntry(PHONE_TYPE_FAX, "tel:" + faxPhoneNumber));
        }
        String emailAddress = registrarContact.getEmailAddress();
        if (emailAddress != null) {
            vcardBuilder.add(ImmutableList.of("email", ImmutableMap.of(), "text", emailAddress));
        }
        jsonBuilder.put("vcardArray", ImmutableList.of("vcard", vcardBuilder.build()));
        if (whoisServer != null) {
            jsonBuilder.put("port43", whoisServer);
        }
        return jsonBuilder.build();
    }

    /** Converts a domain registry contact type into a role as defined by RFC 7483. */
    private static String convertContactTypeToRdapRole(DesignatedContact.Type contactType) {
        switch (contactType) {
        case REGISTRANT:
            return RdapEntityRole.REGISTRANT.rfc7483String;
        case TECH:
            return RdapEntityRole.TECH.rfc7483String;
        case BILLING:
            return RdapEntityRole.BILLING.rfc7483String;
        case ADMIN:
            return RdapEntityRole.ADMIN.rfc7483String;
        default:
            throw new AssertionError();
        }
    }

    /**
     * Creates the list of RDAP roles for a registrar contact, using the visibleInWhoisAs* flags.
     */
    private static ImmutableList<String> makeRdapRoleList(RegistrarContact registrarContact) {
        ImmutableList.Builder<String> rolesBuilder = new ImmutableList.Builder<>();
        if (registrarContact.getVisibleInWhoisAsAdmin()) {
            rolesBuilder.add(RdapEntityRole.ADMIN.rfc7483String);
        }
        if (registrarContact.getVisibleInWhoisAsTech()) {
            rolesBuilder.add(RdapEntityRole.TECH.rfc7483String);
        }
        return rolesBuilder.build();
    }

    /** Checks whether the registrar contact should be visible (because it has visible roles). */
    private static boolean isVisible(RegistrarContact registrarContact) {
        return registrarContact.getVisibleInWhoisAsAdmin() || registrarContact.getVisibleInWhoisAsTech();
    }

    /**
     * Creates an event list for a domain, host or contact resource.
     */
    private static ImmutableList<Object> makeEvents(EppResource resource, DateTime now) {
        ImmutableList.Builder<Object> eventsBuilder = new ImmutableList.Builder<>();
        for (HistoryEntry historyEntry : ofy().load().type(HistoryEntry.class).ancestor(resource)
                .order("modificationTime")) {
            // Only create an event if this is a type we care about.
            if (!historyEntryTypeToRdapEventActionMap.containsKey(historyEntry.getType())) {
                continue;
            }
            RdapEventAction eventAction = historyEntryTypeToRdapEventActionMap.get(historyEntry.getType());
            eventsBuilder
                    .add(makeEvent(eventAction, historyEntry.getClientId(), historyEntry.getModificationTime()));
        }
        if (resource instanceof DomainResource) {
            DateTime expirationTime = ((DomainResource) resource).getRegistrationExpirationTime();
            if (expirationTime != null) {
                eventsBuilder.add(makeEvent(RdapEventAction.EXPIRATION, null, expirationTime));
            }
        }
        if ((resource.getLastEppUpdateTime() != null)
                && resource.getLastEppUpdateTime().isAfter(resource.getCreationTime())) {
            eventsBuilder.add(makeEvent(RdapEventAction.LAST_CHANGED, null, resource.getLastEppUpdateTime()));
        }
        eventsBuilder.add(makeEvent(RdapEventAction.LAST_UPDATE_OF_RDAP_DATABASE, null, now));
        return eventsBuilder.build();
    }

    /**
     * Creates an event list for a {@link Registrar}.
     */
    private static ImmutableList<Object> makeEvents(Registrar registrar, DateTime now) {
        ImmutableList.Builder<Object> eventsBuilder = new ImmutableList.Builder<>();
        eventsBuilder.add(makeEvent(RdapEventAction.REGISTRATION, registrar.getIanaIdentifier().toString(),
                registrar.getCreationTime()));
        if ((registrar.getLastUpdateTime() != null)
                && registrar.getLastUpdateTime().isAfter(registrar.getCreationTime())) {
            eventsBuilder.add(makeEvent(RdapEventAction.LAST_CHANGED, null, registrar.getLastUpdateTime()));
        }
        eventsBuilder.add(makeEvent(RdapEventAction.LAST_UPDATE_OF_RDAP_DATABASE, null, now));
        return eventsBuilder.build();
    }

    /**
     * Creates an RDAP event object as defined by RFC 7483.
     */
    private static ImmutableMap<String, Object> makeEvent(RdapEventAction eventAction, @Nullable String eventActor,
            DateTime eventDate) {
        ImmutableMap.Builder<String, Object> jsonBuilder = new ImmutableMap.Builder<>();
        jsonBuilder.put("eventAction", eventAction.getDisplayName());
        if (eventActor != null) {
            jsonBuilder.put("eventActor", eventActor);
        }
        jsonBuilder.put("eventDate", eventDate.toString());
        return jsonBuilder.build();
    }

    /**
     * Creates a vCard address entry: array of strings specifying the components of the address.
     *
     * @see <a href="https://tools.ietf.org/html/rfc7095">
     *        RFC 7095: jCard: The JSON Format for vCard</a>
     */
    private static ImmutableList<Object> makeVCardAddressEntry(Address address) {
        if (address == null) {
            return null;
        }
        ImmutableList.Builder<Object> jsonBuilder = new ImmutableList.Builder<>();
        jsonBuilder.add(""); // PO box
        jsonBuilder.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
        // if there is more than one line:
        //
        //   RFC7095 provides two examples of structured addresses, and one of the examples shows a
        //   street JSON element that contains several data elements. The example showing (see below)
        //   several data elements is the expected output when two or more <contact:street> elements
        //   exists in the contact object.
        //
        //   ["adr", {}, "text",
        //    [
        //    "", "",
        //    ["My Street", "Left Side", "Second Shack"],
        //    "Hometown", "PA", "18252", "U.S.A."
        //    ]
        //   ]
        ImmutableList<String> street = address.getStreet();
        if (street.isEmpty()) {
            jsonBuilder.add("");
        } else if (street.size() == 1) {
            jsonBuilder.add(street.get(0));
        } else {
            jsonBuilder.add(street);
        }
        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.<Object>of("adr", ImmutableMap.of(), "text", jsonBuilder.build());
    }

    /** Creates a vCard phone number entry. */
    private static ImmutableList<Object> makePhoneEntry(ImmutableMap<String, ImmutableList<String>> type,
            String phoneNumber) {
        return ImmutableList.<Object>of("tel", type, "uri", phoneNumber);
    }

    /** Creates a phone string in URI format, as per the vCard spec. */
    private static String makePhoneString(ContactPhoneNumber phoneNumber) {
        String phoneString = String.format("tel:%s", phoneNumber.getPhoneNumber());
        if (phoneNumber.getExtension() != null) {
            phoneString = phoneString + ";ext=" + phoneNumber.getExtension();
        }
        return phoneString;
    }

    /**
     * Creates a string array of status values; the spec indicates that OK should be listed as
     * "active".
     */
    private static ImmutableList<String> makeStatusValueList(ImmutableSet<StatusValue> statusValues) {
        return FluentIterable.from(statusValues)
                .transform(Functions.forMap(statusToRdapStatusMap, RdapStatus.OBSCURED))
                .transform(new Function<RdapStatus, String>() {
                    @Override
                    public String apply(RdapStatus status) {
                        return status.getDisplayName();
                    }
                }).toSortedSet(Ordering.natural()).asList();
    }

    /**
     * Creates a self link as directed by the spec.
     *
     * @see <a href="https://tools.ietf.org/html/rfc7483">
     *        RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP)</a>
     */
    private static ImmutableMap<String, String> 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 <a href="https://tools.ietf.org/html/rfc7483">
     *        RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP)</a>
     */
    ImmutableMap<String, Object> makeError(int status, String title, String description) {
        return ImmutableMap.<String, Object>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);
    }
}