diff --git a/core/src/main/java/google/registry/flows/ResourceFlowUtils.java b/core/src/main/java/google/registry/flows/ResourceFlowUtils.java
index 5076c4f0d..cb8bd7c7d 100644
--- a/core/src/main/java/google/registry/flows/ResourceFlowUtils.java
+++ b/core/src/main/java/google/registry/flows/ResourceFlowUtils.java
@@ -42,6 +42,7 @@ import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
+import google.registry.model.domain.DomainContent;
import google.registry.model.domain.Period;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.AuthInfo;
@@ -255,7 +256,7 @@ public final class ResourceFlowUtils {
* @param domain is the domain already projected at approvalTime
*/
public static DateTime computeExDateForApprovalTime(
- DomainBase domain, DateTime approvalTime, Period period) {
+ DomainContent domain, DateTime approvalTime, Period period) {
boolean inAutoRenew = domain.getGracePeriodStatuses().contains(GracePeriodStatus.AUTO_RENEW);
// inAutoRenew is set to false if the period is zero because a zero-period transfer should not
// subsume an autorenew.
diff --git a/core/src/main/java/google/registry/model/contact/ContactHistory.java b/core/src/main/java/google/registry/model/contact/ContactHistory.java
index 9e42943b2..76c3abe97 100644
--- a/core/src/main/java/google/registry/model/contact/ContactHistory.java
+++ b/core/src/main/java/google/registry/model/contact/ContactHistory.java
@@ -25,8 +25,8 @@ import javax.persistence.Entity;
* A persisted history entry representing an EPP modification to a contact.
*
*
In addition to the general history fields (e.g. action time, registrar ID) we also persist a
- * copy of the host entity at this point in time. We persist a raw {@link ContactBase} so that the
- * foreign-keyed fields in that class can refer to this object.
+ * copy of the contact entity at this point in time. We persist a raw {@link ContactBase} so that
+ * the foreign-keyed fields in that class can refer to this object.
*/
@Entity
@javax.persistence.Table(
diff --git a/core/src/main/java/google/registry/model/domain/DomainBase.java b/core/src/main/java/google/registry/model/domain/DomainBase.java
index b0ed78094..a24cb061d 100644
--- a/core/src/main/java/google/registry/model/domain/DomainBase.java
+++ b/core/src/main/java/google/registry/model/domain/DomainBase.java
@@ -14,79 +14,23 @@
package google.registry.model.domain;
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Strings.emptyToNull;
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
-import static com.google.common.collect.Sets.difference;
-import static com.google.common.collect.Sets.intersection;
-import static com.google.common.collect.Sets.union;
-import static google.registry.model.EppResourceUtils.projectResourceOntoBuilderAtTime;
-import static google.registry.model.EppResourceUtils.setAutomaticTransferSuccessProperties;
-import static google.registry.model.ofy.ObjectifyService.ofy;
-import static google.registry.util.CollectionUtils.forceEmptyToNull;
-import static google.registry.util.CollectionUtils.nullToEmpty;
-import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
-import static google.registry.util.CollectionUtils.union;
-import static google.registry.util.DateTimeUtils.END_OF_TIME;
-import static google.registry.util.DateTimeUtils.earliestOf;
-import static google.registry.util.DateTimeUtils.isBeforeOrAt;
-import static google.registry.util.DateTimeUtils.leapSafeAddYears;
-import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
-import static google.registry.util.DomainNameUtils.getTldFromDomainName;
-import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
-
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSortedSet;
-import com.google.common.collect.Ordering;
-import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
-import com.googlecode.objectify.annotation.Ignore;
-import com.googlecode.objectify.annotation.IgnoreSave;
-import com.googlecode.objectify.annotation.Index;
-import com.googlecode.objectify.annotation.OnLoad;
-import com.googlecode.objectify.condition.IfNull;
-import google.registry.flows.ResourceFlowUtils;
import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
-import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.annotations.ReportedOn;
-import google.registry.model.billing.BillingEvent;
-import google.registry.model.contact.ContactResource;
-import google.registry.model.domain.DesignatedContact.Type;
-import google.registry.model.domain.launch.LaunchNotice;
-import google.registry.model.domain.rgp.GracePeriodStatus;
-import google.registry.model.domain.secdns.DelegationSignerData;
-import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.HostResource;
-import google.registry.model.poll.PollMessage;
-import google.registry.model.registry.Registry;
-import google.registry.model.transfer.DomainTransferData;
-import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import google.registry.persistence.WithStringVKey;
import google.registry.schema.replay.DatastoreAndSqlEntity;
-import google.registry.util.CollectionUtils;
-import java.util.HashSet;
-import java.util.Objects;
-import java.util.Optional;
import java.util.Set;
-import java.util.function.Predicate;
-import javax.annotation.Nullable;
import javax.persistence.Access;
import javax.persistence.AccessType;
-import javax.persistence.AttributeOverride;
-import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
-import javax.persistence.Embedded;
import javax.persistence.JoinTable;
-import javax.persistence.PostLoad;
-import javax.persistence.Transient;
import org.joda.time.DateTime;
-import org.joda.time.Interval;
/**
* A persistable domain resource including mutable and non-mutable fields.
@@ -112,197 +56,8 @@ import org.joda.time.Interval;
@WithStringVKey
@ExternalMessagingName("domain")
@Access(AccessType.FIELD)
-public class DomainBase extends EppResource
- implements DatastoreAndSqlEntity,
- ForeignKeyedEppResource,
- ResourceWithTransferData {
-
- /** The max number of years that a domain can be registered for, as set by ICANN policy. */
- public static final int MAX_REGISTRATION_YEARS = 10;
-
- /** Status values which prohibit DNS information from being published. */
- private static final ImmutableSet DNS_PUBLISHING_PROHIBITED_STATUSES =
- ImmutableSet.of(
- StatusValue.CLIENT_HOLD,
- StatusValue.INACTIVE,
- StatusValue.PENDING_DELETE,
- StatusValue.SERVER_HOLD);
-
- /**
- * Fully qualified domain name (puny-coded), which serves as the foreign key for this domain.
- *
- * This is only unique in the sense that for any given lifetime specified as the time range
- * from (creationTime, deletionTime) there can only be one domain in Datastore with this name.
- * However, there can be many domains with the same name and non-overlapping lifetimes.
- *
- * @invariant fullyQualifiedDomainName == fullyQualifiedDomainName.toLowerCase(Locale.ENGLISH)
- */
- // TODO(b/158858642): Rename this to domainName when we are off Datastore
- @Column(name = "domainName")
- @Index
- String fullyQualifiedDomainName;
-
- /** The top level domain this is under, dernormalized from {@link #fullyQualifiedDomainName}. */
- @Index
- String tld;
-
- /** References to hosts that are the nameservers for the domain. */
- @Index
- @ElementCollection
- @JoinTable(name = "DomainHost")
- Set> nsHosts;
-
- /**
- * The union of the contacts visible via {@link #getContacts} and {@link #getRegistrant}.
- *
- * These are stored in one field so that we can query across all contacts at once.
- */
- @Transient Set allContacts;
-
- /**
- * Contacts as they are stored in cloud SQL.
- *
- * This information is duplicated in allContacts, and must be kept in sync with it.
- */
- @Ignore VKey adminContact;
-
- @Ignore VKey billingContact;
- @Ignore VKey techContact;
- @Ignore VKey registrantContact;
-
- /** Authorization info (aka transfer secret) of the domain. */
- @Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "pw.value", column = @Column(name = "auth_info_value")),
- @AttributeOverride(name = "pw.repoId", column = @Column(name = "auth_info_repo_id")),
- })
- DomainAuthInfo authInfo;
-
- /**
- * Data used to construct DS records for this domain.
- *
- * This is {@literal @}XmlTransient because it needs to be returned under the "extension" tag
- * of an info response rather than inside the "infData" tag.
- */
- @Transient Set dsData;
-
- /**
- * The claims notice supplied when this application or domain was created, if there was one. It's
- * {@literal @}XmlTransient because it's not returned in an info response.
- */
- @IgnoreSave(IfNull.class)
- @Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "noticeId.tcnId", column = @Column(name = "launch_notice_tcn_id")),
- @AttributeOverride(
- name = "noticeId.validatorId",
- column = @Column(name = "launch_notice_validator_id")),
- @AttributeOverride(
- name = "expirationTime",
- column = @Column(name = "launch_notice_expiration_time")),
- @AttributeOverride(
- name = "acceptedTime",
- column = @Column(name = "launch_notice_accepted_time")),
- })
- LaunchNotice launchNotice;
-
- /**
- * Name of first IDN table associated with TLD that matched the characters in this domain label.
- *
- * @see google.registry.tldconfig.idn.IdnLabelValidator#findValidIdnTableForTld
- */
- @IgnoreSave(IfNull.class)
- String idnTableName;
-
- /** Fully qualified host names of this domain's active subordinate hosts. */
- Set subordinateHosts;
-
- /** When this domain's registration will expire. */
- DateTime registrationExpirationTime;
-
- /**
- * The poll message associated with this domain being deleted.
- *
- * This field should be null if the domain is not in pending delete. If it is, the field should
- * refer to a {@link PollMessage} timed to when the domain is fully deleted. If the domain is
- * restored, the message should be deleted.
- */
- @Column(name = "deletion_poll_message_id")
- VKey deletePollMessage;
-
- /**
- * The recurring billing event associated with this domain's autorenewals.
- *
- * The recurrence should be open ended unless the domain is in pending delete or fully deleted,
- * in which case it should be closed at the time the delete was requested. Whenever the domain's
- * {@link #registrationExpirationTime} is changed the recurrence should be closed, a new one
- * should be created, and this field should be updated to point to the new one.
- */
- @Column(name = "billing_recurrence_id")
- VKey autorenewBillingEvent;
-
- /**
- * The recurring poll message associated with this domain's autorenewals.
- *
- * The recurrence should be open ended unless the domain is in pending delete or fully deleted,
- * in which case it should be closed at the time the delete was requested. Whenever the domain's
- * {@link #registrationExpirationTime} is changed the recurrence should be closed, a new one
- * should be created, and this field should be updated to point to the new one.
- */
- @Column(name = "autorenew_poll_message_id")
- VKey autorenewPollMessage;
-
- /** The unexpired grace periods for this domain (some of which may not be active yet). */
- @Transient @ElementCollection Set gracePeriods;
-
- /**
- * The id of the signed mark that was used to create this domain in sunrise.
- *
- * Will only be populated for domains created in sunrise.
- */
- @IgnoreSave(IfNull.class)
- String smdId;
-
- /** Data about any pending or past transfers on this domain. */
- DomainTransferData transferData;
-
- /**
- * The time that this resource was last transferred.
- *
- *
Can be null if the resource has never been transferred.
- */
- DateTime lastTransferTime;
-
- @OnLoad
- void load() {
- // Reconstitute all of the contacts so that they have VKeys.
- allContacts =
- allContacts.stream().map(contact -> contact.reconstitute()).collect(toImmutableSet());
- setContactFields(allContacts, true);
- }
-
- @PostLoad
- void postLoad() {
- // Reconstitute the contact list.
- ImmutableSet.Builder contactsBuilder =
- new ImmutableSet.Builder();
-
- if (registrantContact != null) {
- contactsBuilder.add(
- DesignatedContact.create(DesignatedContact.Type.REGISTRANT, registrantContact));
- }
- if (billingContact != null) {
- contactsBuilder.add(DesignatedContact.create(DesignatedContact.Type.BILLING, billingContact));
- }
- if (techContact != null) {
- contactsBuilder.add(DesignatedContact.create(DesignatedContact.Type.TECH, techContact));
- }
- if (adminContact != null) {
- contactsBuilder.add(DesignatedContact.create(DesignatedContact.Type.ADMIN, adminContact));
- }
-
- allContacts = contactsBuilder.build();
- }
+public class DomainBase extends DomainContent
+ implements DatastoreAndSqlEntity, ForeignKeyedEppResource {
@Override
@javax.persistence.Id
@@ -311,326 +66,12 @@ public class DomainBase extends EppResource
return super.getRepoId();
}
- public ImmutableSet getSubordinateHosts() {
- return nullToEmptyImmutableCopy(subordinateHosts);
- }
-
- public DateTime getRegistrationExpirationTime() {
- return registrationExpirationTime;
- }
-
- public VKey getDeletePollMessage() {
- return deletePollMessage;
- }
-
- public VKey getAutorenewBillingEvent() {
- return autorenewBillingEvent;
- }
-
- public VKey getAutorenewPollMessage() {
- return autorenewPollMessage;
- }
-
- public ImmutableSet getGracePeriods() {
- return nullToEmptyImmutableCopy(gracePeriods);
- }
-
- public String getSmdId() {
- return smdId;
- }
-
- @Override
- public DomainTransferData getTransferData() {
- return Optional.ofNullable(transferData).orElse(DomainTransferData.EMPTY);
- }
-
- @Override
- public DateTime getLastTransferTime() {
- return lastTransferTime;
- }
-
- @Override
- public String getForeignKey() {
- return fullyQualifiedDomainName;
- }
-
- public String getDomainName() {
- return fullyQualifiedDomainName;
- }
-
- public ImmutableSet getDsData() {
- return nullToEmptyImmutableCopy(dsData);
- }
-
- public LaunchNotice getLaunchNotice() {
- return launchNotice;
- }
-
- public String getIdnTableName() {
- return idnTableName;
- }
-
- public ImmutableSet> getNameservers() {
- return nullToEmptyImmutableCopy(nsHosts);
- }
-
- public final String getCurrentSponsorClientId() {
- return getPersistedCurrentSponsorClientId();
- }
-
- /** Returns true if DNS information should be published for the given domain. */
- public boolean shouldPublishToDns() {
- return intersection(getStatusValues(), DNS_PUBLISHING_PROHIBITED_STATUSES).isEmpty();
- }
-
- /**
- * Returns the Registry Grace Period Statuses for this domain.
- *
- * This collects all statuses from the domain's {@link GracePeriod} entries and also adds the
- * PENDING_DELETE status if needed.
- */
- public ImmutableSet getGracePeriodStatuses() {
- Set gracePeriodStatuses = new HashSet<>();
- for (GracePeriod gracePeriod : getGracePeriods()) {
- gracePeriodStatuses.add(gracePeriod.getType());
- }
- if (getStatusValues().contains(StatusValue.PENDING_DELETE)
- && !gracePeriodStatuses.contains(GracePeriodStatus.REDEMPTION)) {
- gracePeriodStatuses.add(GracePeriodStatus.PENDING_DELETE);
- }
- return ImmutableSet.copyOf(gracePeriodStatuses);
- }
-
- /** Returns the subset of grace periods having the specified type. */
- public ImmutableSet getGracePeriodsOfType(GracePeriodStatus gracePeriodType) {
- ImmutableSet.Builder builder = new ImmutableSet.Builder<>();
- for (GracePeriod gracePeriod : getGracePeriods()) {
- if (gracePeriod.getType() == gracePeriodType) {
- builder.add(gracePeriod);
- }
- }
- return builder.build();
- }
-
- /**
- * The logic in this method, which handles implicit server approval of transfers, very closely
- * parallels the logic in {@code DomainTransferApproveFlow} which handles explicit client
- * approvals.
- */
- @Override
- public DomainBase cloneProjectedAtTime(final DateTime now) {
-
- DomainTransferData transferData = getTransferData();
- DateTime transferExpirationTime = transferData.getPendingTransferExpirationTime();
-
- // If there's a pending transfer that has expired, handle it.
- if (TransferStatus.PENDING.equals(transferData.getTransferStatus())
- && isBeforeOrAt(transferExpirationTime, now)) {
- // Project until just before the transfer time. This will handle the case of an autorenew
- // before the transfer was even requested or during the request period.
- // If the transfer time is precisely the moment that the domain expires, there will not be an
- // autorenew billing event (since we end the recurrence at transfer time and recurrences are
- // exclusive of their ending), and we can just proceed with the transfer.
- DomainBase domainAtTransferTime =
- cloneProjectedAtTime(transferExpirationTime.minusMillis(1));
-
- DateTime expirationDate = transferData.getTransferredRegistrationExpirationTime();
- if (expirationDate == null) {
- // Extend the registration by the correct number of years from the expiration time
- // that was current on the domain right before the transfer, capped at 10 years from
- // the moment of the transfer.
- expirationDate =
- ResourceFlowUtils.computeExDateForApprovalTime(
- domainAtTransferTime, transferExpirationTime, transferData.getTransferPeriod());
- }
- // If we are within an autorenew grace period, the transfer will subsume the autorenew. There
- // will already be a cancellation written in advance by the transfer request flow, so we don't
- // need to worry about billing, but we do need to cancel out the expiration time increase.
- // The transfer period saved in the transfer data will be one year, unless the superuser
- // extension set the transfer period to zero.
- // Set the expiration, autorenew events, and grace period for the transfer. (Transfer ends
- // all other graces).
- Builder builder =
- domainAtTransferTime
- .asBuilder()
- .setRegistrationExpirationTime(expirationDate)
- // Set the speculatively-written new autorenew events as the domain's autorenew
- // events.
- .setAutorenewBillingEvent(transferData.getServerApproveAutorenewEvent())
- .setAutorenewPollMessage(transferData.getServerApproveAutorenewPollMessage());
- if (transferData.getTransferPeriod().getValue() == 1) {
- // Set the grace period using a key to the prescheduled transfer billing event. Not using
- // GracePeriod.forBillingEvent() here in order to avoid the actual Datastore fetch.
- builder.setGracePeriods(
- ImmutableSet.of(
- GracePeriod.create(
- GracePeriodStatus.TRANSFER,
- transferExpirationTime.plus(
- Registry.get(getTld()).getTransferGracePeriodLength()),
- transferData.getGainingClientId(),
- transferData.getServerApproveBillingEvent())));
- } else {
- // There won't be a billing event, so we don't need a grace period
- builder.setGracePeriods(ImmutableSet.of());
- }
- // Set all remaining transfer properties.
- setAutomaticTransferSuccessProperties(builder, transferData);
- builder
- .setLastEppUpdateTime(transferExpirationTime)
- .setLastEppUpdateClientId(transferData.getGainingClientId());
- // Finish projecting to now.
- return builder.build().cloneProjectedAtTime(now);
- }
-
- Optional newLastEppUpdateTime = Optional.empty();
-
- // There is no transfer. Do any necessary autorenews for active domains.
-
- Builder builder = asBuilder();
- if (isBeforeOrAt(registrationExpirationTime, now) && END_OF_TIME.equals(getDeletionTime())) {
- // Autorenew by the number of years between the old expiration time and now.
- DateTime lastAutorenewTime = leapSafeAddYears(
- registrationExpirationTime,
- new Interval(registrationExpirationTime, now).toPeriod().getYears());
- DateTime newExpirationTime = lastAutorenewTime.plusYears(1);
- builder
- .setRegistrationExpirationTime(newExpirationTime)
- .addGracePeriod(
- GracePeriod.createForRecurring(
- GracePeriodStatus.AUTO_RENEW,
- lastAutorenewTime.plus(Registry.get(getTld()).getAutoRenewGracePeriodLength()),
- getCurrentSponsorClientId(),
- autorenewBillingEvent));
- newLastEppUpdateTime = Optional.of(lastAutorenewTime);
- }
-
- // Remove any grace periods that have expired.
- DomainBase almostBuilt = builder.build();
- builder = almostBuilt.asBuilder();
- for (GracePeriod gracePeriod : almostBuilt.getGracePeriods()) {
- if (isBeforeOrAt(gracePeriod.getExpirationTime(), now)) {
- builder.removeGracePeriod(gracePeriod);
- if (!newLastEppUpdateTime.isPresent()
- || isBeforeOrAt(newLastEppUpdateTime.get(), gracePeriod.getExpirationTime())) {
- newLastEppUpdateTime = Optional.of(gracePeriod.getExpirationTime());
- }
- }
- }
-
- // It is possible that the lastEppUpdateClientId is different from current sponsor client
- // id, so we have to do the comparison instead of having one variable just storing the most
- // recent time.
- if (newLastEppUpdateTime.isPresent()) {
- if (getLastEppUpdateTime() == null
- || newLastEppUpdateTime.get().isAfter(getLastEppUpdateTime())) {
- builder
- .setLastEppUpdateTime(newLastEppUpdateTime.get())
- .setLastEppUpdateClientId(getCurrentSponsorClientId());
- }
- }
-
- // Handle common properties like setting or unsetting linked status. This also handles the
- // general case of pending transfers for other resource types, but since we've always handled
- // a pending transfer by this point that's a no-op for domains.
- projectResourceOntoBuilderAtTime(almostBuilt, builder, now);
- return builder.build();
- }
-
- /** Return what the expiration time would be if the given number of years were added to it. */
- public static DateTime extendRegistrationWithCap(
- DateTime now,
- DateTime currentExpirationTime,
- @Nullable Integer extendedRegistrationYears) {
- // We must cap registration at the max years (aka 10), even if that truncates the last year.
- return earliestOf(
- leapSafeAddYears(
- currentExpirationTime,
- Optional.ofNullable(extendedRegistrationYears).orElse(0)),
- leapSafeAddYears(now, MAX_REGISTRATION_YEARS));
- }
-
- /** Loads and returns the fully qualified host names of all linked nameservers. */
- public ImmutableSortedSet loadNameserverHostNames() {
- return ofy()
- .load()
- .keys(getNameservers().stream().map(VKey::getOfyKey).collect(toImmutableSet()))
- .values()
- .stream()
- .map(HostResource::getHostName)
- .collect(toImmutableSortedSet(Ordering.natural()));
- }
-
- /** A key to the registrant who registered this domain. */
- public VKey getRegistrant() {
- return registrantContact;
- }
-
- public VKey getAdminContact() {
- return adminContact;
- }
-
- public VKey getBillingContact() {
- return billingContact;
- }
-
- public VKey getTechContact() {
- return techContact;
- }
-
- /** Associated contacts for the domain (other than registrant). */
- public ImmutableSet getContacts() {
- return nullToEmpty(allContacts)
- .stream()
- .filter(IS_REGISTRANT.negate())
- .collect(toImmutableSet());
- }
-
- public DomainAuthInfo getAuthInfo() {
- return authInfo;
- }
-
- /** Returns all referenced contacts from this domain or application. */
- public ImmutableSet> getReferencedContacts() {
- return nullToEmptyImmutableCopy(allContacts)
- .stream()
- .map(DesignatedContact::getContactKey)
- .filter(Objects::nonNull)
- .collect(toImmutableSet());
- }
-
- public String getTld() {
- return tld;
- }
-
- /**
- * Sets the individual contact fields from {@code contacts}.
- *
- * The registrant field is only set if {@code includeRegistrant} is true, as this field needs
- * to be set in some circumstances but not in others.
- */
- private void setContactFields(Set contacts, boolean includeRegistrant) {
-
- // Set the individual contact fields.
- for (DesignatedContact contact : contacts) {
- switch (contact.getType()) {
- case BILLING:
- billingContact = contact.getContactKey();
- break;
- case TECH:
- techContact = contact.getContactKey();
- break;
- case ADMIN:
- adminContact = contact.getContactKey();
- break;
- case REGISTRANT:
- if (includeRegistrant) {
- registrantContact = contact.getContactKey();
- }
- break;
- default:
- throw new IllegalArgumentException("Unknown contact resource type: " + contact.getType());
- }
- }
+ @ElementCollection
+ @JoinTable(name = "DomainHost")
+ @Access(AccessType.PROPERTY)
+ @Column(name = "host_repo_id")
+ public Set> getNsHosts() {
+ return super.nsHosts;
}
@Override
@@ -638,9 +79,14 @@ public class DomainBase extends EppResource
return VKey.create(DomainBase.class, getRepoId(), Key.create(this));
}
- /** Predicate to determine if a given {@link DesignatedContact} is the registrant. */
- private static final Predicate IS_REGISTRANT =
- (DesignatedContact contact) -> DesignatedContact.Type.REGISTRANT.equals(contact.type);
+ @Override
+ public DomainBase cloneProjectedAtTime(final DateTime now) {
+ return cloneDomainProjectedAtTime(this, now);
+ }
+
+ public static VKey createVKey(Key key) {
+ return VKey.create(DomainBase.class, key.getName(), key);
+ }
/** An override of {@link EppResource#asBuilder} with tighter typing. */
@Override
@@ -649,199 +95,12 @@ public class DomainBase extends EppResource
}
/** A builder for constructing {@link DomainBase}, since it is immutable. */
- public static class Builder extends EppResource.Builder
- implements BuilderWithTransferData {
+ public static class Builder extends DomainContent.Builder {
public Builder() {}
Builder(DomainBase instance) {
super(instance);
}
-
- @Override
- public DomainBase build() {
- DomainBase instance = getInstance();
- // If TransferData is totally empty, set it to null.
- if (DomainTransferData.EMPTY.equals(getInstance().transferData)) {
- setTransferData(null);
- }
- // A DomainBase has status INACTIVE if there are no nameservers.
- if (getInstance().getNameservers().isEmpty()) {
- addStatusValue(StatusValue.INACTIVE);
- } else { // There are nameservers, so make sure INACTIVE isn't there.
- removeStatusValue(StatusValue.INACTIVE);
- }
-
- checkArgumentNotNull(emptyToNull(instance.fullyQualifiedDomainName), "Missing domainName");
- if (instance.getRegistrant() == null
- && instance.allContacts.stream().anyMatch(IS_REGISTRANT)) {
- throw new IllegalArgumentException("registrant is null but is in allContacts");
- }
- checkArgumentNotNull(instance.getRegistrant(), "Missing registrant");
- instance.tld = getTldFromDomainName(instance.fullyQualifiedDomainName);
- return super.build();
- }
-
- public Builder setDomainName(String domainName) {
- checkArgument(
- domainName.equals(canonicalizeDomainName(domainName)),
- "Domain name must be in puny-coded, lower-case form");
- getInstance().fullyQualifiedDomainName = domainName;
- return thisCastToDerived();
- }
-
- public Builder setDsData(ImmutableSet dsData) {
- getInstance().dsData = dsData;
- return thisCastToDerived();
- }
-
- public Builder setRegistrant(VKey registrant) {
- // Replace the registrant contact inside allContacts.
- getInstance().allContacts = union(
- getInstance().getContacts(),
- DesignatedContact.create(Type.REGISTRANT, checkArgumentNotNull(registrant)));
-
- // Set the registrant field specifically.
- getInstance().registrantContact = registrant;
- return thisCastToDerived();
- }
-
- public Builder setAuthInfo(DomainAuthInfo authInfo) {
- getInstance().authInfo = authInfo;
- return thisCastToDerived();
- }
-
- public Builder setNameservers(VKey nameserver) {
- getInstance().nsHosts = ImmutableSet.of(nameserver);
- return thisCastToDerived();
- }
-
- public Builder setNameservers(ImmutableSet> nameservers) {
- getInstance().nsHosts = forceEmptyToNull(nameservers);
- return thisCastToDerived();
- }
-
- public Builder addNameserver(VKey nameserver) {
- return addNameservers(ImmutableSet.of(nameserver));
- }
-
- public Builder addNameservers(ImmutableSet> nameservers) {
- return setNameservers(
- ImmutableSet.copyOf(union(getInstance().getNameservers(), nameservers)));
- }
-
- public Builder removeNameserver(VKey nameserver) {
- return removeNameservers(ImmutableSet.of(nameserver));
- }
-
- public Builder removeNameservers(ImmutableSet> nameservers) {
- return setNameservers(
- ImmutableSet.copyOf(difference(getInstance().getNameservers(), nameservers)));
- }
-
- public Builder setContacts(DesignatedContact contact) {
- return setContacts(ImmutableSet.of(contact));
- }
-
- public Builder setContacts(ImmutableSet contacts) {
- checkArgument(contacts.stream().noneMatch(IS_REGISTRANT), "Registrant cannot be a contact");
-
- // Replace the non-registrant contacts inside allContacts.
- getInstance().allContacts =
- Streams.concat(
- nullToEmpty(getInstance().allContacts).stream().filter(IS_REGISTRANT),
- contacts.stream())
- .collect(toImmutableSet());
-
- // Set the individual fields.
- getInstance().setContactFields(contacts, false);
- return thisCastToDerived();
- }
-
- public Builder addContacts(ImmutableSet contacts) {
- return setContacts(ImmutableSet.copyOf(union(getInstance().getContacts(), contacts)));
- }
-
- public Builder removeContacts(ImmutableSet contacts) {
- return setContacts(ImmutableSet.copyOf(difference(getInstance().getContacts(), contacts)));
- }
-
- public Builder setLaunchNotice(LaunchNotice launchNotice) {
- getInstance().launchNotice = launchNotice;
- return thisCastToDerived();
- }
-
- public Builder setIdnTableName(String idnTableName) {
- getInstance().idnTableName = idnTableName;
- return thisCastToDerived();
- }
-
- public Builder setSubordinateHosts(ImmutableSet subordinateHosts) {
- getInstance().subordinateHosts = subordinateHosts;
- return thisCastToDerived();
- }
-
- public Builder addSubordinateHost(String hostToAdd) {
- return setSubordinateHosts(ImmutableSet.copyOf(
- union(getInstance().getSubordinateHosts(), hostToAdd)));
- }
-
- public Builder removeSubordinateHost(String hostToRemove) {
- return setSubordinateHosts(ImmutableSet.copyOf(
- CollectionUtils.difference(getInstance().getSubordinateHosts(), hostToRemove)));
- }
-
- public Builder setRegistrationExpirationTime(DateTime registrationExpirationTime) {
- getInstance().registrationExpirationTime = registrationExpirationTime;
- return this;
- }
-
- public Builder setDeletePollMessage(VKey deletePollMessage) {
- getInstance().deletePollMessage = deletePollMessage;
- return this;
- }
-
- public Builder setAutorenewBillingEvent(VKey autorenewBillingEvent) {
- getInstance().autorenewBillingEvent = autorenewBillingEvent;
- return this;
- }
-
- public Builder setAutorenewPollMessage(VKey autorenewPollMessage) {
- getInstance().autorenewPollMessage = autorenewPollMessage;
- return this;
- }
-
- public Builder setSmdId(String smdId) {
- getInstance().smdId = smdId;
- return this;
- }
-
- public Builder setGracePeriods(ImmutableSet gracePeriods) {
- getInstance().gracePeriods = gracePeriods;
- return this;
- }
-
- public Builder addGracePeriod(GracePeriod gracePeriod) {
- getInstance().gracePeriods = union(getInstance().getGracePeriods(), gracePeriod);
- return this;
- }
-
- public Builder removeGracePeriod(GracePeriod gracePeriod) {
- getInstance().gracePeriods = CollectionUtils
- .difference(getInstance().getGracePeriods(), gracePeriod);
- return this;
- }
-
- @Override
- public Builder setTransferData(DomainTransferData transferData) {
- getInstance().transferData = transferData;
- return thisCastToDerived();
- }
-
- @Override
- public Builder setLastTransferTime(DateTime lastTransferTime) {
- getInstance().lastTransferTime = lastTransferTime;
- return thisCastToDerived();
- }
}
}
diff --git a/core/src/main/java/google/registry/model/domain/DomainContent.java b/core/src/main/java/google/registry/model/domain/DomainContent.java
new file mode 100644
index 000000000..58f06b671
--- /dev/null
+++ b/core/src/main/java/google/registry/model/domain/DomainContent.java
@@ -0,0 +1,832 @@
+// 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.model.domain;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
+import static com.google.common.collect.Sets.difference;
+import static com.google.common.collect.Sets.intersection;
+import static google.registry.model.EppResourceUtils.projectResourceOntoBuilderAtTime;
+import static google.registry.model.EppResourceUtils.setAutomaticTransferSuccessProperties;
+import static google.registry.model.ofy.ObjectifyService.ofy;
+import static google.registry.util.CollectionUtils.forceEmptyToNull;
+import static google.registry.util.CollectionUtils.nullToEmpty;
+import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
+import static google.registry.util.CollectionUtils.union;
+import static google.registry.util.DateTimeUtils.END_OF_TIME;
+import static google.registry.util.DateTimeUtils.earliestOf;
+import static google.registry.util.DateTimeUtils.isBeforeOrAt;
+import static google.registry.util.DateTimeUtils.leapSafeAddYears;
+import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
+import static google.registry.util.DomainNameUtils.getTldFromDomainName;
+import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
+import com.googlecode.objectify.Key;
+import com.googlecode.objectify.annotation.Ignore;
+import com.googlecode.objectify.annotation.IgnoreSave;
+import com.googlecode.objectify.annotation.Index;
+import com.googlecode.objectify.annotation.OnLoad;
+import com.googlecode.objectify.condition.IfNull;
+import google.registry.flows.ResourceFlowUtils;
+import google.registry.model.EppResource;
+import google.registry.model.EppResource.ResourceWithTransferData;
+import google.registry.model.billing.BillingEvent;
+import google.registry.model.contact.ContactResource;
+import google.registry.model.domain.launch.LaunchNotice;
+import google.registry.model.domain.rgp.GracePeriodStatus;
+import google.registry.model.domain.secdns.DelegationSignerData;
+import google.registry.model.eppcommon.StatusValue;
+import google.registry.model.host.HostResource;
+import google.registry.model.poll.PollMessage;
+import google.registry.model.registry.Registry;
+import google.registry.model.transfer.DomainTransferData;
+import google.registry.model.transfer.TransferStatus;
+import google.registry.persistence.VKey;
+import google.registry.util.CollectionUtils;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+import javax.annotation.Nullable;
+import javax.persistence.Access;
+import javax.persistence.AccessType;
+import javax.persistence.AttributeOverride;
+import javax.persistence.AttributeOverrides;
+import javax.persistence.Column;
+import javax.persistence.ElementCollection;
+import javax.persistence.Embeddable;
+import javax.persistence.Embedded;
+import javax.persistence.MappedSuperclass;
+import javax.persistence.PostLoad;
+import javax.persistence.Transient;
+import org.joda.time.DateTime;
+import org.joda.time.Interval;
+
+/**
+ * A persistable domain resource including mutable and non-mutable fields.
+ *
+ * This class deliberately does not include an {@link javax.persistence.Id} so that any
+ * foreign-keyed fields can refer to the proper parent entity's ID, whether we're storing this in
+ * the DB itself or as part of another entity.
+ *
+ *
For historical reasons, the name of this class is "DomainContent". Ideally it would be
+ * "DomainBase" for parallelism with the other {@link EppResource} entity classes, but because that
+ * name is already taken by {@link DomainBase} (also for historical reasons), we can't use it. Once
+ * we are no longer on Datastore, we can rename the classes.
+ *
+ * @see RFC 5731
+ */
+@MappedSuperclass
+@Embeddable
+@Access(AccessType.FIELD)
+public class DomainContent extends EppResource
+ implements ResourceWithTransferData {
+
+ /** The max number of years that a domain can be registered for, as set by ICANN policy. */
+ public static final int MAX_REGISTRATION_YEARS = 10;
+
+ /** Status values which prohibit DNS information from being published. */
+ private static final ImmutableSet DNS_PUBLISHING_PROHIBITED_STATUSES =
+ ImmutableSet.of(
+ StatusValue.CLIENT_HOLD,
+ StatusValue.INACTIVE,
+ StatusValue.PENDING_DELETE,
+ StatusValue.SERVER_HOLD);
+
+ /**
+ * Fully qualified domain name (puny-coded), which serves as the foreign key for this domain.
+ *
+ * This is only unique in the sense that for any given lifetime specified as the time range
+ * from (creationTime, deletionTime) there can only be one domain in Datastore with this name.
+ * However, there can be many domains with the same name and non-overlapping lifetimes.
+ *
+ * @invariant fullyQualifiedDomainName == fullyQualifiedDomainName.toLowerCase(Locale.ENGLISH)
+ */
+ // TODO(b/158858642): Rename this to domainName when we are off Datastore
+ @Column(name = "domainName")
+ @Index
+ String fullyQualifiedDomainName;
+
+ /** The top level domain this is under, dernormalized from {@link #fullyQualifiedDomainName}. */
+ @Index String tld;
+
+ /** References to hosts that are the nameservers for the domain. */
+ @Index @Transient Set> nsHosts;
+
+ /**
+ * The union of the contacts visible via {@link #getContacts} and {@link #getRegistrant}.
+ *
+ * These are stored in one field so that we can query across all contacts at once.
+ */
+ @Transient Set allContacts;
+
+ /**
+ * Contacts as they are stored in cloud SQL.
+ *
+ * This information is duplicated in allContacts, and must be kept in sync with it.
+ */
+ @Ignore VKey adminContact;
+
+ @Ignore VKey billingContact;
+ @Ignore VKey techContact;
+ @Ignore VKey registrantContact;
+
+ /** Authorization info (aka transfer secret) of the domain. */
+ @Embedded
+ @AttributeOverrides({
+ @AttributeOverride(name = "pw.value", column = @Column(name = "auth_info_value")),
+ @AttributeOverride(name = "pw.repoId", column = @Column(name = "auth_info_repo_id")),
+ })
+ DomainAuthInfo authInfo;
+
+ /**
+ * Data used to construct DS records for this domain.
+ *
+ * This is {@literal @}XmlTransient because it needs to be returned under the "extension" tag
+ * of an info response rather than inside the "infData" tag.
+ */
+ @Transient Set dsData;
+
+ /**
+ * The claims notice supplied when this application or domain was created, if there was one. It's
+ * {@literal @}XmlTransient because it's not returned in an info response.
+ */
+ @IgnoreSave(IfNull.class)
+ @Embedded
+ @AttributeOverrides({
+ @AttributeOverride(name = "noticeId.tcnId", column = @Column(name = "launch_notice_tcn_id")),
+ @AttributeOverride(
+ name = "noticeId.validatorId",
+ column = @Column(name = "launch_notice_validator_id")),
+ @AttributeOverride(
+ name = "expirationTime",
+ column = @Column(name = "launch_notice_expiration_time")),
+ @AttributeOverride(
+ name = "acceptedTime",
+ column = @Column(name = "launch_notice_accepted_time")),
+ })
+ LaunchNotice launchNotice;
+
+ /**
+ * Name of first IDN table associated with TLD that matched the characters in this domain label.
+ *
+ * @see google.registry.tldconfig.idn.IdnLabelValidator#findValidIdnTableForTld
+ */
+ @IgnoreSave(IfNull.class)
+ String idnTableName;
+
+ /** Fully qualified host names of this domain's active subordinate hosts. */
+ Set subordinateHosts;
+
+ /** When this domain's registration will expire. */
+ DateTime registrationExpirationTime;
+
+ /**
+ * The poll message associated with this domain being deleted.
+ *
+ * This field should be null if the domain is not in pending delete. If it is, the field should
+ * refer to a {@link PollMessage} timed to when the domain is fully deleted. If the domain is
+ * restored, the message should be deleted.
+ */
+ @Column(name = "deletion_poll_message_id")
+ VKey deletePollMessage;
+
+ /**
+ * The recurring billing event associated with this domain's autorenewals.
+ *
+ * The recurrence should be open ended unless the domain is in pending delete or fully deleted,
+ * in which case it should be closed at the time the delete was requested. Whenever the domain's
+ * {@link #registrationExpirationTime} is changed the recurrence should be closed, a new one
+ * should be created, and this field should be updated to point to the new one.
+ */
+ @Column(name = "billing_recurrence_id")
+ VKey autorenewBillingEvent;
+
+ /**
+ * The recurring poll message associated with this domain's autorenewals.
+ *
+ * The recurrence should be open ended unless the domain is in pending delete or fully deleted,
+ * in which case it should be closed at the time the delete was requested. Whenever the domain's
+ * {@link #registrationExpirationTime} is changed the recurrence should be closed, a new one
+ * should be created, and this field should be updated to point to the new one.
+ */
+ @Column(name = "autorenew_poll_message_id")
+ VKey autorenewPollMessage;
+
+ /** The unexpired grace periods for this domain (some of which may not be active yet). */
+ @Transient @ElementCollection Set gracePeriods;
+
+ /**
+ * The id of the signed mark that was used to create this domain in sunrise.
+ *
+ * Will only be populated for domains created in sunrise.
+ */
+ @IgnoreSave(IfNull.class)
+ String smdId;
+
+ /** Data about any pending or past transfers on this domain. */
+ DomainTransferData transferData;
+
+ /**
+ * The time that this resource was last transferred.
+ *
+ *
Can be null if the resource has never been transferred.
+ */
+ DateTime lastTransferTime;
+
+ @OnLoad
+ void load() {
+ // Reconstitute all of the contacts so that they have VKeys.
+ allContacts =
+ allContacts.stream().map(contact -> contact.reconstitute()).collect(toImmutableSet());
+ setContactFields(allContacts, true);
+ }
+
+ @PostLoad
+ void postLoad() {
+ // Reconstitute the contact list.
+ ImmutableSet.Builder contactsBuilder =
+ new ImmutableSet.Builder();
+
+ if (registrantContact != null) {
+ contactsBuilder.add(
+ DesignatedContact.create(DesignatedContact.Type.REGISTRANT, registrantContact));
+ }
+ if (billingContact != null) {
+ contactsBuilder.add(DesignatedContact.create(DesignatedContact.Type.BILLING, billingContact));
+ }
+ if (techContact != null) {
+ contactsBuilder.add(DesignatedContact.create(DesignatedContact.Type.TECH, techContact));
+ }
+ if (adminContact != null) {
+ contactsBuilder.add(DesignatedContact.create(DesignatedContact.Type.ADMIN, adminContact));
+ }
+
+ allContacts = contactsBuilder.build();
+ }
+
+ public ImmutableSet getSubordinateHosts() {
+ return nullToEmptyImmutableCopy(subordinateHosts);
+ }
+
+ public DateTime getRegistrationExpirationTime() {
+ return registrationExpirationTime;
+ }
+
+ public VKey getDeletePollMessage() {
+ return deletePollMessage;
+ }
+
+ public VKey getAutorenewBillingEvent() {
+ return autorenewBillingEvent;
+ }
+
+ public VKey getAutorenewPollMessage() {
+ return autorenewPollMessage;
+ }
+
+ public ImmutableSet getGracePeriods() {
+ return nullToEmptyImmutableCopy(gracePeriods);
+ }
+
+ public String getSmdId() {
+ return smdId;
+ }
+
+ @Override
+ public DomainTransferData getTransferData() {
+ return Optional.ofNullable(transferData).orElse(DomainTransferData.EMPTY);
+ }
+
+ @Override
+ public DateTime getLastTransferTime() {
+ return lastTransferTime;
+ }
+
+ @Override
+ public String getForeignKey() {
+ return fullyQualifiedDomainName;
+ }
+
+ public String getDomainName() {
+ return fullyQualifiedDomainName;
+ }
+
+ public ImmutableSet getDsData() {
+ return nullToEmptyImmutableCopy(dsData);
+ }
+
+ public LaunchNotice getLaunchNotice() {
+ return launchNotice;
+ }
+
+ public String getIdnTableName() {
+ return idnTableName;
+ }
+
+ public ImmutableSet> getNameservers() {
+ return nullToEmptyImmutableCopy(nsHosts);
+ }
+
+ // Hibernate needs this in order to populate nsHosts but no one else should ever use it
+ @SuppressWarnings("UnusedMethod")
+ private void setNsHosts(Set> nsHosts) {
+ this.nsHosts = nsHosts;
+ }
+
+ public final String getCurrentSponsorClientId() {
+ return getPersistedCurrentSponsorClientId();
+ }
+
+ /** Returns true if DNS information should be published for the given domain. */
+ public boolean shouldPublishToDns() {
+ return intersection(getStatusValues(), DNS_PUBLISHING_PROHIBITED_STATUSES).isEmpty();
+ }
+
+ /**
+ * Returns the Registry Grace Period Statuses for this domain.
+ *
+ * This collects all statuses from the domain's {@link GracePeriod} entries and also adds the
+ * PENDING_DELETE status if needed.
+ */
+ public ImmutableSet getGracePeriodStatuses() {
+ Set gracePeriodStatuses = new HashSet<>();
+ for (GracePeriod gracePeriod : getGracePeriods()) {
+ gracePeriodStatuses.add(gracePeriod.getType());
+ }
+ if (getStatusValues().contains(StatusValue.PENDING_DELETE)
+ && !gracePeriodStatuses.contains(GracePeriodStatus.REDEMPTION)) {
+ gracePeriodStatuses.add(GracePeriodStatus.PENDING_DELETE);
+ }
+ return ImmutableSet.copyOf(gracePeriodStatuses);
+ }
+
+ /** Returns the subset of grace periods having the specified type. */
+ public ImmutableSet getGracePeriodsOfType(GracePeriodStatus gracePeriodType) {
+ ImmutableSet.Builder builder = new ImmutableSet.Builder<>();
+ for (GracePeriod gracePeriod : getGracePeriods()) {
+ if (gracePeriod.getType() == gracePeriodType) {
+ builder.add(gracePeriod);
+ }
+ }
+ return builder.build();
+ }
+
+ @Override
+ public DomainContent cloneProjectedAtTime(final DateTime now) {
+ return cloneDomainProjectedAtTime(this, now);
+ }
+
+ /**
+ * The logic in this method, which handles implicit server approval of transfers, very closely
+ * parallels the logic in {@code DomainTransferApproveFlow} which handles explicit client
+ * approvals.
+ */
+ protected static T cloneDomainProjectedAtTime(T domain, DateTime now) {
+ DomainTransferData transferData = domain.getTransferData();
+ DateTime transferExpirationTime = transferData.getPendingTransferExpirationTime();
+
+ // If there's a pending transfer that has expired, handle it.
+ if (TransferStatus.PENDING.equals(transferData.getTransferStatus())
+ && isBeforeOrAt(transferExpirationTime, now)) {
+ // Project until just before the transfer time. This will handle the case of an autorenew
+ // before the transfer was even requested or during the request period.
+ // If the transfer time is precisely the moment that the domain expires, there will not be an
+ // autorenew billing event (since we end the recurrence at transfer time and recurrences are
+ // exclusive of their ending), and we can just proceed with the transfer.
+ T domainAtTransferTime =
+ cloneDomainProjectedAtTime(domain, transferExpirationTime.minusMillis(1));
+
+ DateTime expirationDate = transferData.getTransferredRegistrationExpirationTime();
+ if (expirationDate == null) {
+ // Extend the registration by the correct number of years from the expiration time
+ // that was current on the domain right before the transfer, capped at 10 years from
+ // the moment of the transfer.
+ expirationDate =
+ ResourceFlowUtils.computeExDateForApprovalTime(
+ domainAtTransferTime, transferExpirationTime, transferData.getTransferPeriod());
+ }
+ // If we are within an autorenew grace period, the transfer will subsume the autorenew. There
+ // will already be a cancellation written in advance by the transfer request flow, so we don't
+ // need to worry about billing, but we do need to cancel out the expiration time increase.
+ // The transfer period saved in the transfer data will be one year, unless the superuser
+ // extension set the transfer period to zero.
+ // Set the expiration, autorenew events, and grace period for the transfer. (Transfer ends
+ // all other graces).
+ Builder builder =
+ domainAtTransferTime
+ .asBuilder()
+ .setRegistrationExpirationTime(expirationDate)
+ // Set the speculatively-written new autorenew events as the domain's autorenew
+ // events.
+ .setAutorenewBillingEvent(transferData.getServerApproveAutorenewEvent())
+ .setAutorenewPollMessage(transferData.getServerApproveAutorenewPollMessage());
+ if (transferData.getTransferPeriod().getValue() == 1) {
+ // Set the grace period using a key to the prescheduled transfer billing event. Not using
+ // GracePeriod.forBillingEvent() here in order to avoid the actual Datastore fetch.
+ builder.setGracePeriods(
+ ImmutableSet.of(
+ GracePeriod.create(
+ GracePeriodStatus.TRANSFER,
+ transferExpirationTime.plus(
+ Registry.get(domain.getTld()).getTransferGracePeriodLength()),
+ transferData.getGainingClientId(),
+ transferData.getServerApproveBillingEvent())));
+ } else {
+ // There won't be a billing event, so we don't need a grace period
+ builder.setGracePeriods(ImmutableSet.of());
+ }
+ // Set all remaining transfer properties.
+ setAutomaticTransferSuccessProperties(builder, transferData);
+ builder
+ .setLastEppUpdateTime(transferExpirationTime)
+ .setLastEppUpdateClientId(transferData.getGainingClientId());
+ // Finish projecting to now.
+ return (T) builder.build().cloneProjectedAtTime(now);
+ }
+
+ Optional newLastEppUpdateTime = Optional.empty();
+
+ // There is no transfer. Do any necessary autorenews for active domains.
+
+ Builder builder = domain.asBuilder();
+ if (isBeforeOrAt(domain.getRegistrationExpirationTime(), now)
+ && END_OF_TIME.equals(domain.getDeletionTime())) {
+ // Autorenew by the number of years between the old expiration time and now.
+ DateTime lastAutorenewTime =
+ leapSafeAddYears(
+ domain.getRegistrationExpirationTime(),
+ new Interval(domain.getRegistrationExpirationTime(), now).toPeriod().getYears());
+ DateTime newExpirationTime = lastAutorenewTime.plusYears(1);
+ builder
+ .setRegistrationExpirationTime(newExpirationTime)
+ .addGracePeriod(
+ GracePeriod.createForRecurring(
+ GracePeriodStatus.AUTO_RENEW,
+ lastAutorenewTime.plus(
+ Registry.get(domain.getTld()).getAutoRenewGracePeriodLength()),
+ domain.getCurrentSponsorClientId(),
+ domain.getAutorenewBillingEvent()));
+ newLastEppUpdateTime = Optional.of(lastAutorenewTime);
+ }
+
+ // Remove any grace periods that have expired.
+ T almostBuilt = (T) builder.build();
+ builder = almostBuilt.asBuilder();
+ for (GracePeriod gracePeriod : almostBuilt.getGracePeriods()) {
+ if (isBeforeOrAt(gracePeriod.getExpirationTime(), now)) {
+ builder.removeGracePeriod(gracePeriod);
+ if (!newLastEppUpdateTime.isPresent()
+ || isBeforeOrAt(newLastEppUpdateTime.get(), gracePeriod.getExpirationTime())) {
+ newLastEppUpdateTime = Optional.of(gracePeriod.getExpirationTime());
+ }
+ }
+ }
+
+ // It is possible that the lastEppUpdateClientId is different from current sponsor client
+ // id, so we have to do the comparison instead of having one variable just storing the most
+ // recent time.
+ if (newLastEppUpdateTime.isPresent()) {
+ if (domain.getLastEppUpdateTime() == null
+ || newLastEppUpdateTime.get().isAfter(domain.getLastEppUpdateTime())) {
+ builder
+ .setLastEppUpdateTime(newLastEppUpdateTime.get())
+ .setLastEppUpdateClientId(domain.getCurrentSponsorClientId());
+ }
+ }
+
+ // Handle common properties like setting or unsetting linked status. This also handles the
+ // general case of pending transfers for other resource types, but since we've always handled
+ // a pending transfer by this point that's a no-op for domains.
+ projectResourceOntoBuilderAtTime(almostBuilt, builder, now);
+ return (T) builder.build();
+ }
+
+ /** Return what the expiration time would be if the given number of years were added to it. */
+ public static DateTime extendRegistrationWithCap(
+ DateTime now, DateTime currentExpirationTime, @Nullable Integer extendedRegistrationYears) {
+ // We must cap registration at the max years (aka 10), even if that truncates the last year.
+ return earliestOf(
+ leapSafeAddYears(
+ currentExpirationTime, Optional.ofNullable(extendedRegistrationYears).orElse(0)),
+ leapSafeAddYears(now, MAX_REGISTRATION_YEARS));
+ }
+
+ /** Loads and returns the fully qualified host names of all linked nameservers. */
+ public ImmutableSortedSet loadNameserverHostNames() {
+ return ofy().load()
+ .keys(getNameservers().stream().map(VKey::getOfyKey).collect(toImmutableSet())).values()
+ .stream()
+ .map(HostResource::getHostName)
+ .collect(toImmutableSortedSet(Ordering.natural()));
+ }
+
+ /** A key to the registrant who registered this domain. */
+ public VKey getRegistrant() {
+ return registrantContact;
+ }
+
+ public VKey getAdminContact() {
+ return adminContact;
+ }
+
+ public VKey getBillingContact() {
+ return billingContact;
+ }
+
+ public VKey getTechContact() {
+ return techContact;
+ }
+
+ /** Associated contacts for the domain (other than registrant). */
+ public ImmutableSet getContacts() {
+ return nullToEmpty(allContacts).stream()
+ .filter(IS_REGISTRANT.negate())
+ .collect(toImmutableSet());
+ }
+
+ public DomainAuthInfo getAuthInfo() {
+ return authInfo;
+ }
+
+ /** Returns all referenced contacts from this domain or application. */
+ public ImmutableSet> getReferencedContacts() {
+ return nullToEmptyImmutableCopy(allContacts).stream()
+ .map(DesignatedContact::getContactKey)
+ .filter(Objects::nonNull)
+ .collect(toImmutableSet());
+ }
+
+ public String getTld() {
+ return tld;
+ }
+
+ /**
+ * Sets the individual contact fields from {@code contacts}.
+ *
+ * The registrant field is only set if {@code includeRegistrant} is true, as this field needs
+ * to be set in some circumstances but not in others.
+ */
+ protected void setContactFields(Set contacts, boolean includeRegistrant) {
+ // Set the individual contact fields.
+ for (DesignatedContact contact : contacts) {
+ switch (contact.getType()) {
+ case BILLING:
+ billingContact = contact.getContactKey();
+ break;
+ case TECH:
+ techContact = contact.getContactKey();
+ break;
+ case ADMIN:
+ adminContact = contact.getContactKey();
+ break;
+ case REGISTRANT:
+ if (includeRegistrant) {
+ registrantContact = contact.getContactKey();
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown contact resource type: " + contact.getType());
+ }
+ }
+ }
+
+ @Override
+ public VKey createVKey() {
+ return VKey.create(DomainBase.class, getRepoId(), Key.create(this));
+ }
+
+ public static VKey createVKey(Key key) {
+ return VKey.create(DomainBase.class, key.getName(), key);
+ }
+
+ /** Predicate to determine if a given {@link DesignatedContact} is the registrant. */
+ protected static final Predicate IS_REGISTRANT =
+ (DesignatedContact contact) -> DesignatedContact.Type.REGISTRANT.equals(contact.type);
+
+ /** An override of {@link EppResource#asBuilder} with tighter typing. */
+ @Override
+ public Builder asBuilder() {
+ return new Builder<>(clone(this));
+ }
+
+ /** A builder for constructing {@link DomainBase}, since it is immutable. */
+ public static class Builder>
+ extends EppResource.Builder implements BuilderWithTransferData {
+
+ public Builder() {}
+
+ Builder(T instance) {
+ super(instance);
+ }
+
+ @Override
+ public T build() {
+ T instance = getInstance();
+ // If TransferData is totally empty, set it to null.
+ if (DomainTransferData.EMPTY.equals(getInstance().transferData)) {
+ setTransferData(null);
+ }
+ // A DomainBase has status INACTIVE if there are no nameservers.
+ if (getInstance().getNameservers().isEmpty()) {
+ addStatusValue(StatusValue.INACTIVE);
+ } else { // There are nameservers, so make sure INACTIVE isn't there.
+ removeStatusValue(StatusValue.INACTIVE);
+ }
+
+ checkArgumentNotNull(emptyToNull(instance.fullyQualifiedDomainName), "Missing domainName");
+ if (instance.getRegistrant() == null
+ && instance.allContacts.stream().anyMatch(IS_REGISTRANT)) {
+ throw new IllegalArgumentException("registrant is null but is in allContacts");
+ }
+ checkArgumentNotNull(instance.getRegistrant(), "Missing registrant");
+ instance.tld = getTldFromDomainName(instance.fullyQualifiedDomainName);
+ return super.build();
+ }
+
+ public B setDomainName(String domainName) {
+ checkArgument(
+ domainName.equals(canonicalizeDomainName(domainName)),
+ "Domain name must be in puny-coded, lower-case form");
+ getInstance().fullyQualifiedDomainName = domainName;
+ return thisCastToDerived();
+ }
+
+ public B setDsData(ImmutableSet dsData) {
+ getInstance().dsData = dsData;
+ return thisCastToDerived();
+ }
+
+ public B setRegistrant(VKey registrant) {
+ // Replace the registrant contact inside allContacts.
+ getInstance().allContacts =
+ union(
+ getInstance().getContacts(),
+ DesignatedContact.create(
+ DesignatedContact.Type.REGISTRANT, checkArgumentNotNull(registrant)));
+
+ // Set the registrant field specifically.
+ getInstance().registrantContact = registrant;
+ return thisCastToDerived();
+ }
+
+ public B setAuthInfo(DomainAuthInfo authInfo) {
+ getInstance().authInfo = authInfo;
+ return thisCastToDerived();
+ }
+
+ public B setNameservers(VKey nameserver) {
+ getInstance().nsHosts = ImmutableSet.of(nameserver);
+ return thisCastToDerived();
+ }
+
+ public B setNameservers(ImmutableSet> nameservers) {
+ getInstance().nsHosts = forceEmptyToNull(nameservers);
+ return thisCastToDerived();
+ }
+
+ public B addNameserver(VKey nameserver) {
+ return addNameservers(ImmutableSet.of(nameserver));
+ }
+
+ public B addNameservers(ImmutableSet> nameservers) {
+ return setNameservers(
+ ImmutableSet.copyOf(Sets.union(getInstance().getNameservers(), nameservers)));
+ }
+
+ public B removeNameserver(VKey nameserver) {
+ return removeNameservers(ImmutableSet.of(nameserver));
+ }
+
+ public B removeNameservers(ImmutableSet> nameservers) {
+ return setNameservers(
+ ImmutableSet.copyOf(difference(getInstance().getNameservers(), nameservers)));
+ }
+
+ public B setContacts(DesignatedContact contact) {
+ return setContacts(ImmutableSet.of(contact));
+ }
+
+ public B setContacts(ImmutableSet contacts) {
+ checkArgument(contacts.stream().noneMatch(IS_REGISTRANT), "Registrant cannot be a contact");
+
+ // Replace the non-registrant contacts inside allContacts.
+ getInstance().allContacts =
+ Streams.concat(
+ nullToEmpty(getInstance().allContacts).stream().filter(IS_REGISTRANT),
+ contacts.stream())
+ .collect(toImmutableSet());
+
+ // Set the individual fields.
+ getInstance().setContactFields(contacts, false);
+ return thisCastToDerived();
+ }
+
+ public B addContacts(ImmutableSet contacts) {
+ return setContacts(ImmutableSet.copyOf(Sets.union(getInstance().getContacts(), contacts)));
+ }
+
+ public B removeContacts(ImmutableSet contacts) {
+ return setContacts(ImmutableSet.copyOf(difference(getInstance().getContacts(), contacts)));
+ }
+
+ public B setLaunchNotice(LaunchNotice launchNotice) {
+ getInstance().launchNotice = launchNotice;
+ return thisCastToDerived();
+ }
+
+ public B setIdnTableName(String idnTableName) {
+ getInstance().idnTableName = idnTableName;
+ return thisCastToDerived();
+ }
+
+ public B setSubordinateHosts(ImmutableSet subordinateHosts) {
+ getInstance().subordinateHosts = subordinateHosts;
+ return thisCastToDerived();
+ }
+
+ public B addSubordinateHost(String hostToAdd) {
+ return setSubordinateHosts(
+ ImmutableSet.copyOf(union(getInstance().getSubordinateHosts(), hostToAdd)));
+ }
+
+ public B removeSubordinateHost(String hostToRemove) {
+ return setSubordinateHosts(
+ ImmutableSet.copyOf(
+ CollectionUtils.difference(getInstance().getSubordinateHosts(), hostToRemove)));
+ }
+
+ public B setRegistrationExpirationTime(DateTime registrationExpirationTime) {
+ getInstance().registrationExpirationTime = registrationExpirationTime;
+ return thisCastToDerived();
+ }
+
+ public B setDeletePollMessage(VKey deletePollMessage) {
+ getInstance().deletePollMessage = deletePollMessage;
+ return thisCastToDerived();
+ }
+
+ public B setAutorenewBillingEvent(VKey autorenewBillingEvent) {
+ getInstance().autorenewBillingEvent = autorenewBillingEvent;
+ return thisCastToDerived();
+ }
+
+ public B setAutorenewPollMessage(VKey autorenewPollMessage) {
+ getInstance().autorenewPollMessage = autorenewPollMessage;
+ return thisCastToDerived();
+ }
+
+ public B setSmdId(String smdId) {
+ getInstance().smdId = smdId;
+ return thisCastToDerived();
+ }
+
+ public B setGracePeriods(ImmutableSet gracePeriods) {
+ getInstance().gracePeriods = gracePeriods;
+ return thisCastToDerived();
+ }
+
+ public B addGracePeriod(GracePeriod gracePeriod) {
+ getInstance().gracePeriods = union(getInstance().getGracePeriods(), gracePeriod);
+ return thisCastToDerived();
+ }
+
+ public B removeGracePeriod(GracePeriod gracePeriod) {
+ getInstance().gracePeriods =
+ CollectionUtils.difference(getInstance().getGracePeriods(), gracePeriod);
+ return thisCastToDerived();
+ }
+
+ @Override
+ public B setTransferData(DomainTransferData transferData) {
+ getInstance().transferData = transferData;
+ return thisCastToDerived();
+ }
+
+ @Override
+ public B setLastTransferTime(DateTime lastTransferTime) {
+ getInstance().lastTransferTime = lastTransferTime;
+ return thisCastToDerived();
+ }
+ }
+}
diff --git a/core/src/main/java/google/registry/model/domain/DomainHistory.java b/core/src/main/java/google/registry/model/domain/DomainHistory.java
new file mode 100644
index 000000000..197d5e4f1
--- /dev/null
+++ b/core/src/main/java/google/registry/model/domain/DomainHistory.java
@@ -0,0 +1,111 @@
+// Copyright 2020 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.model.domain;
+
+import com.googlecode.objectify.Key;
+import google.registry.model.EppResource;
+import google.registry.model.contact.ContactResource;
+import google.registry.model.host.HostResource;
+import google.registry.model.reporting.HistoryEntry;
+import google.registry.persistence.VKey;
+import java.util.Set;
+import javax.persistence.Access;
+import javax.persistence.AccessType;
+import javax.persistence.Column;
+import javax.persistence.ElementCollection;
+import javax.persistence.Entity;
+import javax.persistence.JoinTable;
+
+/**
+ * A persisted history entry representing an EPP modification to a domain.
+ *
+ * In addition to the general history fields (e.g. action time, registrar ID) we also persist a
+ * copy of the domain entity at this point in time. We persist a raw {@link DomainContent} so that
+ * the foreign-keyed fields in that class can refer to this object.
+ */
+@Entity
+@javax.persistence.Table(
+ indexes = {
+ @javax.persistence.Index(columnList = "creationTime"),
+ @javax.persistence.Index(columnList = "historyRegistrarId"),
+ @javax.persistence.Index(columnList = "historyType"),
+ @javax.persistence.Index(columnList = "historyModificationTime")
+ })
+public class DomainHistory extends HistoryEntry {
+ // Store DomainContent instead of DomainBase so we don't pick up its @Id
+ DomainContent domainContent;
+
+ @Column(nullable = false)
+ VKey domainRepoId;
+
+ @ElementCollection
+ @JoinTable(name = "DomainHistoryHost")
+ @Access(AccessType.PROPERTY)
+ @Column(name = "host_repo_id")
+ public Set> getNsHosts() {
+ return domainContent.nsHosts;
+ }
+
+ /** The state of the {@link DomainContent} object at this point in time. */
+ public DomainContent getDomainContent() {
+ return domainContent;
+ }
+
+ /** The key to the {@link ContactResource} this is based off of. */
+ public VKey getDomainRepoId() {
+ return domainRepoId;
+ }
+
+ // Hibernate needs this in order to populate nsHosts but no one else should ever use it
+ @SuppressWarnings("UnusedMethod")
+ private void setNsHosts(Set> nsHosts) {
+ if (domainContent != null) {
+ domainContent.nsHosts = nsHosts;
+ }
+ }
+
+ @Override
+ public Builder asBuilder() {
+ return new Builder(clone(this));
+ }
+
+ public static class Builder extends HistoryEntry.Builder {
+
+ public Builder() {}
+
+ public Builder(DomainHistory instance) {
+ super(instance);
+ }
+
+ public Builder setDomainContent(DomainContent domainContent) {
+ getInstance().domainContent = domainContent;
+ return this;
+ }
+
+ public Builder setDomainRepoId(VKey domainRepoId) {
+ getInstance().domainRepoId = domainRepoId;
+ domainRepoId.maybeGetOfyKey().ifPresent(parent -> getInstance().parent = parent);
+ return this;
+ }
+
+ // We can remove this once all HistoryEntries are converted to History objects
+ @Override
+ public Builder setParent(Key extends EppResource> parent) {
+ super.setParent(parent);
+ getInstance().domainRepoId = VKey.create(DomainBase.class, parent.getName(), parent);
+ return this;
+ }
+ }
+}
diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml
index 7ddc6cb5c..1a1f225ad 100644
--- a/core/src/main/resources/META-INF/persistence.xml
+++ b/core/src/main/resources/META-INF/persistence.xml
@@ -25,6 +25,7 @@
google.registry.model.contact.ContactHistory
google.registry.model.contact.ContactResource
google.registry.model.domain.DomainBase
+ google.registry.model.domain.DomainHistory
google.registry.model.host.HostHistory
google.registry.model.host.HostResource
google.registry.model.registrar.Registrar
diff --git a/core/src/test/java/google/registry/model/history/ContactHistoryTest.java b/core/src/test/java/google/registry/model/history/ContactHistoryTest.java
index 8e060adbd..6bb484d7e 100644
--- a/core/src/test/java/google/registry/model/history/ContactHistoryTest.java
+++ b/core/src/test/java/google/registry/model/history/ContactHistoryTest.java
@@ -1,4 +1,4 @@
-// Copyright 2017 The Nomulus Authors. All Rights Reserved.
+// Copyright 2020 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.
@@ -15,7 +15,9 @@
package google.registry.model.history;
import static com.google.common.truth.Truth.assertThat;
+import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
+import static google.registry.testing.DatastoreHelper.newContactResourceWithRoid;
import static google.registry.testing.SqlHelper.saveRegistrar;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -24,7 +26,6 @@ import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.eppcommon.Trid;
import google.registry.model.reporting.HistoryEntry;
-import google.registry.model.transfer.ContactTransferData;
import google.registry.persistence.VKey;
import org.junit.jupiter.api.Test;
@@ -37,26 +38,18 @@ public class ContactHistoryTest extends EntityTestCase {
@Test
void testPersistence() {
- saveRegistrar("registrar1");
-
- ContactResource contact =
- new ContactResource.Builder()
- .setRepoId("contact1")
- .setContactId("contactId")
- .setCreationClientId("registrar1")
- .setPersistedCurrentSponsorClientId("registrar1")
- .setTransferData(new ContactTransferData.Builder().build())
- .build();
+ saveRegistrar("TheRegistrar");
+ ContactResource contact = newContactResourceWithRoid("contactId", "contact1");
jpaTm().transact(() -> jpaTm().saveNew(contact));
- VKey contactVKey = VKey.createSql(ContactResource.class, "contact1");
+ VKey contactVKey = contact.createVKey();
ContactResource contactFromDb = jpaTm().transact(() -> jpaTm().load(contactVKey));
ContactHistory contactHistory =
new ContactHistory.Builder()
.setType(HistoryEntry.Type.HOST_CREATE)
.setXmlBytes("".getBytes(UTF_8))
.setModificationTime(fakeClock.nowUtc())
- .setClientId("registrar1")
+ .setClientId("TheRegistrar")
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason("reason")
@@ -70,18 +63,15 @@ public class ContactHistoryTest extends EntityTestCase {
() -> {
ContactHistory fromDatabase = jpaTm().load(VKey.createSql(ContactHistory.class, 1L));
assertContactHistoriesEqual(fromDatabase, contactHistory);
+ assertThat(fromDatabase.getContactRepoId().getSqlKey())
+ .isEqualTo(contactHistory.getContactRepoId().getSqlKey());
});
}
- private void assertContactHistoriesEqual(ContactHistory one, ContactHistory two) {
- // enough of the fields get changed during serialization that we can't depend on .equals()
- assertThat(one.getClientId()).isEqualTo(two.getClientId());
- assertThat(one.getContactRepoId()).isEqualTo(two.getContactRepoId());
- assertThat(one.getBySuperuser()).isEqualTo(two.getBySuperuser());
- assertThat(one.getRequestedByRegistrar()).isEqualTo(two.getRequestedByRegistrar());
- assertThat(one.getReason()).isEqualTo(two.getReason());
- assertThat(one.getTrid()).isEqualTo(two.getTrid());
- assertThat(one.getType()).isEqualTo(two.getType());
- assertThat(one.getContactBase().getContactId()).isEqualTo(two.getContactBase().getContactId());
+ static void assertContactHistoriesEqual(ContactHistory one, ContactHistory two) {
+ assertAboutImmutableObjects().that(one)
+ .isEqualExceptFields(two, "contactBase", "contactRepoId", "parent");
+ assertAboutImmutableObjects().that(one.getContactBase())
+ .isEqualExceptFields(two.getContactBase(), "repoId");
}
}
diff --git a/core/src/test/java/google/registry/model/history/DomainHistoryTest.java b/core/src/test/java/google/registry/model/history/DomainHistoryTest.java
new file mode 100644
index 000000000..86a36e7d3
--- /dev/null
+++ b/core/src/test/java/google/registry/model/history/DomainHistoryTest.java
@@ -0,0 +1,89 @@
+// Copyright 2020 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.model.history;
+
+import static com.google.common.truth.Truth.assertThat;
+import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
+import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
+import static google.registry.testing.DatastoreHelper.newContactResourceWithRoid;
+import static google.registry.testing.DatastoreHelper.newDomainBase;
+import static google.registry.testing.DatastoreHelper.newHostResourceWithRoid;
+import static google.registry.testing.SqlHelper.saveRegistrar;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import google.registry.model.EntityTestCase;
+import google.registry.model.contact.ContactResource;
+import google.registry.model.domain.DomainBase;
+import google.registry.model.domain.DomainHistory;
+import google.registry.model.eppcommon.Trid;
+import google.registry.model.host.HostResource;
+import google.registry.model.reporting.HistoryEntry;
+import google.registry.persistence.VKey;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link DomainHistory}. */
+public class DomainHistoryTest extends EntityTestCase {
+
+ public DomainHistoryTest() {
+ super(JpaEntityCoverageCheck.ENABLED);
+ }
+
+ @Test
+ public void testPersistence() {
+ saveRegistrar("TheRegistrar");
+
+ HostResource host = newHostResourceWithRoid("ns1.example.com", "host1");
+ jpaTm().transact(() -> jpaTm().saveNew(host));
+ ContactResource contact = newContactResourceWithRoid("contactId", "contact1");
+ jpaTm().transact(() -> jpaTm().saveNew(contact));
+
+ DomainBase domain =
+ newDomainBase("example.tld", "domainRepoId", contact)
+ .asBuilder()
+ .setNameservers(host.createVKey())
+ .build();
+ jpaTm().transact(() -> jpaTm().saveNew(domain));
+
+ DomainHistory domainHistory =
+ new DomainHistory.Builder()
+ .setType(HistoryEntry.Type.DOMAIN_CREATE)
+ .setXmlBytes("".getBytes(UTF_8))
+ .setModificationTime(fakeClock.nowUtc())
+ .setClientId("TheRegistrar")
+ .setTrid(Trid.create("ABC-123", "server-trid"))
+ .setBySuperuser(false)
+ .setReason("reason")
+ .setRequestedByRegistrar(true)
+ .setDomainContent(domain)
+ .setDomainRepoId(domain.createVKey())
+ .build();
+ jpaTm().transact(() -> jpaTm().saveNew(domainHistory));
+
+ jpaTm()
+ .transact(
+ () -> {
+ DomainHistory fromDatabase =
+ jpaTm().load(VKey.createSql(DomainHistory.class, domainHistory.getId()));
+ assertDomainHistoriesEqual(fromDatabase, domainHistory);
+ assertThat(fromDatabase.getDomainRepoId().getSqlKey())
+ .isEqualTo(domainHistory.getDomainRepoId().getSqlKey());
+ });
+ }
+
+ static void assertDomainHistoriesEqual(DomainHistory one, DomainHistory two) {
+ assertAboutImmutableObjects().that(one)
+ .isEqualExceptFields(two, "domainContent", "domainRepoId", "parent");
+ }
+}
diff --git a/core/src/test/java/google/registry/model/history/HostHistoryTest.java b/core/src/test/java/google/registry/model/history/HostHistoryTest.java
index 3ded202a3..1b125a5d1 100644
--- a/core/src/test/java/google/registry/model/history/HostHistoryTest.java
+++ b/core/src/test/java/google/registry/model/history/HostHistoryTest.java
@@ -14,12 +14,13 @@
package google.registry.model.history;
+import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
+import static google.registry.testing.DatastoreHelper.newHostResourceWithRoid;
import static google.registry.testing.SqlHelper.saveRegistrar;
import static java.nio.charset.StandardCharsets.UTF_8;
-import com.google.common.collect.ImmutableSet;
import google.registry.model.EntityTestCase;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.HostHistory;
@@ -37,16 +38,9 @@ public class HostHistoryTest extends EntityTestCase {
@Test
void testPersistence() {
- saveRegistrar("registrar1");
+ saveRegistrar("TheRegistrar");
- HostResource host =
- new HostResource.Builder()
- .setRepoId("host1")
- .setHostName("ns1.example.com")
- .setCreationClientId("TheRegistrar")
- .setPersistedCurrentSponsorClientId("TheRegistrar")
- .setInetAddresses(ImmutableSet.of())
- .build();
+ HostResource host = newHostResourceWithRoid("ns1.example.com", "host1");
jpaTm().transact(() -> jpaTm().saveNew(host));
VKey hostVKey = VKey.createSql(HostResource.class, "host1");
HostResource hostFromDb = jpaTm().transact(() -> jpaTm().load(hostVKey));
@@ -55,7 +49,7 @@ public class HostHistoryTest extends EntityTestCase {
.setType(HistoryEntry.Type.HOST_CREATE)
.setXmlBytes("".getBytes(UTF_8))
.setModificationTime(fakeClock.nowUtc())
- .setClientId("registrar1")
+ .setClientId("TheRegistrar")
.setTrid(Trid.create("ABC-123", "server-trid"))
.setBySuperuser(false)
.setReason("reason")
@@ -70,6 +64,8 @@ public class HostHistoryTest extends EntityTestCase {
HostHistory fromDatabase =
jpaTm().load(VKey.createSql(HostHistory.class, hostHistory.getId()));
assertHostHistoriesEqual(fromDatabase, hostHistory);
+ assertThat(fromDatabase.getHostRepoId().getSqlKey())
+ .isEqualTo(hostHistory.getHostRepoId().getSqlKey());
});
}
diff --git a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java
index 2611fa0ca..d76184bf5 100644
--- a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java
+++ b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java
@@ -20,6 +20,7 @@ import google.registry.model.billing.BillingEventTest;
import google.registry.model.contact.ContactResourceTest;
import google.registry.model.domain.DomainBaseSqlTest;
import google.registry.model.history.ContactHistoryTest;
+import google.registry.model.history.DomainHistoryTest;
import google.registry.model.history.HostHistoryTest;
import google.registry.model.poll.PollMessageTest;
import google.registry.model.registry.RegistryLockDaoTest;
@@ -77,6 +78,7 @@ import org.junit.runner.RunWith;
ContactResourceTest.class,
CursorDaoTest.class,
DomainBaseSqlTest.class,
+ DomainHistoryTest.class,
HostHistoryTest.class,
LockDaoTest.class,
PollMessageTest.class,
diff --git a/core/src/test/java/google/registry/testing/DatastoreHelper.java b/core/src/test/java/google/registry/testing/DatastoreHelper.java
index 4948a8c47..6407c8fcf 100644
--- a/core/src/test/java/google/registry/testing/DatastoreHelper.java
+++ b/core/src/test/java/google/registry/testing/DatastoreHelper.java
@@ -119,12 +119,16 @@ public class DatastoreHelper {
String.class));
public static HostResource newHostResource(String hostName) {
+ return newHostResourceWithRoid(hostName, generateNewContactHostRoid());
+ }
+
+ public static HostResource newHostResourceWithRoid(String hostName, String repoId) {
return new HostResource.Builder()
.setHostName(hostName)
.setCreationClientId("TheRegistrar")
.setPersistedCurrentSponsorClientId("TheRegistrar")
.setCreationTimeForTest(START_OF_TIME)
- .setRepoId(generateNewContactHostRoid())
+ .setRepoId(repoId)
.build();
}
diff --git a/db/src/main/resources/sql/flyway/V44__create_domain_history.sql b/db/src/main/resources/sql/flyway/V44__create_domain_history.sql
new file mode 100644
index 000000000..962fb61b4
--- /dev/null
+++ b/db/src/main/resources/sql/flyway/V44__create_domain_history.sql
@@ -0,0 +1,102 @@
+-- Copyright 2020 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.
+
+CREATE TABLE "DomainHistory" (
+ history_revision_id int8 NOT NULL,
+ history_by_superuser boolean NOT NULL,
+ history_registrar_id text,
+ history_modification_time timestamptz NOT NULL,
+ history_reason text NOT NULL,
+ history_requested_by_registrar boolean NOT NULL,
+ history_client_transaction_id text,
+ history_server_transaction_id text,
+ history_type text NOT NULL,
+ history_xml_bytes bytea NOT NULL,
+ admin_contact text,
+ auth_info_repo_id text,
+ auth_info_value text,
+ billing_recurrence_id int8,
+ autorenew_poll_message_id int8,
+ billing_contact text,
+ deletion_poll_message_id int8,
+ domain_name text,
+ idn_table_name text,
+ last_transfer_time timestamptz,
+ launch_notice_accepted_time timestamptz,
+ launch_notice_expiration_time timestamptz,
+ launch_notice_tcn_id text,
+ launch_notice_validator_id text,
+ registrant_contact text,
+ registration_expiration_time timestamptz,
+ smd_id text,
+ subordinate_hosts text[],
+ tech_contact text,
+ tld text,
+ transfer_billing_cancellation_id int8,
+ transfer_billing_recurrence_id int8,
+ transfer_autorenew_poll_message_id int8,
+ transfer_billing_event_id int8,
+ transfer_renew_period_unit text,
+ transfer_renew_period_value int4,
+ transfer_registration_expiration_time timestamptz,
+ transfer_gaining_poll_message_id int8,
+ transfer_losing_poll_message_id int8,
+ transfer_client_txn_id text,
+ transfer_server_txn_id text,
+ transfer_gaining_registrar_id text,
+ transfer_losing_registrar_id text,
+ transfer_pending_expiration_time timestamptz,
+ transfer_request_time timestamptz,
+ transfer_status text,
+ creation_registrar_id text NOT NULL,
+ creation_time timestamptz NOT NULL,
+ current_sponsor_registrar_id text NOT NULL,
+ deletion_time timestamptz,
+ last_epp_update_registrar_id text,
+ last_epp_update_time timestamptz,
+ statuses text[],
+ update_timestamp timestamptz,
+ domain_repo_id text NOT NULL,
+ PRIMARY KEY (history_revision_id)
+);
+
+CREATE TABLE "DomainHistoryHost" (
+ domain_history_history_revision_id int8 NOT NULL,
+ host_repo_id text
+);
+
+ALTER TABLE IF EXISTS "DomainHost" RENAME ns_hosts TO host_repo_id;
+
+CREATE INDEX IDXrh4xmrot9bd63o382ow9ltfig ON "DomainHistory" (creation_time);
+CREATE INDEX IDXaro1omfuaxjwmotk3vo00trwm ON "DomainHistory" (history_registrar_id);
+CREATE INDEX IDXsu1nam10cjes9keobapn5jvxj ON "DomainHistory" (history_type);
+CREATE INDEX IDX6w3qbtgce93cal2orjg1tw7b7 ON "DomainHistory" (history_modification_time);
+
+ALTER TABLE IF EXISTS "DomainHistory"
+ ADD CONSTRAINT fk_domain_history_registrar_id
+ FOREIGN KEY (history_registrar_id)
+ REFERENCES "Registrar";
+
+ALTER TABLE IF EXISTS "DomainHistory"
+ ADD CONSTRAINT fk_domain_history_domain_repo_id
+ FOREIGN KEY (domain_repo_id)
+ REFERENCES "Domain";
+
+ALTER TABLE ONLY public."DomainHistory" ALTER COLUMN history_revision_id
+ SET DEFAULT nextval('public."history_id_sequence"'::regclass);
+
+ALTER TABLE IF EXISTS "DomainHistoryHost"
+ ADD CONSTRAINT FK6b8eqdxwe3guc56tgpm89atx
+ FOREIGN KEY (domain_history_history_revision_id)
+ REFERENCES "DomainHistory";
diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated
index ffb4dd34c..e8fcce36e 100644
--- a/db/src/main/resources/sql/schema/db-schema.sql.generated
+++ b/db/src/main/resources/sql/schema/db-schema.sql.generated
@@ -268,9 +268,73 @@ create sequence history_id_sequence start 1 increment 1;
primary key (repo_id)
);
+ create table "DomainHistory" (
+ history_revision_id int8 not null,
+ history_by_superuser boolean not null,
+ history_registrar_id text,
+ history_modification_time timestamptz not null,
+ history_reason text not null,
+ history_requested_by_registrar boolean not null,
+ history_client_transaction_id text,
+ history_server_transaction_id text,
+ history_type text not null,
+ history_xml_bytes bytea not null,
+ admin_contact text,
+ auth_info_repo_id text,
+ auth_info_value text,
+ billing_recurrence_id int8,
+ autorenew_poll_message_id int8,
+ billing_contact text,
+ deletion_poll_message_id int8,
+ domain_name text,
+ idn_table_name text,
+ last_transfer_time timestamptz,
+ launch_notice_accepted_time timestamptz,
+ launch_notice_expiration_time timestamptz,
+ launch_notice_tcn_id text,
+ launch_notice_validator_id text,
+ registrant_contact text,
+ registration_expiration_time timestamptz,
+ smd_id text,
+ subordinate_hosts text[],
+ tech_contact text,
+ tld text,
+ transfer_billing_cancellation_id int8,
+ transfer_billing_recurrence_id int8,
+ transfer_autorenew_poll_message_id int8,
+ transfer_billing_event_id int8,
+ transfer_renew_period_unit text,
+ transfer_renew_period_value int4,
+ transfer_registration_expiration_time timestamptz,
+ transfer_gaining_poll_message_id int8,
+ transfer_losing_poll_message_id int8,
+ transfer_client_txn_id text,
+ transfer_server_txn_id text,
+ transfer_gaining_registrar_id text,
+ transfer_losing_registrar_id text,
+ transfer_pending_expiration_time timestamptz,
+ transfer_request_time timestamptz,
+ transfer_status text,
+ creation_registrar_id text not null,
+ creation_time timestamptz not null,
+ current_sponsor_registrar_id text not null,
+ deletion_time timestamptz,
+ last_epp_update_registrar_id text,
+ last_epp_update_time timestamptz,
+ statuses text[],
+ update_timestamp timestamptz,
+ domain_repo_id text not null,
+ primary key (history_revision_id)
+ );
+
+ create table "DomainHistoryHost" (
+ domain_history_history_revision_id int8 not null,
+ host_repo_id text
+ );
+
create table "DomainHost" (
domain_repo_id text not null,
- ns_hosts text
+ host_repo_id text
);
create table "GracePeriod" (
@@ -528,6 +592,10 @@ create index IDXhsjqiy2lyobfymplb28nm74lm on "Domain" (current_sponsor_registrar
create index IDX5mnf0wn20tno4b9do88j61klr on "Domain" (deletion_time);
create index IDXc5aw4pk1vkd6ymhvkpanmoadv on "Domain" (domain_name);
create index IDXrwl38wwkli1j7gkvtywi9jokq on "Domain" (tld);
+create index IDXrh4xmrot9bd63o382ow9ltfig on "DomainHistory" (creation_time);
+create index IDXaro1omfuaxjwmotk3vo00trwm on "DomainHistory" (history_registrar_id);
+create index IDXsu1nam10cjes9keobapn5jvxj on "DomainHistory" (history_type);
+create index IDX6w3qbtgce93cal2orjg1tw7b7 on "DomainHistory" (history_modification_time);
create index IDXfg2nnjlujxo6cb9fha971bq2n on "HostHistory" (creation_time);
create index IDX1iy7njgb7wjmj9piml4l2g0qi on "HostHistory" (history_registrar_id);
create index IDXkkwbwcwvrdkkqothkiye4jiff on "HostHistory" (host_name);
@@ -554,6 +622,11 @@ create index spec11threatmatch_check_date_idx on "Spec11ThreatMatch" (check_date
foreign key (revision_id)
references "ClaimsList";
+ alter table if exists "DomainHistoryHost"
+ add constraint FK378h8v3j8qd8xtjn2e0bcmrtj
+ foreign key (domain_history_history_revision_id)
+ references "DomainHistory";
+
alter table if exists "DomainHost"
add constraint FKeq1guccbre1yk3oosgp2io554
foreign key (domain_repo_id)
diff --git a/db/src/main/resources/sql/schema/nomulus.golden.sql b/db/src/main/resources/sql/schema/nomulus.golden.sql
index 704e2423d..52e9988c0 100644
--- a/db/src/main/resources/sql/schema/nomulus.golden.sql
+++ b/db/src/main/resources/sql/schema/nomulus.golden.sql
@@ -405,13 +405,86 @@ CREATE TABLE public."Domain" (
);
+--
+-- Name: DomainHistory; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public."DomainHistory" (
+ history_revision_id bigint DEFAULT nextval('public.history_id_sequence'::regclass) NOT NULL,
+ history_by_superuser boolean NOT NULL,
+ history_registrar_id text,
+ history_modification_time timestamp with time zone NOT NULL,
+ history_reason text NOT NULL,
+ history_requested_by_registrar boolean NOT NULL,
+ history_client_transaction_id text,
+ history_server_transaction_id text,
+ history_type text NOT NULL,
+ history_xml_bytes bytea NOT NULL,
+ admin_contact text,
+ auth_info_repo_id text,
+ auth_info_value text,
+ billing_recurrence_id bigint,
+ autorenew_poll_message_id bigint,
+ billing_contact text,
+ deletion_poll_message_id bigint,
+ domain_name text,
+ idn_table_name text,
+ last_transfer_time timestamp with time zone,
+ launch_notice_accepted_time timestamp with time zone,
+ launch_notice_expiration_time timestamp with time zone,
+ launch_notice_tcn_id text,
+ launch_notice_validator_id text,
+ registrant_contact text,
+ registration_expiration_time timestamp with time zone,
+ smd_id text,
+ subordinate_hosts text[],
+ tech_contact text,
+ tld text,
+ transfer_billing_cancellation_id bigint,
+ transfer_billing_recurrence_id bigint,
+ transfer_autorenew_poll_message_id bigint,
+ transfer_billing_event_id bigint,
+ transfer_renew_period_unit text,
+ transfer_renew_period_value integer,
+ transfer_registration_expiration_time timestamp with time zone,
+ transfer_gaining_poll_message_id bigint,
+ transfer_losing_poll_message_id bigint,
+ transfer_client_txn_id text,
+ transfer_server_txn_id text,
+ transfer_gaining_registrar_id text,
+ transfer_losing_registrar_id text,
+ transfer_pending_expiration_time timestamp with time zone,
+ transfer_request_time timestamp with time zone,
+ transfer_status text,
+ creation_registrar_id text NOT NULL,
+ creation_time timestamp with time zone NOT NULL,
+ current_sponsor_registrar_id text NOT NULL,
+ deletion_time timestamp with time zone,
+ last_epp_update_registrar_id text,
+ last_epp_update_time timestamp with time zone,
+ statuses text[],
+ update_timestamp timestamp with time zone,
+ domain_repo_id text NOT NULL
+);
+
+
+--
+-- Name: DomainHistoryHost; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public."DomainHistoryHost" (
+ domain_history_history_revision_id bigint NOT NULL,
+ host_repo_id text
+);
+
+
--
-- Name: DomainHost; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."DomainHost" (
domain_repo_id text NOT NULL,
- ns_hosts text
+ host_repo_id text
);
@@ -933,6 +1006,14 @@ ALTER TABLE ONLY public."Cursor"
ADD CONSTRAINT "Cursor_pkey" PRIMARY KEY (scope, type);
+--
+-- Name: DomainHistory DomainHistory_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public."DomainHistory"
+ ADD CONSTRAINT "DomainHistory_pkey" PRIMARY KEY (history_revision_id);
+
+
--
-- Name: Domain Domain_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -1131,6 +1212,13 @@ CREATE INDEX idx6py6ocrab0ivr76srcd2okpnq ON public."BillingEvent" USING btree (
CREATE INDEX idx6syykou4nkc7hqa5p8r92cpch ON public."BillingRecurrence" USING btree (event_time);
+--
+-- Name: idx6w3qbtgce93cal2orjg1tw7b7; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX idx6w3qbtgce93cal2orjg1tw7b7 ON public."DomainHistory" USING btree (history_modification_time);
+
+
--
-- Name: idx73l103vc5900ig3p4odf0cngt; Type: INDEX; Schema: public; Owner: -
--
@@ -1166,6 +1254,13 @@ CREATE INDEX idx_registry_lock_registrar_id ON public."RegistryLock" USING btree
CREATE INDEX idx_registry_lock_verification_code ON public."RegistryLock" USING btree (verification_code);
+--
+-- Name: idxaro1omfuaxjwmotk3vo00trwm; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX idxaro1omfuaxjwmotk3vo00trwm ON public."DomainHistory" USING btree (history_registrar_id);
+
+
--
-- Name: idxaydgox62uno9qx8cjlj5lauye; Type: INDEX; Schema: public; Owner: -
--
@@ -1285,6 +1380,13 @@ CREATE INDEX idxplxf9v56p0wg8ws6qsvd082hk ON public."BillingEvent" USING btree (
CREATE INDEX idxqa3g92jc17e8dtiaviy4fet4x ON public."BillingCancellation" USING btree (billing_time);
+--
+-- Name: idxrh4xmrot9bd63o382ow9ltfig; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX idxrh4xmrot9bd63o382ow9ltfig ON public."DomainHistory" USING btree (creation_time);
+
+
--
-- Name: idxrwl38wwkli1j7gkvtywi9jokq; Type: INDEX; Schema: public; Owner: -
--
@@ -1292,6 +1394,13 @@ CREATE INDEX idxqa3g92jc17e8dtiaviy4fet4x ON public."BillingCancellation" USING
CREATE INDEX idxrwl38wwkli1j7gkvtywi9jokq ON public."Domain" USING btree (tld);
+--
+-- Name: idxsu1nam10cjes9keobapn5jvxj; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX idxsu1nam10cjes9keobapn5jvxj ON public."DomainHistory" USING btree (history_type);
+
+
--
-- Name: idxsudwswtwqnfnx2o1hx4s0k0g5; Type: INDEX; Schema: public; Owner: -
--
@@ -1395,6 +1504,14 @@ ALTER TABLE ONLY public."HostHistory"
ADD CONSTRAINT fk3d09knnmxrt6iniwnp8j2ykga FOREIGN KEY (history_registrar_id) REFERENCES public."Registrar"(registrar_id);
+--
+-- Name: DomainHistoryHost fk6b8eqdxwe3guc56tgpm89atx; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public."DomainHistoryHost"
+ ADD CONSTRAINT fk6b8eqdxwe3guc56tgpm89atx FOREIGN KEY (domain_history_history_revision_id) REFERENCES public."DomainHistory"(history_revision_id);
+
+
--
-- Name: ClaimsEntry fk6sc6at5hedffc0nhdcab6ivuq; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -1531,6 +1648,22 @@ ALTER TABLE ONLY public."Domain"
ADD CONSTRAINT fk_domain_deletion_poll_message_id FOREIGN KEY (deletion_poll_message_id) REFERENCES public."PollMessage"(poll_message_id);
+--
+-- Name: DomainHistory fk_domain_history_domain_repo_id; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public."DomainHistory"
+ ADD CONSTRAINT fk_domain_history_domain_repo_id FOREIGN KEY (domain_repo_id) REFERENCES public."Domain"(repo_id);
+
+
+--
+-- Name: DomainHistory fk_domain_history_registrar_id; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public."DomainHistory"
+ ADD CONSTRAINT fk_domain_history_registrar_id FOREIGN KEY (history_registrar_id) REFERENCES public."Registrar"(registrar_id);
+
+
--
-- Name: Domain fk_domain_registrant_contact; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -1592,7 +1725,7 @@ ALTER TABLE ONLY public."Domain"
--
ALTER TABLE ONLY public."DomainHost"
- ADD CONSTRAINT fk_domainhost_host_valid FOREIGN KEY (ns_hosts) REFERENCES public."HostResource"(repo_id);
+ ADD CONSTRAINT fk_domainhost_host_valid FOREIGN KEY (host_repo_id) REFERENCES public."HostResource"(repo_id);
--