google-nomulus/java/google/registry/model/domain/DomainBase.java
gbrodman 5d25290d42 Remove compareTo() from DelegationSignerData
The only reason why it existed was so that we could get tests to print information in a consistent order and there are other ways of doing that. By removing compareTo we can use the properties of the extended ImmutableObject properly and properly implement the RFC https://tools.ietf.org/html/rfc5910#page-18

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=240170488
2019-03-29 15:56:15 -04:00

688 lines
27 KiB
Java

// 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 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.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.IgnoreSave;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.condition.IfNull;
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.TransferData;
import google.registry.model.transfer.TransferStatus;
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 org.joda.time.DateTime;
import org.joda.time.Interval;
/**
* A persistable domain resource including mutable and non-mutable fields.
*
* <p>For historical reasons, the name of this entity is "DomainBase". Ideally it would be
* "DomainResource" for linguistic parallelism with the other {@link EppResource} entity classes,
* but that would necessitate a complex data migration which isn't worth it.
*
* @see <a href="https://tools.ietf.org/html/rfc5731">RFC 5731</a>
*/
@ReportedOn
@Entity
@ExternalMessagingName("domain")
public class DomainBase extends EppResource
implements 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<StatusValue> 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.
*
* <p>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()
*/
@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
Set<Key<HostResource>> nsHosts;
/**
* The union of the contacts visible via {@link #getContacts} and {@link #getRegistrant}.
*
* <p>These are stored in one field so that we can query across all contacts at once.
*/
Set<DesignatedContact> allContacts;
/** Authorization info (aka transfer secret) of the domain. */
DomainAuthInfo authInfo;
/**
* Data used to construct DS records for this domain.
*
* <p>This is {@literal @}XmlTransient because it needs to be returned under the "extension" tag
* of an info response rather than inside the "infData" tag.
*/
Set<DelegationSignerData> 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)
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<String> subordinateHosts;
/** When this domain's registration will expire. */
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.
*/
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.
*/
Key<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.
*/
Key<PollMessage.Autorenew> autorenewPollMessage;
/** The unexpired grace periods for this domain (some of which may not be active yet). */
Set<GracePeriod> gracePeriods;
/**
* The id of the signed mark that was used to create this domain in sunrise.
*
* <p>Will only be populated for domains created in sunrise.
*/
@IgnoreSave(IfNull.class)
String smdId;
/** Data about any pending or past transfers on this domain. */
TransferData transferData;
/**
* The time that this resource was last transferred.
*
* <p>Can be null if the resource has never been transferred.
*/
DateTime lastTransferTime;
public ImmutableSet<String> getSubordinateHosts() {
return nullToEmptyImmutableCopy(subordinateHosts);
}
public DateTime getRegistrationExpirationTime() {
return registrationExpirationTime;
}
public Key<PollMessage.OneTime> getDeletePollMessage() {
return deletePollMessage;
}
public Key<BillingEvent.Recurring> getAutorenewBillingEvent() {
return autorenewBillingEvent;
}
public Key<PollMessage.Autorenew> getAutorenewPollMessage() {
return autorenewPollMessage;
}
public ImmutableSet<GracePeriod> getGracePeriods() {
return nullToEmptyImmutableCopy(gracePeriods);
}
public String getSmdId() {
return smdId;
}
@Override
public final TransferData getTransferData() {
return Optional.ofNullable(transferData).orElse(TransferData.EMPTY);
}
@Override
public DateTime getLastTransferTime() {
return lastTransferTime;
}
@Override
public String getForeignKey() {
return fullyQualifiedDomainName;
}
public String getFullyQualifiedDomainName() {
return fullyQualifiedDomainName;
}
public ImmutableSet<DelegationSignerData> getDsData() {
return nullToEmptyImmutableCopy(dsData);
}
public LaunchNotice getLaunchNotice() {
return launchNotice;
}
public String getIdnTableName() {
return idnTableName;
}
public ImmutableSet<Key<HostResource>> 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.
*
* <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);
}
/** Returns the subset of grace periods having the specified type. */
public ImmutableSet<GracePeriod> getGracePeriodsOfType(GracePeriodStatus gracePeriodType) {
ImmutableSet.Builder<GracePeriod> 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) {
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.
DomainBase 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 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.
int extraYears = transferData.getTransferPeriod().getValue();
if (domainAtTransferTime.getGracePeriodStatuses().contains(GracePeriodStatus.AUTO_RENEW)) {
extraYears = 0;
}
// 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());
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<DateTime> newLastEppUpdateTime = Optional.empty();
// 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));
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<String> loadNameserverFullyQualifiedHostNames() {
return ofy()
.load()
.keys(getNameservers())
.values()
.stream()
.map(HostResource::getFullyQualifiedHostName)
.collect(toImmutableSortedSet(Ordering.natural()));
}
/** A key to the registrant who registered this domain. */
public Key<ContactResource> getRegistrant() {
return nullToEmpty(allContacts)
.stream()
.filter(IS_REGISTRANT)
.findFirst()
.get()
.getContactKey();
}
/** Associated contacts for the domain (other than registrant). */
public ImmutableSet<DesignatedContact> 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<Key<ContactResource>> getReferencedContacts() {
return nullToEmptyImmutableCopy(allContacts)
.stream()
.map(DesignatedContact::getContactKey)
.filter(Objects::nonNull)
.collect(toImmutableSet());
}
public String getTld() {
return tld;
}
/** Predicate to determine if a given {@link DesignatedContact} is the registrant. */
private static final Predicate<DesignatedContact> 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<DomainBase, Builder>
implements BuilderWithTransferData<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 (TransferData.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 fullyQualifiedDomainName");
checkArgument(instance.allContacts.stream().anyMatch(IS_REGISTRANT), "Missing registrant");
instance.tld = getTldFromDomainName(instance.fullyQualifiedDomainName);
return super.build();
}
public Builder setFullyQualifiedDomainName(String fullyQualifiedDomainName) {
checkArgument(
fullyQualifiedDomainName.equals(canonicalizeDomainName(fullyQualifiedDomainName)),
"Domain name must be in puny-coded, lower-case form");
getInstance().fullyQualifiedDomainName = fullyQualifiedDomainName;
return thisCastToDerived();
}
public Builder setDsData(ImmutableSet<DelegationSignerData> dsData) {
getInstance().dsData = dsData;
return thisCastToDerived();
}
public Builder setRegistrant(Key<ContactResource> registrant) {
// Replace the registrant contact inside allContacts.
getInstance().allContacts = union(
getInstance().getContacts(),
DesignatedContact.create(Type.REGISTRANT, checkArgumentNotNull(registrant)));
return thisCastToDerived();
}
public Builder setAuthInfo(DomainAuthInfo authInfo) {
getInstance().authInfo = authInfo;
return thisCastToDerived();
}
public Builder setNameservers(Key<HostResource> nameserver) {
getInstance().nsHosts = ImmutableSet.of(nameserver);
return thisCastToDerived();
}
public Builder setNameservers(ImmutableSet<Key<HostResource>> nameservers) {
getInstance().nsHosts = forceEmptyToNull(nameservers);
return thisCastToDerived();
}
public Builder addNameserver(Key<HostResource> nameserver) {
return addNameservers(ImmutableSet.of(nameserver));
}
public Builder addNameservers(ImmutableSet<Key<HostResource>> nameservers) {
return setNameservers(
ImmutableSet.copyOf(union(getInstance().getNameservers(), nameservers)));
}
public Builder removeNameserver(Key<HostResource> nameserver) {
return removeNameservers(ImmutableSet.of(nameserver));
}
public Builder removeNameservers(ImmutableSet<Key<HostResource>> nameservers) {
return setNameservers(
ImmutableSet.copyOf(difference(getInstance().getNameservers(), nameservers)));
}
public Builder setContacts(DesignatedContact contact) {
return setContacts(ImmutableSet.of(contact));
}
public Builder setContacts(ImmutableSet<DesignatedContact> 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());
return thisCastToDerived();
}
public Builder addContacts(ImmutableSet<DesignatedContact> contacts) {
return setContacts(ImmutableSet.copyOf(union(getInstance().getContacts(), contacts)));
}
public Builder removeContacts(ImmutableSet<DesignatedContact> 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<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(
CollectionUtils.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(
Key<BillingEvent.Recurring> autorenewBillingEvent) {
getInstance().autorenewBillingEvent = autorenewBillingEvent;
return this;
}
public Builder setAutorenewPollMessage(
Key<PollMessage.Autorenew> autorenewPollMessage) {
getInstance().autorenewPollMessage = autorenewPollMessage;
return this;
}
public Builder setSmdId(String smdId) {
getInstance().smdId = smdId;
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 = CollectionUtils
.difference(getInstance().getGracePeriods(), gracePeriod);
return this;
}
@Override
public Builder setTransferData(TransferData transferData) {
getInstance().transferData = transferData;
return thisCastToDerived();
}
@Override
public Builder setLastTransferTime(DateTime lastTransferTime) {
getInstance().lastTransferTime = lastTransferTime;
return thisCastToDerived();
}
}
}