google-nomulus/java/google/registry/model/domain/DomainResource.java
cgoldfeder 23b66b0bb4 Load foreign keys more efficiently for xml marshalling.
Before this CL, each contact and host was independently
loaded via the ReferenceUnion adapter. Since fields are
processed serially by JAXB, this means worst-case there
were 17 loads, best case 3 (the 3 required contacts) and
usual case 5-6 (some hosts). This CL reduces that to 1
datastore roundtrip in all cases.

A side effect of this CL is the further hollowing-out of
ReferenceUnion, since it no longer is involved in
marshalling at all.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=123712842
2016-06-06 13:30:53 -04:00

415 lines
16 KiB
Java

// Copyright 2016 The Domain Registry 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.Ref;
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 org.joda.time.DateTime;
import org.joda.time.Interval;
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;
/** 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<StatusValue> 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<String> subordinateHosts;
/** When this domain's registration will expire. */
@XmlElement(name = "exDate")
DateTime registrationExpirationTime;
/**
* The poll message associated with this domain being deleted.
*
* <p>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<PollMessage.OneTime> deletePollMessage;
/**
* The recurring billing event associated with this domain's autorenewals.
*
* <p>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
Ref<BillingEvent.Recurring> autorenewBillingEvent;
/**
* The recurring poll message associated with this domain's autorenewals.
*
* <p>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
Ref<PollMessage.Autorenew> autorenewPollMessage;
/** The unexpired grace periods for this domain (some of which may not be active yet). */
@XmlTransient
Set<GracePeriod> 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 reference to the application used to allocate this domain. Will only be populated for domains
* allocated from an application.
*/
@IgnoreSave(IfNull.class)
@XmlTransient
Ref<DomainApplication> application;
public ImmutableSet<String> getSubordinateHosts() {
return nullToEmptyImmutableCopy(subordinateHosts);
}
public DateTime getRegistrationExpirationTime() {
return registrationExpirationTime;
}
public Key<PollMessage.OneTime> getDeletePollMessage() {
return deletePollMessage;
}
public Ref<BillingEvent.Recurring> getAutorenewBillingEvent() {
return autorenewBillingEvent;
}
public Ref<PollMessage.Autorenew> getAutorenewPollMessage() {
return autorenewPollMessage;
}
public ImmutableSet<GracePeriod> getGracePeriods() {
return nullToEmptyImmutableCopy(gracePeriods);
}
public String getSmdId() {
return smdId;
}
public DateTime getApplicationTime() {
return applicationTime;
}
public Ref<DomainApplication> 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.
*
* <p>This collects all statuses from the domain's {@link GracePeriod} entries and also adds the
* PENDING_DELETE status if needed.
*/
public ImmutableSet<GracePeriodStatus> getGracePeriodStatuses() {
Set<GracePeriodStatus> 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);
}
/**
* 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 ref 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(),
Ref.create(transferData.getServerApproveBillingEvent().key()))));
// 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(),
Ref.create(autorenewBillingEvent.key())));
}
// 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<DomainResource, 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<String> 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<PollMessage.OneTime> deletePollMessage) {
getInstance().deletePollMessage = deletePollMessage;
return this;
}
public Builder setAutorenewBillingEvent(Ref<BillingEvent.Recurring> autorenewBillingEvent) {
getInstance().autorenewBillingEvent = autorenewBillingEvent;
return this;
}
public Builder setAutorenewPollMessage(Ref<PollMessage.Autorenew> 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(Ref<DomainApplication> application) {
getInstance().application = application;
return this;
}
public Builder setGracePeriods(ImmutableSet<GracePeriod> 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;
}
}
}