// Copyright 2016 The Nomulus Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package google.registry.model.domain; 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.Ofy.RECOMMENDED_MEMCACHE_EXPIRATION; import static google.registry.util.CollectionUtils.difference; import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy; import static google.registry.util.CollectionUtils.union; import static google.registry.util.DateTimeUtils.earliestOf; import static google.registry.util.DateTimeUtils.isBeforeOrAt; import static google.registry.util.DateTimeUtils.leapSafeAddYears; import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import com.googlecode.objectify.annotation.Cache; import com.googlecode.objectify.annotation.EntitySubclass; import com.googlecode.objectify.annotation.IgnoreSave; import com.googlecode.objectify.condition.IfNull; import google.registry.model.EppResource.ForeignKeyedEppResource; import google.registry.model.annotations.ExternalMessagingName; import google.registry.model.billing.BillingEvent; import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.eppcommon.StatusValue; import google.registry.model.poll.PollMessage; import google.registry.model.registry.Registry; import google.registry.model.transfer.TransferData; import google.registry.model.transfer.TransferStatus; import java.util.HashSet; import java.util.Set; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlType; import org.joda.time.DateTime; import org.joda.time.Interval; /** A persistable domain resource including mutable and non-mutable fields. */ @XmlRootElement(name = "infData") @XmlType(propOrder = { "fullyQualifiedDomainName", "repoId", "status", "marshalledRegistrant", "marshalledContacts", "marshalledNameservers", "subordinateHosts", "currentSponsorClientId", "creationClientId", "creationTime", "lastEppUpdateClientId", "lastEppUpdateTime", "registrationExpirationTime", "lastTransferTime", "authInfo"}) @Cache(expirationSeconds = RECOMMENDED_MEMCACHE_EXPIRATION) @EntitySubclass(index = true) @ExternalMessagingName("domain") public class DomainResource extends DomainBase implements ForeignKeyedEppResource { /** 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 host names of this domain's active subordinate hosts. */ @XmlElement(name = "host") Set subordinateHosts; /** When this domain's registration will expire. */ @XmlElement(name = "exDate") 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. */ @XmlTransient Key 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. */ @XmlTransient Key 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. */ @XmlTransient Key autorenewPollMessage; /** The unexpired grace periods for this domain (some of which may not be active yet). */ @XmlTransient Set gracePeriods; /** * The id of the signed mark that was used to create the sunrise application for this domain. * Will only be populated for domains allocated from a sunrise application. */ @IgnoreSave(IfNull.class) @XmlTransient String smdId; /** * The time that the application used to allocate this domain was created. Will only be populated * for domains allocated from an application. */ @IgnoreSave(IfNull.class) @XmlTransient DateTime applicationTime; /** * A key to the application used to allocate this domain. Will only be populated for domains * allocated from an application. */ @IgnoreSave(IfNull.class) @XmlTransient Key application; public ImmutableSet getSubordinateHosts() { return nullToEmptyImmutableCopy(subordinateHosts); } public DateTime getRegistrationExpirationTime() { return registrationExpirationTime; } public Key getDeletePollMessage() { return deletePollMessage; } public Key getAutorenewBillingEvent() { return autorenewBillingEvent; } public Key getAutorenewPollMessage() { return autorenewPollMessage; } public ImmutableSet getGracePeriods() { return nullToEmptyImmutableCopy(gracePeriods); } public String getSmdId() { return smdId; } public DateTime getApplicationTime() { return applicationTime; } public Key getApplication() { return application; } @Override public String getForeignKey() { return fullyQualifiedDomainName; } /** 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 Registry Grace Period expiration date for the specified type of grace period for * this domain, or null if there is no grace period of the specified type. */ public Optional getGracePeriodExpirationTime(GracePeriodStatus gracePeriodType) { for (GracePeriod gracePeriod : getGracePeriods()) { if (gracePeriod.getType() == gracePeriodType) { return Optional.of(gracePeriod.getExpirationTime()); } } return Optional.absent(); } /** * Checks to see if the domain is in a particular type of grace period at the specified time. We * only check the expiration time, because grace periods are always assumed to start at the * beginning of time. This could be confusing if asOfDate is in the past. For instance, the Add * Grace Period will appear to last from the beginning of time until 5 days after the domain is * created. */ public boolean doesAnyGracePeriodOfTypeExpireAfter( GracePeriodStatus gracePeriodType, DateTime asOfDate) { for (GracePeriod gracePeriod : getGracePeriods()) { if ((gracePeriod.getType() == gracePeriodType) && gracePeriod.getExpirationTime().isAfter(asOfDate)) { return true; } } return false; } /** * 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 DomainResource cloneProjectedAtTime(final DateTime now) { TransferData 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. DomainResource domainAtTransferTime = cloneProjectedAtTime(transferExpirationTime.minusMillis(1)); // 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 reduce the number of years added to the // expiration time by one to account for the year added by the autorenew. int extraYears = transferData.getExtendedRegistrationYears(); if (domainAtTransferTime.getGracePeriodStatuses().contains(GracePeriodStatus.AUTO_RENEW)) { extraYears--; } // Set the expiration, autorenew events, and grace period for the transfer. (Transfer ends // all other graces). Builder builder = domainAtTransferTime.asBuilder() // 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. .setRegistrationExpirationTime(extendRegistrationWithCap( transferExpirationTime, domainAtTransferTime.getRegistrationExpirationTime(), extraYears)) // Set the speculatively-written new autorenew events as the domain's autorenew events. .setAutorenewBillingEvent(transferData.getServerApproveAutorenewEvent()) .setAutorenewPollMessage(transferData.getServerApproveAutorenewPollMessage()) // 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. .setGracePeriods(ImmutableSet.of(GracePeriod.create( GracePeriodStatus.TRANSFER, transferExpirationTime.plus(Registry.get(getTld()).getTransferGracePeriodLength()), transferData.getGainingClientId(), transferData.getServerApproveBillingEvent()))); // Set all remaining transfer properties. setAutomaticTransferSuccessProperties(builder, transferData); // Finish projecting to now. return builder.build().cloneProjectedAtTime(now); } // There is no transfer. Do any necessary autorenews. Builder builder = asBuilder(); if (isBeforeOrAt(registrationExpirationTime, now)) { // 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)); } // Remove any grace periods that have expired. DomainResource almostBuilt = builder.build(); builder = almostBuilt.asBuilder(); for (GracePeriod gracePeriod : almostBuilt.getGracePeriods()) { if (isBeforeOrAt(gracePeriod.getExpirationTime(), now)) { builder.removeGracePeriod(gracePeriod); } } // 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, Integer extendedRegistrationYears) { // We must cap registration at the max years (aka 10), even if that truncates the last year. return earliestOf( leapSafeAddYears( currentExpirationTime, Optional.fromNullable(extendedRegistrationYears).or(0)), leapSafeAddYears(now, MAX_REGISTRATION_YEARS)); } @Override public Builder asBuilder() { return new Builder(clone(this)); } /** A builder for constructing {@link DomainResource}, since it is immutable. */ public static class Builder extends DomainBase.Builder { public Builder() {} private Builder(DomainResource instance) { super(instance); } @Override public DomainResource build() { // A DomainResource 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); } // This must be called after we add or remove INACTIVE, since that affects whether we get OK. return super.build(); } 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( difference(getInstance().getSubordinateHosts(), hostToRemove))); } public Builder setRegistrationExpirationTime(DateTime registrationExpirationTime) { getInstance().registrationExpirationTime = registrationExpirationTime; return this; } public Builder setDeletePollMessage(Key deletePollMessage) { getInstance().deletePollMessage = deletePollMessage; return this; } public Builder setAutorenewBillingEvent(Key autorenewBillingEvent) { getInstance().autorenewBillingEvent = autorenewBillingEvent; return this; } public Builder setAutorenewPollMessage(Key autorenewPollMessage) { getInstance().autorenewPollMessage = autorenewPollMessage; return this; } public Builder setSmdId(String smdId) { getInstance().smdId = smdId; return this; } public Builder setApplicationTime(DateTime applicationTime) { getInstance().applicationTime = applicationTime; return this; } public Builder setApplication(Key application) { getInstance().application = application; 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 = difference(getInstance().getGracePeriods(), gracePeriod); return this; } } }