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