Compare commits

..
Author SHA1 Message Date
Lai JiangandGitHub bd4af052a6 Remove ofy support from Address (#1759) 2022-08-26 12:35:48 -04:00
Pavlo TkachandGitHub 78249e1329 Replace PubApi master calls with replica (#1742) 2022-08-26 10:15:30 -04:00
gbrodmanandGitHub 7aec579d96 Add DB annotations to console User and related classes (#1757)
We added the DB code last week, this is the corresponding bit now that
that has been released.
2022-08-25 16:54:39 -04:00
Lai JiangandGitHub b9f8faa165 Drop autorenew poll message history id column from the domain table (#1743)
We stopped using the column since #1732.
2022-08-25 15:52:32 -04:00
Pavlo TkachandGitHub b0e4e86586 Add registry email to bcc for outgoing DNS failure emails (#1755) 2022-08-25 14:15:20 -04:00
gbrodmanandGitHub 3412f4417f Allow UserAuthInfo to contain either old GAE Users or new console Users (#1744)
This means that LegacyAuthenticationMechanism or a to-be-created
OAuth2AuthenticationMechanism) can return a UserAuthInfo object that
contains either the GAE User or the console User as appropriate. The
goal is that the non-auth flows shouldn't have to know about which user
type it is. Note: the registry lock flow (for now) needs to know about
the separate types of auth because it is a separate level of auth from
the standard AuthenticatedRegistrarAccessor.

The AuthenticatedRegistrarAccessor code is a bit odd because the new
role system doesn't quite fit neatly into the old registrar ->
OWNER,ADMIN system but this is a fine approximation. Basically, any
new registrar role will map to the old OWNER role.
2022-08-24 14:18:32 -04:00
sarahcaseybotandGitHub db6329a070 Add the PackagePromotion table (#1745)
* Add the PackagePromotion table

* Add long id

* Add NOT NULL

* fix formatting

* make package price non null

* Add not nulls to java file

* Fix broken tests from merge conflicts
2022-08-24 14:16:34 -04:00
gbrodmanandGitHub 02af277148 Allow usage of allocation tokens in nomulus create_domain (#1756)
Useful when doing internal registrations like get.boo
2022-08-24 13:18:53 -04:00
sarahcaseybotandGitHub 8b02f76ae9 Add currentPackageToken on create flow (#1751)
* Add currentPackageToken on create flow

* Change to Truth8 assertion

* Add check for specified renewal behavior
2022-08-23 14:47:41 -04:00
gbrodmanandGitHub 6dd96c247a Reset the claims list cache in any test that saves to it (#1754) 2022-08-22 15:58:45 -04:00
gbrodmanandGitHub 919c744d8c Update currently-active ICANN-provided SMD test file (#1753)
The test files they provided before have expired, and they only provide
one valid currently-active test file now, so only test that one.

The test files are located at https://newgtlds.icann.org/en/about/trademark-clearinghouse/registries-registrars
2022-08-22 13:59:38 -04:00
gbrodmanandGitHub 5bccd65bd7 Add main method to ResaveAllEppResourcesPipeline (#1748)
Not sure how this got missed before, I am pretty sure we tested this on
alpha.
2022-08-22 12:39:34 -04:00
Lai JiangandGitHub 5268e35155 Remove redundant test extension (#1752)
This extension field is already defined in the super class.

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1752)
<!-- Reviewable:end -->
2022-08-21 12:15:15 -04:00
Lai JiangandGitHub 6e201450f0 Remove ofy support from PollMessage (#1732)
Also deletes the autorenew poll message history revision id field in
Domain, which is only needed to recreate the ofy key for the poll
message. The column already contains null values in it, making it
impossible to depend on it. The column itself will be deleted from the
schema after this PR is deployed.

The logic to update autorenew recurrence end time is changed
accordingly: When a poll message already exists, we simply update the
endtime, but when it no longer exists, i. e. when it's deleted
speculatively after a transfer request, we recreate one using the
history entry id that resulted in its creation (e. g. cancelled or rejected
transfer).

This should fix b/240984498. Though the exact reason for that bug is
still unclear to me. Namely, it throws an NPE at this line during an
explicit domain transfer approval:

https://cs.opensource.google/nomulus/nomulus/+/master:core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java;l=603;bpv=1;bpt=0;drc=ede919d7dcdb7f209b074563b3d449ebee19118a

The domain in question has a null autorenewPollMessageHistoryId, but
that in itself should not have caused an NPE because we are not
operating on the null pointer. On that line the only possible way to
throw an NPE is for the domain itself to be null, but if that were the
case, the NPE would have been thrown at line 599 where we called a
method on the domain object.

Regardless of the cause, with this PR we are using an explicitly
provided history id and checking for its nullness before using it. If a
similar issue arises again, we should have a better idea why.

Lastly, the way poll message id is constructed is largely simplified in
PollMessageExternalKeyConverter as a result of the removal ofy parent
keys in PollMessage. This does present a possibility of failure when
immediately before deployment, a registrar requests a poll message and
received the old id, but by the time the registrar acks the id, the new
version is deployed and therefore does not recognize the old key. The
likelihood of this happening should be slim, and we could have prevented
it by letting the converter recognize both the old and the new key.
However, we would like to eventually phase out the old key, and in
theory a registrar could ack a poll message at any time after it was
requested. So, there is not a safe time by which all the old ids are
acked, lest we develop some elaborate scheme to keep track of which
messages were sent with an old id when requested and which of these old
ids are acked. Only then can we be truly safe to phase out the old id.
The benefit does not seem to warrant the effort. If a registrar does
encounter a situation like this, they could open a support bug to have
us manually ack the poll message for them.
2022-08-19 14:24:03 -04:00
sarahcaseybotandGitHub dda9a3ef7e Flyway files for PackagePromotion table (#1746)
* Include missing file

* Fix merge conflicts

* make package price non null
2022-08-19 12:53:58 -04:00
gbrodmanandGitHub 8c1fb6bf00 Add another TestCacheExtension usage (#1750) 2022-08-19 10:17:15 -04:00
gbrodmanandGitHub 4e21152f04 Add TestCacheExtension in ResourceFlowTC to fix flakes (#1749)
Basically, what's happening here is that some flow tests are adding
things to the claims list cache which is stored statically, meaning that
some other tests can pick those up when they shouldn't. By adding the
extension in RFTC, it'll clear out the caches after each test.
2022-08-18 15:04:29 -04:00
gbrodmanandGitHub 22193474d5 Add Flyway and golden files for console User object (#1747) 2022-08-17 16:48:14 -04:00
Pavlo TkachandGitHub efd5244ebd Add email notification when DNS update fails (#1734) 2022-08-16 12:59:08 -04:00
gbrodmanandGitHub 87e5d19fe5 Allow anchor tenant creation via allocation token behavior (#1735)
* Allow anchor tenant creation via allocation token behavior

This also enforces that non-superusers cannot create registrations on
trademarked names prior to the sunrise period, even if they have an
allocation token with ANCHOR_TENANT behavior.
2022-08-15 12:42:16 -04:00
Lai JiangandGitHub bbb6174c9f Remove InjectExtension (#1739)
It is only used to set the clock used by Ofy, and it admits itself being
an ugly hack...

Also applied IntelliJ suggestions on touched files.
2022-08-12 14:56:53 -04:00
gbrodmanandGitHub 2b826651e6 Create a registry lock permission and corresponding account manager role (#1740)
* Create a registry lock permission and corresponding account manager role

This allows us to distinguish between standard account managers and
users that might have the registry lock permission. This will make the
registry lock password-setting flow easier (user can reset their
password iff they have the REGISTRY_LOCK permission, instead of having a
separate boolean) and allows us to easily determine whether or not a
user should have access to registry lock views in the UI.
2022-08-12 12:18:09 -04:00
Lai JiangandGitHub e4132db8ed Delete SetClockExtension (#1738)
We no longer write to commit logs, and the ReplayExtension that this
extension is supposed to be used with is already deleted.
2022-08-11 13:16:30 -04:00
Pavlo TkachandGitHub 45d90e7c68 Extend IP validation test with message verification (#1736) 2022-08-10 13:27:55 -04:00
sarahcaseybotandGitHub 028005906a Add allocation token to transfer command (#1737) 2022-08-09 15:18:23 -04:00
sarahcaseybotandGitHub 78d78e21cb Add CurrentPackageToken to Domain table (#1720)
* Add allocation token to Domain table

* Add tests

* Change column name

* change test names
2022-08-09 11:23:21 -04:00
Lai JiangandGitHub 2f3ac2e43b Remove unused columns in BillingCancellation (#1721)
We stopped using these columns after BillingVKey is removed in
https://github.com/google/nomulus/pull/1710.
2022-08-08 10:28:30 -04:00
gbrodmanandGitHub 632e3831e5 Add caches to ClaimsListDao and ClaimsList (#1731)
We cache the ClaimsList Java object for six hours (we don't expect it to
change frequently, and the cron job to update it only runs every twelve
hours). Subsequent calls to ClaimsListDao::get will return the cached
value.

Within the ClaimsList Java object itself, we cache any labels that we
have retrieved. While we already have a form of a cache here in the
"labelsToKeys" map, that only handles situations where we've loaded the
entire map from the database. We want to have a non-guaranteed cache in
order to make repeated calls to getClaimKey fast.
2022-08-05 17:29:59 -04:00
Ben McIlwainandGitHub 9ff25f9a67 Make domain transfers use (and retain) the renewal price/behavior (#1701)
* Use the new renewal price logic in transfer flow

* Fix build

* Add renewal handling on all transfer flows

* Merge branch 'master' into transfer-retain-renewal-price

* Merge branch 'master' into transfer-retain-renewal-price

* Add more tests
2022-08-05 15:53:27 -04:00
gbrodmanandGitHub eb1a314666 Add base object classes for new user/role permissioning model (#1707)
* Add base object classes for new user/role permissioning model

- Adds the permissions themselves
- Adds the six roles that a user may have -- three global, three
  per-registrar
- Adds the mapping from role -> set of permissions
- Adds a UserRoles object to encapsulate the answer to the question of
  "does this user have this permission?"
- Adds a User class as a base to show how we will use the new UserRoles
  object
2022-08-05 14:18:16 -04:00
Ben McIlwainandGitHub 0e182546f9 Rename HostResource -> Host (#1733)
* Rename HostResource -> Host
2022-08-05 10:28:45 -04:00
Pavlo TkachandGitHub ad06ba2e1e Extend registrar allowed IPs auth exception text with IP address (#1726) 2022-08-03 15:24:00 -04:00
Lai JiangandGitHub c903ed4c13 For some reason after the upgrade to Gradle, the core.jar file is no (#1730)
longer included in the generated WAR, even though the deploy_jar
configuration is specified as a dependency.

I could not figure out a way to tweak the configuration dependency to
have core.jar pulled into the .war, so I decided to just explicitly pick
it from its known location.

TESTED=deployed to alpha and verified that the instances can start.
2022-08-03 10:26:44 -04:00
Ben McIlwainandGitHub f6d2a7ff91 Rename DomainContent -> DomainBase (#1729)
* Rename DomainContent -> DomainBase

This is a follow-up to PR #1725 which renamed DomainBase to Domain. Now, the
class naming hierarchy has the same structure as ContactBase/HostBase.
2022-08-02 17:21:17 -04:00
Pavlo TkachandGitHub 35530616d6 Add docker prerequisite to install guide (#1728) 2022-08-02 16:20:59 -04:00
Ben McIlwainandGitHub ede919d7dc Rename DomainBase -> Domain (#1725)
* Rename DomainBase -> Domain

This was a long time coming, but we couldn't do it until we left Datastore, as
the Java class name has to match the Datastore entity name.

Subsequent PRs will rename ContactResource to Contact and HostResource to Host,
so that everything matches the SQL table names (and is shorter!).

* Merge branch 'master' into rename-domainbase
2022-08-02 16:03:30 -04:00
Lai JiangandGitHub 827b7db227 Make Kythe run work with Gradle 7 (#1727)
The fix is based on b/240627423. I tested locally and was able to build
with the -PenableCrossReferencing=true flag successfully.

TESTED=run the kythe GCB pipeline locally.
2022-08-02 13:19:47 -04:00
Lai JiangandGitHub 1aefd9a78d Remove ofy support from BillingEvent (#1710)
This PR turns out to be more massive than I would have liked but it
  deals with all billing event related stuff, which are more or link all
  intertwined:

  * Remove all billing events as Ofy entities.
  * Add a temporary annotation to allow BillingEvent's ID to be
    auto-allocated by ofy while not lacking the Ofy @Id annotation.
  * Remove Modification, which is only used in ofy.
  * Remove BillingVKey, as we do not need to store the ofy key parent
     information anymore. The VKey for a billing event now just contain
     its primary key, and can be converted by VKeyConverter.
  * Remove BigQuery related code in the billing pipeline.

  Note that after BillingVKey is removed, several columns in
  BillingCancellation are no longer needed. The change to database schema
  will be handled in https://github.com/google/nomulus/pull/1721 after
  this PR is deployed to production.

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1710)
<!-- Reviewable:end -->
2022-08-02 11:36:28 -04:00
389 changed files with 12106 additions and 10693 deletions
+6
View File
@@ -26,6 +26,7 @@ project.convention.plugins['war'].webAppDirName =
apply plugin: 'com.google.cloud.tools.appengine'
def coreResourcesDir = "${rootDir}/core/build/resources/main"
def coreLibsDir = "${rootDir}/core/build/libs"
// Get the web.xml file for the service.
war {
@@ -38,6 +39,10 @@ war {
from("${coreResourcesDir}/google/registry/ui/html") {
include "*.html"
}
from("${coreLibsDir}") {
include "core.jar"
into("WEB-INF/lib")
}
}
if (project.path == ":services:default") {
@@ -98,6 +103,7 @@ rootProject.deploy.dependsOn appengineDeployAll
rootProject.stage.dependsOn appengineStage
tasks['war'].dependsOn ':core:compileProdJS'
tasks['war'].dependsOn ':core:processResources'
tasks['war'].dependsOn ':core:jar'
// Impose verification for all of the deployment tasks. We haven't found a
// better way to do this other than to apply to each of them independently.
+2 -2
View File
@@ -207,8 +207,8 @@ allprojects {
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.fork = true
options.forkOptions.executable =
"${project.rootDir}/kythe/extractors/javac-wrapper.sh"
options.forkOptions.javaHome =
file("${System.env.REAL_JAVA_HOME}")
}
}
}
@@ -29,7 +29,7 @@ import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.EppResource;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.HostResource;
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
import google.registry.request.Action.Service;
import google.registry.util.CloudTasksUtils;
@@ -140,8 +140,8 @@ public final class AsyncTaskEnqueuer {
}
/** Enqueues a task to asynchronously refresh DNS for a renamed host. */
public void enqueueAsyncDnsRefresh(HostResource host, DateTime now) {
VKey<HostResource> hostKey = host.createVKey();
public void enqueueAsyncDnsRefresh(Host host, DateTime now) {
VKey<Host> hostKey = host.createVKey();
logger.atInfo().log("Enqueuing async DNS refresh for renamed host %s.", hostKey);
addTaskToQueueWithRetry(
asyncDnsRefreshPullQueue,
@@ -32,7 +32,7 @@ import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.flows.StatelessRequestSessionMetadata;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppoutput.EppOutput;
import google.registry.persistence.transaction.QueryComposer.Comparator;
@@ -128,10 +128,10 @@ public class DeleteExpiredDomainsAction implements Runnable {
logger.atInfo().log(
"Deleting non-renewing domains with autorenew end times up through %s.", runTime);
ImmutableList<DomainBase> domainsToDelete =
ImmutableList<Domain> domainsToDelete =
tm().transact(
() ->
tm().createQueryComposer(DomainBase.class)
tm().createQueryComposer(Domain.class)
.where("autorenewEndTime", Comparator.LTE, runTime)
.where("deletionTime", Comparator.EQ, END_OF_TIME)
.list());
@@ -145,10 +145,9 @@ public class DeleteExpiredDomainsAction implements Runnable {
"Found %d domains to delete: %s.",
domainsToDelete.size(),
String.join(
", ",
domainsToDelete.stream().map(DomainBase::getDomainName).collect(toImmutableList())));
", ", domainsToDelete.stream().map(Domain::getDomainName).collect(toImmutableList())));
int successes = 0;
for (DomainBase domain : domainsToDelete) {
for (Domain domain : domainsToDelete) {
if (runDomainDeleteFlow(domain)) {
successes++;
}
@@ -163,7 +162,7 @@ public class DeleteExpiredDomainsAction implements Runnable {
}
/** Runs the actual domain delete flow and returns whether the deletion was successful. */
private boolean runDomainDeleteFlow(DomainBase domain) {
private boolean runDomainDeleteFlow(Domain domain) {
logger.atInfo().log("Attempting to delete domain '%s'.", domain.getDomainName());
// Create a new transaction that the flow's execution will be enlisted in that loads the domain
// transactionally. This way we can ensure that nothing else has modified the domain in question
@@ -171,7 +170,7 @@ public class DeleteExpiredDomainsAction implements Runnable {
Optional<EppOutput> eppOutput =
tm().transact(
() -> {
DomainBase transDomain = tm().loadByKey(domain.createVKey());
Domain transDomain = tm().loadByKey(domain.createVKey());
if (!domain.getAutorenewEndTime().isPresent()
|| domain.getAutorenewEndTime().get().isAfter(tm().getTransactionTime())) {
logger.atSevere().log(
@@ -30,8 +30,8 @@ import google.registry.flows.poll.PollFlowUtils;
import google.registry.model.EppResource;
import google.registry.model.EppResourceUtils;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.HostResource;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntryDao;
@@ -43,8 +43,8 @@ import google.registry.util.Clock;
import javax.inject.Inject;
/**
* Hard deletes load-test ContactResources, HostResources, their subordinate history entries, and
* the associated ForeignKey and EppResourceIndex entities.
* Hard deletes load-test ContactResources, Hosts, their subordinate history entries, and the
* associated ForeignKey and EppResourceIndex entities.
*
* <p>This only deletes contacts and hosts, NOT domains. To delete domains, use {@link
* DeleteProberDataAction} and pass it the TLD(s) that the load test domains were created on. Note
@@ -93,7 +93,7 @@ public class DeleteLoadTestDataAction implements Runnable {
() -> {
LOAD_TEST_REGISTRARS.forEach(this::deletePollMessages);
tm().loadAllOfStream(ContactResource.class).forEach(this::deleteContact);
tm().loadAllOfStream(HostResource.class).forEach(this::deleteHost);
tm().loadAllOfStream(Host.class).forEach(this::deleteHost);
});
}
@@ -123,19 +123,19 @@ public class DeleteLoadTestDataAction implements Runnable {
deleteResource(contact);
}
private void deleteHost(HostResource host) {
private void deleteHost(Host host) {
if (!LOAD_TEST_REGISTRARS.contains(host.getPersistedCurrentSponsorRegistrarId())) {
return;
}
VKey<HostResource> hostVKey = host.createVKey();
VKey<Host> hostVKey = host.createVKey();
// We can remove hosts from linked domains, so we should do so then delete the hosts
ImmutableSet<VKey<DomainBase>> linkedDomains =
ImmutableSet<VKey<Domain>> linkedDomains =
EppResourceUtils.getLinkedDomainKeys(hostVKey, clock.nowUtc(), null);
tm().loadByKeys(linkedDomains)
.values()
.forEach(
domain -> {
ImmutableSet<VKey<HostResource>> remainingHosts =
ImmutableSet<VKey<Host>> remainingHosts =
domain.getNsHosts().stream()
.filter(vkey -> !vkey.equals(hostVKey))
.collect(toImmutableSet());
@@ -37,7 +37,7 @@ import google.registry.config.RegistryEnvironment;
import google.registry.dns.DnsQueue;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.EppResourceUtils;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.tld.Registry.TldType;
import google.registry.request.Action;
@@ -53,8 +53,8 @@ import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* Deletes all prober DomainBases and their subordinate history entries, poll messages, and billing
* events, along with their ForeignKeyDomainIndex and EppResourceIndex entities.
* Deletes all prober {@link Domain}s and their subordinate history entries, poll messages, and
* billing events, along with their ForeignKeyDomainIndex and EppResourceIndex entities.
*/
@Action(
service = Action.Service.BACKEND,
@@ -154,7 +154,7 @@ public class DeleteProberDataAction implements Runnable {
// keeping track of which domains to hard-delete (there can be many, so we batch them up)
ScrollableResults scrollableResult =
jpaTm()
.query(DOMAIN_QUERY_STRING, DomainBase.class)
.query(DOMAIN_QUERY_STRING, Domain.class)
.setParameter("tlds", deletableTlds)
.setParameter(
"creationTimeCutoff", CreateAutoTimestamp.create(now.minus(DOMAIN_USED_DURATION)))
@@ -166,7 +166,7 @@ public class DeleteProberDataAction implements Runnable {
ImmutableList.Builder<String> domainRepoIdsToHardDelete = new ImmutableList.Builder<>();
ImmutableList.Builder<String> hostNamesToHardDelete = new ImmutableList.Builder<>();
for (int i = 1; scrollableResult.next(); i = (i + 1) % BATCH_SIZE) {
DomainBase domain = (DomainBase) scrollableResult.get(0);
Domain domain = (Domain) scrollableResult.get(0);
processDomain(
domain,
domainRepoIdsToHardDelete,
@@ -187,7 +187,7 @@ public class DeleteProberDataAction implements Runnable {
}
private void processDomain(
DomainBase domain,
Domain domain,
ImmutableList.Builder<String> domainRepoIdsToHardDelete,
ImmutableList.Builder<String> hostNamesToHardDelete,
AtomicInteger softDeletedDomains,
@@ -251,8 +251,8 @@ public class DeleteProberDataAction implements Runnable {
}
// Take a DNS queue + admin registrar id as input so that it can be called from the mapper as well
private void softDeleteDomain(DomainBase domain) {
DomainBase deletedDomain =
private void softDeleteDomain(Domain domain) {
Domain deletedDomain =
domain.asBuilder().setDeletionTime(tm().getTransactionTime()).setStatusValues(null).build();
DomainHistory historyEntry =
new DomainHistory.Builder()
@@ -43,7 +43,7 @@ import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.common.Cursor;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.Period;
import google.registry.model.reporting.DomainTransactionRecord;
@@ -253,9 +253,7 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
final ImmutableSet<DateTime> billingTimes =
getBillingTimesInScope(eventTimes, cursorTime, executeTime, tld);
VKey<DomainBase> domainKey =
VKey.create(
DomainBase.class, recurring.getDomainRepoId(), recurring.getParentKey().getParent());
VKey<Domain> domainKey = VKey.createSql(Domain.class, recurring.getDomainRepoId());
Iterable<OneTime> oneTimesForDomain;
oneTimesForDomain =
tm().createQueryComposer(OneTime.class)
@@ -311,11 +309,11 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
.getRenewCost())
.setEventTime(eventTime)
.setFlags(union(recurring.getFlags(), Flag.SYNTHETIC))
.setParent(historyEntry)
.setDomainHistory(historyEntry)
.setPeriodYears(1)
.setReason(recurring.getReason())
.setSyntheticCreationTime(executeTime)
.setCancellationMatchingBillingEvent(recurring.createVKey())
.setCancellationMatchingBillingEvent(recurring)
.setTargetId(recurring.getTargetId())
.build());
}
@@ -27,7 +27,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.RegistryLock;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.registrar.Registrar;
@@ -125,7 +125,7 @@ public class RelockDomainAction implements Runnable {
private void relockDomain() {
RegistryLock oldLock = null;
DomainBase domain;
Domain domain;
try {
oldLock =
RegistryLockDao.getByRevisionId(oldUnlockRevisionId)
@@ -134,7 +134,7 @@ public class RelockDomainAction implements Runnable {
new IllegalArgumentException(
String.format("Unknown revision ID %d", oldUnlockRevisionId)));
domain =
tm().loadByKey(VKey.create(DomainBase.class, oldLock.getRepoId()))
tm().loadByKey(VKey.create(Domain.class, oldLock.getRepoId()))
.cloneProjectedAtTime(tm().getTransactionTime());
} catch (Throwable t) {
handleTransientFailure(Optional.ofNullable(oldLock), t);
@@ -180,7 +180,7 @@ public class RelockDomainAction implements Runnable {
}
}
private void verifyDomainAndLockState(RegistryLock oldLock, DomainBase domain) {
private void verifyDomainAndLockState(RegistryLock oldLock, Domain domain) {
// Domain shouldn't be deleted or have a pending transfer/delete
String domainName = domain.getDomainName();
ImmutableSet<StatusValue> statusValues = domain.getStatusValues();
@@ -20,6 +20,7 @@ import dagger.Lazy;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.model.domain.Domain;
import google.registry.persistence.PersistenceModule;
import google.registry.persistence.PersistenceModule.BeamBulkQueryJpaTm;
import google.registry.persistence.PersistenceModule.BeamJpaTm;
@@ -53,8 +54,7 @@ public interface RegistryPipelineComponent {
/**
* Returns a {@link JpaTransactionManager} optimized for bulk loading multi-level JPA entities
* ({@link google.registry.model.domain.DomainBase} and {@link
* google.registry.model.domain.DomainHistory}). Please refer to {@link
* ({@link Domain} and {@link google.registry.model.domain.DomainHistory}). Please refer to {@link
* google.registry.model.bulkquery.BulkQueryEntities} for more information.
*/
@BeamBulkQueryJpaTm
@@ -14,47 +14,38 @@
package google.registry.beam.invoicing;
import static google.registry.beam.BeamUtils.checkFieldsNotNull;
import static google.registry.beam.BeamUtils.extractField;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.reporting.billing.BillingModule;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.util.stream.Collectors;
import org.apache.avro.generic.GenericRecord;
import java.util.regex.Pattern;
import org.apache.beam.sdk.coders.AtomicCoder;
import org.apache.beam.sdk.coders.Coder;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.io.gcp.bigquery.SchemaAndRecord;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
/**
* A POJO representing a single billable event, parsed from a {@code SchemaAndRecord}.
*
* <p>This is a trivially serializable class that allows Beam to transform the results of a Bigquery
* query into a standard Java representation, giving us the type guarantees and ease of manipulation
* Bigquery lacks, while localizing any Bigquery-side failures to the {@link #parseFromRecord}
* function.
* <p>This is a trivially serializable class that allows Beam to transform the results of a Cloud
* SQL query into a standard Java representation, giving us the type guarantees and ease of
* manipulation Cloud SQL lacks.
*/
@AutoValue
public abstract class BillingEvent implements Serializable {
private static final long serialVersionUID = -3593088371541450077L;
private static final DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss zzz");
/** The amount we multiply the price for sunrise creates. This is currently a 15% discount. */
private static final double SUNRISE_DISCOUNT_PRICE_MODIFIER = 0.85;
private static final Pattern SYNTHETIC_REGEX = Pattern.compile("SYNTHETIC", Pattern.LITERAL);
private static final ImmutableList<String> FIELD_NAMES =
ImmutableList.of(
@@ -115,67 +106,7 @@ public abstract class BillingEvent implements Serializable {
/** Returns a list of space-delimited flags associated with the event. */
abstract String flags();
/**
* Constructs a {@code BillingEvent} from a {@code SchemaAndRecord}.
*
* @see <a
* href=http://avro.apache.org/docs/1.7.7/api/java/org/apache/avro/generic/GenericData.Record.html>
* Apache AVRO GenericRecord</a>
*/
static BillingEvent parseFromRecord(SchemaAndRecord schemaAndRecord) {
checkFieldsNotNull(FIELD_NAMES, schemaAndRecord);
GenericRecord record = schemaAndRecord.getRecord();
String flags = extractField(record, "flags");
double amount = getDiscountedAmount(Double.parseDouble(extractField(record, "amount")), flags);
return create(
// We need to chain parsers off extractField because GenericRecord only returns
// Objects, which contain a string representation of their underlying types.
Long.parseLong(extractField(record, "id")),
// Bigquery provides UNIX timestamps with microsecond precision.
new DateTime(Long.parseLong(extractField(record, "billingTime")) / 1000, DateTimeZone.UTC),
new DateTime(Long.parseLong(extractField(record, "eventTime")) / 1000, DateTimeZone.UTC),
extractField(record, "registrarId"),
extractField(record, "billingId"),
extractField(record, "poNumber"),
extractField(record, "tld"),
extractField(record, "action"),
extractField(record, "domain"),
extractField(record, "repositoryId"),
Integer.parseInt(extractField(record, "years")),
extractField(record, "currency"),
amount,
flags);
}
/**
* Applies a discount to sunrise creates and anchor tenant creates if applicable.
*
* Currently sunrise creates are discounted 15% and anchor tenant creates are free for 2 years.
* All anchor tenant creates are enforced to be 2 years in
* {@link google.registry.flows.domain.DomainCreateFlow#verifyAnchorTenantValidPeriod}.
*/
private static double getDiscountedAmount(double amount, String flags) {
// Apply a configurable discount to sunrise creates.
if (flags.contains(Flag.SUNRISE.name())) {
amount =
Double.parseDouble(
new DecimalFormat("#.##").format(amount * SUNRISE_DISCOUNT_PRICE_MODIFIER));
}
// Anchor tenant creates are free for the initial create. This is enforced to be a 2 year period
// upon domain create.
if (flags.contains(Flag.ANCHOR_TENANT.name())) {
amount = 0;
}
return amount;
}
/**
* Creates a concrete {@code BillingEvent}.
*
* <p>This should only be used outside this class for testing- instances of {@code BillingEvent}
* should otherwise come from {@link #parseFromRecord}.
*/
@VisibleForTesting
/** Creates a concrete {@link BillingEvent}. */
static BillingEvent create(
long id,
DateTime billingTime,
@@ -209,7 +140,7 @@ public abstract class BillingEvent implements Serializable {
}
static String getHeader() {
return FIELD_NAMES.stream().collect(Collectors.joining(","));
return String.join(",", FIELD_NAMES);
}
/**
@@ -242,7 +173,7 @@ public abstract class BillingEvent implements Serializable {
currency(),
String.format("%.2f", amount()),
// Strip out the 'synthetic' flag, which is internal only.
flags().replace("SYNTHETIC", "").trim()));
SYNTHETIC_REGEX.matcher(flags()).replaceAll("").trim()));
}
/** Returns the grouping key for this {@code BillingEvent}, to generate the overall invoice. */
@@ -274,6 +205,8 @@ public abstract class BillingEvent implements Serializable {
@AutoValue
abstract static class InvoiceGroupingKey implements Serializable {
private static final long serialVersionUID = -151561764235256205L;
private static final ImmutableList<String> INVOICE_HEADERS =
ImmutableList.of(
"StartDate",
@@ -345,6 +278,8 @@ public abstract class BillingEvent implements Serializable {
/** Coder that provides deterministic (de)serialization for {@code InvoiceGroupingKey}. */
static class InvoiceGroupingKeyCoder extends AtomicCoder<InvoiceGroupingKey> {
private static final long serialVersionUID = 6680701524304107547L;
@Override
public void encode(InvoiceGroupingKey value, OutputStream outStream) throws IOException {
Coder<String> stringCoder = StringUtf8Coder.of();
@@ -24,16 +24,14 @@ import google.registry.beam.common.RegistryJpaIO.Read;
import google.registry.beam.invoicing.BillingEvent.InvoiceGroupingKey;
import google.registry.beam.invoicing.BillingEvent.InvoiceGroupingKey.InvoiceGroupingKeyCoder;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.model.registrar.Registrar;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.reporting.billing.BillingModule;
import google.registry.util.DomainNameUtils;
import google.registry.util.SqlTemplate;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;
@@ -66,8 +64,7 @@ import org.joda.money.CurrencyUnit;
*/
public class InvoicingPipeline implements Serializable {
private static final DateTimeFormatter TIMESTAMP_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS");
private static final long serialVersionUID = 5386330443625580081L;
private static final Pattern SQL_COMMENT_REGEX =
Pattern.compile("^\\s*--.*\\n", Pattern.MULTILINE);
@@ -107,8 +104,7 @@ public class InvoicingPipeline implements Serializable {
}
private static Optional<BillingEvent> parseRow(Object[] row) {
google.registry.model.billing.BillingEvent.OneTime oneTime =
(google.registry.model.billing.BillingEvent.OneTime) row[0];
OneTime oneTime = (OneTime) row[0];
Registrar registrar = (Registrar) row[1];
CurrencyUnit currency = oneTime.getCost().getCurrencyUnit();
if (!registrar.getBillingAccountMap().containsKey(currency)) {
@@ -140,6 +136,9 @@ public class InvoicingPipeline implements Serializable {
/** Transform that converts a {@code BillingEvent} into an invoice CSV row. */
private static class GenerateInvoiceRows
extends PTransform<PCollection<BillingEvent>, PCollection<String>> {
private static final long serialVersionUID = -8090619008258393728L;
@Override
public PCollection<String> expand(PCollection<BillingEvent> input) {
return input
@@ -203,32 +202,13 @@ public class InvoicingPipeline implements Serializable {
TextIO.sink().withHeader(BillingEvent.getHeader())));
}
/** Create the Bigquery query for a given project and yearMonth at runtime. */
static String makeQuery(String yearMonth, String projectId) {
// Get the timestamp endpoints capturing the entire month with microsecond precision
YearMonth reportingMonth = YearMonth.parse(yearMonth);
LocalDateTime firstMoment = reportingMonth.atDay(1).atTime(LocalTime.MIDNIGHT);
LocalDateTime lastMoment = reportingMonth.atEndOfMonth().atTime(LocalTime.MAX);
// Construct the month's query by filling in the billing_events.sql template
return SqlTemplate.create(getQueryFromFile(InvoicingPipeline.class, "billing_events.sql"))
.put("FIRST_TIMESTAMP_OF_MONTH", firstMoment.format(TIMESTAMP_FORMATTER))
.put("LAST_TIMESTAMP_OF_MONTH", lastMoment.format(TIMESTAMP_FORMATTER))
.put("PROJECT_ID", projectId)
.put("DATASTORE_EXPORT_DATA_SET", "latest_datastore_export")
.put("ONETIME_TABLE", "OneTime")
.put("REGISTRY_TABLE", "Registry")
.put("REGISTRAR_TABLE", "Registrar")
.put("CANCELLATION_TABLE", "Cancellation")
.build();
}
/** Create the Cloud SQL query for a given yearMonth at runtime. */
static String makeCloudSqlQuery(String yearMonth) {
YearMonth endMonth = YearMonth.parse(yearMonth).plusMonths(1);
String queryWithComments =
SqlTemplate.create(
getQueryFromFile(InvoicingPipeline.class, "cloud_sql_billing_events.sql"))
.put("FIRST_TIMESTAMP_OF_MONTH", yearMonth.concat("-01"))
.put("FIRST_TIMESTAMP_OF_MONTH", yearMonth + "-01")
.put(
"LAST_TIMESTAMP_OF_MONTH",
String.format("%d-%d-01", endMonth.getYear(), endMonth.getMonthValue()))
@@ -47,10 +47,10 @@ import google.registry.gcs.GcsUtils;
import google.registry.model.EppResource;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.host.Host;
import google.registry.model.host.HostHistory;
import google.registry.model.host.HostResource;
import google.registry.model.rde.RdeMode;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.Type;
@@ -132,7 +132,7 @@ import org.joda.time.DateTime;
* that are soft-deleted by watermark. The history is emitted as pairs of (resource repo ID: history
* revision ID) from the SQL query.
*
* <h3>{@link DomainBase}</h3>
* <h3>{@link Domain}</h3>
*
* After the most recent (live) domain resources are loaded from the corresponding history objects,
* we marshall them to deposit fragments and emit the (pending deposit: deposit fragment) pairs for
@@ -148,7 +148,7 @@ import org.joda.time.DateTime;
* then loaded from the remaining referenced contact histories, and marshalled into (pending
* deposit: deposit fragment) pairs.
*
* <h3>{@link HostResource}</h3>
* <h3>{@link Host}</h3>
*
* Similar to {@link ContactResource}, we join the most recent host history with referenced hosts to
* find most recent referenced hosts. For external hosts we do the same treatment as we did on
@@ -195,7 +195,7 @@ public class RdePipeline implements Serializable {
private static final ImmutableMap<Class<? extends HistoryEntry>, String> EPP_RESOURCE_FIELD_NAME =
ImmutableMap.of(
DomainHistory.class,
"domainContent",
"domainBase",
ContactHistory.class,
"contactBase",
HostHistory.class,
@@ -467,18 +467,17 @@ public class RdePipeline implements Serializable {
Counter activeDomainCounter = Metrics.counter("RDE", "ActiveDomainBase");
Counter domainFragmentCounter = Metrics.counter("RDE", "DomainFragment");
Counter referencedContactCounter = Metrics.counter("RDE", "ReferencedContactResource");
Counter referencedHostCounter = Metrics.counter("RDE", "ReferencedHostResource");
Counter referencedHostCounter = Metrics.counter("RDE", "ReferencedHost");
return domainHistories.apply(
"Map DomainHistory to DepositFragment "
+ "and emit referenced ContactResource and HostResource",
"Map DomainHistory to DepositFragment " + "and emit referenced ContactResource and Host",
ParDo.of(
new DoFn<KV<String, Long>, KV<PendingDeposit, DepositFragment>>() {
@ProcessElement
public void processElement(
@Element KV<String, Long> kv, MultiOutputReceiver receiver) {
activeDomainCounter.inc();
DomainBase domain =
(DomainBase)
Domain domain =
(Domain)
loadResourceByHistoryEntryId(
DomainHistory.class, kv.getKey(), kv.getValue());
pendingDeposits.stream()
@@ -565,10 +564,10 @@ public class RdePipeline implements Serializable {
private PCollectionTuple processHostHistories(
PCollection<KV<String, PendingDeposit>> referencedHosts,
PCollection<KV<String, Long>> hostHistories) {
Counter subordinateHostCounter = Metrics.counter("RDE", "SubordinateHostResource");
Counter externalHostCounter = Metrics.counter("RDE", "ExternalHostResource");
Counter subordinateHostCounter = Metrics.counter("RDE", "SubordinateHost");
Counter externalHostCounter = Metrics.counter("RDE", "ExternalHost");
Counter externalHostFragmentCounter = Metrics.counter("RDE", "ExternalHostFragment");
return removeUnreferencedResource(referencedHosts, hostHistories, HostResource.class)
return removeUnreferencedResource(referencedHosts, hostHistories, Host.class)
.apply(
"Map external DomainResource to DepositFragment and process subordinate domains",
ParDo.of(
@@ -576,8 +575,8 @@ public class RdePipeline implements Serializable {
@ProcessElement
public void processElement(
@Element KV<String, CoGbkResult> kv, MultiOutputReceiver receiver) {
HostResource host =
(HostResource)
Host host =
(Host)
loadResourceByHistoryEntryId(
HostHistory.class,
kv.getKey(),
@@ -631,11 +630,9 @@ public class RdePipeline implements Serializable {
Counter referencedSubordinateHostCounter = Metrics.counter("RDE", "ReferencedSubordinateHost");
return KeyedPCollectionTuple.of(HOST_TO_PENDING_DEPOSIT, superordinateDomains)
.and(REVISION_ID, domainHistories)
.apply("Join Host:PendingDeposits with DomainHistory on Domain", CoGroupByKey.create())
.apply(
"Join HostResource:PendingDeposits with DomainHistory on DomainResource",
CoGroupByKey.create())
.apply(
" Remove unreferenced DomainResource",
" Remove unreferenced Domain",
Filter.by(
kv -> {
boolean toInclude =
@@ -647,15 +644,15 @@ public class RdePipeline implements Serializable {
return toInclude;
}))
.apply(
"Map subordinate HostResource to DepositFragment",
"Map subordinate Host to DepositFragment",
FlatMapElements.into(
kvs(
TypeDescriptor.of(PendingDeposit.class),
TypeDescriptor.of(DepositFragment.class)))
.via(
(KV<String, CoGbkResult> kv) -> {
DomainBase superordinateDomain =
(DomainBase)
Domain superordinateDomain =
(Domain)
loadResourceByHistoryEntryId(
DomainHistory.class,
kv.getKey(),
@@ -664,8 +661,8 @@ public class RdePipeline implements Serializable {
new ImmutableSet.Builder<>();
for (KV<String, CoGbkResult> hostToPendingDeposits :
kv.getValue().getAll(HOST_TO_PENDING_DEPOSIT)) {
HostResource host =
(HostResource)
Host host =
(Host)
loadResourceByHistoryEntryId(
HostHistory.class,
hostToPendingDeposits.getKey(),
@@ -23,8 +23,9 @@ import google.registry.beam.common.RegistryJpaIO;
import google.registry.beam.common.RegistryJpaIO.Read;
import google.registry.model.EppResource;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.HostResource;
import google.registry.model.host.Host;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
import google.registry.util.DateTimeUtils;
@@ -32,6 +33,7 @@ import java.io.Serializable;
import java.util.concurrent.ThreadLocalRandom;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.GroupIntoBatches;
import org.apache.beam.sdk.transforms.ParDo;
@@ -51,7 +53,7 @@ import org.joda.time.DateTime;
public class ResaveAllEppResourcesPipeline implements Serializable {
private static final ImmutableSet<Class<? extends EppResource>> EPP_RESOURCE_CLASSES =
ImmutableSet.of(ContactResource.class, DomainBase.class, HostResource.class);
ImmutableSet.of(ContactResource.class, Domain.class, Host.class);
/**
* There exist three possible situations where we know we'll want to project domains to the
@@ -111,16 +113,16 @@ public class ResaveAllEppResourcesPipeline implements Serializable {
* transfers, grace periods).
*
* <p>The logic of what might have changed is paraphrased from {@link
* google.registry.model.domain.DomainContent#cloneProjectedAtTime(DateTime)}.
* DomainBase#cloneProjectedAtTime(DateTime)}.
*/
private void fastResaveDomains(Pipeline pipeline) {
Read<DomainBase, DomainBase> read =
Read<Domain, Domain> read =
RegistryJpaIO.read(
DOMAINS_TO_PROJECT_QUERY,
ImmutableMap.of("END_OF_TIME", DateTimeUtils.END_OF_TIME),
DomainBase.class,
Domain.class,
d -> d);
projectAndResaveResources(pipeline, DomainBase.class, read);
projectAndResaveResources(pipeline, Domain.class, read);
}
/** Projects all resources to the current time and saves them. */
@@ -171,4 +173,13 @@ public class ResaveAllEppResourcesPipeline implements Serializable {
resource.cloneProjectedAtTime(jpaTm().getTransactionTime()))));
}
}
public static void main(String[] args) {
PipelineOptionsFactory.register(ResaveAllEppResourcesPipelineOptions.class);
ResaveAllEppResourcesPipelineOptions options =
PipelineOptionsFactory.fromArgs(args)
.withValidation()
.as(ResaveAllEppResourcesPipelineOptions.class);
new ResaveAllEppResourcesPipeline(options).run();
}
}
@@ -26,7 +26,7 @@ import google.registry.beam.common.RegistryJpaIO;
import google.registry.beam.common.RegistryJpaIO.Read;
import google.registry.beam.spec11.SafeBrowsingTransforms.EvaluateSafeBrowsingFn;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.reporting.Spec11ThreatMatch;
import google.registry.model.reporting.Spec11ThreatMatch.ThreatType;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
@@ -127,22 +127,21 @@ public class Spec11Pipeline implements Serializable {
@ProcessElement
public void processElement(
@Element KV<String, String> input, OutputReceiver<DomainNameInfo> output) {
DomainBase domainBase =
Domain domain =
jpaTm()
.transact(
() ->
jpaTm()
.loadByKey(
VKey.createSql(DomainBase.class, input.getKey())));
.loadByKey(VKey.createSql(Domain.class, input.getKey())));
String emailAddress = input.getValue();
if (emailAddress == null) {
emailAddress = "";
}
DomainNameInfo domainNameInfo =
DomainNameInfo.create(
domainBase.getDomainName(),
domainBase.getRepoId(),
domainBase.getCurrentSponsorRegistrarId(),
domain.getDomainName(),
domain.getRepoId(),
domain.getCurrentSponsorRegistrarId(),
emailAddress);
output.output(domainNameInfo);
}
@@ -1300,6 +1300,36 @@ public final class RegistryConfig {
return config.sslCertificateValidation.expirationWarningEmailSubjectText;
}
@Provides
@Config("dnsUpdateFailEmailSubjectText")
public static String provideDnsUpdateFailEmailSubjectText(RegistryConfigSettings config) {
return config.dnsUpdate.dnsUpdateFailEmailSubjectText;
}
@Provides
@Config("dnsUpdateFailEmailBodyText")
public static String provideDnsUpdateFailEmailBodyText(RegistryConfigSettings config) {
return config.dnsUpdate.dnsUpdateFailEmailBodyText;
}
@Provides
@Config("dnsUpdateFailRegistryName")
public static String provideDnsUpdateFailRegistryName(RegistryConfigSettings config) {
return config.dnsUpdate.dnsUpdateFailRegistryName;
}
@Provides
@Config("registrySupportEmail")
public static InternetAddress provideRegistrySupportEmail(RegistryConfigSettings config) {
return parseEmailAddress(config.dnsUpdate.registrySupportEmail);
}
@Provides
@Config("registryCcEmail")
public static InternetAddress provideRegistryCcEmail(RegistryConfigSettings config) {
return parseEmailAddress(config.dnsUpdate.registryCcEmail);
}
@Provides
@Config("allowedEcdsaCurves")
public static ImmutableSet<String> provideAllowedEcdsaCurves(RegistryConfigSettings config) {
@@ -1427,6 +1457,11 @@ public final class RegistryConfig {
return CONFIG_SETTINGS.get().caching.eppResourceMaxCachedEntries;
}
/** Returns the amount of time that a particular claims list should be cached. */
public static java.time.Duration getClaimsListCacheDuration() {
return java.time.Duration.ofSeconds(CONFIG_SETTINGS.get().caching.claimsListCachingSeconds);
}
/** Returns the email address that outgoing emails from the app are sent from. */
public static InternetAddress getGSuiteOutgoingEmailAddress() {
return parseEmailAddress(CONFIG_SETTINGS.get().gSuite.outgoingEmailAddress);
@@ -42,6 +42,7 @@ public class RegistryConfigSettings {
public RegistryTool registryTool;
public SslCertificateValidation sslCertificateValidation;
public ContactHistory contactHistory;
public DnsUpdate dnsUpdate;
/** Configuration options that apply to the entire App Engine project. */
public static class AppEngine {
@@ -155,6 +156,7 @@ public class RegistryConfigSettings {
public boolean eppResourceCachingEnabled;
public int eppResourceCachingSeconds;
public int eppResourceMaxCachedEntries;
public int claimsListCachingSeconds;
}
/** Configuration for ICANN monthly reporting. */
@@ -245,4 +247,13 @@ public class RegistryConfigSettings {
public int minMonthsBeforeWipeOut;
public int wipeOutQueryBatchSize;
}
/** Configuration for dns update. */
public static class DnsUpdate {
public String dnsUpdateFailEmailSubjectText;
public String dnsUpdateFailEmailBodyText;
public String dnsUpdateFailRegistryName;
public String registrySupportEmail;
public String registryCcEmail;
}
}
@@ -294,6 +294,10 @@ caching:
# have to be very large to achieve the vast majority of possible gains.
eppResourceMaxCachedEntries: 500
# Length of time that a claims list will be cached after retrieval. A fairly
# long duration is acceptable because claims lists don't change frequently.
claimsListCachingSeconds: 21600 # six hours
oAuth:
# OAuth scopes to detect on access tokens. Superset of requiredOauthScopes.
availableOauthScopes:
@@ -473,6 +477,29 @@ contactHistory:
# The batch size for querying ContactHistory table in the database.
wipeOutQueryBatchSize: 500
# Configuration options relevant to the DNS update functionality.
dnsUpdate:
dnsUpdateFailRegistryName: Example name
registrySupportEmail: email@example.com
registryCcEmail: email@example.com
# Email subject text template to notify partners after repeatedly failing DNS update
dnsUpdateFailEmailSubjectText: "[ACTION REQUIRED]: Incomplete DNS Update"
# Email body text template for failing DNS update that accepts 5 parameters:
# registrar name, domain or host address, 'domain' or 'host' as a string that failed,
# registry support email (see dnsUpdateFailRegistrySupportEmail) and registry display name
dnsUpdateFailEmailBodyText: >
Dear %1$s,
We are contacting you regarding the changes you recently made to one of your %2$ss.
The DNS update for the %3$s %2$s failed to process. Please review your %2$s's DNS records
and ensure that it is valid before trying another update.
If you have any questions or require additional support, please contact us
at %4$s.
Regards,
%5$s
# Configuration options for checking SSL certificates.
sslCertificateValidation:
# A map specifying the maximum amount of days the certificate can be valid.
@@ -14,6 +14,7 @@
package google.registry.dns;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.dns.DnsConstants.DNS_PUBLISH_PUSH_QUEUE_NAME;
import static google.registry.dns.DnsModule.PARAM_DNS_WRITER;
import static google.registry.dns.DnsModule.PARAM_DOMAINS;
@@ -22,6 +23,7 @@ import static google.registry.dns.DnsModule.PARAM_LOCK_INDEX;
import static google.registry.dns.DnsModule.PARAM_NUM_PUBLISH_LOCKS;
import static google.registry.dns.DnsModule.PARAM_PUBLISH_TASK_ENQUEUED;
import static google.registry.dns.DnsModule.PARAM_REFRESH_REQUEST_CREATED;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.RequestParameters.PARAM_TLD;
import static google.registry.util.CollectionUtils.nullToEmpty;
@@ -32,11 +34,16 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.InternetDomainName;
import dagger.Lazy;
import google.registry.config.RegistryConfig.Config;
import google.registry.dns.DnsMetrics.ActionStatus;
import google.registry.dns.DnsMetrics.CommitStatus;
import google.registry.dns.DnsMetrics.PublishStatus;
import google.registry.dns.writer.DnsWriter;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.tld.Registry;
import google.registry.request.Action;
import google.registry.request.Action.Service;
@@ -49,10 +56,13 @@ import google.registry.request.lock.LockHandler;
import google.registry.util.Clock;
import google.registry.util.CloudTasksUtils;
import google.registry.util.DomainNameUtils;
import google.registry.util.EmailMessage;
import google.registry.util.SendEmailService;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.mail.internet.InternetAddress;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -102,6 +112,13 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
private final Clock clock;
private final CloudTasksUtils cloudTasksUtils;
private final Response response;
private final SendEmailService sendEmailService;
private final String dnsUpdateFailEmailSubjectText;
private final String dnsUpdateFailEmailBodyText;
private final String dnsUpdateFailRegistryName;
private final Lazy<InternetAddress> registrySupportEmail;
private final Lazy<InternetAddress> registryCcEmail;
private final InternetAddress gSuiteOutgoingEmailAddress;
@Inject
public PublishDnsUpdatesAction(
@@ -114,6 +131,12 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
@Parameter(PARAM_HOSTS) Set<String> hosts,
@Parameter(PARAM_TLD) String tld,
@Config("publishDnsUpdatesLockDuration") Duration timeout,
@Config("dnsUpdateFailEmailSubjectText") String dnsUpdateFailEmailSubjectText,
@Config("dnsUpdateFailEmailBodyText") String dnsUpdateFailEmailBodyText,
@Config("dnsUpdateFailRegistryName") String dnsUpdateFailRegistryName,
@Config("registrySupportEmail") Lazy<InternetAddress> registrySupportEmail,
@Config("registryCcEmail") Lazy<InternetAddress> registryCcEmail,
@Config("gSuiteOutgoingEmailAddress") InternetAddress gSuiteOutgoingEmailAddress,
@Header(APP_ENGINE_RETRY_HEADER) Optional<Integer> appEngineRetryCount,
@Header(CLOUD_TASKS_RETRY_HEADER) Optional<Integer> cloudTasksRetryCount,
DnsQueue dnsQueue,
@@ -122,11 +145,13 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
LockHandler lockHandler,
Clock clock,
CloudTasksUtils cloudTasksUtils,
SendEmailService sendEmailService,
Response response) {
this.dnsQueue = dnsQueue;
this.dnsWriterProxy = dnsWriterProxy;
this.dnsMetrics = dnsMetrics;
this.timeout = timeout;
this.sendEmailService = sendEmailService;
this.retryCount =
cloudTasksRetryCount.orElse(
appEngineRetryCount.orElseThrow(
@@ -143,6 +168,12 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
this.clock = clock;
this.cloudTasksUtils = cloudTasksUtils;
this.response = response;
this.dnsUpdateFailEmailBodyText = dnsUpdateFailEmailBodyText;
this.dnsUpdateFailEmailSubjectText = dnsUpdateFailEmailSubjectText;
this.dnsUpdateFailRegistryName = dnsUpdateFailRegistryName;
this.registrySupportEmail = registrySupportEmail;
this.registryCcEmail = registryCcEmail;
this.gSuiteOutgoingEmailAddress = gSuiteOutgoingEmailAddress;
}
private void recordActionResult(ActionStatus status) {
@@ -209,9 +240,35 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
} else if (retryCount < RETRIES_BEFORE_PERMANENT_FAILURE) {
// If the batch only contains 1 name, allow it more retries
throw e;
} else {
// By the time we get here there's either single domain or a single host
domains.stream()
.findFirst()
.ifPresent(
dn -> {
Optional<Domain> domain = loadByForeignKey(Domain.class, dn, clock.nowUtc());
if (domain.isPresent()) {
notifyWithEmailAboutDnsUpdateFailure(
domain.get().getCurrentSponsorRegistrarId(), dn, false);
} else {
logger.atSevere().log(String.format("Domain entity for %s not found", dn));
}
});
hosts.stream()
.findFirst()
.ifPresent(
hn -> {
Optional<Host> host = loadByForeignKey(Host.class, hn, clock.nowUtc());
if (host.isPresent()) {
notifyWithEmailAboutDnsUpdateFailure(
host.get().getPersistedCurrentSponsorRegistrarId(), hn, true);
} else {
logger.atSevere().log(String.format("Host entity for %s not found", hn));
}
});
}
// If we get here, we should terminate this task as it is likely a perpetually failing task.
// TODO(b/237302821): Send an email notifying partner the dns update failed
recordActionResult(ActionStatus.MAX_RETRIES_EXCEEDED);
response.setStatus(SC_ACCEPTED);
logger.atSevere().withCause(e).log("Terminated task after too many retries");
@@ -219,6 +276,53 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
return null;
}
private InternetAddress emailToInternetAddress(String email) {
try {
return new InternetAddress(email, true);
} catch (Exception e) {
logger.atWarning().withCause(e).log(
String.format(
"Could not parse email contact %s to send DNS failure notification", email));
return null;
}
}
/** Sends an email to partners regarding a failure during DNS update */
private void notifyWithEmailAboutDnsUpdateFailure(
String registrarId, String hostOrDomainName, Boolean isHost) {
Optional<Registrar> registrar = Registrar.loadByRegistrarIdCached(registrarId);
if (registrar.isPresent()) {
String body =
String.format(
dnsUpdateFailEmailBodyText,
registrar.get().getRegistrarName(),
hostOrDomainName,
isHost ? "host" : "domain",
registrySupportEmail.get().getAddress(),
dnsUpdateFailRegistryName);
ImmutableList<InternetAddress> recipients =
registrar.get().getContacts().stream()
.filter(c -> c.getTypes().contains(RegistrarPoc.Type.ADMIN))
.map(RegistrarPoc::getEmailAddress)
.map(this::emailToInternetAddress)
.collect(toImmutableList());
sendEmailService.sendEmail(
EmailMessage.newBuilder()
.setBody(body)
.setSubject(dnsUpdateFailEmailSubjectText)
.setRecipients(recipients)
.addBcc(registryCcEmail.get())
.setFrom(gSuiteOutgoingEmailAddress)
.build());
} else {
logger.atSevere().log(String.format("Could not find registrar %s", registrarId));
}
}
/** Splits the domains and hosts in a batch into smaller batches and adds them to the queue. */
private void splitBatch() {
ImmutableList<String> domainList = ImmutableList.copyOf(domains);
@@ -294,8 +398,7 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
int domainsPublished = 0;
int domainsRejected = 0;
for (String domain : nullToEmpty(domains)) {
if (!DomainNameUtils.isUnder(
InternetDomainName.from(domain), InternetDomainName.from(tld))) {
if (!DomainNameUtils.isUnder(InternetDomainName.from(domain), InternetDomainName.from(tld))) {
logger.atSevere().log("%s: skipping domain %s not under TLD.", tld, domain);
domainsRejected += 1;
} else {
@@ -310,8 +413,7 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
int hostsPublished = 0;
int hostsRejected = 0;
for (String host : nullToEmpty(hosts)) {
if (!DomainNameUtils.isUnder(
InternetDomainName.from(host), InternetDomainName.from(tld))) {
if (!DomainNameUtils.isUnder(InternetDomainName.from(host), InternetDomainName.from(tld))) {
logger.atSevere().log("%s: skipping host %s not under TLD.", tld, host);
hostsRejected += 1;
} else {
@@ -20,8 +20,8 @@ import google.registry.dns.DnsConstants.TargetType;
import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.HostResource;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.request.Action;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.HttpException.NotFoundException;
@@ -62,11 +62,11 @@ public final class RefreshDnsAction implements Runnable {
}
switch (type) {
case DOMAIN:
loadAndVerifyExistence(DomainBase.class, domainOrHostName);
loadAndVerifyExistence(Domain.class, domainOrHostName);
dnsQueue.addDomainRefreshTask(domainOrHostName);
break;
case HOST:
verifyHostIsSubordinate(loadAndVerifyExistence(HostResource.class, domainOrHostName));
verifyHostIsSubordinate(loadAndVerifyExistence(Host.class, domainOrHostName));
dnsQueue.addHostRefreshTask(domainOrHostName);
break;
default:
@@ -86,7 +86,7 @@ public final class RefreshDnsAction implements Runnable {
domainOrHostName)));
}
private static void verifyHostIsSubordinate(HostResource host) {
private static void verifyHostIsSubordinate(Host host) {
if (!host.isSubordinate()) {
throw new BadRequestException(
String.format("%s isn't a subordinate hostname", host.getHostName()));
@@ -36,9 +36,9 @@ import google.registry.config.RegistryConfig.Config;
import google.registry.dns.writer.BaseDnsWriter;
import google.registry.dns.writer.DnsWriter;
import google.registry.dns.writer.DnsWriterZone;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.host.HostResource;
import google.registry.model.host.Host;
import google.registry.model.tld.Registries;
import google.registry.util.Clock;
import google.registry.util.Concurrent;
@@ -121,13 +121,12 @@ public class CloudDnsWriter extends BaseDnsWriter {
String absoluteDomainName = getAbsoluteHostName(domainName);
// Load the target domain. Note that it can be absent if this domain was just deleted.
Optional<DomainBase> domainBase =
loadByForeignKey(DomainBase.class, domainName, clock.nowUtc());
Optional<Domain> domain = loadByForeignKey(Domain.class, domainName, clock.nowUtc());
// Return early if no DNS records should be published.
// desiredRecordsBuilder is populated with an empty set to indicate that all existing records
// should be deleted.
if (!domainBase.isPresent() || !domainBase.get().shouldPublishToDns()) {
if (!domain.isPresent() || !domain.get().shouldPublishToDns()) {
desiredRecords.put(absoluteDomainName, ImmutableSet.of());
return;
}
@@ -135,7 +134,7 @@ public class CloudDnsWriter extends BaseDnsWriter {
ImmutableSet.Builder<ResourceRecordSet> domainRecords = new ImmutableSet.Builder<>();
// Construct DS records (if any).
Set<DelegationSignerData> dsData = domainBase.get().getDsData();
Set<DelegationSignerData> dsData = domain.get().getDsData();
if (!dsData.isEmpty()) {
HashSet<String> dsRrData = new HashSet<>();
for (DelegationSignerData ds : dsData) {
@@ -154,8 +153,8 @@ public class CloudDnsWriter extends BaseDnsWriter {
}
// Construct NS records (if any).
Set<String> nameserverData = domainBase.get().loadNameserverHostNames();
Set<String> subordinateHosts = domainBase.get().getSubordinateHosts();
Set<String> nameserverData = domain.get().loadNameserverHostNames();
Set<String> subordinateHosts = domain.get().getSubordinateHosts();
if (!nameserverData.isEmpty()) {
HashSet<String> nsRrData = new HashSet<>();
for (String hostName : nameserverData) {
@@ -191,7 +190,7 @@ public class CloudDnsWriter extends BaseDnsWriter {
// Load the target host. Note that it can be absent if this host was just deleted.
// desiredRecords is populated with an empty set to indicate that all existing records
// should be deleted.
Optional<HostResource> host = loadByForeignKey(HostResource.class, hostName, clock.nowUtc());
Optional<Host> host = loadByForeignKey(Host.class, hostName, clock.nowUtc());
// Return early if the host is deleted.
if (!host.isPresent()) {
@@ -27,9 +27,9 @@ import com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig.Config;
import google.registry.dns.writer.BaseDnsWriter;
import google.registry.dns.writer.DnsWriterZone;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.host.HostResource;
import google.registry.model.host.Host;
import google.registry.model.tld.Registries;
import google.registry.util.Clock;
import java.io.IOException;
@@ -127,12 +127,11 @@ public class DnsUpdateWriter extends BaseDnsWriter {
* this domain refresh request
*/
private void publishDomain(String domainName, String requestingHostName) {
Optional<DomainBase> domainOptional =
loadByForeignKey(DomainBase.class, domainName, clock.nowUtc());
Optional<Domain> domainOptional = loadByForeignKey(Domain.class, domainName, clock.nowUtc());
update.delete(toAbsoluteName(domainName), Type.ANY);
// If the domain is now deleted, then don't update DNS for it.
if (domainOptional.isPresent()) {
DomainBase domain = domainOptional.get();
Domain domain = domainOptional.get();
// As long as the domain exists, orphan glues should be cleaned.
deleteSubordinateHostAddressSet(domain, requestingHostName, update);
if (domain.shouldPublishToDns()) {
@@ -184,7 +183,7 @@ public class DnsUpdateWriter extends BaseDnsWriter {
}
}
private RRset makeDelegationSignerSet(DomainBase domain) {
private RRset makeDelegationSignerSet(Domain domain) {
RRset signerSet = new RRset();
for (DelegationSignerData signerData : domain.getDsData()) {
DSRecord dsRecord =
@@ -202,7 +201,7 @@ public class DnsUpdateWriter extends BaseDnsWriter {
}
private void deleteSubordinateHostAddressSet(
DomainBase domain, String additionalHost, Update update) {
Domain domain, String additionalHost, Update update) {
for (String hostName :
union(
domain.getSubordinateHosts(),
@@ -213,17 +212,17 @@ public class DnsUpdateWriter extends BaseDnsWriter {
}
}
private void addInBailiwickNameServerSet(DomainBase domain, Update update) {
private void addInBailiwickNameServerSet(Domain domain, Update update) {
for (String hostName :
intersection(domain.loadNameserverHostNames(), domain.getSubordinateHosts())) {
Optional<HostResource> host = loadByForeignKey(HostResource.class, hostName, clock.nowUtc());
Optional<Host> host = loadByForeignKey(Host.class, hostName, clock.nowUtc());
checkState(host.isPresent(), "Host %s cannot be loaded", hostName);
update.add(makeAddressSet(host.get()));
update.add(makeV6AddressSet(host.get()));
}
}
private RRset makeNameServerSet(DomainBase domain) {
private RRset makeNameServerSet(Domain domain) {
RRset nameServerSet = new RRset();
for (String hostName : domain.loadNameserverHostNames()) {
NSRecord record =
@@ -237,7 +236,7 @@ public class DnsUpdateWriter extends BaseDnsWriter {
return nameServerSet;
}
private RRset makeAddressSet(HostResource host) {
private RRset makeAddressSet(Host host) {
RRset addressSet = new RRset();
for (InetAddress address : host.getInetAddresses()) {
if (address instanceof Inet4Address) {
@@ -253,7 +252,7 @@ public class DnsUpdateWriter extends BaseDnsWriter {
return addressSet;
}
private RRset makeV6AddressSet(HostResource host) {
private RRset makeV6AddressSet(Host host) {
RRset addressSet = new RRset();
for (InetAddress address : host.getInetAddresses()) {
if (address instanceof Inet6Address) {
@@ -6,53 +6,53 @@
<property name="searchName" direction="asc"/>
</datastore-index>
<!-- For finding domain resources by registrar. -->
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<datastore-index kind="Domain" ancestor="false" source="manual">
<property name="currentSponsorClientId" direction="asc"/>
<property name="deletionTime" direction="asc"/>
</datastore-index>
<!-- For finding domain resources by TLD. -->
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<datastore-index kind="Domain" ancestor="false" source="manual">
<property name="tld" direction="asc"/>
<property name="deletionTime" direction="asc"/>
</datastore-index>
<!-- For finding domain resources by registrar. -->
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<datastore-index kind="Domain" ancestor="false" source="manual">
<property name="currentSponsorClientId" direction="asc"/>
<property name="deletionTime" direction="asc"/>
</datastore-index>
<!-- For finding the most recently created domain resources. -->
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<datastore-index kind="Domain" ancestor="false" source="manual">
<property name="tld" direction="asc"/>
<property name="creationTime" direction="desc"/>
</datastore-index>
<!-- For finding host resources by registrar. -->
<datastore-index kind="HostResource" ancestor="false" source="manual">
<datastore-index kind="Host" ancestor="false" source="manual">
<property name="currentSponsorClientId" direction="asc"/>
<property name="deletionTime" direction="asc"/>
<property name="fullyQualifiedHostName" direction="asc"/>
</datastore-index>
<!-- For determining the active domains linked to a given contact. -->
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<datastore-index kind="Domain" ancestor="false" source="manual">
<property name="allContacts.contact" direction="asc"/>
<property name="deletionTime" direction="asc"/>
</datastore-index>
<!-- For determining the active domains linked to a given host. -->
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<datastore-index kind="Domain" ancestor="false" source="manual">
<property name="nsHosts" direction="asc"/>
<property name="deletionTime" direction="asc"/>
</datastore-index>
<!-- For deleting expired not-previously-deleted domains. -->
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<datastore-index kind="Domain" ancestor="false" source="manual">
<property name="deletionTime" direction="asc"/>
<property name="autorenewEndTime" direction="asc"/>
</datastore-index>
<!-- For RDAP searches by linked nameserver. -->
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<datastore-index kind="Domain" ancestor="false" source="manual">
<property name="nsHosts" direction="asc"/>
<property name="deletionTime" direction="asc"/>
</datastore-index>
<!-- For WHOIS IP address lookup -->
<datastore-index kind="HostResource" ancestor="false" source="manual">
<datastore-index kind="Host" ancestor="false" source="manual">
<property name="inetAddresses" direction="asc"/>
<property name="deletionTime" direction="asc"/>
</datastore-index>
@@ -74,20 +74,20 @@
<property name="modificationTime" direction="asc"/>
</datastore-index>
<!-- For RDAP. -->
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<datastore-index kind="Domain" ancestor="false" source="manual">
<property name="currentSponsorClientId" direction="asc"/>
<property name="fullyQualifiedDomainName" direction="asc"/>
</datastore-index>
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<datastore-index kind="Domain" ancestor="false" source="manual">
<property name="currentSponsorClientId" direction="asc"/>
<property name="tld" direction="asc"/>
<property name="fullyQualifiedDomainName" direction="asc"/>
</datastore-index>
<datastore-index kind="DomainBase" ancestor="false" source="manual">
<datastore-index kind="Domain" ancestor="false" source="manual">
<property name="tld" direction="asc"/>
<property name="fullyQualifiedDomainName" direction="asc"/>
</datastore-index>
<datastore-index kind="HostResource" ancestor="false" source="manual">
<datastore-index kind="Host" ancestor="false" source="manual">
<property name="deletionTime" direction="asc"/>
<property name="fullyQualifiedHostName" direction="asc"/>
</datastore-index>
@@ -44,7 +44,7 @@ import dagger.Module;
import dagger.Provides;
import google.registry.flows.domain.DomainFlowUtils.BadCommandForRegistryPhaseException;
import google.registry.flows.domain.DomainFlowUtils.InvalidIdnDomainLabelException;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.tld.Registry;
import google.registry.model.tld.label.ReservationType;
@@ -156,8 +156,7 @@ public class CheckApiAction implements Runnable {
}
private boolean checkExists(String domainString, DateTime now) {
return !ForeignKeyIndex.loadCached(DomainBase.class, ImmutableList.of(domainString), now)
.isEmpty();
return !ForeignKeyIndex.loadCached(Domain.class, ImmutableList.of(domainString), now).isEmpty();
}
private Optional<String> checkReserved(InternetDomainName domainName) {
@@ -39,8 +39,8 @@ import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.Domain;
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;
@@ -147,7 +147,7 @@ public final class ResourceFlowUtils {
}
/** Check that the given AuthInfo is either missing or else is valid for the given resource. */
public static void verifyOptionalAuthInfo(Optional<AuthInfo> authInfo, DomainBase domain)
public static void verifyOptionalAuthInfo(Optional<AuthInfo> authInfo, Domain domain)
throws EppException {
if (authInfo.isPresent()) {
verifyAuthInfo(authInfo.get(), domain);
@@ -155,7 +155,7 @@ public final class ResourceFlowUtils {
}
/** Check that the given {@link AuthInfo} is valid for the given domain. */
public static void verifyAuthInfo(AuthInfo authInfo, DomainBase domain) throws EppException {
public static void verifyAuthInfo(AuthInfo authInfo, Domain domain) throws EppException {
final String authRepoId = authInfo.getPw().getRepoId();
String authPassword = authInfo.getPw().getValue();
if (authRepoId == null) {
@@ -236,7 +236,7 @@ public final class ResourceFlowUtils {
* @param domain is the domain already projected at approvalTime
*/
public static DateTime computeExDateForApprovalTime(
DomainContent domain, DateTime approvalTime, Period period) {
DomainBase 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.
@@ -246,7 +246,7 @@ public final class ResourceFlowUtils {
if (period.getValue() == 0) {
inAutoRenew = false;
}
return DomainBase.extendRegistrationWithCap(
return Domain.extendRegistrationWithCap(
approvalTime,
domain.getRegistrationExpirationTime(),
period.getValue() - (inAutoRenew ? 1 : 0));
@@ -114,7 +114,7 @@ public class TlsCredentials implements TransportCredentials {
"Authentication error: IP address %s is not allow-listed for registrar %s; allow list is:"
+ " %s",
clientInetAddr, registrar.getRegistrarId(), ipAddressAllowList);
throw new BadRegistrarIpAddressException();
throw new BadRegistrarIpAddressException(clientInetAddr);
}
@VisibleForTesting
@@ -216,8 +216,13 @@ public class TlsCredentials implements TransportCredentials {
/** Registrar IP address is not in stored allow list. */
public static class BadRegistrarIpAddressException extends AuthenticationErrorException {
BadRegistrarIpAddressException() {
super("Registrar IP address is not in stored allow list");
BadRegistrarIpAddressException(Optional<InetAddress> clientInetAddr) {
super(
clientInetAddr.isPresent()
? String.format(
"Registrar IP address %s is not in stored allow list",
clientInetAddr.get().getHostAddress())
: "Registrar IP address is not in stored allow list");
}
}
@@ -22,7 +22,7 @@ import google.registry.flows.FlowMetadata;
import google.registry.flows.SessionMetadata;
import google.registry.flows.domain.DomainCreateFlow;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppResponse.ResponseData;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
@@ -125,10 +125,9 @@ public class DomainCreateFlowCustomLogic extends BaseFlowCustomLogic {
public abstract static class BeforeSaveParameters extends ImmutableObject {
/**
* The new {@link DomainBase} entity that is going to be persisted at the end of the
* transaction.
* The new {@link Domain} entity that is going to be persisted at the end of the transaction.
*/
public abstract DomainBase newDomain();
public abstract Domain newDomain();
/**
* The new {@link HistoryEntry} entity for the domain's creation that is going to be persisted
@@ -162,7 +161,7 @@ public class DomainCreateFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setNewDomain(DomainBase newDomain);
public abstract Builder setNewDomain(Domain newDomain);
public abstract Builder setHistoryEntry(HistoryEntry historyEntry);
@@ -21,7 +21,7 @@ import google.registry.flows.FlowMetadata;
import google.registry.flows.SessionMetadata;
import google.registry.flows.domain.DomainDeleteFlow;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.model.eppoutput.Result;
@@ -83,7 +83,7 @@ public class DomainDeleteFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue
public abstract static class AfterValidationParameters extends ImmutableObject {
public abstract DomainBase existingDomain();
public abstract Domain existingDomain();
public static Builder newBuilder() {
return new AutoValue_DomainDeleteFlowCustomLogic_AfterValidationParameters.Builder();
@@ -93,7 +93,7 @@ public class DomainDeleteFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setExistingDomain(DomainBase existingDomain);
public abstract Builder setExistingDomain(Domain existingDomain);
public abstract AfterValidationParameters build();
}
@@ -109,9 +109,9 @@ public class DomainDeleteFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue
public abstract static class BeforeSaveParameters extends ImmutableObject {
public abstract DomainBase existingDomain();
public abstract Domain existingDomain();
public abstract DomainBase newDomain();
public abstract Domain newDomain();
public abstract HistoryEntry historyEntry();
@@ -125,9 +125,9 @@ public class DomainDeleteFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setExistingDomain(DomainBase existingDomain);
public abstract Builder setExistingDomain(Domain existingDomain);
public abstract Builder setNewDomain(DomainBase newDomain);
public abstract Builder setNewDomain(Domain newDomain);
public abstract Builder setHistoryEntry(HistoryEntry historyEntry);
@@ -21,7 +21,7 @@ import google.registry.flows.FlowMetadata;
import google.registry.flows.SessionMetadata;
import google.registry.flows.domain.DomainInfoFlow;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainInfoData;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
@@ -53,8 +53,8 @@ public class DomainInfoFlowCustomLogic extends BaseFlowCustomLogic {
/**
* A hook that runs before the response is returned.
*
* <p>This takes the {@link DomainBase} and {@link ResponseExtension}s as input and returns
* them, potentially with modifications.
* <p>This takes the {@link Domain} and {@link ResponseExtension}s as input and returns them,
* potentially with modifications.
*/
@SuppressWarnings("unused")
public BeforeResponseReturnData beforeResponse(BeforeResponseParameters parameters)
@@ -69,7 +69,7 @@ public class DomainInfoFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue
public abstract static class AfterValidationParameters extends ImmutableObject {
public abstract DomainBase domain();
public abstract Domain domain();
public static Builder newBuilder() {
return new AutoValue_DomainInfoFlowCustomLogic_AfterValidationParameters.Builder();
@@ -79,7 +79,7 @@ public class DomainInfoFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setDomain(DomainBase domain);
public abstract Builder setDomain(Domain domain);
public abstract AfterValidationParameters build();
}
@@ -89,7 +89,7 @@ public class DomainInfoFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue
public abstract static class BeforeResponseParameters extends ImmutableObject {
public abstract DomainBase domain();
public abstract Domain domain();
public abstract DomainInfoData resData();
@@ -103,7 +103,7 @@ public class DomainInfoFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setDomain(DomainBase domain);
public abstract Builder setDomain(Domain domain);
public abstract Builder setResData(DomainInfoData resData);
@@ -21,7 +21,7 @@ import google.registry.flows.FlowMetadata;
import google.registry.flows.SessionMetadata;
import google.registry.flows.domain.DomainRenewFlow;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppResponse.ResponseData;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
@@ -68,8 +68,8 @@ public class DomainRenewFlowCustomLogic extends BaseFlowCustomLogic {
/**
* A hook that runs before the response is returned.
*
* <p>This takes the {@link DomainBase} and {@link ResponseExtension}s as input and returns
* them, potentially with modifications.
* <p>This takes the {@link Domain} and {@link ResponseExtension}s as input and returns them,
* potentially with modifications.
*/
@SuppressWarnings("unused")
public BeforeResponseReturnData beforeResponse(BeforeResponseParameters parameters)
@@ -84,7 +84,7 @@ public class DomainRenewFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue
public abstract static class AfterValidationParameters extends ImmutableObject {
public abstract DomainBase existingDomain();
public abstract Domain existingDomain();
public abstract int years();
@@ -98,7 +98,7 @@ public class DomainRenewFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setExistingDomain(DomainBase existingDomain);
public abstract Builder setExistingDomain(Domain existingDomain);
public abstract Builder setYears(int years);
@@ -118,9 +118,9 @@ public class DomainRenewFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue
public abstract static class BeforeSaveParameters extends ImmutableObject {
public abstract DomainBase existingDomain();
public abstract Domain existingDomain();
public abstract DomainBase newDomain();
public abstract Domain newDomain();
public abstract HistoryEntry historyEntry();
@@ -138,9 +138,9 @@ public class DomainRenewFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setExistingDomain(DomainBase existingDomain);
public abstract Builder setExistingDomain(Domain existingDomain);
public abstract Builder setNewDomain(DomainBase newDomain);
public abstract Builder setNewDomain(Domain newDomain);
public abstract Builder setHistoryEntry(HistoryEntry historyEntry);
@@ -158,7 +158,7 @@ public class DomainRenewFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue
public abstract static class BeforeResponseParameters extends ImmutableObject {
public abstract DomainBase domain();
public abstract Domain domain();
public abstract ResponseData resData();
@@ -172,7 +172,7 @@ public class DomainRenewFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue.Builder
public abstract static class Builder {
public abstract BeforeResponseParameters.Builder setDomain(DomainBase domain);
public abstract BeforeResponseParameters.Builder setDomain(Domain domain);
public abstract BeforeResponseParameters.Builder setResData(ResponseData resData);
@@ -20,7 +20,7 @@ import google.registry.flows.FlowMetadata;
import google.registry.flows.SessionMetadata;
import google.registry.flows.domain.DomainUpdateFlow;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.eppinput.EppInput;
import google.registry.model.reporting.HistoryEntry;
@@ -65,7 +65,7 @@ public class DomainUpdateFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue
public abstract static class AfterValidationParameters extends ImmutableObject {
public abstract DomainBase existingDomain();
public abstract Domain existingDomain();
public static Builder newBuilder() {
return new AutoValue_DomainUpdateFlowCustomLogic_AfterValidationParameters.Builder();
@@ -75,7 +75,7 @@ public class DomainUpdateFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setExistingDomain(DomainBase existingDomain);
public abstract Builder setExistingDomain(Domain existingDomain);
public abstract AfterValidationParameters build();
}
@@ -91,9 +91,9 @@ public class DomainUpdateFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue
public abstract static class BeforeSaveParameters extends ImmutableObject {
public abstract DomainBase existingDomain();
public abstract Domain existingDomain();
public abstract DomainBase newDomain();
public abstract Domain newDomain();
public abstract HistoryEntry historyEntry();
@@ -107,9 +107,9 @@ public class DomainUpdateFlowCustomLogic extends BaseFlowCustomLogic {
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setExistingDomain(DomainBase existingDomain);
public abstract Builder setExistingDomain(Domain existingDomain);
public abstract Builder setNewDomain(DomainBase newDomain);
public abstract Builder setNewDomain(Domain newDomain);
public abstract Builder setHistoryEntry(HistoryEntry historyEntry);
@@ -54,7 +54,7 @@ import google.registry.flows.domain.token.AllocationTokenDomainCheckResults;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.model.EppResource;
import google.registry.model.billing.BillingEvent;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Check;
import google.registry.model.domain.fee.FeeCheckCommandExtension;
import google.registry.model.domain.fee.FeeCheckCommandExtensionItem;
@@ -169,8 +169,8 @@ public final class DomainCheckFlow implements Flow {
// TODO: Use as of date from fee extension v0.12 instead of now, if specified.
.setAsOfDate(now)
.build());
ImmutableMap<String, ForeignKeyIndex<DomainBase>> existingDomains =
ForeignKeyIndex.load(DomainBase.class, domainNames, now);
ImmutableMap<String, ForeignKeyIndex<Domain>> existingDomains =
ForeignKeyIndex.load(Domain.class, domainNames, now);
Optional<AllocationTokenExtension> allocationTokenExtension =
eppInput.getSingleExtension(AllocationTokenExtension.class);
Optional<AllocationTokenDomainCheckResults> tokenDomainCheckResults =
@@ -227,7 +227,7 @@ public final class DomainCheckFlow implements Flow {
private Optional<String> getMessageForCheck(
InternetDomainName domainName,
ImmutableMap<String, ForeignKeyIndex<DomainBase>> existingDomains,
ImmutableMap<String, ForeignKeyIndex<Domain>> existingDomains,
ImmutableMap<InternetDomainName, String> tokenCheckResults,
ImmutableMap<String, TldState> tldStates,
Optional<AllocationToken> allocationToken) {
@@ -251,7 +251,7 @@ public final class DomainCheckFlow implements Flow {
/** Handle the fee check extension. */
private ImmutableList<? extends ResponseExtension> getResponseExtensions(
ImmutableMap<String, InternetDomainName> domainNames,
ImmutableMap<String, ForeignKeyIndex<DomainBase>> existingDomains,
ImmutableMap<String, ForeignKeyIndex<Domain>> existingDomains,
ImmutableSet<String> availableDomains,
DateTime now,
Optional<AllocationToken> allocationToken)
@@ -264,7 +264,7 @@ public final class DomainCheckFlow implements Flow {
FeeCheckCommandExtension<?, ?> feeCheck = feeCheckOpt.get();
ImmutableList.Builder<FeeCheckResponseExtensionItem> responseItems =
new ImmutableList.Builder<>();
ImmutableMap<String, DomainBase> domainObjs =
ImmutableMap<String, Domain> domainObjs =
loadDomainsForRestoreChecks(feeCheck, domainNames, existingDomains);
ImmutableMap<String, BillingEvent.Recurring> recurrences =
loadRecurrencesForDomains(domainObjs);
@@ -272,12 +272,12 @@ public final class DomainCheckFlow implements Flow {
for (FeeCheckCommandExtensionItem feeCheckItem : feeCheck.getItems()) {
for (String domainName : getDomainNamesToCheckForFee(feeCheckItem, domainNames.keySet())) {
FeeCheckResponseExtensionItem.Builder<?> builder = feeCheckItem.createResponseBuilder();
Optional<DomainBase> domainBase = Optional.ofNullable(domainObjs.get(domainName));
Optional<Domain> domain = Optional.ofNullable(domainObjs.get(domainName));
handleFeeRequest(
feeCheckItem,
builder,
domainNames.get(domainName),
domainBase,
domain,
feeCheck.getCurrency(),
now,
pricingLogic,
@@ -301,10 +301,10 @@ public final class DomainCheckFlow implements Flow {
* nicer in Cloud SQL when we can SELECT just the fields we want rather than having to load the
* entire entity.
*/
private ImmutableMap<String, DomainBase> loadDomainsForRestoreChecks(
private ImmutableMap<String, Domain> loadDomainsForRestoreChecks(
FeeCheckCommandExtension<?, ?> feeCheck,
ImmutableMap<String, InternetDomainName> domainNames,
ImmutableMap<String, ForeignKeyIndex<DomainBase>> existingDomains) {
ImmutableMap<String, ForeignKeyIndex<Domain>> existingDomains) {
ImmutableList<String> restoreCheckDomains;
if (feeCheck instanceof FeeCheckCommandExtensionV06) {
// The V06 fee extension supports specifying the command fees to check on a per-domain basis.
@@ -326,25 +326,25 @@ public final class DomainCheckFlow implements Flow {
}
// Filter down to just domains we know exist and then use the EppResource cache to load them.
ImmutableMap<String, VKey<DomainBase>> existingDomainsToLoad =
ImmutableMap<String, VKey<Domain>> existingDomainsToLoad =
restoreCheckDomains.stream()
.filter(existingDomains::containsKey)
.collect(toImmutableMap(d -> d, d -> existingDomains.get(d).getResourceKey()));
ImmutableMap<VKey<? extends EppResource>, EppResource> loadedDomains =
EppResource.loadCached(ImmutableList.copyOf(existingDomainsToLoad.values()));
return ImmutableMap.copyOf(
Maps.transformEntries(existingDomainsToLoad, (k, v) -> (DomainBase) loadedDomains.get(v)));
Maps.transformEntries(existingDomainsToLoad, (k, v) -> (Domain) loadedDomains.get(v)));
}
private ImmutableMap<String, BillingEvent.Recurring> loadRecurrencesForDomains(
ImmutableMap<String, DomainBase> domainObjs) {
ImmutableMap<String, Domain> domainObjs) {
return tm().transact(
() -> {
ImmutableMap<VKey<? extends BillingEvent.Recurring>, BillingEvent.Recurring>
recurrences =
tm().loadByKeys(
domainObjs.values().stream()
.map(DomainBase::getAutorenewBillingEvent)
.map(Domain::getAutorenewBillingEvent)
.collect(toImmutableSet()));
return ImmutableMap.copyOf(
Maps.transformValues(
@@ -83,7 +83,7 @@ import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand;
import google.registry.model.domain.DomainCommand.Create;
import google.registry.model.domain.DomainHistory;
@@ -153,6 +153,7 @@ import org.joda.time.Duration;
* @error {@link DomainCreateFlow.AnchorTenantCreatePeriodException}
* @error {@link DomainCreateFlow.MustHaveSignedMarksInCurrentPhaseException}
* @error {@link DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException}
* @error {@link DomainCreateFlow.NoTrademarkedRegistrationsBeforeSunriseException}
* @error {@link DomainCreateFlow.SignedMarksOnlyDuringSunriseException}
* @error {@link DomainFlowTmchUtils.NoMarksFoundMatchingDomainException}
* @error {@link DomainFlowTmchUtils.FoundMarkNotYetValidException}
@@ -247,7 +248,7 @@ public final class DomainCreateFlow implements TransactionalFlow {
verifyUnitIsYears(period);
int years = period.getValue();
validateRegistrationPeriod(years);
verifyResourceDoesNotExist(DomainBase.class, targetId, now, registrarId);
verifyResourceDoesNotExist(Domain.class, targetId, now, registrarId);
// Validate that this is actually a legal domain name on a TLD that the registrar has access to.
InternetDomainName domainName = validateDomainName(command.getFullyQualifiedDomainName());
String domainLabel = domainName.parts().get(0);
@@ -333,9 +334,9 @@ public final class DomainCreateFlow implements TransactionalFlow {
validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class));
DateTime registrationExpirationTime = leapSafeAddYears(now, years);
String repoId = createDomainRepoId(allocateId(), registry.getTldStr());
Key<DomainHistory> domainHistoryKey =
Key.create(Key.create(DomainBase.class, repoId), DomainHistory.class, allocateId());
historyBuilder.setId(domainHistoryKey.getId());
long historyRevisionId = allocateId();
DomainHistoryId domainHistoryId = new DomainHistoryId(repoId, historyRevisionId);
historyBuilder.setId(historyRevisionId);
// Bill for the create.
BillingEvent.OneTime createBillingEvent =
createOneTimeBillingEvent(
@@ -345,17 +346,17 @@ public final class DomainCreateFlow implements TransactionalFlow {
isReserved(domainName, isSunriseCreate),
years,
feesAndCredits,
domainHistoryKey,
domainHistoryId,
allocationToken,
now);
// Create a new autorenew billing event and poll message starting at the expiration time.
BillingEvent.Recurring autorenewBillingEvent =
createAutorenewBillingEvent(
domainHistoryKey,
domainHistoryId,
registrationExpirationTime,
getRenewalPriceInfo(isAnchorTenant, allocationToken, feesAndCredits));
PollMessage.Autorenew autorenewPollMessage =
createAutorenewPollMessage(domainHistoryKey, registrationExpirationTime);
createAutorenewPollMessage(domainHistoryId, registrationExpirationTime);
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
entitiesToSave.add(createBillingEvent, autorenewBillingEvent, autorenewPollMessage);
// Bill for EAP cost, if any.
@@ -368,16 +369,15 @@ public final class DomainCreateFlow implements TransactionalFlow {
reservationTypes.contains(NAME_COLLISION)
? ImmutableSet.of(SERVER_HOLD)
: ImmutableSet.of();
DomainBase domain =
new DomainBase.Builder()
Domain domain =
new Domain.Builder()
.setCreationRegistrarId(registrarId)
.setPersistedCurrentSponsorRegistrarId(registrarId)
.setRepoId(repoId)
.setIdnTableName(validateDomainNameWithIdnTables(domainName))
.setRegistrationExpirationTime(registrationExpirationTime)
.setAutorenewBillingEvent(autorenewBillingEvent.createVKey())
.setAutorenewPollMessage(
autorenewPollMessage.createVKey(), autorenewPollMessage.getHistoryRevisionId())
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
.setLaunchNotice(hasClaimsNotice ? launchCreate.get().getNotice() : null)
.setSmdId(signedMarkId)
.setDsData(secDnsCreate.map(SecDnsCreateExtension::getDsData).orElse(null))
@@ -390,6 +390,11 @@ public final class DomainCreateFlow implements TransactionalFlow {
.addGracePeriod(
GracePeriod.forBillingEvent(GracePeriodStatus.ADD, repoId, createBillingEvent))
.build();
if (allocationToken.isPresent()
&& allocationToken.get().getTokenType().equals(TokenType.PACKAGE)) {
domain =
domain.asBuilder().setCurrentPackageToken(allocationToken.get().createVKey()).build();
}
DomainHistory domainHistory =
buildDomainHistory(domain, registry, now, period, registry.getAddGracePeriodLength());
if (reservationTypes.contains(NAME_COLLISION)) {
@@ -481,7 +486,8 @@ public final class DomainCreateFlow implements TransactionalFlow {
boolean isValidReservedCreate,
boolean hasSignedMarks)
throws NoGeneralRegistrationsInCurrentPhaseException,
MustHaveSignedMarksInCurrentPhaseException {
MustHaveSignedMarksInCurrentPhaseException,
NoTrademarkedRegistrationsBeforeSunriseException {
// We allow general registration during GA.
TldState currentState = registry.getTldState(now);
if (currentState.equals(GENERAL_AVAILABILITY)) {
@@ -496,16 +502,23 @@ public final class DomainCreateFlow implements TransactionalFlow {
// Bypass most TLD state checks if that behavior is specified by the token
if (behavior.equals(RegistrationBehavior.BYPASS_TLD_STATE)
|| behavior.equals(RegistrationBehavior.ANCHOR_TENANT)) {
// If bypassing TLD state checks, a post-sunrise state is always fine
if (!currentState.equals(START_DATE_SUNRISE)
&& registry.getTldStateTransitions().headMap(now).containsValue(START_DATE_SUNRISE)) {
return;
}
// Non-trademarked names with the state check bypassed are always available
if (!claimsList.getClaimKey(domainLabel).isPresent()) {
return;
}
if (!currentState.equals(START_DATE_SUNRISE)) {
// Trademarked domains cannot be registered until after the sunrise period has ended, unless
// a valid signed mark is provided. Signed marks can only be provided during sunrise.
// Thus, when bypassing TLD state checks, a post-sunrise state is always fine.
if (registry.getTldStateTransitions().headMap(now).containsValue(START_DATE_SUNRISE)) {
return;
} else {
// If sunrise hasn't happened yet, trademarked domains are unavailable
throw new NoTrademarkedRegistrationsBeforeSunriseException(domainLabel);
}
}
}
// Otherwise, signed marks are necessary and sufficient in the sunrise period
if (currentState.equals(START_DATE_SUNRISE)) {
if (!hasSignedMarks) {
@@ -530,7 +543,7 @@ public final class DomainCreateFlow implements TransactionalFlow {
}
private DomainHistory buildDomainHistory(
DomainBase domain, Registry registry, DateTime now, Period period, Duration addGracePeriod) {
Domain domain, Registry registry, DateTime now, Period period, Duration addGracePeriod) {
// We ignore prober transactions
if (registry.getTldType() == TldType.REAL) {
historyBuilder
@@ -552,7 +565,7 @@ public final class DomainCreateFlow implements TransactionalFlow {
boolean isReserved,
int years,
FeesAndCredits feesAndCredits,
Key<DomainHistory> domainHistoryKey,
DomainHistoryId domainHistoryId,
Optional<AllocationToken> allocationToken,
DateTime now) {
ImmutableSet.Builder<Flag> flagsBuilder = new ImmutableSet.Builder<>();
@@ -581,12 +594,12 @@ public final class DomainCreateFlow implements TransactionalFlow {
? registry.getAnchorTenantAddGracePeriodLength()
: registry.getAddGracePeriodLength()))
.setFlags(flagsBuilder.build())
.setParent(domainHistoryKey)
.setDomainHistoryId(domainHistoryId)
.build();
}
private Recurring createAutorenewBillingEvent(
Key<DomainHistory> domainHistoryKey,
DomainHistoryId domainHistoryId,
DateTime registrationExpirationTime,
RenewalPriceInfo renewalpriceInfo) {
return new BillingEvent.Recurring.Builder()
@@ -596,21 +609,20 @@ public final class DomainCreateFlow implements TransactionalFlow {
.setRegistrarId(registrarId)
.setEventTime(registrationExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(domainHistoryKey)
.setDomainHistoryId(domainHistoryId)
.setRenewalPriceBehavior(renewalpriceInfo.renewalPriceBehavior())
.setRenewalPrice(renewalpriceInfo.renewalPrice())
.build();
}
private Autorenew createAutorenewPollMessage(
Key<DomainHistory> domainHistoryKey, DateTime registrationExpirationTime) {
DomainHistoryId domainHistoryId, DateTime registrationExpirationTime) {
return new PollMessage.Autorenew.Builder()
.setTargetId(targetId)
.setRegistrarId(registrarId)
.setEventTime(registrationExpirationTime)
.setMsg("Domain was auto-renewed.")
.setDomainHistoryId(
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
.setDomainHistoryId(domainHistoryId)
.build();
}
@@ -625,7 +637,7 @@ public final class DomainCreateFlow implements TransactionalFlow {
.setEventTime(createBillingEvent.getEventTime())
.setBillingTime(createBillingEvent.getBillingTime())
.setFlags(createBillingEvent.getFlags())
.setParent(createBillingEvent.getParentKey())
.setDomainHistoryId(createBillingEvent.getDomainHistoryId())
.build();
}
@@ -646,13 +658,12 @@ public final class DomainCreateFlow implements TransactionalFlow {
.build();
}
private void enqueueTasks(
DomainBase newDomain, boolean hasSignedMarks, boolean hasClaimsNotice) {
private void enqueueTasks(Domain newDomain, boolean hasSignedMarks, boolean hasClaimsNotice) {
if (newDomain.shouldPublishToDns()) {
dnsQueue.addDomainRefreshTask(newDomain.getDomainName());
}
if (hasClaimsNotice || hasSignedMarks) {
LordnTaskUtils.enqueueDomainBaseTask(newDomain);
LordnTaskUtils.enqueueDomainTask(newDomain);
}
}
@@ -726,6 +737,17 @@ public final class DomainCreateFlow implements TransactionalFlow {
}
}
/** Trademarked domains cannot be registered before the sunrise period. */
static class NoTrademarkedRegistrationsBeforeSunriseException
extends ParameterValuePolicyErrorException {
public NoTrademarkedRegistrationsBeforeSunriseException(String domainLabel) {
super(
String.format(
"The trademarked label %s cannot be registered before the sunrise period.",
domainLabel));
}
}
/** Anchor tenant domain create is for the wrong number of years. */
static class AnchorTenantCreatePeriodException extends ParameterValuePolicyErrorException {
public AnchorTenantCreatePeriodException(int invalidYears) {
@@ -64,7 +64,8 @@ import google.registry.flows.custom.DomainDeleteFlowCustomLogic.BeforeSaveParame
import google.registry.flows.custom.EntityChanges;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
import google.registry.model.domain.DomainBase;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.GracePeriod;
@@ -146,13 +147,13 @@ public final class DomainDeleteFlow implements TransactionalFlow {
extensionManager.validate();
DateTime now = tm().getTransactionTime();
// Loads the target resource if it exists
DomainBase existingDomain = loadAndVerifyExistence(DomainBase.class, targetId, now);
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
Registry registry = Registry.get(existingDomain.getTld());
verifyDeleteAllowed(existingDomain, registry, now);
flowCustomLogic.afterValidation(
AfterValidationParameters.newBuilder().setExistingDomain(existingDomain).build());
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
DomainBase.Builder builder;
Domain.Builder builder;
if (existingDomain.getStatusValues().contains(StatusValue.PENDING_TRANSFER)) {
builder =
denyPendingTransfer(existingDomain, TransferStatus.SERVER_CANCELLED, now, registrarId)
@@ -233,7 +234,12 @@ public final class DomainDeleteFlow implements TransactionalFlow {
// No cancellation is written if the grace period was not for a billable event.
if (gracePeriod.hasBillingEvent()) {
entitiesToSave.add(
BillingEvent.Cancellation.forGracePeriod(gracePeriod, now, domainHistoryKey, targetId));
BillingEvent.Cancellation.forGracePeriod(
gracePeriod,
now,
new DomainHistoryId(
domainHistoryKey.getParent().getName(), domainHistoryKey.getId()),
targetId));
if (gracePeriod.getOneTimeBillingEvent() != null) {
// Take the amount of amount of registration time being refunded off the expiration time.
// This can be either add grace periods or renew grace periods.
@@ -248,15 +254,17 @@ public final class DomainDeleteFlow implements TransactionalFlow {
}
builder.setRegistrationExpirationTime(newExpirationTime);
DomainBase newDomain = builder.build();
Domain newDomain = builder.build();
DomainHistory domainHistory =
buildDomainHistory(newDomain, registry, now, durationUntilDelete, inAddGracePeriod);
updateForeignKeyIndexDeletionTime(newDomain);
handlePendingTransferOnDelete(existingDomain, newDomain, now, domainHistory);
// Close the autorenew billing event and poll message. This may delete the poll message. Store
// the updated recurring billing event, we'll need it later and can't reload it.
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
BillingEvent.Recurring recurringBillingEvent =
updateAutorenewRecurrenceEndTime(existingDomain, now);
updateAutorenewRecurrenceEndTime(
existingDomain, existingRecurring, now, domainHistory.getDomainHistoryId());
// If there's a pending transfer, the gaining client's autorenew billing
// event and poll message will already have been deleted in
// ResourceDeleteFlow since it's listed in serverApproveEntities.
@@ -289,7 +297,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
.build();
}
private void verifyDeleteAllowed(DomainBase existingDomain, Registry registry, DateTime now)
private void verifyDeleteAllowed(Domain existingDomain, Registry registry, DateTime now)
throws EppException {
verifyNoDisallowedStatuses(existingDomain, DISALLOWED_STATUSES);
verifyOptionalAuthInfo(authInfo, existingDomain);
@@ -304,7 +312,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
}
private DomainHistory buildDomainHistory(
DomainBase domain,
Domain domain,
Registry registry,
DateTime now,
Duration durationUntilDelete,
@@ -337,7 +345,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
}
private PollMessage.OneTime createDeletePollMessage(
DomainBase existingDomain, Key<DomainHistory> domainHistoryKey, DateTime deletionTime) {
Domain existingDomain, Key<DomainHistory> domainHistoryKey, DateTime deletionTime) {
Optional<MetadataExtension> metadataExtension =
eppInput.getSingleExtension(MetadataExtension.class);
boolean hasMetadataMessage =
@@ -362,7 +370,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
}
private PollMessage.OneTime createImmediateDeletePollMessage(
DomainBase existingDomain,
Domain existingDomain,
Key<DomainHistory> domainHistoryKey,
DateTime now,
DateTime deletionTime) {
@@ -384,7 +392,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
@Nullable
private ImmutableList<FeeTransformResponseExtension> getResponseExtensions(
BillingEvent.Recurring recurringBillingEvent, DomainBase existingDomain, DateTime now) {
BillingEvent.Recurring recurringBillingEvent, Domain existingDomain, DateTime now) {
FeeTransformResponseExtension.Builder feeResponseBuilder = getDeleteResponseBuilder();
if (feeResponseBuilder == null) {
return ImmutableList.of();
@@ -25,7 +25,7 @@ import static com.google.common.collect.Iterables.any;
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.domain.DomainBase.MAX_REGISTRATION_YEARS;
import static google.registry.model.domain.Domain.MAX_REGISTRATION_YEARS;
import static google.registry.model.tld.Registries.findTldForName;
import static google.registry.model.tld.Registries.getTlds;
import static google.registry.model.tld.Registry.TldState.GENERAL_AVAILABILITY;
@@ -82,7 +82,7 @@ import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Create;
import google.registry.model.domain.DomainCommand.CreateOrUpdate;
import google.registry.model.domain.DomainCommand.InvalidReferencesException;
@@ -112,9 +112,10 @@ import google.registry.model.domain.secdns.SecDnsUpdateExtension;
import google.registry.model.domain.secdns.SecDnsUpdateExtension.Add;
import google.registry.model.domain.secdns.SecDnsUpdateExtension.Remove;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.RegistrationBehavior;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.model.host.HostResource;
import google.registry.model.host.Host;
import google.registry.model.poll.PollMessage;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.Registrar.State;
@@ -270,7 +271,13 @@ public class DomainFlowUtils {
&& token.get().getDomainName().get().equals(domainName.toString())) {
return true;
}
// Otherwise check whether the metadata extension is being used by a superuser to specify that
// Otherwise, check to see if we're using the specialized anchor tenant registration behavior on
// the allocation token
if (token.isPresent()
&& token.get().getRegistrationBehavior().equals(RegistrationBehavior.ANCHOR_TENANT)) {
return true;
}
// Otherwise, check whether the metadata extension is being used by a superuser to specify that
// it's an anchor tenant creation.
return metadataExtension.isPresent() && metadataExtension.get().getIsAnchorTenant();
}
@@ -375,7 +382,7 @@ public class DomainFlowUtils {
static void verifyNotInPendingDelete(
Set<DesignatedContact> contacts,
VKey<ContactResource> registrant,
Set<VKey<HostResource>> nameservers)
Set<VKey<Host>> nameservers)
throws EppException {
ImmutableList.Builder<VKey<? extends EppResource>> keysToLoad = new ImmutableList.Builder<>();
contacts.stream().map(DesignatedContact::getContactKey).forEach(keysToLoad::add);
@@ -553,7 +560,7 @@ public class DomainFlowUtils {
* Fills in a builder with the data needed for an autorenew billing event for this domain. This
* does not copy over the id of the current autorenew billing event.
*/
public static BillingEvent.Recurring.Builder newAutorenewBillingEvent(DomainBase domain) {
public static BillingEvent.Recurring.Builder newAutorenewBillingEvent(Domain domain) {
return new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
@@ -566,7 +573,7 @@ public class DomainFlowUtils {
* Fills in a builder with the data needed for an autorenew poll message for this domain. This
* does not copy over the id of the current autorenew poll message.
*/
public static PollMessage.Autorenew.Builder newAutorenewPollMessage(DomainBase domain) {
public static PollMessage.Autorenew.Builder newAutorenewPollMessage(Domain domain) {
return new PollMessage.Autorenew.Builder()
.setTargetId(domain.getDomainName())
.setRegistrarId(domain.getCurrentSponsorRegistrarId())
@@ -583,7 +590,11 @@ public class DomainFlowUtils {
*
* <p>Returns the new autorenew recurring billing event.
*/
public static Recurring updateAutorenewRecurrenceEndTime(DomainBase domain, DateTime newEndTime) {
public static Recurring updateAutorenewRecurrenceEndTime(
Domain domain,
Recurring existingRecurring,
DateTime newEndTime,
@Nullable DomainHistoryId historyId) {
Optional<PollMessage.Autorenew> autorenewPollMessage =
tm().loadByKeyIfPresent(domain.getAutorenewPollMessage());
@@ -591,33 +602,34 @@ public class DomainFlowUtils {
// create a new one at the same id. This can happen if a transfer was requested on a domain
// where all autorenew poll messages had already been delivered (this would cause the poll
// message to be deleted), and then subsequently the transfer was canceled, rejected, or deleted
// (which would cause the poll message to be recreated here).
PollMessage.Autorenew updatedAutorenewPollMessage =
autorenewPollMessage.isPresent()
? autorenewPollMessage.get().asBuilder().setAutorenewEndTime(newEndTime).build()
: newAutorenewPollMessage(domain)
.setId((Long) domain.getAutorenewPollMessage().getSqlKey())
.setAutorenewEndTime(newEndTime)
.setDomainHistoryId(
new DomainHistoryId(
domain.getRepoId(), domain.getAutorenewPollMessageHistoryId()))
.build();
// (which would cause the poll message to be recreated here). In the latter case, the history id
// of the event that created the new poll message will also be used.
PollMessage.Autorenew updatedAutorenewPollMessage;
if (autorenewPollMessage.isPresent()) {
updatedAutorenewPollMessage =
autorenewPollMessage.get().asBuilder().setAutorenewEndTime(newEndTime).build();
} else {
checkNotNull(
historyId, "Cannot create a new autorenew poll message without a domain history id");
updatedAutorenewPollMessage =
newAutorenewPollMessage(domain)
.setId((Long) domain.getAutorenewPollMessage().getSqlKey())
.setAutorenewEndTime(newEndTime)
.setDomainHistoryId(historyId)
.build();
}
// If the resultant autorenew poll message would have no poll messages to deliver, then just
// delete it. Otherwise save it with the new end time.
// delete it. Otherwise, save it with the new end time.
if (isAtOrAfter(updatedAutorenewPollMessage.getEventTime(), newEndTime)) {
autorenewPollMessage.ifPresent(autorenew -> tm().delete(autorenew));
} else {
tm().put(updatedAutorenewPollMessage);
}
Recurring recurring =
tm().loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRecurrenceEndTime(newEndTime)
.build();
tm().put(recurring);
return recurring;
Recurring newRecurring = existingRecurring.asBuilder().setRecurrenceEndTime(newEndTime).build();
tm().put(newRecurring);
return newRecurring;
}
/**
@@ -628,7 +640,7 @@ public class DomainFlowUtils {
FeeQueryCommandExtensionItem feeRequest,
FeeQueryResponseExtensionItem.Builder<?, ?> builder,
InternetDomainName domainName,
Optional<DomainBase> domain,
Optional<Domain> domain,
@Nullable CurrencyUnit topLevelCurrency,
DateTime currentDate,
DomainPricingLogic pricingLogic,
@@ -708,7 +720,10 @@ public class DomainFlowUtils {
throw new TransfersAreAlwaysForOneYearException();
}
builder.setAvailIfSupported(true);
fees = pricingLogic.getTransferPrice(registry, domainNameString, now).getFees();
fees =
pricingLogic
.getTransferPrice(registry, domainNameString, now, recurringBillingEvent)
.getFees();
break;
case UPDATE:
builder.setAvailIfSupported(true);
@@ -767,7 +782,7 @@ public class DomainFlowUtils {
final Optional<? extends FeeTransformCommandExtension> feeCommand,
FeesAndCredits feesAndCredits)
throws EppException {
if (isDomainPremium(domainName, priceTime) && !feeCommand.isPresent()) {
if (feesAndCredits.hasAnyPremiumFees() && !feeCommand.isPresent()) {
throw new FeesRequiredForPremiumNameException();
}
validateFeesAckedIfPresent(feeCommand, feesAndCredits);
@@ -878,7 +893,7 @@ public class DomainFlowUtils {
/**
* Check whether a new expiration time (via a renew) does not extend beyond a maximum number of
* years (e.g. {@link DomainBase#MAX_REGISTRATION_YEARS}) from "now".
* years (e.g. {@link Domain#MAX_REGISTRATION_YEARS}) from "now".
*
* @throws ExceedsMaxRegistrationYearsException if the new registration period is too long
*/
@@ -891,7 +906,7 @@ public class DomainFlowUtils {
/**
* Check that a new registration period (via a create) does not extend beyond a maximum number of
* years (e.g. {@link DomainBase#MAX_REGISTRATION_YEARS}).
* years (e.g. {@link Domain#MAX_REGISTRATION_YEARS}).
*
* @throws ExceedsMaxRegistrationYearsException if the new registration period is too long
*/
@@ -947,7 +962,7 @@ public class DomainFlowUtils {
}
/** If a domain "clientUpdateProhibited" set, updates must clear it or fail. */
static void verifyClientUpdateNotProhibited(Update command, DomainBase existingResource)
static void verifyClientUpdateNotProhibited(Update command, Domain existingResource)
throws ResourceHasClientUpdateProhibitedException {
if (existingResource.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
&& !command
@@ -1120,13 +1135,13 @@ public class DomainFlowUtils {
* the most recent HistoryEntry that fits the above criteria, with negated reportAmounts.
*/
static ImmutableSet<DomainTransactionRecord> createCancelingRecords(
DomainBase domainBase,
Domain domain,
final DateTime now,
Duration maxSearchPeriod,
final ImmutableSet<TransactionReportField> cancelableFields) {
List<? extends HistoryEntry> recentHistoryEntries =
findRecentHistoryEntries(domainBase, now, maxSearchPeriod);
findRecentHistoryEntries(domain, now, maxSearchPeriod);
Optional<? extends HistoryEntry> entryToCancel =
Streams.findLast(
recentHistoryEntries.stream()
@@ -1165,14 +1180,14 @@ public class DomainFlowUtils {
}
private static List<? extends HistoryEntry> findRecentHistoryEntries(
DomainBase domainBase, DateTime now, Duration maxSearchPeriod) {
Domain domain, DateTime now, Duration maxSearchPeriod) {
return jpaTm()
.query(
"FROM DomainHistory WHERE modificationTime >= :beginning AND domainRepoId = "
+ ":repoId ORDER BY modificationTime ASC",
DomainHistory.class)
.setParameter("beginning", now.minus(maxSearchPeriod))
.setParameter("repoId", domainBase.getRepoId())
.setParameter("repoId", domain.getRepoId())
.getResultList();
}
@@ -36,7 +36,7 @@ import google.registry.flows.custom.DomainInfoFlowCustomLogic;
import google.registry.flows.custom.DomainInfoFlowCustomLogic.AfterValidationParameters;
import google.registry.flows.custom.DomainInfoFlowCustomLogic.BeforeResponseParameters;
import google.registry.flows.custom.DomainInfoFlowCustomLogic.BeforeResponseReturnData;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Info;
import google.registry.model.domain.DomainCommand.Info.HostsRequest;
import google.registry.model.domain.DomainInfoData;
@@ -96,8 +96,8 @@ public final class DomainInfoFlow implements Flow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = clock.nowUtc();
DomainBase domain = verifyExistence(
DomainBase.class, targetId, loadByForeignKey(DomainBase.class, targetId, now));
Domain domain =
verifyExistence(Domain.class, targetId, loadByForeignKey(Domain.class, targetId, now));
verifyOptionalAuthInfo(authInfo, domain);
flowCustomLogic.afterValidation(
AfterValidationParameters.newBuilder().setDomain(domain).build());
@@ -142,8 +142,8 @@ public final class DomainInfoFlow implements Flow {
.build();
}
private ImmutableList<ResponseExtension> getDomainResponseExtensions(
DomainBase domain, DateTime now) throws EppException {
private ImmutableList<ResponseExtension> getDomainResponseExtensions(Domain domain, DateTime now)
throws EppException {
ImmutableList.Builder<ResponseExtension> extensions = new ImmutableList.Builder<>();
addSecDnsExtensionIfPresent(extensions, domain.getDsData());
ImmutableSet<GracePeriodStatus> gracePeriodStatuses = domain.getGracePeriodStatuses();
@@ -195,9 +195,14 @@ public final class DomainPricingLogic {
}
/** Returns a new transfer price for the pricer. */
FeesAndCredits getTransferPrice(Registry registry, String domainName, DateTime dateTime)
FeesAndCredits getTransferPrice(
Registry registry,
String domainName,
DateTime dateTime,
@Nullable Recurring recurringBillingEvent)
throws EppException {
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
FeesAndCredits renewPrice =
getRenewPrice(registry, domainName, dateTime, 1, recurringBillingEvent);
return customLogic.customizeTransferPrice(
TransferPriceParameters.newBuilder()
.setFeesAndCredits(
@@ -205,9 +210,9 @@ public final class DomainPricingLogic {
.setCurrency(registry.getCurrency())
.addFeeOrCredit(
Fee.create(
domainPrices.getRenewCost().getAmount(),
renewPrice.getRenewCost().getAmount(),
FeeType.RENEW,
domainPrices.isPremium()))
renewPrice.hasAnyPremiumFees()))
.build())
.setRegistry(registry)
.setDomainName(InternetDomainName.from(domainName))
@@ -57,7 +57,7 @@ import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Renew;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
@@ -166,7 +166,7 @@ public final class DomainRenewFlow implements TransactionalFlow {
DateTime now = tm().getTransactionTime();
Renew command = (Renew) resourceCommand;
// Loads the target resource if it exists
DomainBase existingDomain = loadAndVerifyExistence(DomainBase.class, targetId, now);
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
@@ -210,7 +210,9 @@ public final class DomainRenewFlow implements TransactionalFlow {
.setEventTime(newExpirationTime)
.setRenewalPrice(existingRecurringBillingEvent.getRenewalPrice().orElse(null))
.setRenewalPriceBehavior(existingRecurringBillingEvent.getRenewalPriceBehavior())
.setParent(domainHistoryKey)
.setDomainHistoryId(
new DomainHistoryId(
domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
.build();
PollMessage.Autorenew newAutorenewPollMessage =
newAutorenewPollMessage(existingDomain)
@@ -220,17 +222,20 @@ public final class DomainRenewFlow implements TransactionalFlow {
domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
.build();
// End the old autorenew billing event and poll message now. This may delete the poll message.
updateAutorenewRecurrenceEndTime(existingDomain, now);
DomainBase newDomain =
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
updateAutorenewRecurrenceEndTime(
existingDomain,
existingRecurring,
now,
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()));
Domain newDomain =
existingDomain
.asBuilder()
.setLastEppUpdateTime(now)
.setLastEppUpdateRegistrarId(registrarId)
.setRegistrationExpirationTime(newExpirationTime)
.setAutorenewBillingEvent(newAutorenewEvent.createVKey())
.setAutorenewPollMessage(
newAutorenewPollMessage.createVKey(),
newAutorenewPollMessage.getHistoryRevisionId())
.setAutorenewPollMessage(newAutorenewPollMessage.createVKey())
.addGracePeriod(
GracePeriod.forBillingEvent(
GracePeriodStatus.RENEW, existingDomain.getRepoId(), explicitRenewEvent))
@@ -273,7 +278,7 @@ public final class DomainRenewFlow implements TransactionalFlow {
}
private DomainHistory buildDomainHistory(
DomainBase newDomain, DateTime now, Period period, Duration renewGracePeriod) {
Domain newDomain, DateTime now, Period period, Duration renewGracePeriod) {
Optional<MetadataExtension> metadataExtensionOpt =
eppInput.getSingleExtension(MetadataExtension.class);
if (metadataExtensionOpt.isPresent()) {
@@ -297,10 +302,8 @@ public final class DomainRenewFlow implements TransactionalFlow {
.build();
}
private void verifyRenewAllowed(
Optional<AuthInfo> authInfo,
DomainBase existingDomain,
Renew command) throws EppException {
private void verifyRenewAllowed(Optional<AuthInfo> authInfo, Domain existingDomain, Renew command)
throws EppException {
verifyOptionalAuthInfo(authInfo, existingDomain);
verifyNoDisallowedStatuses(existingDomain, RENEW_DISALLOWED_STATUSES);
if (!isSuperuser) {
@@ -332,7 +335,8 @@ public final class DomainRenewFlow implements TransactionalFlow {
.setEventTime(now)
.setAllocationToken(allocationToken.map(AllocationToken::createVKey).orElse(null))
.setBillingTime(now.plus(Registry.get(tld).getRenewGracePeriodLength()))
.setParent(domainHistoryKey)
.setDomainHistoryId(
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
.build();
}
@@ -50,7 +50,7 @@ import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.OneTime;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Update;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
@@ -140,7 +140,7 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
extensionManager.validate();
Update command = (Update) resourceCommand;
DateTime now = tm().getTransactionTime();
DomainBase existingDomain = loadAndVerifyExistence(DomainBase.class, targetId, now);
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
boolean isExpired = existingDomain.getRegistrationExpirationTime().isBefore(now);
FeesAndCredits feesAndCredits =
pricingLogic.getRestorePrice(
@@ -150,6 +150,8 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
verifyRestoreAllowed(command, existingDomain, feeUpdate, feesAndCredits, now);
Key<DomainHistory> domainHistoryKey = createHistoryKey(existingDomain, DomainHistory.class);
historyBuilder.setId(domainHistoryKey.getId());
DomainHistoryId domainHistoryId =
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId());
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
DateTime newExpirationTime =
@@ -158,17 +160,17 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
// a year and bill for it immediately, with no grace period.
if (isExpired) {
entitiesToSave.add(
createRenewBillingEvent(domainHistoryKey, feesAndCredits.getRenewCost(), now));
createRenewBillingEvent(domainHistoryId, feesAndCredits.getRenewCost(), now));
}
// Always bill for the restore itself.
entitiesToSave.add(
createRestoreBillingEvent(domainHistoryKey, feesAndCredits.getRestoreCost(), now));
createRestoreBillingEvent(domainHistoryId, feesAndCredits.getRestoreCost(), now));
BillingEvent.Recurring autorenewEvent =
newAutorenewBillingEvent(existingDomain)
.setEventTime(newExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(domainHistoryKey)
.setDomainHistoryId(domainHistoryId)
.build();
PollMessage.Autorenew autorenewPollMessage =
newAutorenewPollMessage(existingDomain)
@@ -178,7 +180,7 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
new DomainHistoryId(
domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
.build();
DomainBase newDomain =
Domain newDomain =
performRestore(
existingDomain,
newExpirationTime,
@@ -197,7 +199,7 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
.build();
}
private DomainHistory buildDomainHistory(DomainBase newDomain, DateTime now) {
private DomainHistory buildDomainHistory(Domain newDomain, DateTime now) {
return historyBuilder
.setType(DOMAIN_RESTORE)
.setDomain(newDomain)
@@ -210,10 +212,11 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
private void verifyRestoreAllowed(
Update command,
DomainBase existingDomain,
Domain existingDomain,
Optional<FeeUpdateCommandExtension> feeUpdate,
FeesAndCredits feesAndCredits,
DateTime now) throws EppException {
DateTime now)
throws EppException {
verifyOptionalAuthInfo(authInfo, existingDomain);
if (!isSuperuser) {
verifyResourceOwnership(registrarId, existingDomain);
@@ -233,8 +236,8 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
validateFeeChallenge(targetId, now, feeUpdate, feesAndCredits);
}
private static DomainBase performRestore(
DomainBase existingDomain,
private static Domain performRestore(
Domain existingDomain,
DateTime newExpirationTime,
BillingEvent.Recurring autorenewEvent,
PollMessage.Autorenew autorenewPollMessage,
@@ -248,8 +251,7 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
.setGracePeriods(null)
.setDeletePollMessage(null)
.setAutorenewBillingEvent(autorenewEvent.createVKey())
.setAutorenewPollMessage(
autorenewPollMessage.createVKey(), autorenewPollMessage.getHistoryRevisionId())
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
// Clear the autorenew end time so if it had expired but is now explicitly being restored,
// it won't immediately be deleted again.
.setAutorenewEndTime(Optional.empty())
@@ -259,19 +261,17 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
}
private OneTime createRenewBillingEvent(
Key<DomainHistory> domainHistoryKey, Money renewCost, DateTime now) {
return prepareBillingEvent(domainHistoryKey, renewCost, now).setReason(Reason.RENEW).build();
DomainHistoryId domainHistoryId, Money renewCost, DateTime now) {
return prepareBillingEvent(domainHistoryId, renewCost, now).setReason(Reason.RENEW).build();
}
private BillingEvent.OneTime createRestoreBillingEvent(
Key<DomainHistory> domainHistoryKey, Money restoreCost, DateTime now) {
return prepareBillingEvent(domainHistoryKey, restoreCost, now)
.setReason(Reason.RESTORE)
.build();
DomainHistoryId domainHistoryId, Money restoreCost, DateTime now) {
return prepareBillingEvent(domainHistoryId, restoreCost, now).setReason(Reason.RESTORE).build();
}
private OneTime.Builder prepareBillingEvent(
Key<DomainHistory> domainHistoryKey, Money cost, DateTime now) {
DomainHistoryId domainHistoryId, Money cost, DateTime now) {
return new BillingEvent.OneTime.Builder()
.setTargetId(targetId)
.setRegistrarId(registrarId)
@@ -279,7 +279,7 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
.setBillingTime(now)
.setPeriodYears(1)
.setCost(cost)
.setParent(domainHistoryKey);
.setDomainHistoryId(domainHistoryId);
}
private static ImmutableList<FeeTransformResponseExtension> createResponseExtensions(
@@ -31,7 +31,6 @@ import static google.registry.model.ResourceTransferUtils.approvePendingTransfer
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.TRANSFER_SUCCESSFUL;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.pricing.PricingEngineProxy.getDomainRenewCost;
import static google.registry.util.CollectionUtils.union;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
@@ -45,17 +44,21 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainBase;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationTokenExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.DomainTransactionRecord;
@@ -85,6 +88,18 @@ import org.joda.time.DateTime;
* @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException}
* @error {@link google.registry.flows.exceptions.NotPendingTransferException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_APPROVE)
public final class DomainTransferApproveFlow implements TransactionalFlow {
@@ -96,19 +111,29 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
@Inject @Superuser boolean isSuperuser;
@Inject DomainHistory.Builder historyBuilder;
@Inject EppResponse.Builder responseBuilder;
@Inject DomainPricingLogic pricingLogic;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject EppInput eppInput;
@Inject DomainTransferApproveFlow() {}
/**
* The logic in this flow, which handles client approvals, very closely parallels the logic in
* {@link DomainBase#cloneProjectedAtTime} which handles implicit server approvals.
* {@link Domain#cloneProjectedAtTime} which handles implicit server approvals.
*/
@Override
public EppResponse run() throws EppException {
extensionManager.register(MetadataExtension.class);
extensionManager.register(MetadataExtension.class, AllocationTokenExtension.class);
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
DomainBase existingDomain = loadAndVerifyExistence(DomainBase.class, targetId, now);
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Registry.get(existingDomain.getTld()),
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
verifyOptionalAuthInfo(authInfo, existingDomain);
verifyHasPendingTransfer(existingDomain);
verifyResourceOwnership(registrarId, existingDomain);
@@ -120,10 +145,11 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
String gainingRegistrarId = transferData.getGainingRegistrarId();
// Create a transfer billing event for 1 year, unless the superuser extension was used to set
// the transfer period to zero. There is not a transfer cost if the transfer period is zero.
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
Key<DomainHistory> domainHistoryKey = createHistoryKey(existingDomain, DomainHistory.class);
historyBuilder.setId(domainHistoryKey.getId());
Optional<BillingEvent.OneTime> billingEvent =
(transferData.getTransferPeriod().getValue() == 0)
transferData.getTransferPeriod().getValue() == 0
? Optional.empty()
: Optional.of(
new BillingEvent.OneTime.Builder()
@@ -131,10 +157,19 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
.setTargetId(targetId)
.setRegistrarId(gainingRegistrarId)
.setPeriodYears(1)
.setCost(getDomainRenewCost(targetId, transferData.getTransferRequestTime(), 1))
.setCost(
pricingLogic
.getTransferPrice(
Registry.get(tld),
targetId,
transferData.getTransferRequestTime(),
existingRecurring)
.getRenewCost())
.setEventTime(now)
.setBillingTime(now.plus(Registry.get(tld).getTransferGracePeriodLength()))
.setParent(domainHistoryKey)
.setDomainHistoryId(
new DomainHistoryId(
domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
.build());
ImmutableList.Builder<ImmutableObject> entitiesToSave = new ImmutableList.Builder<>();
// If we are within an autorenew grace period, cancel the autorenew billing event and don't
@@ -143,19 +178,27 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
getOnlyElement(existingDomain.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW), null);
if (autorenewGrace != null) {
// During a normal transfer, if the domain is in the auto-renew grace period, the auto-renew
// billing event is cancelled and the gaining registrar is charged for the one year renewal.
// billing event is cancelled and the gaining registrar is charged for the one-year renewal.
// But, if the superuser extension is used to request a transfer without an additional year
// then the gaining registrar is not charged for the one year renewal and the losing registrar
// then the gaining registrar is not charged for the one-year renewal and the losing registrar
// still needs to be charged for the auto-renew.
if (billingEvent.isPresent()) {
entitiesToSave.add(
BillingEvent.Cancellation.forGracePeriod(
autorenewGrace, now, domainHistoryKey, targetId));
autorenewGrace,
now,
new DomainHistoryId(
domainHistoryKey.getParent().getName(), domainHistoryKey.getId()),
targetId));
}
}
// Close the old autorenew event and poll message at the transfer time (aka now). This may end
// up deleting the poll message.
updateAutorenewRecurrenceEndTime(existingDomain, now);
updateAutorenewRecurrenceEndTime(
existingDomain,
existingRecurring,
now,
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()));
DateTime newExpirationTime =
computeExDateForApprovalTime(existingDomain, now, transferData.getTransferPeriod());
// Create a new autorenew event starting at the expiration time.
@@ -166,8 +209,12 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
.setTargetId(targetId)
.setRegistrarId(gainingRegistrarId)
.setEventTime(newExpirationTime)
.setRenewalPriceBehavior(existingRecurring.getRenewalPriceBehavior())
.setRenewalPrice(existingRecurring.getRenewalPrice().orElse(null))
.setRecurrenceEndTime(END_OF_TIME)
.setParent(domainHistoryKey)
.setDomainHistoryId(
new DomainHistoryId(
domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
.build();
// Create a new autorenew poll message.
PollMessage.Autorenew gainingClientAutorenewPollMessage =
@@ -182,9 +229,9 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
.build();
// Construct the post-transfer domain.
DomainBase partiallyApprovedDomain =
Domain partiallyApprovedDomain =
approvePendingTransfer(existingDomain, TransferStatus.CLIENT_APPROVED, now);
DomainBase newDomain =
Domain newDomain =
partiallyApprovedDomain
.asBuilder()
// Update the transferredRegistrationExpirationTime here since approvePendingTransfer()
@@ -197,9 +244,7 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
.build())
.setRegistrationExpirationTime(newExpirationTime)
.setAutorenewBillingEvent(autorenewEvent.createVKey())
.setAutorenewPollMessage(
gainingClientAutorenewPollMessage.createVKey(),
gainingClientAutorenewPollMessage.getHistoryRevisionId())
.setAutorenewPollMessage(gainingClientAutorenewPollMessage.createVKey())
// Remove all the old grace periods and add a new one for the transfer.
.setGracePeriods(
billingEvent
@@ -238,7 +283,7 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
}
private DomainHistory buildDomainHistory(
DomainBase newDomain, Registry registry, DateTime now, String gainingRegistrarId) {
Domain newDomain, Registry registry, DateTime now, String gainingRegistrarId) {
ImmutableSet<DomainTransactionRecord> cancelingRecords =
createCancelingRecords(
newDomain,
@@ -40,7 +40,8 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.domain.DomainBase;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
@@ -90,7 +91,7 @@ public final class DomainTransferCancelFlow implements TransactionalFlow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
DomainBase existingDomain = loadAndVerifyExistence(DomainBase.class, targetId, now);
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingDomain);
verifyHasPendingTransfer(existingDomain);
verifyTransferInitiator(registrarId, existingDomain);
@@ -104,7 +105,7 @@ public final class DomainTransferCancelFlow implements TransactionalFlow {
.setId(domainHistoryKey.getId())
.setOtherRegistrarId(existingDomain.getTransferData().getLosingRegistrarId());
DomainBase newDomain =
Domain newDomain =
denyPendingTransfer(existingDomain, TransferStatus.CLIENT_CANCELLED, now, registrarId);
DomainHistory domainHistory = buildDomainHistory(newDomain, registry, now);
tm().putAll(
@@ -114,7 +115,9 @@ public final class DomainTransferCancelFlow implements TransactionalFlow {
targetId, newDomain.getTransferData(), null, domainHistoryKey));
// Reopen the autorenew event and poll message that we closed for the implicit transfer. This
// may recreate the autorenew poll message if it was deleted when the transfer request was made.
updateAutorenewRecurrenceEndTime(existingDomain, END_OF_TIME);
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
updateAutorenewRecurrenceEndTime(
existingDomain, existingRecurring, END_OF_TIME, domainHistory.getDomainHistoryId());
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
tm().delete(existingDomain.getTransferData().getServerApproveEntities());
@@ -123,7 +126,7 @@ public final class DomainTransferCancelFlow implements TransactionalFlow {
.build();
}
private DomainHistory buildDomainHistory(DomainBase newDomain, Registry registry, DateTime now) {
private DomainHistory buildDomainHistory(Domain newDomain, Registry registry, DateTime now) {
ImmutableSet<DomainTransactionRecord> cancelingRecords =
createCancelingRecords(
newDomain,
@@ -28,7 +28,7 @@ import google.registry.flows.ResourceFlowUtils;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.NoTransferHistoryToQueryException;
import google.registry.flows.exceptions.NotAuthorizedToViewTransferException;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
@@ -71,7 +71,7 @@ public final class DomainTransferQueryFlow implements Flow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate(); // There are no legal extensions for this flow.
DateTime now = clock.nowUtc();
DomainBase domain = loadAndVerifyExistence(DomainBase.class, targetId, now);
Domain domain = loadAndVerifyExistence(Domain.class, targetId, now);
verifyOptionalAuthInfo(authInfo, domain);
// Most of the fields on the transfer response are required, so there's no way to return valid
// XML if the object has never been transferred (and hence the fields aren't populated).
@@ -42,7 +42,8 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.domain.DomainBase;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
@@ -92,7 +93,7 @@ public final class DomainTransferRejectFlow implements TransactionalFlow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
DomainBase existingDomain = loadAndVerifyExistence(DomainBase.class, targetId, now);
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
Registry registry = Registry.get(existingDomain.getTld());
Key<DomainHistory> domainHistoryKey = createHistoryKey(existingDomain, DomainHistory.class);
historyBuilder
@@ -105,7 +106,7 @@ public final class DomainTransferRejectFlow implements TransactionalFlow {
if (!isSuperuser) {
checkAllowedAccessToTld(registrarId, existingDomain.getTld());
}
DomainBase newDomain =
Domain newDomain =
denyPendingTransfer(existingDomain, TransferStatus.CLIENT_REJECTED, now, registrarId);
DomainHistory domainHistory = buildDomainHistory(newDomain, registry, now);
tm().putAll(
@@ -115,7 +116,9 @@ public final class DomainTransferRejectFlow implements TransactionalFlow {
targetId, newDomain.getTransferData(), null, now, domainHistoryKey));
// Reopen the autorenew event and poll message that we closed for the implicit transfer. This
// may end up recreating the poll message if it was deleted upon the transfer request.
updateAutorenewRecurrenceEndTime(existingDomain, END_OF_TIME);
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
updateAutorenewRecurrenceEndTime(
existingDomain, existingRecurring, END_OF_TIME, domainHistory.getDomainHistoryId());
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
tm().delete(existingDomain.getTransferData().getServerApproveEntities());
@@ -124,7 +127,7 @@ public final class DomainTransferRejectFlow implements TransactionalFlow {
.build();
}
private DomainHistory buildDomainHistory(DomainBase newDomain, Registry registry, DateTime now) {
private DomainHistory buildDomainHistory(Domain newDomain, Registry registry, DateTime now) {
ImmutableSet<DomainTransactionRecord> cancelingRecords =
createCancelingRecords(
newDomain,
@@ -47,19 +47,23 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.InvalidTransferPeriodValueException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.flows.exceptions.TransferPeriodMustBeOneYearException;
import google.registry.flows.exceptions.TransferPeriodZeroAndFeeTransferExtensionException;
import google.registry.model.domain.DomainBase;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Transfer;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.FeeTransferCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.superuser.DomainTransferRequestSuperuserExtension;
import google.registry.model.domain.token.AllocationTokenExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
@@ -116,6 +120,18 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.PremiumNameBlockedException}
* @error {@link DomainFlowUtils.RegistrarMustBeActiveForThisOperationException}
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotInPromotionException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForRegistrarException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_REQUEST)
public final class DomainTransferRequestFlow implements TransactionalFlow {
@@ -137,6 +153,8 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
@Inject AsyncTaskEnqueuer asyncTaskEnqueuer;
@Inject EppResponse.Builder responseBuilder;
@Inject DomainPricingLogic pricingLogic;
@Inject AllocationTokenFlowUtils allocationTokenFlowUtils;
@Inject DomainTransferRequestFlow() {}
@Override
@@ -144,12 +162,19 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
extensionManager.register(
DomainTransferRequestSuperuserExtension.class,
FeeTransferCommandExtension.class,
MetadataExtension.class);
MetadataExtension.class,
AllocationTokenExtension.class);
validateRegistrarIsLoggedIn(gainingClientId);
verifyRegistrarIsActive(gainingClientId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
DomainBase existingDomain = loadAndVerifyExistence(DomainBase.class, targetId, now);
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Registry.get(existingDomain.getTld()),
gainingClientId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
Optional<DomainTransferRequestSuperuserExtension> superuserExtension =
eppInput.getSingleExtension(DomainTransferRequestSuperuserExtension.class);
Period period =
@@ -168,10 +193,12 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
throw new TransferPeriodZeroAndFeeTransferExtensionException();
}
// If the period is zero, then there is no fee for the transfer.
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
Optional<FeesAndCredits> feesAndCredits =
(period.getValue() == 0)
? Optional.empty()
: Optional.of(pricingLogic.getTransferPrice(registry, targetId, now));
: Optional.of(
pricingLogic.getTransferPrice(registry, targetId, now, existingRecurring));
if (feesAndCredits.isPresent()) {
validateFeeChallenge(targetId, now, feeTransfer, feesAndCredits.get());
}
@@ -180,9 +207,12 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
.setId(domainHistoryKey.getId())
.setOtherRegistrarId(existingDomain.getCurrentSponsorRegistrarId());
DateTime automaticTransferTime =
superuserExtension.isPresent()
? now.plusDays(superuserExtension.get().getAutomaticTransferLength())
: now.plus(registry.getAutomaticTransferLength());
superuserExtension
.map(
domainTransferRequestSuperuserExtension ->
now.plusDays(
domainTransferRequestSuperuserExtension.getAutomaticTransferLength()))
.orElseGet(() -> now.plus(registry.getAutomaticTransferLength()));
// If the domain will be in the auto-renew grace period at the moment of transfer, the transfer
// will subsume the autorenew, so we don't add the normal extra year from the transfer.
// The gaining registrar is still billed for the extra year; the losing registrar will get a
@@ -190,7 +220,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
//
// See b/19430703#comment17 and https://www.icann.org/news/advisory-2002-06-06-en for the
// policy documentation for transfers subsuming autorenews within the autorenew grace period.
DomainBase domainAtTransferTime = existingDomain.cloneProjectedAtTime(automaticTransferTime);
Domain domainAtTransferTime = existingDomain.cloneProjectedAtTime(automaticTransferTime);
// The new expiration time if there is a server approval.
DateTime serverApproveNewExpirationTime =
computeExDateForApprovalTime(domainAtTransferTime, automaticTransferTime, period);
@@ -201,6 +231,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
serverApproveNewExpirationTime,
domainHistoryKey,
existingDomain,
existingRecurring,
trid,
gainingClientId,
feesAndCredits.map(FeesAndCredits::getTotalCost),
@@ -230,8 +261,12 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
// the poll message if it has no events left. Note that if the automatic transfer succeeds, then
// cloneProjectedAtTime() will replace these old autorenew entities with the server approve ones
// that we've created in this flow and stored in pendingTransferData.
updateAutorenewRecurrenceEndTime(existingDomain, automaticTransferTime);
DomainBase newDomain =
updateAutorenewRecurrenceEndTime(
existingDomain,
existingRecurring,
automaticTransferTime,
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()));
Domain newDomain =
existingDomain
.asBuilder()
.setTransferData(pendingTransferData)
@@ -255,7 +290,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
}
private void verifyTransferAllowed(
DomainBase existingDomain,
Domain existingDomain,
Period period,
DateTime now,
Optional<DomainTransferRequestSuperuserExtension> superuserExtension)
@@ -294,7 +329,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
*
* <p>Even if not required, this policy is desirable because it dramatically simplifies the logic
* in transfer flows. Registrars appear to never request 2+ year transfers in practice, and they
* can always decompose an multi-year transfer into a 1-year transfer followed by a manual renewal
* can always decompose a multi-year transfer into a 1-year transfer followed by a manual renewal
* afterwards. The <a href="https://tools.ietf.org/html/rfc5731#section-3.2.4">EPP Domain RFC,
* section 3.2.4</a> says about EPP transfer periods that "the number of units available MAY be
* subject to limits imposed by the server" so we're just limiting the units to one.
@@ -320,7 +355,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
}
private DomainHistory buildDomainHistory(
DomainBase newDomain, Registry registry, DateTime now, Period period) {
Domain newDomain, Registry registry, DateTime now, Period period) {
return historyBuilder
.setType(DOMAIN_TRANSFER_REQUEST)
.setPeriod(period)
@@ -337,9 +372,9 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
}
private DomainTransferResponse createResponse(
Period period, DomainBase existingDomain, DomainBase newDomain, DateTime now) {
Period period, Domain existingDomain, Domain newDomain, DateTime now) {
// If the registration were approved this instant, this is what the new expiration would be,
// because we cap at 10 years from the moment of approval. This is different than the server
// because we cap at 10 years from the moment of approval. This is different from the server
// approval new expiration time, which is capped at 10 years from the server approve time.
DateTime approveNowExtendedRegistrationTime =
computeExDateForApprovalTime(existingDomain, now, period);
@@ -24,7 +24,8 @@ import com.googlecode.objectify.Key;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DomainBase;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.GracePeriod;
@@ -109,7 +110,8 @@ public final class DomainTransferUtils {
DateTime automaticTransferTime,
DateTime serverApproveNewExpirationTime,
Key<DomainHistory> domainHistoryKey,
DomainBase existingDomain,
Domain existingDomain,
Recurring existingRecurring,
Trid trid,
String gainingRegistrarId,
Optional<Money> transferCost,
@@ -144,7 +146,11 @@ public final class DomainTransferUtils {
return builder
.add(
createGainingClientAutorenewEvent(
serverApproveNewExpirationTime, domainHistoryKey, targetId, gainingRegistrarId))
existingRecurring,
serverApproveNewExpirationTime,
domainHistoryKey,
targetId,
gainingRegistrarId))
.add(
createGainingClientAutorenewPollMessage(
serverApproveNewExpirationTime, domainHistoryKey, targetId, gainingRegistrarId))
@@ -239,6 +245,7 @@ public final class DomainTransferUtils {
}
private static BillingEvent.Recurring createGainingClientAutorenewEvent(
Recurring existingRecurring,
DateTime serverApproveNewExpirationTime,
Key<DomainHistory> domainHistoryKey,
String targetId,
@@ -250,7 +257,10 @@ public final class DomainTransferUtils {
.setRegistrarId(gainingRegistrarId)
.setEventTime(serverApproveNewExpirationTime)
.setRecurrenceEndTime(END_OF_TIME)
.setParent(domainHistoryKey)
.setRenewalPriceBehavior(existingRecurring.getRenewalPriceBehavior())
.setRenewalPrice(existingRecurring.getRenewalPrice().orElse(null))
.setDomainHistoryId(
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
.build();
}
@@ -275,17 +285,20 @@ public final class DomainTransferUtils {
DateTime now,
Key<DomainHistory> domainHistoryKey,
String targetId,
DomainBase existingDomain,
Domain existingDomain,
Optional<Money> transferCost) {
DomainBase domainAtTransferTime =
existingDomain.cloneProjectedAtTime(automaticTransferTime);
Domain domainAtTransferTime = existingDomain.cloneProjectedAtTime(automaticTransferTime);
GracePeriod autorenewGracePeriod =
getOnlyElement(
domainAtTransferTime.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW), null);
if (autorenewGracePeriod != null && transferCost.isPresent()) {
return Optional.of(
BillingEvent.Cancellation.forGracePeriod(
autorenewGracePeriod, now, domainHistoryKey, targetId)
autorenewGracePeriod,
now,
new DomainHistoryId(
domainHistoryKey.getParent().getName(), domainHistoryKey.getId()),
targetId)
.asBuilder()
.setEventTime(automaticTransferTime)
.build());
@@ -308,7 +321,8 @@ public final class DomainTransferUtils {
.setPeriodYears(1)
.setEventTime(automaticTransferTime)
.setBillingTime(automaticTransferTime.plus(registry.getTransferGracePeriodLength()))
.setParent(domainHistoryKey)
.setDomainHistoryId(
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
.build();
}
@@ -67,7 +67,7 @@ import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Update;
import google.registry.model.domain.DomainCommand.Update.AddRemove;
import google.registry.model.domain.DomainCommand.Update.Change;
@@ -173,11 +173,11 @@ public final class DomainUpdateFlow implements TransactionalFlow {
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Update command = cloneAndLinkReferences((Update) resourceCommand, now);
DomainBase existingDomain = loadAndVerifyExistence(DomainBase.class, targetId, now);
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
verifyUpdateAllowed(command, existingDomain, now);
flowCustomLogic.afterValidation(
AfterValidationParameters.newBuilder().setExistingDomain(existingDomain).build());
DomainBase newDomain = performUpdate(command, existingDomain, now);
Domain newDomain = performUpdate(command, existingDomain, now);
DomainHistory domainHistory =
historyBuilder.setType(DOMAIN_UPDATE).setDomain(newDomain).build();
validateNewState(newDomain);
@@ -204,7 +204,7 @@ public final class DomainUpdateFlow implements TransactionalFlow {
}
/** Fail if the object doesn't exist or was deleted. */
private void verifyUpdateAllowed(Update command, DomainBase existingDomain, DateTime now)
private void verifyUpdateAllowed(Update command, Domain existingDomain, DateTime now)
throws EppException {
verifyOptionalAuthInfo(authInfo, existingDomain);
AddRemove add = command.getInnerAdd();
@@ -233,8 +233,7 @@ public final class DomainUpdateFlow implements TransactionalFlow {
tld, add.getNameserverFullyQualifiedHostNames());
}
private DomainBase performUpdate(Update command, DomainBase domain, DateTime now)
throws EppException {
private Domain performUpdate(Update command, Domain domain, DateTime now) throws EppException {
AddRemove add = command.getInnerAdd();
AddRemove remove = command.getInnerRemove();
checkSameValuesNotAddedAndRemoved(add.getNameservers(), remove.getNameservers());
@@ -251,7 +250,7 @@ public final class DomainUpdateFlow implements TransactionalFlow {
Sets.union(Sets.difference(domain.getContacts(), remove.getContacts()), add.getContacts());
validateNoDuplicateContacts(newContacts);
DomainBase.Builder domainBuilder =
Domain.Builder domainBuilder =
domain
.asBuilder()
// Handle the secDNS extension. As dsData in secDnsUpdate is read from EPP input and
@@ -293,7 +292,7 @@ public final class DomainUpdateFlow implements TransactionalFlow {
}
}
private void validateNewState(DomainBase newDomain) throws EppException {
private void validateNewState(Domain newDomain) throws EppException {
validateRequiredContactsPresent(newDomain.getRegistrant(), newDomain.getContacts());
validateDsData(newDomain.getDsData());
validateNameserversCountForTld(
@@ -304,7 +303,7 @@ public final class DomainUpdateFlow implements TransactionalFlow {
/** Some status updates cost money. Bill only once no matter how many of them are changed. */
private Optional<BillingEvent.OneTime> createBillingEventForStatusUpdates(
DomainBase existingDomain, DomainBase newDomain, DomainHistory historyEntry, DateTime now) {
Domain existingDomain, Domain newDomain, DomainHistory historyEntry, DateTime now) {
Optional<MetadataExtension> metadataExtension =
eppInput.getSingleExtension(MetadataExtension.class);
if (metadataExtension.isPresent() && metadataExtension.get().getRequestedByRegistrar()) {
@@ -320,7 +319,7 @@ public final class DomainUpdateFlow implements TransactionalFlow {
.setCost(Registry.get(existingDomain.getTld()).getServerStatusChangeCost())
.setEventTime(now)
.setBillingTime(now)
.setParent(historyEntry)
.setDomainHistory(historyEntry)
.build());
}
}
@@ -330,7 +329,7 @@ public final class DomainUpdateFlow implements TransactionalFlow {
/** Enqueues a poll message iff a superuser is adding/removing server statuses. */
private Optional<PollMessage.OneTime> createPollMessageForServerStatusUpdates(
DomainBase existingDomain, DomainBase newDomain, DomainHistory historyEntry, DateTime now) {
Domain existingDomain, Domain newDomain, DomainHistory historyEntry, DateTime now) {
if (registrarId.equals(existingDomain.getPersistedCurrentSponsorRegistrarId())) {
// Don't send a poll message when a superuser registrar is updating its own domain.
return Optional.empty();
@@ -19,7 +19,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.tld.Registry;
@@ -46,7 +46,7 @@ public class AllocationTokenCustomLogic {
/** Performs additional custom logic for validating a token on an existing domain. */
public AllocationToken validateToken(
DomainBase domain, AllocationToken token, Registry registry, String registrarId, DateTime now)
Domain domain, AllocationToken token, Registry registry, String registrarId, DateTime now)
throws EppException {
// Do nothing.
return token;
@@ -26,7 +26,7 @@ import google.registry.flows.EppException;
import google.registry.flows.EppException.AssociationProhibitsOperationException;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
@@ -171,7 +171,7 @@ public class AllocationTokenFlowUtils {
/** Verifies and returns the allocation token if one is specified, otherwise does nothing. */
public Optional<AllocationToken> verifyAllocationTokenIfPresent(
DomainBase existingDomain,
Domain existingDomain,
Registry registry,
String registrarId,
DateTime now,
@@ -30,8 +30,8 @@ import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.CheckData.HostCheck;
import google.registry.model.eppoutput.CheckData.HostCheckData;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.host.Host;
import google.registry.model.host.HostCommand.Check;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.util.Clock;
import javax.inject.Inject;
@@ -61,8 +61,7 @@ public final class HostCheckFlow implements Flow {
extensionManager.validate(); // There are no legal extensions for this flow.
ImmutableList<String> hostnames = ((Check) resourceCommand).getTargetIds();
verifyTargetIdCount(hostnames, maxChecks);
ImmutableSet<String> existingIds =
checkResourcesExist(HostResource.class, hostnames, clock.nowUtc());
ImmutableSet<String> existingIds = checkResourcesExist(Host.class, hostnames, clock.nowUtc());
ImmutableList.Builder<HostCheck> checks = new ImmutableList.Builder<>();
for (String hostname : hostnames) {
boolean unused = !existingIds.contains(hostname);
@@ -41,14 +41,14 @@ import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
import google.registry.flows.exceptions.ResourceCreateContentionException;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.CreateData.HostCreateData;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.host.Host;
import google.registry.model.host.HostCommand.Create;
import google.registry.model.host.HostHistory;
import google.registry.model.host.HostResource;
import google.registry.model.index.EppResourceIndex;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
@@ -105,11 +105,11 @@ public final class HostCreateFlow implements TransactionalFlow {
extensionManager.validate();
Create command = (Create) resourceCommand;
DateTime now = tm().getTransactionTime();
verifyResourceDoesNotExist(HostResource.class, targetId, now, registrarId);
verifyResourceDoesNotExist(Host.class, targetId, now, registrarId);
// The superordinate domain of the host object if creating an in-bailiwick host, or null if
// creating an external host. This is looked up before we actually create the Host object so
// we can detect error conditions earlier.
Optional<DomainBase> superordinateDomain =
Optional<Domain> superordinateDomain =
lookupSuperordinateDomain(validateHostName(targetId), now);
verifySuperordinateDomainNotInPendingDelete(superordinateDomain.orElse(null));
verifySuperordinateDomainOwnership(registrarId, superordinateDomain.orElse(null));
@@ -121,14 +121,14 @@ public final class HostCreateFlow implements TransactionalFlow {
? new SubordinateHostMustHaveIpException()
: new UnexpectedExternalHostIpException();
}
HostResource newHost =
new HostResource.Builder()
Host newHost =
new Host.Builder()
.setCreationRegistrarId(registrarId)
.setPersistedCurrentSponsorRegistrarId(registrarId)
.setHostName(targetId)
.setInetAddresses(command.getInetAddresses())
.setRepoId(createRepoId(allocateId(), roidSuffix))
.setSuperordinateDomain(superordinateDomain.map(DomainBase::createVKey).orElse(null))
.setSuperordinateDomain(superordinateDomain.map(Domain::createVKey).orElse(null))
.build();
historyBuilder.setType(HOST_CREATE).setHost(newHost);
ImmutableSet<ImmutableObject> entitiesToSave =
@@ -38,8 +38,8 @@ import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.host.Host;
import google.registry.model.host.HostHistory;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import javax.inject.Inject;
@@ -92,8 +92,8 @@ public final class HostDeleteFlow implements TransactionalFlow {
extensionManager.validate();
DateTime now = tm().getTransactionTime();
validateHostName(targetId);
checkLinkedDomains(targetId, now, HostResource.class);
HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now);
checkLinkedDomains(targetId, now, Host.class);
Host existingHost = loadAndVerifyExistence(Host.class, targetId, now);
verifyNoDisallowedStatuses(existingHost, DISALLOWED_STATUSES);
if (!isSuperuser) {
// Hosts transfer with their superordinate domains, so for hosts with a superordinate domain,
@@ -104,8 +104,7 @@ public final class HostDeleteFlow implements TransactionalFlow {
: existingHost;
verifyResourceOwnership(registrarId, owningResource);
}
HostResource newHost =
existingHost.asBuilder().setStatusValues(null).setDeletionTime(now).build();
Host newHost = existingHost.asBuilder().setStatusValues(null).setDeletionTime(now).build();
if (existingHost.isSubordinate()) {
dnsQueue.addHostRefreshTask(existingHost.getHostName());
tm().update(
@@ -29,7 +29,7 @@ import google.registry.flows.EppException.ParameterValuePolicyErrorException;
import google.registry.flows.EppException.ParameterValueRangeErrorException;
import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.util.Idn;
import java.util.Optional;
@@ -77,8 +77,8 @@ public class HostFlowUtils {
}
}
/** Return the {@link DomainBase} this host is subordinate to, or null for external hosts. */
public static Optional<DomainBase> lookupSuperordinateDomain(
/** Return the {@link Domain} this host is subordinate to, or null for external hosts. */
public static Optional<Domain> lookupSuperordinateDomain(
InternetDomainName hostName, DateTime now) throws EppException {
Optional<InternetDomainName> tld = findTldForName(hostName);
if (!tld.isPresent()) {
@@ -90,8 +90,7 @@ public class HostFlowUtils {
hostName.parts().stream()
.skip(hostName.parts().size() - (tld.get().parts().size() + 1))
.collect(joining("."));
Optional<DomainBase> superordinateDomain =
loadByForeignKey(DomainBase.class, domainName, now);
Optional<Domain> superordinateDomain = loadByForeignKey(Domain.class, domainName, now);
if (!superordinateDomain.isPresent() || !isActive(superordinateDomain.get(), now)) {
throw new SuperordinateDomainDoesNotExistException(domainName);
}
@@ -101,12 +100,12 @@ public class HostFlowUtils {
/** Superordinate domain for this hostname does not exist. */
static class SuperordinateDomainDoesNotExistException extends ObjectDoesNotExistException {
public SuperordinateDomainDoesNotExistException(String domainName) {
super(DomainBase.class, domainName);
super(Domain.class, domainName);
}
}
/** Ensure that the superordinate domain is sponsored by the provided registrar ID. */
static void verifySuperordinateDomainOwnership(String registrarId, DomainBase superordinateDomain)
static void verifySuperordinateDomainOwnership(String registrarId, Domain superordinateDomain)
throws EppException {
if (superordinateDomain != null
&& !registrarId.equals(superordinateDomain.getCurrentSponsorRegistrarId())) {
@@ -122,7 +121,7 @@ public class HostFlowUtils {
}
/** Ensure that the superordinate domain is not in pending delete. */
static void verifySuperordinateDomainNotInPendingDelete(DomainBase superordinateDomain)
static void verifySuperordinateDomainNotInPendingDelete(Domain superordinateDomain)
throws EppException {
if ((superordinateDomain != null)
&& superordinateDomain.getStatusValues().contains(StatusValue.PENDING_DELETE)) {
@@ -27,11 +27,11 @@ import google.registry.flows.Flow;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.host.Host;
import google.registry.model.host.HostInfoData;
import google.registry.model.host.HostResource;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.util.Clock;
import javax.inject.Inject;
@@ -65,7 +65,7 @@ public final class HostInfoFlow implements Flow {
extensionManager.validate(); // There are no legal extensions for this flow.
validateHostName(targetId);
DateTime now = clock.nowUtc();
HostResource host = loadAndVerifyExistence(HostResource.class, targetId, now);
Host host = loadAndVerifyExistence(Host.class, targetId, now);
ImmutableSet.Builder<StatusValue> statusValues = new ImmutableSet.Builder<>();
statusValues.addAll(host.getStatusValues());
if (isLinked(host.createVKey(), now)) {
@@ -76,7 +76,7 @@ public final class HostInfoFlow implements Flow {
// the client id, last transfer time, and pending transfer status need to be read off of it. If
// there is no superordinate domain, the host's own values for these fields will be correct.
if (host.isSubordinate()) {
DomainBase superordinateDomain =
Domain superordinateDomain =
tm().transact(
() -> tm().loadByKey(host.getSuperordinateDomain()).cloneProjectedAtTime(now));
hostInfoDataBuilder
@@ -47,16 +47,16 @@ import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.host.Host;
import google.registry.model.host.HostCommand.Update;
import google.registry.model.host.HostCommand.Update.AddRemove;
import google.registry.model.host.HostCommand.Update.Change;
import google.registry.model.host.HostHistory;
import google.registry.model.host.HostResource;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.persistence.VKey;
@@ -133,30 +133,30 @@ public final class HostUpdateFlow implements TransactionalFlow {
String suppliedNewHostName = change.getFullyQualifiedHostName();
DateTime now = tm().getTransactionTime();
validateHostName(targetId);
HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now);
Host existingHost = loadAndVerifyExistence(Host.class, targetId, now);
boolean isHostRename = suppliedNewHostName != null;
String oldHostName = targetId;
String newHostName = firstNonNull(suppliedNewHostName, oldHostName);
DomainBase oldSuperordinateDomain =
Domain oldSuperordinateDomain =
existingHost.isSubordinate()
? tm().loadByKey(existingHost.getSuperordinateDomain()).cloneProjectedAtTime(now)
: null;
// Note that lookupSuperordinateDomain calls cloneProjectedAtTime on the domain for us.
Optional<DomainBase> newSuperordinateDomain =
Optional<Domain> newSuperordinateDomain =
lookupSuperordinateDomain(validateHostName(newHostName), now);
verifySuperordinateDomainNotInPendingDelete(newSuperordinateDomain.orElse(null));
EppResource owningResource = firstNonNull(oldSuperordinateDomain, existingHost);
verifyUpdateAllowed(
command, existingHost, newSuperordinateDomain.orElse(null), owningResource, isHostRename);
if (isHostRename && loadAndGetKey(HostResource.class, newHostName, now) != null) {
if (isHostRename && loadAndGetKey(Host.class, newHostName, now) != null) {
throw new HostAlreadyExistsException(newHostName);
}
AddRemove add = command.getInnerAdd();
AddRemove remove = command.getInnerRemove();
checkSameValuesNotAddedAndRemoved(add.getStatusValues(), remove.getStatusValues());
checkSameValuesNotAddedAndRemoved(add.getInetAddresses(), remove.getInetAddresses());
VKey<DomainBase> newSuperordinateDomainKey =
newSuperordinateDomain.map(DomainBase::createVKey).orElse(null);
VKey<Domain> newSuperordinateDomainKey =
newSuperordinateDomain.map(Domain::createVKey).orElse(null);
// If the superordinateDomain field is changing, set the lastSuperordinateChange to now.
DateTime lastSuperordinateChange =
Objects.equals(newSuperordinateDomainKey, existingHost.getSuperordinateDomain())
@@ -175,7 +175,7 @@ public final class HostUpdateFlow implements TransactionalFlow {
newSuperordinateDomain.isPresent()
? newSuperordinateDomain.get().getCurrentSponsorRegistrarId()
: owningResource.getPersistedCurrentSponsorRegistrarId();
HostResource newHost =
Host newHost =
existingHost
.asBuilder()
.setHostName(newHostName)
@@ -210,11 +210,11 @@ public final class HostUpdateFlow implements TransactionalFlow {
private void verifyUpdateAllowed(
Update command,
HostResource existingHost,
DomainBase newSuperordinateDomain,
Host existingHost,
Domain newSuperordinateDomain,
EppResource owningResource,
boolean isHostRename)
throws EppException {
throws EppException {
if (!isSuperuser) {
// Verify that the host belongs to this registrar, either directly or because it is currently
// subordinate to a domain owned by this registrar.
@@ -237,8 +237,8 @@ public final class HostUpdateFlow implements TransactionalFlow {
verifyNoDisallowedStatuses(existingHost, DISALLOWED_STATUSES);
}
private void verifyHasIpsIffIsExternal(
Update command, HostResource existingHost, HostResource newHost) throws EppException {
private void verifyHasIpsIffIsExternal(Update command, Host existingHost, Host newHost)
throws EppException {
boolean wasSubordinate = existingHost.isSubordinate();
boolean willBeSubordinate = newHost.isSubordinate();
boolean willBeExternal = !willBeSubordinate;
@@ -258,7 +258,7 @@ public final class HostUpdateFlow implements TransactionalFlow {
}
}
private void enqueueTasks(HostResource existingHost, HostResource newHost) {
private void enqueueTasks(Host existingHost, Host newHost) {
// Only update DNS for subordinate hosts. External hosts have no glue to write, so they
// are only written as NS records from the referencing domain.
if (existingHost.isSubordinate()) {
@@ -277,7 +277,7 @@ public final class HostUpdateFlow implements TransactionalFlow {
}
}
private static void updateSuperordinateDomains(HostResource existingHost, HostResource newHost) {
private static void updateSuperordinateDomains(Host existingHost, Host newHost) {
if (existingHost.isSubordinate()
&& newHost.isSubordinate()
&& Objects.equals(
@@ -41,7 +41,7 @@ public abstract class BackupGroupRoot extends ImmutableObject implements UnsafeS
* that this is updated on every save, rather than only in response to an {@code <update>} command
*/
@XmlTransient
// Prevents subclasses from unexpectedly accessing as property (e.g., HostResource), which would
// Prevents subclasses from unexpectedly accessing as property (e.g., Host), which would
// require an unnecessary non-private setter method.
@Access(AccessType.FIELD)
@AttributeOverride(name = "lastUpdateTime", column = @Column(name = "updateTimestamp"))
@@ -20,6 +20,7 @@ import static google.registry.model.IdService.allocateId;
import static google.registry.model.ModelUtils.getAllFields;
import com.googlecode.objectify.annotation.Id;
import google.registry.model.annotations.OfyIdAllocation;
import google.registry.util.TypeUtils.TypeInstantiator;
import java.lang.reflect.Field;
import java.util.Optional;
@@ -59,7 +60,10 @@ public interface Buildable {
// any entity it has one and only one @Id field in its class hierarchy.
Field idField =
getAllFields(instance.getClass()).values().stream()
.filter(field -> field.isAnnotationPresent(Id.class))
.filter(
field ->
field.isAnnotationPresent(Id.class)
|| field.isAnnotationPresent(OfyIdAllocation.class))
.findFirst()
.orElse(null);
if (idField != null
@@ -16,20 +16,18 @@ package google.registry.model;
import com.google.common.collect.ImmutableSet;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.billing.BillingEvent;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.common.GaeUserIdConverter;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.host.Host;
import google.registry.model.host.HostHistory;
import google.registry.model.host.HostResource;
import google.registry.model.index.EppResourceIndex;
import google.registry.model.index.EppResourceIndexBucket;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.poll.PollMessage;
import google.registry.model.rde.RdeRevision;
import google.registry.model.registrar.Registrar;
import google.registry.model.reporting.HistoryEntry;
@@ -44,13 +42,9 @@ public final class EntityClasses {
public static final ImmutableSet<Class<? extends ImmutableObject>> ALL_CLASSES =
ImmutableSet.of(
AllocationToken.class,
BillingEvent.Cancellation.class,
BillingEvent.Modification.class,
BillingEvent.OneTime.class,
BillingEvent.Recurring.class,
ContactHistory.class,
ContactResource.class,
DomainBase.class,
Domain.class,
DomainHistory.class,
EntityGroupRoot.class,
EppResourceIndex.class,
@@ -60,12 +54,9 @@ public final class EntityClasses {
ForeignKeyIndex.ForeignKeyHostIndex.class,
GaeUserIdConverter.class,
HistoryEntry.class,
Host.class,
HostHistory.class,
HostResource.class,
Lock.class,
PollMessage.class,
PollMessage.Autorenew.class,
PollMessage.OneTime.class,
RdeRevision.class,
Registrar.class,
ServerSecret.class);
@@ -33,9 +33,9 @@ import google.registry.model.EppResource.BuilderWithTransferData;
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.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.HostResource;
import google.registry.model.host.Host;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntryDao;
@@ -342,24 +342,24 @@ public final class EppResourceUtils {
* @param now the logical time of the check
* @param limit the maximum number of returned keys, unlimited if null
*/
public static ImmutableSet<VKey<DomainBase>> getLinkedDomainKeys(
public static ImmutableSet<VKey<Domain>> getLinkedDomainKeys(
VKey<? extends EppResource> key, DateTime now, @Nullable Integer limit) {
checkArgument(
key.getKind().equals(ContactResource.class) || key.getKind().equals(HostResource.class),
"key must be either VKey<ContactResource> or VKey<HostResource>, but it is %s",
key.getKind().equals(ContactResource.class) || key.getKind().equals(Host.class),
"key must be either VKey<ContactResource> or VKey<Host>, but it is %s",
key);
boolean isContactKey = key.getKind().equals(ContactResource.class);
if (tm().isOfy()) {
com.googlecode.objectify.cmd.Query<DomainBase> query =
com.googlecode.objectify.cmd.Query<Domain> query =
auditedOfy()
.load()
.type(DomainBase.class)
.type(Domain.class)
.filter(isContactKey ? "allContacts.contact" : "nsHosts", key.getOfyKey())
.filter("deletionTime >", now);
if (limit != null) {
query.limit(limit);
}
return query.keys().list().stream().map(DomainBase::createVKey).collect(toImmutableSet());
return query.keys().list().stream().map(Domain::createVKey).collect(toImmutableSet());
} else {
return tm().transact(
() -> {
@@ -382,16 +382,15 @@ public final class EppResourceUtils {
query.setMaxResults(limit);
}
@SuppressWarnings("unchecked")
ImmutableSet<VKey<DomainBase>> domainBaseKeySet =
(ImmutableSet<VKey<DomainBase>>)
ImmutableSet<VKey<Domain>> domainKeySet =
(ImmutableSet<VKey<Domain>>)
query
.getResultStream()
.map(
repoId ->
DomainBase.createVKey(
Key.create(DomainBase.class, (String) repoId)))
Domain.createVKey(Key.create(Domain.class, (String) repoId)))
.collect(toImmutableSet());
return domainBaseKeySet;
return domainKeySet;
});
}
}
@@ -26,7 +26,7 @@ import google.registry.model.EppResource.BuilderWithTransferData;
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.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.index.ForeignKeyIndex;
@@ -101,7 +101,7 @@ public final class ResourceTransferUtils {
}
private static void assertIsContactOrDomain(EppResource eppResource) {
checkState(eppResource instanceof ContactResource || eppResource instanceof DomainBase);
checkState(eppResource instanceof ContactResource || eppResource instanceof Domain);
}
/** Update the relevant {@link ForeignKeyIndex} to cache the new deletion time. */
@@ -0,0 +1,36 @@
// Copyright 2022 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.annotations;
import google.registry.model.IdService;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to indicate an ID field that needs to be allocated by Ofy.
*
* <p>This annotation is only used for the field of a {@link google.registry.model.Buildable} class
* that was previously annotated by both Ofy's and JPA's {@code @Id} annotations, of which the Ofy
* annotation has been removed. The field still needs to be allocated automatically by the builder,
* via the {@link IdService#allocateId()}.
*
* <p>It should be removed after we switch to using SQL to directly allocate IDs.
*/
@DeleteAfterMigration
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface OfyIdAllocation {}
@@ -18,7 +18,6 @@ import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
@@ -26,31 +25,21 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.condition.IfNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.OfyIdAllocation;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.common.TimeOfYear;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.transfer.TransferData.TransferServerApproveEntity;
import google.registry.persistence.BillingVKey.BillingEventVKey;
import google.registry.persistence.BillingVKey.BillingRecurrenceVKey;
import google.registry.persistence.VKey;
import google.registry.persistence.WithLongVKey;
import google.registry.persistence.converter.JodaMoneyType;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
@@ -58,11 +47,13 @@ import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.MappedSuperclass;
import javax.persistence.PostLoad;
import javax.persistence.Transient;
import javax.persistence.Table;
import org.hibernate.annotations.Columns;
import org.hibernate.annotations.Type;
import org.joda.money.Money;
@@ -76,7 +67,7 @@ public abstract class BillingEvent extends ImmutableObject
/** The reason for the bill, which maps 1:1 to skus in go/registry-billing-skus. */
public enum Reason {
CREATE(true),
@Deprecated // TODO(b/31676071): remove this legacy value once old data is cleaned up.
@Deprecated // DO NOT USE THIS REASON. IT REMAINS BECAUSE OF HISTORICAL DATA. SEE b/31676071.
ERROR(false),
FEE_EARLY_ACCESS(true),
RENEW(true),
@@ -105,9 +96,7 @@ public abstract class BillingEvent extends ImmutableObject
ALLOCATION,
ANCHOR_TENANT,
AUTO_RENEW,
/**
* Landrush billing events are historical only and are no longer created.
*/
/** Landrush billing events are historical only and are no longer created. */
LANDRUSH,
/**
* This flag is used on create {@link OneTime} billing events for domains that were reserved.
@@ -122,7 +111,7 @@ public abstract class BillingEvent extends ImmutableObject
* This flag will be added to any {@link OneTime} events that are created via, e.g., an
* automated process to expand {@link Recurring} events.
*/
SYNTHETIC;
SYNTHETIC
}
/**
@@ -149,37 +138,30 @@ public abstract class BillingEvent extends ImmutableObject
*/
NONPREMIUM,
/**
* This indicates that the renewalPrice in {@link BillingEvent.Recurring} will be used for
* domain renewal.
* This indicates that the renewalPrice in {@link Recurring} will be used for domain renewal.
*
* <p>The renewalPrice has a non-null value iff the price behavior is set to "SPECIFIED". This
* behavior is used with internal registrations.
*/
SPECIFIED;
SPECIFIED
}
/** Entity id. */
@Id @javax.persistence.Id Long id;
@Parent @DoNotHydrate @Transient Key<DomainHistory> parent;
@OfyIdAllocation @Id Long id;
/** The registrar to bill. */
@Index
@Column(name = "registrarId", nullable = false)
String clientId;
/** Revision id of the entry in DomainHistory table that ths bill belongs to. */
@Ignore
@Column(nullable = false)
Long domainHistoryRevisionId;
/** ID of the EPP resource that the bill is for. */
@Ignore
@Column(nullable = false)
String domainRepoId;
/** When this event was created. For recurring events, this is also the recurrence start time. */
@Index
@Column(nullable = false)
DateTime eventTime;
@@ -192,17 +174,7 @@ public abstract class BillingEvent extends ImmutableObject
@Column(name = "domain_name", nullable = false)
String targetId;
@Nullable
Set<Flag> flags;
@PostLoad
void postLoad() {
parent =
Key.create(
Key.create(DomainBase.class, domainRepoId),
DomainHistory.class,
domainHistoryRevisionId);
}
@Nullable Set<Flag> flags;
public String getRegistrarId() {
return clientId;
@@ -232,8 +204,8 @@ public abstract class BillingEvent extends ImmutableObject
return targetId;
}
public Key<DomainHistory> getParentKey() {
return parent;
public DomainHistoryId getDomainHistoryId() {
return new DomainHistoryId(domainRepoId, domainHistoryRevisionId);
}
public ImmutableSet<Flag> getFlags() {
@@ -269,16 +241,6 @@ public abstract class BillingEvent extends ImmutableObject
return thisCastToDerived();
}
public B setDomainHistoryRevisionId(long domainHistoryRevisionId) {
getInstance().domainHistoryRevisionId = domainHistoryRevisionId;
return thisCastToDerived();
}
public B setDomainRepoId(String domainRepoId) {
getInstance().domainRepoId = domainRepoId;
return thisCastToDerived();
}
public B setEventTime(DateTime eventTime) {
getInstance().eventTime = eventTime;
return thisCastToDerived();
@@ -294,14 +256,14 @@ public abstract class BillingEvent extends ImmutableObject
return thisCastToDerived();
}
public B setParent(DomainHistory parent) {
getInstance().parent = Key.create(parent);
public B setDomainHistoryId(DomainHistoryId domainHistoryId) {
getInstance().domainHistoryRevisionId = domainHistoryId.getId();
getInstance().domainRepoId = domainHistoryId.getDomainRepoId();
return thisCastToDerived();
}
public B setParent(Key<DomainHistory> parentKey) {
getInstance().parent = parentKey;
return thisCastToDerived();
public B setDomainHistory(DomainHistory domainHistory) {
return setDomainHistoryId(domainHistory.getDomainHistoryId());
}
@Override
@@ -311,29 +273,23 @@ public abstract class BillingEvent extends ImmutableObject
checkNotNull(instance.clientId, "Registrar ID must be set");
checkNotNull(instance.eventTime, "Event time must be set");
checkNotNull(instance.targetId, "Target ID must be set");
checkNotNull(instance.parent, "Parent must be set");
checkNotNull(instance.parent.getParent(), "parent.getParent() must be set");
checkNotNull(
instance.parent.getParent().getName(), "parent.getParent().getName() must be set");
instance.domainHistoryRevisionId = instance.parent.getId();
instance.domainRepoId = instance.parent.getParent().getName();
checkNotNull(instance.domainHistoryRevisionId, "Domain History Revision ID must be set");
checkNotNull(instance.domainRepoId, "Domain Repo ID must be set");
return super.build();
}
}
/** A one-time billable event. */
@ReportedOn
@Entity
@javax.persistence.Entity(name = "BillingEvent")
@javax.persistence.Table(
@Entity(name = "BillingEvent")
@Table(
indexes = {
@javax.persistence.Index(columnList = "registrarId"),
@javax.persistence.Index(columnList = "eventTime"),
@javax.persistence.Index(columnList = "billingTime"),
@javax.persistence.Index(columnList = "syntheticCreationTime"),
@javax.persistence.Index(columnList = "domainRepoId"),
@javax.persistence.Index(columnList = "allocationToken"),
@javax.persistence.Index(columnList = "cancellation_matching_billing_recurrence_id")
@Index(columnList = "registrarId"),
@Index(columnList = "eventTime"),
@Index(columnList = "billingTime"),
@Index(columnList = "syntheticCreationTime"),
@Index(columnList = "domainRepoId"),
@Index(columnList = "allocationToken"),
@Index(columnList = "cancellation_matching_billing_recurrence_id")
})
@AttributeOverride(name = "id", column = @Column(name = "billing_event_id"))
@WithLongVKey(compositeKey = true)
@@ -345,15 +301,13 @@ public abstract class BillingEvent extends ImmutableObject
Money cost;
/** When the cost should be billed. */
@Index
DateTime billingTime;
/**
* The period in years of the action being billed for, if applicable, otherwise null. Used for
* financial reporting.
*/
@IgnoreSave(IfNull.class)
Integer periodYears = null;
Integer periodYears;
/**
* For {@link Flag#SYNTHETIC} events, when this event was persisted to Datastore (i.e. the
@@ -361,11 +315,10 @@ public abstract class BillingEvent extends ImmutableObject
* needs to be undone, a query on this field will return the complete set of potentially bad
* events.
*/
@Index
DateTime syntheticCreationTime;
/**
* For {@link Flag#SYNTHETIC} events, a {@link Key} to the {@link Recurring} from which this
* For {@link Flag#SYNTHETIC} events, a {@link VKey} to the {@link Recurring} from which this
* {@link OneTime} was created. This is needed in order to properly match billing events against
* {@link Cancellation}s.
*/
@@ -373,20 +326,17 @@ public abstract class BillingEvent extends ImmutableObject
VKey<Recurring> cancellationMatchingBillingEvent;
/**
* For {@link Flag#SYNTHETIC} events, the {@link DomainHistory} revision ID of the the {@link
* For {@link Flag#SYNTHETIC} events, the {@link DomainHistory} revision ID of the {@link
* Recurring} from which this {@link OneTime} was created. This is needed in order to recreate
* the {@link VKey} when reading from SQL.
*/
@Ignore
@Column(name = "recurrence_history_revision_id")
Long recurringEventHistoryRevisionId;
/**
* The {@link AllocationToken} used in the creation of this event, or null if one was not used.
*/
@Index
@Nullable
VKey<AllocationToken> allocationToken;
@Nullable VKey<AllocationToken> allocationToken;
public Money getCost() {
return cost;
@@ -418,11 +368,11 @@ public abstract class BillingEvent extends ImmutableObject
@Override
public VKey<OneTime> createVKey() {
return VKey.create(OneTime.class, getId(), Key.create(this));
return createVKey(getId());
}
public static VKey<OneTime> createVKey(Key<OneTime> key) {
return VKey.create(OneTime.class, key.getId(), key);
public static VKey<OneTime> createVKey(long id) {
return VKey.createSql(OneTime.class, id);
}
@Override
@@ -430,19 +380,6 @@ public abstract class BillingEvent extends ImmutableObject
return new Builder(clone(this));
}
@Override
void postLoad() {
super.postLoad();
if (cancellationMatchingBillingEvent != null) {
cancellationMatchingBillingEvent =
cancellationMatchingBillingEvent.restoreOfy(
DomainBase.class,
domainRepoId,
DomainHistory.class,
recurringEventHistoryRevisionId);
}
}
/** A builder for {@link OneTime} since it is immutable. */
public static class Builder extends BillingEvent.Builder<OneTime, Builder> {
@@ -475,11 +412,11 @@ public abstract class BillingEvent extends ImmutableObject
}
public Builder setCancellationMatchingBillingEvent(
VKey<Recurring> cancellationMatchingBillingEvent) {
getInstance().cancellationMatchingBillingEvent = cancellationMatchingBillingEvent;
// getOfyKey() here is safe, recurring billing event VKeys have a valid ofy key.
Recurring cancellationMatchingBillingEvent) {
getInstance().cancellationMatchingBillingEvent =
cancellationMatchingBillingEvent.createVKey();
getInstance().recurringEventHistoryRevisionId =
cancellationMatchingBillingEvent.getOfyKey().getParent().getId();
cancellationMatchingBillingEvent.getDomainHistoryRevisionId();
return this;
}
@@ -511,12 +448,6 @@ public abstract class BillingEvent extends ImmutableObject
== (instance.cancellationMatchingBillingEvent != null),
"Cancellation matching billing event must be set if and only if the SYNTHETIC flag "
+ "is set.");
// getOfyKey() here is safe, billing event VKeys have a valid ofy key.
checkState(
!instance.getFlags().contains(Flag.SYNTHETIC)
|| (instance.cancellationMatchingBillingEvent.getOfyKey().getParent().getId()
== instance.recurringEventHistoryRevisionId),
"Cancellation matching billing event and its history revision ID does not match.");
return super.build();
}
}
@@ -526,29 +457,26 @@ public abstract class BillingEvent extends ImmutableObject
* A recurring billable event.
*
* <p>Unlike {@link OneTime} events, these do not store an explicit cost, since the cost of the
* recurring event might change and each time we bill for it we need to bill at the current cost,
* recurring event might change and each time we bill for it, we need to bill at the current cost,
* not the value that was in use at the time the recurrence was created.
*/
@ReportedOn
@Entity
@javax.persistence.Entity(name = "BillingRecurrence")
@javax.persistence.Table(
@Entity(name = "BillingRecurrence")
@Table(
indexes = {
@javax.persistence.Index(columnList = "registrarId"),
@javax.persistence.Index(columnList = "eventTime"),
@javax.persistence.Index(columnList = "domainRepoId"),
@javax.persistence.Index(columnList = "recurrenceEndTime"),
@javax.persistence.Index(columnList = "recurrence_time_of_year")
@Index(columnList = "registrarId"),
@Index(columnList = "eventTime"),
@Index(columnList = "domainRepoId"),
@Index(columnList = "recurrenceEndTime"),
@Index(columnList = "recurrence_time_of_year")
})
@AttributeOverride(name = "id", column = @Column(name = "billing_recurrence_id"))
@WithLongVKey(compositeKey = true)
public static class Recurring extends BillingEvent {
/**
* The billing event recurs every year between {@link #eventTime} and this time on the
* [month, day, time] specified in {@link #recurrenceTimeOfYear}.
* The billing event recurs every year between {@link #eventTime} and this time on the [month,
* day, time] specified in {@link #recurrenceTimeOfYear}.
*/
@Index
DateTime recurrenceEndTime;
/**
@@ -564,11 +492,9 @@ public abstract class BillingEvent extends ImmutableObject
* (same day of year, which can be 365 or 366 days later) which is what {@link TimeOfYear} can
* model, whereas the billing time is a fixed {@link org.joda.time.Duration} later.
*/
@Index
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "timeString", column = @Column(name = "recurrence_time_of_year"))
})
@AttributeOverrides(
@AttributeOverride(name = "timeString", column = @Column(name = "recurrence_time_of_year")))
TimeOfYear recurrenceTimeOfYear;
/**
@@ -605,11 +531,11 @@ public abstract class BillingEvent extends ImmutableObject
@Override
public VKey<Recurring> createVKey() {
return VKey.create(Recurring.class, getId(), Key.create(this));
return createVKey(getId());
}
public static VKey<Recurring> createVKey(Key<Recurring> key) {
return VKey.create(Recurring.class, key.getId(), key);
public static VKey<Recurring> createVKey(Long id) {
return VKey.createSql(Recurring.class, id);
}
@Override
@@ -647,8 +573,8 @@ public abstract class BillingEvent extends ImmutableObject
checkNotNull(instance.eventTime);
checkNotNull(instance.reason);
checkArgument(
(instance.renewalPriceBehavior == RenewalPriceBehavior.SPECIFIED)
^ (instance.renewalPrice == null),
instance.renewalPriceBehavior == RenewalPriceBehavior.SPECIFIED
^ instance.renewalPrice == null,
"Renewal price can have a value if and only if the renewal price behavior is"
+ " SPECIFIED");
instance.recurrenceTimeOfYear = TimeOfYear.fromDateTime(instance.eventTime);
@@ -666,47 +592,45 @@ public abstract class BillingEvent extends ImmutableObject
* preserve the immutability of billing events.
*/
@ReportedOn
@Entity
@javax.persistence.Entity(name = "BillingCancellation")
@javax.persistence.Table(
@Entity(name = "BillingCancellation")
@Table(
indexes = {
@javax.persistence.Index(columnList = "registrarId"),
@javax.persistence.Index(columnList = "eventTime"),
@javax.persistence.Index(columnList = "domainRepoId"),
@javax.persistence.Index(columnList = "billingTime"),
@javax.persistence.Index(columnList = "billing_event_id"),
@javax.persistence.Index(columnList = "billing_recurrence_id")
@Index(columnList = "registrarId"),
@Index(columnList = "eventTime"),
@Index(columnList = "domainRepoId"),
@Index(columnList = "billingTime"),
@Index(columnList = "billing_event_id"),
@Index(columnList = "billing_recurrence_id")
})
@AttributeOverride(name = "id", column = @Column(name = "billing_cancellation_id"))
@WithLongVKey(compositeKey = true)
public static class Cancellation extends BillingEvent {
/** The billing time of the charge that is being cancelled. */
@Index
DateTime billingTime;
/**
* The one-time billing event to cancel, or null for autorenew cancellations.
*
* <p>Although the type is {@link Key} the name "ref" is preserved for historical reasons.
* <p>Although the type is {@link VKey} the name "ref" is preserved for historical reasons.
*/
@IgnoreSave(IfNull.class)
BillingEventVKey refOneTime = null;
@Column(name = "billing_event_id")
VKey<OneTime> refOneTime;
/**
* The recurring billing event to cancel, or null for non-autorenew cancellations.
*
* <p>Although the type is {@link Key} the name "ref" is preserved for historical reasons.
* <p>Although the type is {@link VKey} the name "ref" is preserved for historical reasons.
*/
@IgnoreSave(IfNull.class)
BillingRecurrenceVKey refRecurring = null;
@Column(name = "billing_recurrence_id")
VKey<Recurring> refRecurring;
public DateTime getBillingTime() {
return billingTime;
}
public VKey<? extends BillingEvent> getEventKey() {
return firstNonNull(refOneTime, refRecurring).createVKey();
return firstNonNull(refOneTime, refRecurring);
}
/** The mapping from billable grace period types to originating billing event reasons. */
@@ -723,22 +647,23 @@ public abstract class BillingEvent extends ImmutableObject
* using the supplied targetId and deriving other metadata (clientId, billing time, and the
* cancellation reason) from the grace period.
*/
public static BillingEvent.Cancellation forGracePeriod(
public static Cancellation forGracePeriod(
GracePeriod gracePeriod,
DateTime eventTime,
Key<DomainHistory> domainHistoryKey,
DomainHistoryId domainHistoryId,
String targetId) {
checkArgument(gracePeriod.hasBillingEvent(),
checkArgument(
gracePeriod.hasBillingEvent(),
"Cannot create cancellation for grace period without billing event");
BillingEvent.Cancellation.Builder builder =
new BillingEvent.Cancellation.Builder()
Builder builder =
new Builder()
.setReason(checkNotNull(GRACE_PERIOD_TO_REASON.get(gracePeriod.getType())))
.setTargetId(targetId)
.setRegistrarId(gracePeriod.getRegistrarId())
.setEventTime(eventTime)
// The charge being cancelled will take place at the grace period's expiration time.
.setBillingTime(gracePeriod.getExpirationTime())
.setParent(domainHistoryKey);
.setDomainHistoryId(domainHistoryId);
// Set the grace period's billing event using the appropriate Cancellation builder method.
if (gracePeriod.getOneTimeBillingEvent() != null) {
builder.setOneTimeEventKey(gracePeriod.getOneTimeBillingEvent());
@@ -750,11 +675,11 @@ public abstract class BillingEvent extends ImmutableObject
@Override
public VKey<Cancellation> createVKey() {
return VKey.create(Cancellation.class, getId(), Key.create(this));
return createVKey(getId());
}
public static VKey<Cancellation> createVKey(Key<Cancellation> key) {
return VKey.create(Cancellation.class, key.getId(), key);
public static VKey<Cancellation> createVKey(long id) {
return VKey.createSql(Cancellation.class, id);
}
@Override
@@ -776,13 +701,13 @@ public abstract class BillingEvent extends ImmutableObject
return this;
}
public Builder setOneTimeEventKey(VKey<BillingEvent.OneTime> eventKey) {
getInstance().refOneTime = BillingEventVKey.create(eventKey);
public Builder setOneTimeEventKey(VKey<OneTime> eventKey) {
getInstance().refOneTime = eventKey;
return this;
}
public Builder setRecurringEventKey(VKey<BillingEvent.Recurring> eventKey) {
getInstance().refRecurring = BillingRecurrenceVKey.create(eventKey);
public Builder setRecurringEventKey(VKey<Recurring> eventKey) {
getInstance().refRecurring = eventKey;
return this;
}
@@ -791,115 +716,11 @@ public abstract class BillingEvent extends ImmutableObject
Cancellation instance = getInstance();
checkNotNull(instance.billingTime, "Must set billing time");
checkNotNull(instance.reason, "Must set reason");
checkState((instance.refOneTime == null) != (instance.refRecurring == null),
checkState(
(instance.refOneTime == null) != (instance.refRecurring == null),
"Cancellations must have exactly one billing event key set");
return super.build();
}
}
}
/** An event representing a modification of an existing one-time billing event. */
@ReportedOn
@Entity
@WithLongVKey(compositeKey = true)
public static class Modification extends BillingEvent {
/** The change in cost that should be applied to the original billing event. */
Money cost;
/** The one-time billing event to modify. */
Key<BillingEvent.OneTime> eventRef;
/**
* Description of the modification (and presumably why it was issued). This text may appear as a
* line item on an invoice or report about such modifications.
*/
String description;
public Money getCost() {
return cost;
}
public Key<BillingEvent.OneTime> getEventKey() {
return eventRef;
}
public String getDescription() {
return description;
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
@Override
public VKey<Modification> createVKey() {
return VKey.create(Modification.class, getId(), Key.create(this));
}
public static VKey<Modification> createVKey(Key<Modification> key) {
return VKey.create(Modification.class, key.getId(), key);
}
/**
* Create a new Modification billing event which is a refund of the given OneTime billing event
* and that is parented off the given HistoryEntry.
*
* <p>Note that this method may appear to be unused most of the time, but it is kept around
* because it is needed by one-off scrap tools that need to make billing adjustments.
*/
public static Modification createRefundFor(
OneTime billingEvent, DomainHistory historyEntry, String description) {
return new Builder()
.setRegistrarId(billingEvent.getRegistrarId())
.setFlags(billingEvent.getFlags())
.setReason(billingEvent.getReason())
.setTargetId(billingEvent.getTargetId())
.setEventKey(Key.create(billingEvent))
.setEventTime(historyEntry.getModificationTime())
.setDescription(description)
.setCost(billingEvent.getCost().negated())
.setParent(historyEntry)
.build();
}
/** A builder for {@link Modification} since it is immutable. */
public static class Builder extends BillingEvent.Builder<Modification, Builder> {
public Builder() {}
private Builder(Modification instance) {
super(instance);
}
public Builder setCost(Money cost) {
getInstance().cost = cost;
return this;
}
public Builder setEventKey(Key<BillingEvent.OneTime> eventKey) {
getInstance().eventRef = eventKey;
return this;
}
public Builder setDescription(String description) {
getInstance().description = description;
return this;
}
@Override
public Modification build() {
Modification instance = getInstance();
checkNotNull(instance.reason);
checkNotNull(instance.eventRef);
BillingEvent.OneTime billingEvent =
tm().transact(() -> tm().loadByKey(VKey.from(instance.eventRef)));
checkArgument(
Objects.equals(instance.cost.getCurrencyUnit(), billingEvent.cost.getCurrencyUnit()),
"Referenced billing event is in a different currency");
return super.build();
}
}
}
}
@@ -19,28 +19,28 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.GracePeriod.GracePeriodHistory;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.domain.secdns.DomainDsDataHistory;
import google.registry.model.host.HostResource;
import google.registry.model.host.Host;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTransactionManager;
/**
* Utilities for managing an alternative JPA entity model optimized for bulk loading multi-level
* entities such as {@link DomainBase} and {@link DomainHistory}.
* entities such as {@link Domain} and {@link DomainHistory}.
*
* <p>In a bulk query for a multi-level JPA entity type, the JPA framework only generates a bulk
* query (SELECT * FROM table) for the base table. Then, for each row in the base table, additional
* queries are issued to load associated rows in child tables. This can be very slow when an entity
* type has multiple child tables.
*
* <p>We have defined an alternative entity model for {@code DomainBase} and {@code DomainHistory},
* <p>We have defined an alternative entity model for {@link Domain} and {@link DomainHistory},
* where the base table as well as the child tables are mapped to single-level entity types. The
* idea is to load each of these types using a bulk query, and assemble them into the target type in
* memory in a pipeline. The main use case is Datastore-Cloud SQL validation during the Registry
@@ -53,8 +53,8 @@ public class BulkQueryEntities {
*/
public static final ImmutableMap<String, String> JPA_ENTITIES_REPLACEMENTS =
ImmutableMap.of(
DomainBase.class.getCanonicalName(),
DomainBaseLite.class.getCanonicalName(),
Domain.class.getCanonicalName(),
DomainLite.class.getCanonicalName(),
DomainHistory.class.getCanonicalName(),
DomainHistoryLite.class.getCanonicalName());
@@ -64,34 +64,34 @@ public class BulkQueryEntities {
ImmutableList.of(
DomainHost.class.getCanonicalName(), DomainHistoryHost.class.getCanonicalName());
public static DomainBase assembleDomainBase(
DomainBaseLite domainBaseLite,
public static Domain assembleDomain(
DomainLite domainLite,
ImmutableSet<GracePeriod> gracePeriods,
ImmutableSet<DelegationSignerData> delegationSignerData,
ImmutableSet<VKey<HostResource>> nsHosts) {
DomainBase.Builder builder = new DomainBase.Builder();
builder.copyFrom(domainBaseLite);
ImmutableSet<VKey<Host>> nsHosts) {
Domain.Builder builder = new Domain.Builder();
builder.copyFrom(domainLite);
builder.setGracePeriods(gracePeriods);
builder.setDsData(delegationSignerData);
builder.setNameservers(nsHosts);
// Restore the original update timestamp (this gets cleared when we set nameservers or DS data).
builder.setUpdateTimestamp(domainBaseLite.getUpdateTimestamp());
builder.setUpdateTimestamp(domainLite.getUpdateTimestamp());
return builder.build();
}
public static DomainHistory assembleDomainHistory(
DomainHistoryLite domainHistoryLite,
ImmutableSet<DomainDsDataHistory> dsDataHistories,
ImmutableSet<VKey<HostResource>> domainHistoryHosts,
ImmutableSet<VKey<Host>> domainHistoryHosts,
ImmutableSet<GracePeriodHistory> gracePeriodHistories,
ImmutableSet<DomainTransactionRecord> transactionRecords) {
DomainHistory.Builder builder = new DomainHistory.Builder();
builder.copyFrom(domainHistoryLite);
DomainContent rawDomainContent = domainHistoryLite.domainContent;
if (rawDomainContent != null) {
DomainContent newDomainContent =
DomainBase rawDomainBase = domainHistoryLite.domainBase;
if (rawDomainBase != null) {
DomainBase newDomainBase =
domainHistoryLite
.domainContent
.domainBase
.asBuilder()
.setNameservers(domainHistoryHosts)
.setGracePeriods(
@@ -104,9 +104,9 @@ public class BulkQueryEntities {
.collect(toImmutableSet()))
// Restore the original update timestamp (this gets cleared when we set nameservers or
// DS data).
.setUpdateTimestamp(domainHistoryLite.domainContent.getUpdateTimestamp())
.setUpdateTimestamp(domainHistoryLite.domainBase.getUpdateTimestamp())
.build();
builder.setDomain(newDomainContent);
builder.setDomain(newDomainBase);
}
return builder.buildAndAssemble(
dsDataHistories, domainHistoryHosts, gracePeriodHistories, transactionRecords);
@@ -16,7 +16,7 @@ package google.registry.model.bulkquery;
import com.google.common.base.Objects;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.host.HostResource;
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
import java.io.Serializable;
import javax.persistence.Access;
@@ -44,8 +44,8 @@ public class DomainHistoryHost implements Serializable {
return new DomainHistoryId(domainHistoryDomainRepoId, domainHistoryHistoryRevisionId);
}
public VKey<HostResource> getHostVKey() {
return VKey.create(HostResource.class, hostRepoId);
public VKey<Host> getHostVKey() {
return VKey.create(Host.class, hostRepoId);
}
@Override
@@ -15,8 +15,8 @@
package google.registry.model.bulkquery;
import com.googlecode.objectify.Key;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.Period;
@@ -49,9 +49,9 @@ import javax.persistence.PostLoad;
@IdClass(DomainHistoryId.class)
public class DomainHistoryLite extends HistoryEntry {
// Store DomainContent instead of DomainBase so we don't pick up its @Id
// Store DomainBase instead of Domain so we don't pick up its @Id
// Nullable for the sake of pre-Registry-3.0 history objects
@Nullable DomainContent domainContent;
@Nullable DomainBase domainBase;
@Id
@Access(AccessType.PROPERTY)
@@ -64,7 +64,7 @@ public class DomainHistoryLite extends HistoryEntry {
/** This method is private because it is only used by Hibernate. */
@SuppressWarnings("unused")
private void setDomainRepoId(String domainRepoId) {
parent = Key.create(DomainBase.class, domainRepoId);
parent = Key.create(Domain.class, domainRepoId);
}
@Override
@@ -101,9 +101,9 @@ public class DomainHistoryLite extends HistoryEntry {
return super.getId();
}
/** The key to the {@link DomainBase} this is based off of. */
public VKey<DomainBase> getParentVKey() {
return VKey.create(DomainBase.class, getDomainRepoId());
/** The key to the {@link Domain} this is based off of. */
public VKey<Domain> getParentVKey() {
return VKey.create(Domain.class, getDomainRepoId());
}
public DomainHistoryId getDomainHistoryId() {
@@ -112,14 +112,14 @@ public class DomainHistoryLite extends HistoryEntry {
@PostLoad
void postLoad() {
if (domainContent == null) {
if (domainBase == null) {
return;
}
// See inline comments in DomainHistory.postLoad for reasons for the following lines.
if (domainContent.getDomainName() == null) {
domainContent = null;
} else if (domainContent.getRepoId() == null) {
domainContent.setRepoId(parent.getName());
if (domainBase.getDomainName() == null) {
domainBase = null;
} else if (domainBase.getRepoId() == null) {
domainBase.setRepoId(parent.getName());
}
}
}
@@ -15,7 +15,7 @@
package google.registry.model.bulkquery;
import com.google.common.base.Objects;
import google.registry.model.host.HostResource;
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
import java.io.Serializable;
import javax.persistence.Access;
@@ -40,8 +40,8 @@ public class DomainHost implements Serializable {
return domainRepoId;
}
public VKey<HostResource> getHostVKey() {
return VKey.create(HostResource.class, hostRepoId);
public VKey<Host> getHostVKey() {
return VKey.create(Host.class, hostRepoId);
}
@Override
@@ -14,8 +14,8 @@
package google.registry.model.bulkquery;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.persistence.VKey;
import google.registry.persistence.WithStringVKey;
import javax.persistence.Access;
@@ -23,17 +23,17 @@ import javax.persistence.AccessType;
import javax.persistence.Entity;
/**
* A 'light' version of {@link DomainBase} with only base table ("Domain") attributes, which allows
* fast bulk loading. They are used in in-memory assembly of {@code DomainBase} instances along with
* A 'light' version of {@link Domain} with only base table ("Domain") attributes, which allows fast
* bulk loading. They are used in in-memory assembly of {@code Domain} instances along with
* bulk-loaded child entities ({@code GracePeriod} etc). The in-memory assembly achieves much higher
* performance than loading {@code DomainBase} directly.
* performance than loading {@code Domain} directly.
*
* <p>Please refer to {@link BulkQueryEntities} for more information.
*/
@Entity(name = "Domain")
@WithStringVKey
@Access(AccessType.FIELD)
public class DomainBaseLite extends DomainContent {
public class DomainLite extends DomainBase {
@Override
@javax.persistence.Id
@@ -42,7 +42,7 @@ public class DomainBaseLite extends DomainContent {
return super.getRepoId();
}
public static VKey<DomainBaseLite> createVKey(String repoId) {
return VKey.createSql(DomainBaseLite.class, repoId);
public static VKey<DomainLite> createVKey(String repoId) {
return VKey.createSql(DomainLite.class, repoId);
}
}
@@ -53,12 +53,16 @@ public class ClassPathManager {
}
public static <T> Class<T> getClass(String className) {
checkArgument(CLASS_REGISTRY.containsKey(className), "Class not found in class registry");
checkArgument(
CLASS_REGISTRY.containsKey(className), "Class %s not found in class registry", className);
return (Class<T>) CLASS_REGISTRY.get(className);
}
public static <T> String getClassName(Class<T> clazz) {
checkArgument(CLASS_NAME_REGISTRY.containsKey(clazz), "Class not found in class name registry");
checkArgument(
CLASS_NAME_REGISTRY.containsKey(clazz),
"Class %s not found in class name registry",
clazz.getSimpleName());
return CLASS_NAME_REGISTRY.get(clazz);
}
}
@@ -0,0 +1,69 @@
// Copyright 2022 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.console;
/** Permissions that users may have in the UI, either per-registrar or globally. */
public enum ConsolePermission {
/** Add, update, or remove other console users. */
MANAGE_USERS,
/** Add, update, or remove registrars. */
MANAGE_REGISTRARS,
/** Manage related registrars, e.g. when one registrar owns another. */
MANAGE_ACCREDITATION,
/** Set up the EPP connection (e.g. certs). */
CONFIGURE_EPP_CONNECTION,
/** Retrieve the unredacted registrant email from a domain. */
GET_REGISTRANT_EMAIL,
/** Suspend a domain for compliance (non-URS) reasons. */
SUSPEND_DOMAIN,
/** Suspend a domain for the Uniform Rapid Suspension process. */
SUSPEND_DOMAIN_URS,
/** Download a list of domains under management. */
DOWNLOAD_DOMAINS,
/** Change the password for a registrar. */
CHANGE_NOMULUS_PASSWORD,
/** View all possible TLDs. */
VIEW_TLD_PORTFOLIO,
/** Onboarding the registrar(s) to extra programs like registry locking. */
ONBOARD_ADDITIONAL_PROGRAMS,
/** Execute arbitrary EPP commands through the UI. */
EXECUTE_EPP_COMMANDS,
/** Send messages to the registry support team. */
CONTACT_SUPPORT,
/** Access billing and payment details for a registrar. */
ACCESS_BILLING_DETAILS,
/** Access any available documentation about the registry. */
ACCESS_DOCUMENTATION,
/** Change the documentation available in the UI about the registry. */
MANAGE_DOCUMENTATION,
/** Viewing the onboarding status of a registrar on a TLD. */
CHECK_ONBOARDING_STATUS,
/** View premium and/or reserved lists for a TLD. */
VIEW_PREMIUM_RESERVED_LISTS,
/** Sign and/or change legal documents like RRAs. */
UPLOAD_CONTRACTS,
/** Viewing legal documents like RRAs. */
ACCESS_CONTRACTS,
/** View analytics on operations and billing data (possibly TBD). */
VIEW_OPERATIONAL_DATA,
/** Create announcements that can be displayed in the UI. */
SEND_ANNOUNCEMENTS,
/** View announcements in the UI. */
VIEW_ANNOUNCEMENTS,
/** Viewing a record of actions performed in the UI for a particular registrar. */
VIEW_ACTIVITY_LOG,
/** View and perform registry locks. */
REGISTRY_LOCK
}
@@ -0,0 +1,112 @@
// Copyright 2022 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.console;
import com.google.common.collect.ImmutableSet;
/**
* Definitions of the {@link ConsolePermission} sets that each role contains.
*
* <p>Note: within the "registry" and "registrar" groupings, permissions expand hierarchically,
* where each role has all the permissions of the role below it plus some others.
*/
public class ConsoleRoleDefinitions {
/** Permissions for a registry support agent. */
static final ImmutableSet<ConsolePermission> SUPPORT_AGENT_PERMISSIONS =
ImmutableSet.of(
ConsolePermission.MANAGE_USERS,
ConsolePermission.MANAGE_ACCREDITATION,
ConsolePermission.CONFIGURE_EPP_CONNECTION,
ConsolePermission.DOWNLOAD_DOMAINS,
ConsolePermission.VIEW_TLD_PORTFOLIO,
ConsolePermission.ONBOARD_ADDITIONAL_PROGRAMS,
ConsolePermission.CONTACT_SUPPORT,
ConsolePermission.ACCESS_BILLING_DETAILS,
ConsolePermission.ACCESS_DOCUMENTATION,
ConsolePermission.SUSPEND_DOMAIN_URS,
ConsolePermission.CHECK_ONBOARDING_STATUS,
ConsolePermission.VIEW_PREMIUM_RESERVED_LISTS,
ConsolePermission.UPLOAD_CONTRACTS,
ConsolePermission.ACCESS_CONTRACTS,
ConsolePermission.VIEW_OPERATIONAL_DATA,
ConsolePermission.SEND_ANNOUNCEMENTS,
ConsolePermission.VIEW_ANNOUNCEMENTS,
ConsolePermission.VIEW_ACTIVITY_LOG,
ConsolePermission.REGISTRY_LOCK);
/** Permissions for a registry support lead. */
static final ImmutableSet<ConsolePermission> SUPPORT_LEAD_PERMISSIONS =
new ImmutableSet.Builder<ConsolePermission>()
.addAll(SUPPORT_AGENT_PERMISSIONS)
.add(
ConsolePermission.MANAGE_REGISTRARS,
ConsolePermission.GET_REGISTRANT_EMAIL,
ConsolePermission.SUSPEND_DOMAIN,
ConsolePermission.EXECUTE_EPP_COMMANDS,
ConsolePermission.MANAGE_DOCUMENTATION)
.build();
/** Permissions for a registry full-time employee. */
static final ImmutableSet<ConsolePermission> FTE_PERMISSIONS =
new ImmutableSet.Builder<ConsolePermission>()
.addAll(SUPPORT_LEAD_PERMISSIONS)
.add(ConsolePermission.CHANGE_NOMULUS_PASSWORD)
.build();
/** Permissions for a registrar partner account manager. */
static final ImmutableSet<ConsolePermission> ACCOUNT_MANAGER_PERMISSIONS =
ImmutableSet.of(
ConsolePermission.DOWNLOAD_DOMAINS,
ConsolePermission.VIEW_TLD_PORTFOLIO,
ConsolePermission.CONTACT_SUPPORT,
ConsolePermission.ACCESS_DOCUMENTATION,
ConsolePermission.VIEW_PREMIUM_RESERVED_LISTS,
ConsolePermission.VIEW_OPERATIONAL_DATA,
ConsolePermission.VIEW_ANNOUNCEMENTS);
/** Permissions for a registrar partner account manager that can perform registry locks. */
static final ImmutableSet<ConsolePermission> ACCOUNT_MANAGER_WITH_REGISTRY_LOCK_PERMISSIONS =
new ImmutableSet.Builder<ConsolePermission>()
.addAll(ACCOUNT_MANAGER_PERMISSIONS)
.add(ConsolePermission.REGISTRY_LOCK)
.build();
/** Permissions for the tech contact of a registrar. */
static final ImmutableSet<ConsolePermission> TECH_CONTACT_PERMISSIONS =
new ImmutableSet.Builder<ConsolePermission>()
.addAll(ACCOUNT_MANAGER_WITH_REGISTRY_LOCK_PERMISSIONS)
.add(
ConsolePermission.MANAGE_ACCREDITATION,
ConsolePermission.CONFIGURE_EPP_CONNECTION,
ConsolePermission.CHANGE_NOMULUS_PASSWORD,
ConsolePermission.ONBOARD_ADDITIONAL_PROGRAMS,
ConsolePermission.EXECUTE_EPP_COMMANDS,
ConsolePermission.ACCESS_BILLING_DETAILS,
ConsolePermission.CHECK_ONBOARDING_STATUS,
ConsolePermission.UPLOAD_CONTRACTS,
ConsolePermission.ACCESS_CONTRACTS,
ConsolePermission.VIEW_ACTIVITY_LOG)
.build();
/** Permissions for the primary registrar contact. */
static final ImmutableSet<ConsolePermission> PRIMARY_CONTACT_PERMISSIONS =
new ImmutableSet.Builder<ConsolePermission>()
.addAll(TECH_CONTACT_PERMISSIONS)
.add(ConsolePermission.MANAGE_USERS)
.build();
private ConsoleRoleDefinitions() {}
}
@@ -0,0 +1,44 @@
// Copyright 2022 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.console;
import static google.registry.model.console.ConsoleRoleDefinitions.FTE_PERMISSIONS;
import static google.registry.model.console.ConsoleRoleDefinitions.SUPPORT_AGENT_PERMISSIONS;
import static google.registry.model.console.ConsoleRoleDefinitions.SUPPORT_LEAD_PERMISSIONS;
import com.google.common.collect.ImmutableSet;
/** Roles for registry employees that apply across all registrars. */
public enum GlobalRole {
/** The user has no global role, i.e. they're a registrar partner. */
NONE(ImmutableSet.of()),
/** The user is a registry support agent. */
SUPPORT_AGENT(SUPPORT_AGENT_PERMISSIONS),
/** The user is a registry support lead. */
SUPPORT_LEAD(SUPPORT_LEAD_PERMISSIONS),
/** The user is a registry full-time employee. */
FTE(FTE_PERMISSIONS);
private final ImmutableSet<ConsolePermission> permissions;
GlobalRole(ImmutableSet<ConsolePermission> permissions) {
this.permissions = permissions;
}
public boolean hasPermission(ConsolePermission permission) {
return permissions.contains(permission);
}
}
@@ -0,0 +1,45 @@
// Copyright 2022 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.console;
import static google.registry.model.console.ConsoleRoleDefinitions.ACCOUNT_MANAGER_PERMISSIONS;
import static google.registry.model.console.ConsoleRoleDefinitions.ACCOUNT_MANAGER_WITH_REGISTRY_LOCK_PERMISSIONS;
import static google.registry.model.console.ConsoleRoleDefinitions.PRIMARY_CONTACT_PERMISSIONS;
import static google.registry.model.console.ConsoleRoleDefinitions.TECH_CONTACT_PERMISSIONS;
import com.google.common.collect.ImmutableSet;
/** Roles for registrar partners that apply to only one registrar. */
public enum RegistrarRole {
/** The user is a standard account manager at a registrar. */
ACCOUNT_MANAGER(ACCOUNT_MANAGER_PERMISSIONS),
/** The user is an account manager that can perform registry locks. */
ACCOUNT_MANAGER_WITH_REGISTRY_LOCK(ACCOUNT_MANAGER_WITH_REGISTRY_LOCK_PERMISSIONS),
/** The user is a technical contact of a registrar. */
TECH_CONTACT(TECH_CONTACT_PERMISSIONS),
/** The user is the primary contact at a registrar. */
PRIMARY_CONTACT(PRIMARY_CONTACT_PERMISSIONS);
private final ImmutableSet<ConsolePermission> permissions;
RegistrarRole(ImmutableSet<ConsolePermission> permissions) {
this.permissions = permissions;
}
public boolean hasPermission(ConsolePermission permission) {
return permissions.contains(permission);
}
}
@@ -0,0 +1,170 @@
// Copyright 2022 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.console;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.io.BaseEncoding.base64;
import static google.registry.model.registrar.Registrar.checkValidEmail;
import static google.registry.util.PasswordUtils.SALT_SUPPLIER;
import static google.registry.util.PasswordUtils.hashPassword;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
/** A console user, either a registry employee or a registrar partner. */
@Entity
@Table(
indexes = {
@Index(columnList = "gaiaId", name = "user_gaia_id_idx"),
@Index(columnList = "emailAddress", name = "user_email_address_idx")
})
public class User extends ImmutableObject implements Buildable {
/** Autogenerated unique ID of this user. */
@Id private long id;
/** GAIA ID associated with the user in question. */
@Column(nullable = false)
private String gaiaId;
/** Email address of the user in question. */
@Column(nullable = false)
private String emailAddress;
/** Roles (which grant permissions) associated with this user. */
@Column(nullable = false)
private UserRoles userRoles;
/**
* A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64
* encoded SHA256 string.
*/
String registryLockPasswordHash;
/** Randomly generated hash salt. */
String registryLockPasswordSalt;
public long getId() {
return id;
}
public String getGaiaId() {
return gaiaId;
}
public String getEmailAddress() {
return emailAddress;
}
public UserRoles getUserRoles() {
return userRoles;
}
public boolean hasRegistryLockPassword() {
return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt);
}
public boolean verifyRegistryLockPassword(String registryLockPassword) {
if (isNullOrEmpty(registryLockPassword)
|| isNullOrEmpty(registryLockPasswordSalt)
|| isNullOrEmpty(registryLockPasswordHash)) {
return false;
}
return hashPassword(registryLockPassword, registryLockPasswordSalt)
.equals(registryLockPasswordHash);
}
/**
* Whether the user has the registry lock permission on any registrar or globally.
*
* <p>If so, they should be allowed to (re)set their registry lock password.
*/
public boolean hasAnyRegistryLockPermission() {
if (userRoles == null) {
return false;
}
if (userRoles.isAdmin() || userRoles.hasGlobalPermission(ConsolePermission.REGISTRY_LOCK)) {
return true;
}
return userRoles.getRegistrarRoles().values().stream()
.anyMatch(role -> role.hasPermission(ConsolePermission.REGISTRY_LOCK));
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
/** Builder for constructing immutable {@link User} objects. */
public static class Builder extends Buildable.Builder<User> {
public Builder() {}
public Builder(User user) {
super(user);
}
@Override
public User build() {
checkArgumentNotNull(getInstance().gaiaId, "Gaia ID cannot be null");
checkArgumentNotNull(getInstance().emailAddress, "Email address cannot be null");
checkArgumentNotNull(getInstance().userRoles, "User roles cannot be null");
return super.build();
}
public Builder setGaiaId(String gaiaId) {
checkArgument(!isNullOrEmpty(gaiaId), "Gaia ID cannot be null or empty");
getInstance().gaiaId = gaiaId;
return this;
}
public Builder setEmailAddress(String emailAddress) {
getInstance().emailAddress = checkValidEmail(emailAddress);
return this;
}
public Builder setUserRoles(UserRoles userRoles) {
checkArgumentNotNull(userRoles, "User roles cannot be null");
getInstance().userRoles = userRoles;
return this;
}
public Builder removeRegistryLockPassword() {
getInstance().registryLockPasswordHash = null;
getInstance().registryLockPasswordSalt = null;
return this;
}
public Builder setRegistryLockPassword(String registryLockPassword) {
checkArgument(
getInstance().hasAnyRegistryLockPermission(), "User has no registry lock permission");
checkArgument(
!getInstance().hasRegistryLockPassword(), "User already has a password, remove it first");
checkArgument(
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
getInstance().registryLockPasswordSalt = base64().encode(SALT_SUPPLIER.get());
getInstance().registryLockPasswordHash =
hashPassword(registryLockPassword, getInstance().registryLockPasswordSalt);
return this;
}
}
}
@@ -0,0 +1,128 @@
// Copyright 2022 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.console;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableMap;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
/**
* Contains the global and per-registrar roles for a given user.
*
* <p>See <a href="https://go/nomulus-console-authz">go/nomulus-console-authz</a> for more
* information.
*/
@Embeddable
public class UserRoles extends ImmutableObject implements Buildable {
/** Whether the user is a global admin, who has access to everything. */
@Column(nullable = false)
private boolean isAdmin = false;
/**
* The global role (e.g. {@link GlobalRole#SUPPORT_AGENT}) that the user has across all
* registrars.
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private GlobalRole globalRole = GlobalRole.NONE;
/** Any per-registrar roles that this user may have. */
private Map<String, RegistrarRole> registrarRoles = ImmutableMap.of();
/** Whether the user is a global admin, who has access to everything. */
public boolean isAdmin() {
return isAdmin;
}
/**
* The global role (e.g. {@link GlobalRole#SUPPORT_AGENT}) that the user has across all
* registrars.
*/
public GlobalRole getGlobalRole() {
return globalRole;
}
/** Any per-registrar roles that this user may have. */
public Map<String, RegistrarRole> getRegistrarRoles() {
return ImmutableMap.copyOf(registrarRoles);
}
/** If the user has the given permission either globally or on the given registrar. */
public boolean hasPermission(String registrarId, ConsolePermission permission) {
if (hasGlobalPermission(permission)) {
return true;
}
if (!registrarRoles.containsKey(registrarId)) {
return false;
}
return registrarRoles.get(registrarId).hasPermission(permission);
}
/** If the user has the given permission globally across all registrars. */
public boolean hasGlobalPermission(ConsolePermission permission) {
return isAdmin || globalRole.hasPermission(permission);
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
/** Builder for constructing immutable {@link UserRoles} objects. */
public static class Builder extends Buildable.Builder<UserRoles> {
public Builder() {}
private Builder(UserRoles userRoles) {
super(userRoles);
}
@Override
public UserRoles build() {
// Users should only have a global role or per-registrar roles, not both
checkArgument(
getInstance().globalRole.equals(GlobalRole.NONE)
|| getInstance().registrarRoles.isEmpty(),
"Users cannot have both global and per-registrar roles");
return super.build();
}
public Builder setIsAdmin(boolean isAdmin) {
getInstance().isAdmin = isAdmin;
return this;
}
public Builder setGlobalRole(GlobalRole globalRole) {
checkArgumentNotNull(globalRole, "Global role cannot be null");
getInstance().globalRole = globalRole;
return this;
}
public Builder setRegistrarRoles(Map<String, RegistrarRole> registrarRoles) {
checkArgumentNotNull(registrarRoles, "Registrar roles map cannot be null");
getInstance().registrarRoles = ImmutableMap.copyOf(registrarRoles);
return this;
}
}
}
@@ -14,7 +14,6 @@
package google.registry.model.contact;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.eppcommon.Address;
import javax.persistence.Embeddable;
@@ -30,7 +29,6 @@ import javax.persistence.Embeddable;
*
* @see PostalInfo
*/
@Embed
@Embeddable
public class ContactAddress extends Address {
@@ -19,6 +19,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.EppResourceUtils.projectResourceOntoBuilderAtTime;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.annotation.OnLoad;
@@ -68,7 +69,7 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
* Localized postal info for the contact. All contained values must be representable in the 7-bit
* US-ASCII character set. Personal info; cleared by {@link ContactResource.Builder#wipeOut}.
*/
@IgnoreSave(IfNull.class)
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "name", column = @Column(name = "addr_local_name")),
@@ -96,7 +97,7 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
* Internationalized postal info for the contact. Personal info; cleared by {@link
* ContactResource.Builder#wipeOut}.
*/
@IgnoreSave(IfNull.class)
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "name", column = @Column(name = "addr_i18n_name")),
@@ -16,7 +16,6 @@ package google.registry.model.contact;
import static com.google.common.base.Preconditions.checkState;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.Buildable;
import google.registry.model.Buildable.Overlayable;
import google.registry.model.ImmutableObject;
@@ -36,7 +35,6 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
* Implementation of both "postalInfoType" and "chgPostalInfoType" from <a href=
* "http://tools.ietf.org/html/rfc5733">RFC5733</a>.
*/
@Embed
@Embeddable
@XmlType(propOrder = {"name", "org", "address", "type"})
public class PostalInfo extends ImmutableObject
@@ -0,0 +1,210 @@
// 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 com.googlecode.objectify.Key;
import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
import google.registry.persistence.WithStringVKey;
import java.util.Set;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.OneToMany;
import javax.persistence.PostLoad;
import javax.persistence.Table;
import org.hibernate.Hibernate;
import org.joda.time.DateTime;
/**
* A persistable domain resource including mutable and non-mutable fields.
*
* @see <a href="https://tools.ietf.org/html/rfc5731">RFC 5731</a>
*/
@ReportedOn
@com.googlecode.objectify.annotation.Entity
@Entity
@Table(
name = "Domain",
indexes = {
@Index(columnList = "adminContact"),
@Index(columnList = "autorenewEndTime"),
@Index(columnList = "billingContact"),
@Index(columnList = "creationTime"),
@Index(columnList = "currentSponsorRegistrarId"),
@Index(columnList = "deletionTime"),
@Index(columnList = "domainName"),
@Index(columnList = "techContact"),
@Index(columnList = "tld"),
@Index(columnList = "registrantContact"),
@Index(columnList = "dnsRefreshRequestTime"),
@Index(columnList = "billing_recurrence_id"),
@Index(columnList = "transfer_billing_event_id"),
@Index(columnList = "transfer_billing_recurrence_id")
})
@WithStringVKey
@ExternalMessagingName("domain")
@Access(AccessType.FIELD)
public class Domain extends DomainBase implements ForeignKeyedEppResource {
@Override
@javax.persistence.Id
@Access(AccessType.PROPERTY)
public String getRepoId() {
return super.getRepoId();
}
// It seems like this should be FetchType.EAGER, but for some reason when we do that we get a lazy
// load error during the load of a domain.
@ElementCollection
@JoinTable(
name = "DomainHost",
indexes = {
@Index(columnList = "domain_repo_id,host_repo_id", unique = true),
@Index(columnList = "host_repo_id")
})
@Access(AccessType.PROPERTY)
@Column(name = "host_repo_id")
public Set<VKey<Host>> getNsHosts() {
return nsHosts;
}
/**
* Returns the set of {@link GracePeriod} associated with the domain.
*
* <p>This is the getter method specific for Hibernate to access the field, so it is set to
* private. The caller can use the public {@link #getGracePeriods()} to get the grace periods.
*
* <p>Note that we need to set `insertable = false, updatable = false` for @JoinColumn, otherwise
* Hibernate would try to set the foreign key to null(through an UPDATE TABLE sql) instead of
* deleting the whole entry from the table when the {@link GracePeriod} is removed from the set.
*/
@Access(AccessType.PROPERTY)
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(
name = "domainRepoId",
referencedColumnName = "repoId",
insertable = false,
updatable = false)
@SuppressWarnings("UnusedMethod")
private Set<GracePeriod> getInternalGracePeriods() {
return gracePeriods;
}
/**
* Returns the set of {@link DelegationSignerData} associated with the domain.
*
* <p>This is the getter method specific for Hibernate to access the field, so it is set to
* private. The caller can use the public {@link #getDsData()} to get the DS data.
*
* <p>Note that we need to set `insertable = false, updatable = false` for @JoinColumn, otherwise
* Hibernate would try to set the foreign key to null(through an UPDATE TABLE sql) instead of
* deleting the whole entry from the table when the {@link DelegationSignerData} is removed from
* the set.
*/
@Access(AccessType.PROPERTY)
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(
name = "domainRepoId",
referencedColumnName = "repoId",
insertable = false,
updatable = false)
@SuppressWarnings("UnusedMethod")
private Set<DelegationSignerData> getInternalDelegationSignerData() {
return dsData;
}
/** Post-load method to eager load the collections. */
@PostLoad
protected void postLoad() {
// TODO(b/188044616): Determine why Eager loading doesn't work here.
Hibernate.initialize(dsData);
Hibernate.initialize(gracePeriods);
}
@Override
public VKey<Domain> createVKey() {
return VKey.create(Domain.class, getRepoId(), Key.create(this));
}
@Override
public Domain cloneProjectedAtTime(final DateTime now) {
return cloneDomainProjectedAtTime(this, now);
}
public static VKey<Domain> createVKey(Key<Domain> key) {
return VKey.create(Domain.class, key.getName(), key);
}
/** An override of {@link EppResource#asBuilder} with tighter typing. */
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
/** A builder for constructing {@link Domain}, since it is immutable. */
public static class Builder extends DomainBase.Builder<Domain, Builder> {
public Builder() {}
Builder(Domain instance) {
super(instance);
}
public Builder copyFrom(DomainBase domainBase) {
getInstance().copyUpdateTimestamp(domainBase);
return setAuthInfo(domainBase.getAuthInfo())
.setAutorenewPollMessage(domainBase.getAutorenewPollMessage())
.setAutorenewBillingEvent(domainBase.getAutorenewBillingEvent())
.setAutorenewEndTime(domainBase.getAutorenewEndTime())
.setContacts(domainBase.getContacts())
.setCreationRegistrarId(domainBase.getCreationRegistrarId())
.setCreationTime(domainBase.getCreationTime())
.setDomainName(domainBase.getDomainName())
.setDeletePollMessage(domainBase.getDeletePollMessage())
.setDsData(domainBase.getDsData())
.setDeletionTime(domainBase.getDeletionTime())
.setGracePeriods(domainBase.getGracePeriods())
.setIdnTableName(domainBase.getIdnTableName())
.setLastTransferTime(domainBase.getLastTransferTime())
.setLaunchNotice(domainBase.getLaunchNotice())
.setLastEppUpdateRegistrarId(domainBase.getLastEppUpdateRegistrarId())
.setLastEppUpdateTime(domainBase.getLastEppUpdateTime())
.setNameservers(domainBase.getNameservers())
.setPersistedCurrentSponsorRegistrarId(domainBase.getPersistedCurrentSponsorRegistrarId())
.setRegistrant(domainBase.getRegistrant())
.setRegistrationExpirationTime(domainBase.getRegistrationExpirationTime())
.setRepoId(domainBase.getRepoId())
.setSmdId(domainBase.getSmdId())
.setSubordinateHosts(domainBase.getSubordinateHosts())
.setStatusValues(domainBase.getStatusValues())
.setTransferData(domainBase.getTransferData())
.setDnsRefreshRequestTime(domainBase.getDnsRefreshRequestTime())
.setCurrentPackageToken(domainBase.getCurrentPackageToken().orElse(null));
}
}
}
File diff suppressed because it is too large Load Diff
@@ -38,7 +38,7 @@ import google.registry.model.eppinput.ResourceCommand.ResourceCheck;
import google.registry.model.eppinput.ResourceCommand.ResourceCreateOrChange;
import google.registry.model.eppinput.ResourceCommand.ResourceUpdate;
import google.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import google.registry.model.host.HostResource;
import google.registry.model.host.Host;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.persistence.VKey;
import java.util.Set;
@@ -54,7 +54,7 @@ import javax.xml.bind.annotation.XmlValue;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
/** A collection of {@link DomainBase} commands. */
/** A collection of {@link Domain} commands. */
public class DomainCommand {
/** The default validity period (if not specified) is 1 year for all operations. */
@@ -72,7 +72,7 @@ public class DomainCommand {
/** The fields on "chgType" from <a href="http://tools.ietf.org/html/rfc5731">RFC5731</a>. */
@XmlTransient
public static class DomainCreateOrChange<B extends DomainBase.Builder> extends ImmutableObject
public static class DomainCreateOrChange<B extends Domain.Builder> extends ImmutableObject
implements ResourceCreateOrChange<B> {
/** The contactId of the registrant who registered this domain. */
@@ -100,7 +100,7 @@ public class DomainCommand {
}
/**
* A create command for a {@link DomainBase}, mapping "createType" from <a
* A create command for a {@link Domain}, mapping "createType" from <a
* href="http://tools.ietf.org/html/rfc5731">RFC5731</a>.
*/
@XmlRootElement
@@ -113,7 +113,7 @@ public class DomainCommand {
"foreignKeyedDesignatedContacts",
"authInfo"
})
public static class Create extends DomainCreateOrChange<DomainBase.Builder>
public static class Create extends DomainCreateOrChange<Domain.Builder>
implements CreateOrUpdate<Create> {
/** Fully qualified domain name, which serves as a unique identifier for this domain. */
@@ -126,7 +126,7 @@ public class DomainCommand {
Set<String> nameserverFullyQualifiedHostNames;
/** Resolved keys to hosts that are the nameservers for the domain. */
@XmlTransient Set<VKey<HostResource>> nameservers;
@XmlTransient Set<VKey<Host>> nameservers;
/** Foreign keyed associated contacts for the domain (other than registrant). */
@XmlElement(name = "contact")
@@ -156,7 +156,7 @@ public class DomainCommand {
return nullToEmptyImmutableCopy(nameserverFullyQualifiedHostNames);
}
public ImmutableSet<VKey<HostResource>> getNameservers() {
public ImmutableSet<VKey<Host>> getNameservers() {
return nullToEmptyImmutableCopy(nameservers);
}
@@ -196,11 +196,11 @@ public class DomainCommand {
}
}
/** A delete command for a {@link DomainBase}. */
/** A delete command for a {@link Domain}. */
@XmlRootElement
public static class Delete extends AbstractSingleResourceCommand {}
/** An info request for a {@link DomainBase}. */
/** An info request for a {@link Domain}. */
@XmlRootElement
public static class Info extends ImmutableObject implements SingleResourceCommand {
@@ -259,11 +259,11 @@ public class DomainCommand {
}
}
/** A check request for {@link DomainBase}. */
/** A check request for {@link Domain}. */
@XmlRootElement
public static class Check extends ResourceCheck {}
/** A renew command for a {@link DomainBase}. */
/** A renew command for a {@link Domain}. */
@XmlRootElement
public static class Renew extends AbstractSingleResourceCommand {
@XmlElement(name = "curExpDate")
@@ -281,7 +281,7 @@ public class DomainCommand {
}
}
/** A transfer operation for a {@link DomainBase}. */
/** A transfer operation for a {@link Domain}. */
@XmlRootElement
public static class Transfer extends AbstractSingleResourceCommand {
/** The period to extend this domain's registration upon completion of the transfer. */
@@ -300,11 +300,10 @@ public class DomainCommand {
}
}
/** An update to a {@link DomainBase}. */
/** An update to a {@link Domain}. */
@XmlRootElement
@XmlType(propOrder = {"targetId", "innerAdd", "innerRemove", "innerChange"})
public static class Update
extends ResourceUpdate<Update.AddRemove, DomainBase.Builder, Update.Change>
public static class Update extends ResourceUpdate<Update.AddRemove, Domain.Builder, Update.Change>
implements CreateOrUpdate<Update> {
@XmlElement(name = "chg")
@@ -350,7 +349,7 @@ public class DomainCommand {
Set<String> nameserverFullyQualifiedHostNames;
/** Resolved keys to hosts that are the nameservers for the domain. */
@XmlTransient Set<VKey<HostResource>> nameservers;
@XmlTransient Set<VKey<Host>> nameservers;
/** Foreign keyed associated contacts for the domain (other than registrant). */
@XmlElement(name = "contact")
@@ -364,7 +363,7 @@ public class DomainCommand {
return nullSafeImmutableCopy(nameserverFullyQualifiedHostNames);
}
public ImmutableSet<VKey<HostResource>> getNameservers() {
public ImmutableSet<VKey<Host>> getNameservers() {
return nullToEmptyImmutableCopy(nameservers);
}
@@ -383,7 +382,7 @@ public class DomainCommand {
/** The inner change type on a domain update command. */
@XmlType(propOrder = {"registrantContactId", "authInfo"})
public static class Change extends DomainCreateOrChange<DomainBase.Builder> {
public static class Change extends DomainCreateOrChange<Domain.Builder> {
/** Creates a copy of this {@link Change} with hard links to hosts and contacts. */
Change cloneAndLinkReferences(DateTime now) throws InvalidReferencesException {
Change clone = clone(this);
@@ -414,13 +413,13 @@ public class DomainCommand {
}
}
private static Set<VKey<HostResource>> linkHosts(
Set<String> fullyQualifiedHostNames, DateTime now) throws InvalidReferencesException {
private static Set<VKey<Host>> linkHosts(Set<String> fullyQualifiedHostNames, DateTime now)
throws InvalidReferencesException {
if (fullyQualifiedHostNames == null) {
return null;
}
return ImmutableSet.copyOf(
loadByForeignKeysCached(fullyQualifiedHostNames, HostResource.class, now).values());
loadByForeignKeysCached(fullyQualifiedHostNames, Host.class, now).values());
}
private static Set<DesignatedContact> linkContacts(
@@ -1,947 +0,0 @@
// 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.MoreObjects.firstNonNull;
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.persistence.transaction.TransactionManagerFactory.tm;
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.canonicalizeHostname;
import static google.registry.util.DomainNameUtils.getTldFromDomainName;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.base.Predicate;
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.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.condition.IfNull;
import google.registry.dns.RefreshDnsAction;
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.common.EntityGroupRoot;
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.reporting.HistoryEntry;
import google.registry.model.tld.Registry;
import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import google.registry.util.CollectionUtils;
import google.registry.util.DateTimeUtils;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
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.Embeddable;
import javax.persistence.Embedded;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;
import org.hibernate.collection.internal.PersistentSet;
import org.joda.time.DateTime;
import org.joda.time.Interval;
/**
* A persistable domain resource including mutable and non-mutable fields.
*
* <p>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.
*
* <p>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 <a href="https://tools.ietf.org/html/rfc5731">RFC 5731</a>
*/
@MappedSuperclass
@Embeddable
@Access(AccessType.FIELD)
public class DomainContent extends EppResource
implements ResourceWithTransferData<DomainTransferData> {
/** 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(Locale.ENGLISH)
*/
// TODO(b/177567432): 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. */
@EmptySetToNull @Index @Transient Set<VKey<HostResource>> nsHosts;
/** Contacts. */
VKey<ContactResource> adminContact;
VKey<ContactResource> billingContact;
VKey<ContactResource> techContact;
VKey<ContactResource> 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. */
@Transient Set<DelegationSignerData> dsData;
/**
* The claims notice supplied when this domain was created, if there was one.
*
* <p>It's {@literal @}XmlTransient because it's not returned in an info response.
*/
@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<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.
*/
@Column(name = "deletion_poll_message_id")
VKey<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.
*/
@Column(name = "billing_recurrence_id")
VKey<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.
*/
@Column(name = "autorenew_poll_message_id")
VKey<PollMessage.Autorenew> autorenewPollMessage;
/**
* History record for the autorenew poll message.
*
* <p>Here so we can restore the original ofy key from sql.
*/
@Ignore Long autorenewPollMessageHistoryId;
/** The unexpired grace periods for this domain (some of which may not be active yet). */
@Transient 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. */
DomainTransferData transferData;
/**
* The time that this resource was last transferred.
*
* <p>Can be null if the resource has never been transferred.
*/
DateTime lastTransferTime;
/**
* When the domain's autorenewal status will expire.
*
* <p>This will be {@link DateTimeUtils#END_OF_TIME} for the vast majority of domains because all
* domains autorenew indefinitely by default and autorenew can only be countermanded by
* administrators, typically for reasons of the URS process or termination of a registrar for
* nonpayment.
*
* <p>When a domain is scheduled to not autorenew, this field is set to the current value of its
* {@link #registrationExpirationTime}, after which point the next invocation of a periodic
* cronjob will explicitly delete the domain. This field is a DateTime and not a boolean because
* of edge cases that occur during the autorenew grace period. We need to be able to tell the
* difference domains that have reached their life and must be deleted now, and domains that
* happen to be in the autorenew grace period now but should be deleted in roughly a year.
*/
@Index DateTime autorenewEndTime;
/**
* When this domain's DNS was requested to be refreshed, or null if its DNS is up-to-date.
*
* <p>This will almost always be null except in the couple minutes' interval between when a
* DNS-affecting create or update operation takes place and when the {@link RefreshDnsAction}
* runs, which resets this back to null upon completion of the DNS refresh task. This is a {@link
* DateTime} rather than a simple dirty boolean so that the DNS refresh action can order by the
* DNS refresh request time and take action on the oldest ones first.
*
* <p>Note that this is a Cloud SQL-based replacement for the {@code dns-pull} task queue. The
* domains that have a non-null value for this field should be exactly the same as the tasks that
* would be in the {@code dns-pull} queue. Because this is Cloud SQL-specific, it is omitted from
* Datastore.
*
* <p>Note that in the {@link DomainHistory} table this value means something slightly different:
* It means that the given domain action requested a DNS update. Unlike on the {@code Domain}
* table, this value is not then subsequently nulled out once the DNS refresh is complete; rather,
* it remains as a permanent record of which actions were DNS-affecting and which were not.
*/
// TODO(mcilwain): Start using this field once we are further along in the DB migration.
@Ignore DateTime dnsRefreshRequestTime;
/**
* Returns the DNS refresh request time iff this domain's DNS needs refreshing, otherwise absent.
*/
public Optional<DateTime> getDnsRefreshRequestTime() {
return Optional.ofNullable(dnsRefreshRequestTime);
}
public static <T> VKey<T> restoreOfyFrom(Key<DomainBase> domainKey, VKey<T> key, Long historyId) {
if (historyId == null) {
// This is a legacy key (or a null key, in which case this works too)
return VKey.restoreOfyFrom(key, EntityGroupRoot.class, "per-tld");
} else {
return VKey.restoreOfyFrom(key, domainKey, HistoryEntry.class, historyId);
}
}
public ImmutableSet<String> getSubordinateHosts() {
return nullToEmptyImmutableCopy(subordinateHosts);
}
public DateTime getRegistrationExpirationTime() {
return registrationExpirationTime;
}
public VKey<PollMessage.OneTime> getDeletePollMessage() {
return deletePollMessage;
}
public VKey<BillingEvent.Recurring> getAutorenewBillingEvent() {
return autorenewBillingEvent;
}
public VKey<PollMessage.Autorenew> getAutorenewPollMessage() {
return autorenewPollMessage;
}
public Long getAutorenewPollMessageHistoryId() {
return autorenewPollMessageHistoryId;
}
public ImmutableSet<GracePeriod> getGracePeriods() {
return nullToEmptyImmutableCopy(gracePeriods);
}
public String getSmdId() {
return smdId;
}
/**
* Returns the autorenew end time if there is one, otherwise empty.
*
* <p>Note that {@link DateTimeUtils#END_OF_TIME} is used as a sentinel value in the database
* representation to signify that autorenew doesn't end, and is mapped to empty here for the
* purposes of more legible business logic.
*/
public Optional<DateTime> getAutorenewEndTime() {
return Optional.ofNullable(autorenewEndTime.equals(END_OF_TIME) ? null : autorenewEndTime);
}
@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<DelegationSignerData> getDsData() {
return nullToEmptyImmutableCopy(dsData);
}
public LaunchNotice getLaunchNotice() {
return launchNotice;
}
public String getIdnTableName() {
return idnTableName;
}
public ImmutableSet<VKey<HostResource>> 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<VKey<HostResource>> nsHosts) {
this.nsHosts = forceEmptyToNull(nsHosts);
}
// Note: for the two methods below, how we wish to treat the Hibernate setters depends on the
// current state of the object and what's passed in. The key principle is that we wish to maintain
// the link between parent and child objects, meaning that we should keep around whichever of the
// two sets (the parameter vs the class variable and clear/populate that as appropriate.
//
// If the class variable is a PersistentSet and we overwrite it here, Hibernate will throw
// an exception "A collection with cascade=”all-delete-orphan” was no longer referenced by the
// owning entity instance". See https://stackoverflow.com/questions/5587482 for more details.
// Hibernate needs this in order to populate gracePeriods but no one else should ever use it
@SuppressWarnings("UnusedMethod")
private void setInternalGracePeriods(Set<GracePeriod> gracePeriods) {
if (this.gracePeriods instanceof PersistentSet) {
Set<GracePeriod> nonNullGracePeriods = nullToEmpty(gracePeriods);
this.gracePeriods.retainAll(nonNullGracePeriods);
this.gracePeriods.addAll(nonNullGracePeriods);
} else {
this.gracePeriods = gracePeriods;
}
}
// Hibernate needs this in order to populate dsData but no one else should ever use it
@SuppressWarnings("UnusedMethod")
private void setInternalDelegationSignerData(Set<DelegationSignerData> dsData) {
if (this.dsData instanceof PersistentSet) {
Set<DelegationSignerData> nonNullDsData = nullToEmpty(dsData);
this.dsData.retainAll(nonNullDsData);
this.dsData.addAll(nonNullDsData);
} else {
this.dsData = dsData;
}
}
public final String getCurrentSponsorRegistrarId() {
return getPersistedCurrentSponsorRegistrarId();
}
/** 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();
}
@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.
*/
static <T extends DomainContent> 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(),
transferData.getServerApproveAutorenewPollMessageHistoryId());
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,
domain.getRepoId(),
transferExpirationTime.plus(
Registry.get(domain.getTld()).getTransferGracePeriodLength()),
transferData.getGainingRegistrarId(),
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)
.setLastEppUpdateRegistrarId(transferData.getGainingRegistrarId());
// Finish projecting to now.
return (T) builder.build().cloneProjectedAtTime(now);
}
Optional<DateTime> 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,
domain.getRepoId(),
lastAutorenewTime.plus(
Registry.get(domain.getTld()).getAutoRenewGracePeriodLength()),
domain.getCurrentSponsorRegistrarId(),
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())
.setLastEppUpdateRegistrarId(domain.getCurrentSponsorRegistrarId());
}
}
// 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<String> loadNameserverHostNames() {
return tm().transact(
() ->
tm().loadByKeys(getNameservers()).values().stream()
.map(HostResource::getHostName)
.collect(toImmutableSortedSet(Ordering.natural())));
}
/** A key to the registrant who registered this domain. */
public VKey<ContactResource> getRegistrant() {
return registrantContact;
}
public VKey<ContactResource> getAdminContact() {
return adminContact;
}
public VKey<ContactResource> getBillingContact() {
return billingContact;
}
public VKey<ContactResource> getTechContact() {
return techContact;
}
/** Associated contacts for the domain (other than registrant). */
public ImmutableSet<DesignatedContact> getContacts() {
return getAllContacts(false);
}
public DomainAuthInfo getAuthInfo() {
return authInfo;
}
/** Returns all referenced contacts from this domain. */
public ImmutableSet<VKey<ContactResource>> getReferencedContacts() {
return nullToEmptyImmutableCopy(getAllContacts(true)).stream()
.map(DesignatedContact::getContactKey)
.filter(Objects::nonNull)
.collect(toImmutableSet());
}
private ImmutableSet<DesignatedContact> getAllContacts(boolean includeRegistrant) {
ImmutableSet.Builder<DesignatedContact> builder = new ImmutableSet.Builder<>();
if (includeRegistrant && registrantContact != null) {
builder.add(DesignatedContact.create(DesignatedContact.Type.REGISTRANT, registrantContact));
}
if (adminContact != null) {
builder.add(DesignatedContact.create(DesignatedContact.Type.ADMIN, adminContact));
}
if (billingContact != null) {
builder.add(DesignatedContact.create(DesignatedContact.Type.BILLING, billingContact));
}
if (techContact != null) {
builder.add(DesignatedContact.create(DesignatedContact.Type.TECH, techContact));
}
return builder.build();
}
public String getTld() {
return tld;
}
/**
* Sets the individual contact fields from {@code contacts}.
*
* <p>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.
*/
void setContactFields(Set<DesignatedContact> contacts, boolean includeRegistrant) {
// Set the individual contact fields.
billingContact = techContact = adminContact = null;
if (includeRegistrant) {
registrantContact = null;
}
HashSet<DesignatedContact.Type> contactsDiscovered = new HashSet<DesignatedContact.Type>();
for (DesignatedContact contact : contacts) {
checkArgument(
!contactsDiscovered.contains(contact.getType()),
"Duplicate contact type %s in designated contact set.",
contact.getType());
contactsDiscovered.add(contact.getType());
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<DomainBase> createVKey() {
throw new UnsupportedOperationException(
"DomainContent is not an actual persisted entity you can create a key to;"
+ " use DomainBase instead");
}
/** Predicate to determine if a given {@link DesignatedContact} is the registrant. */
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<? extends DomainContent, ?> asBuilder() {
return new Builder<>(clone(this));
}
/** A builder for constructing {@link DomainBase}, since it is immutable. */
public static class Builder<T extends DomainContent, B extends Builder<T, B>>
extends EppResource.Builder<T, B> implements BuilderWithTransferData<DomainTransferData, B> {
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);
}
// If there is no autorenew end time, set it to END_OF_TIME.
instance.autorenewEndTime = firstNonNull(getInstance().autorenewEndTime, END_OF_TIME);
checkArgumentNotNull(emptyToNull(instance.fullyQualifiedDomainName), "Missing domainName");
checkArgumentNotNull(instance.getRegistrant(), "Missing registrant");
instance.tld = getTldFromDomainName(instance.fullyQualifiedDomainName);
T newDomain = super.build();
// Hibernate throws exception if gracePeriods or dsData is null because we enabled all
// cascadable operations and orphan removal.
newDomain.gracePeriods =
newDomain.gracePeriods == null ? ImmutableSet.of() : newDomain.gracePeriods;
newDomain.dsData =
newDomain.dsData == null
? ImmutableSet.of()
: newDomain.dsData.stream()
.map(ds -> ds.cloneWithDomainRepoId(instance.getRepoId()))
.collect(toImmutableSet());
return newDomain;
}
public B setDomainName(String domainName) {
checkArgument(
domainName.equals(canonicalizeHostname(domainName)),
"Domain name %s not in puny-coded, lower-case form",
domainName);
getInstance().fullyQualifiedDomainName = domainName;
return thisCastToDerived();
}
public B setDsData(ImmutableSet<DelegationSignerData> dsData) {
getInstance().dsData = dsData;
getInstance().resetUpdateTimestamp();
return thisCastToDerived();
}
public B setRegistrant(VKey<ContactResource> 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<HostResource> nameserver) {
getInstance().nsHosts = ImmutableSet.of(nameserver);
getInstance().resetUpdateTimestamp();
return thisCastToDerived();
}
public B setNameservers(ImmutableSet<VKey<HostResource>> nameservers) {
getInstance().nsHosts = forceEmptyToNull(nameservers);
getInstance().resetUpdateTimestamp();
return thisCastToDerived();
}
public B addNameserver(VKey<HostResource> nameserver) {
return addNameservers(ImmutableSet.of(nameserver));
}
public B addNameservers(ImmutableSet<VKey<HostResource>> nameservers) {
return setNameservers(
ImmutableSet.copyOf(Sets.union(getInstance().getNameservers(), nameservers)));
}
public B removeNameserver(VKey<HostResource> nameserver) {
return removeNameservers(ImmutableSet.of(nameserver));
}
public B removeNameservers(ImmutableSet<VKey<HostResource>> nameservers) {
return setNameservers(
ImmutableSet.copyOf(difference(getInstance().getNameservers(), nameservers)));
}
public B setContacts(DesignatedContact contact) {
return setContacts(ImmutableSet.of(contact));
}
public B setContacts(ImmutableSet<DesignatedContact> contacts) {
checkArgument(contacts.stream().noneMatch(IS_REGISTRANT), "Registrant cannot be a contact");
// Set the individual fields.
getInstance().setContactFields(contacts, false);
return thisCastToDerived();
}
public B addContacts(ImmutableSet<DesignatedContact> contacts) {
return setContacts(ImmutableSet.copyOf(Sets.union(getInstance().getContacts(), contacts)));
}
public B removeContacts(ImmutableSet<DesignatedContact> 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<String> 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<PollMessage.OneTime> deletePollMessage) {
getInstance().deletePollMessage = deletePollMessage;
return thisCastToDerived();
}
public B setAutorenewBillingEvent(VKey<BillingEvent.Recurring> autorenewBillingEvent) {
getInstance().autorenewBillingEvent = autorenewBillingEvent;
return thisCastToDerived();
}
public B setAutorenewPollMessage(
@Nullable VKey<PollMessage.Autorenew> autorenewPollMessage,
@Nullable Long autorenewPollMessageHistoryId) {
getInstance().autorenewPollMessage = autorenewPollMessage;
getInstance().autorenewPollMessageHistoryId = autorenewPollMessageHistoryId;
return thisCastToDerived();
}
public B setDnsRefreshRequestTime(Optional<DateTime> dnsRefreshRequestTime) {
getInstance().dnsRefreshRequestTime = dnsRefreshRequestTime.orElse(null);
return thisCastToDerived();
}
public B setSmdId(String smdId) {
getInstance().smdId = smdId;
return thisCastToDerived();
}
public B setGracePeriods(ImmutableSet<GracePeriod> gracePeriods) {
getInstance().gracePeriods = gracePeriods;
getInstance().resetUpdateTimestamp();
return thisCastToDerived();
}
public B addGracePeriod(GracePeriod gracePeriod) {
getInstance().gracePeriods = union(getInstance().getGracePeriods(), gracePeriod);
getInstance().resetUpdateTimestamp();
return thisCastToDerived();
}
public B removeGracePeriod(GracePeriod gracePeriod) {
getInstance().gracePeriods =
CollectionUtils.difference(getInstance().getGracePeriods(), gracePeriod);
getInstance().resetUpdateTimestamp();
return thisCastToDerived();
}
/**
* Sets the autorenew end time, or clears it if empty is passed.
*
* <p>Note that {@link DateTimeUtils#END_OF_TIME} is used as a sentinel value in the database
* representation to signify that autorenew doesn't end, and is mapped to empty here for the
* purposes of more legible business logic.
*/
public B setAutorenewEndTime(Optional<DateTime> autorenewEndTime) {
getInstance().autorenewEndTime = autorenewEndTime.orElse(END_OF_TIME);
return thisCastToDerived();
}
@Override
public B setTransferData(DomainTransferData transferData) {
getInstance().transferData = transferData;
return thisCastToDerived();
}
@Override
public B setLastTransferTime(DateTime lastTransferTime) {
getInstance().lastTransferTime = lastTransferTime;
return thisCastToDerived();
}
}
}
@@ -26,7 +26,7 @@ import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.GracePeriod.GracePeriodHistory;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.domain.secdns.DomainDsDataHistory;
import google.registry.model.host.HostResource;
import google.registry.model.host.Host;
import google.registry.model.reporting.DomainTransactionRecord;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
@@ -59,8 +59,8 @@ import org.hibernate.Hibernate;
* A persisted history entry representing an EPP modification to a domain.
*
* <p>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.
* copy of the domain entity at this point in time. We persist a raw {@link DomainBase} so that the
* foreign-keyed fields in that class can refer to this object.
*
* <p>This class is only marked as a Datastore entity subclass and registered with Objectify so that
* when building it its ID can be auto-populated by Objectify. It is converted to its superclass
@@ -80,9 +80,9 @@ import org.hibernate.Hibernate;
@IdClass(DomainHistoryId.class)
public class DomainHistory extends HistoryEntry {
// Store DomainContent instead of DomainBase so we don't pick up its @Id
// Store DomainBase instead of Domain so we don't pick up its @Id
// Nullable for the sake of pre-Registry-3.0 history objects
@DoNotCompare @Nullable DomainContent domainContent;
@DoNotCompare @Nullable DomainBase domainBase;
@Id
@Access(AccessType.PROPERTY)
@@ -95,12 +95,12 @@ public class DomainHistory extends HistoryEntry {
/** This method is private because it is only used by Hibernate. */
@SuppressWarnings("unused")
private void setDomainRepoId(String domainRepoId) {
parent = Key.create(DomainBase.class, domainRepoId);
parent = Key.create(Domain.class, domainRepoId);
}
// We could have reused domainContent.nsHosts here, but Hibernate throws a weird exception after
// We could have reused domainBase.nsHosts here, but Hibernate throws a weird exception after
// we change to use a composite primary key.
// TODO(b/166776754): Investigate if we can reuse domainContent.nsHosts for storing host keys.
// TODO(b/166776754): Investigate if we can reuse domainBase.nsHosts for storing host keys.
@DoNotCompare
@ElementCollection
@JoinTable(
@@ -113,7 +113,7 @@ public class DomainHistory extends HistoryEntry {
})
@ImmutableObject.EmptySetToNull
@Column(name = "host_repo_id")
Set<VKey<HostResource>> nsHosts;
Set<VKey<Host>> nsHosts;
@DoNotCompare
@OneToMany(
@@ -221,8 +221,8 @@ public class DomainHistory extends HistoryEntry {
return new DomainHistoryId(getDomainRepoId(), getId());
}
/** Returns keys to the {@link HostResource} that are the nameservers for the domain. */
public Set<VKey<HostResource>> getNsHosts() {
/** Returns keys to the {@link Host} that are the nameservers for the domain. */
public Set<VKey<Host>> getNsHosts() {
return nsHosts;
}
@@ -232,18 +232,18 @@ public class DomainHistory extends HistoryEntry {
}
/**
* The values of all the fields on the {@link DomainContent} object after the action represented
* by this history object was executed.
* The values of all the fields on the {@link DomainBase} object after the action represented by
* this history object was executed.
*
* <p>Will be absent for objects created prior to the Registry 3.0 SQL migration.
*/
public Optional<DomainContent> getDomainContent() {
return Optional.ofNullable(domainContent);
public Optional<DomainBase> getDomainBase() {
return Optional.ofNullable(domainBase);
}
/** The key to the {@link DomainBase} this is based off of. */
public VKey<DomainBase> getParentVKey() {
return VKey.create(DomainBase.class, getDomainRepoId());
/** The key to the {@link Domain} this is based off of. */
public VKey<Domain> getParentVKey() {
return VKey.create(Domain.class, getDomainRepoId());
}
public Set<GracePeriodHistory> getGracePeriodHistories() {
@@ -259,8 +259,7 @@ public class DomainHistory extends HistoryEntry {
@Override
public Optional<? extends EppResource> getResourceAtPointInTime() {
return getDomainContent()
.map(domainContent -> new DomainBase.Builder().copyFrom(domainContent).build());
return getDomainBase().map(domainBase -> new Domain.Builder().copyFrom(domainBase).build());
}
@PostLoad
@@ -271,39 +270,39 @@ public class DomainHistory extends HistoryEntry {
Hibernate.initialize(dsDataHistories);
Hibernate.initialize(gracePeriodHistories);
if (domainContent != null) {
domainContent.nsHosts = nullToEmptyImmutableCopy(nsHosts);
domainContent.gracePeriods =
if (domainBase != null) {
domainBase.nsHosts = nullToEmptyImmutableCopy(nsHosts);
domainBase.gracePeriods =
gracePeriodHistories.stream()
.map(GracePeriod::createFromHistory)
.collect(toImmutableSet());
domainContent.dsData =
domainBase.dsData =
dsDataHistories.stream().map(DelegationSignerData::create).collect(toImmutableSet());
// Normally Hibernate would see that the domain fields are all null and would fill
// domainContent with a null object. Unfortunately, the updateTimestamp is never null in SQL.
if (domainContent.getDomainName() == null) {
domainContent = null;
// domainBase with a null object. Unfortunately, the updateTimestamp is never null in SQL.
if (domainBase.getDomainName() == null) {
domainBase = null;
} else {
if (domainContent.getRepoId() == null) {
// domainContent still hasn't been fully constructed yet, so it's ok to go in and mutate
if (domainBase.getRepoId() == null) {
// domainBase still hasn't been fully constructed yet, so it's ok to go in and mutate
// it. In fact, we have to because going through the builder causes the hash codes of
// contained objects to be calculated prematurely.
domainContent.setRepoId(parent.getName());
domainBase.setRepoId(parent.getName());
}
}
}
}
private static void fillAuxiliaryFieldsFromDomain(DomainHistory domainHistory) {
if (domainHistory.domainContent != null) {
domainHistory.nsHosts = nullToEmptyImmutableCopy(domainHistory.domainContent.nsHosts);
if (domainHistory.domainBase != null) {
domainHistory.nsHosts = nullToEmptyImmutableCopy(domainHistory.domainBase.nsHosts);
domainHistory.dsDataHistories =
nullToEmptyImmutableCopy(domainHistory.domainContent.getDsData()).stream()
nullToEmptyImmutableCopy(domainHistory.domainBase.getDsData()).stream()
.filter(dsData -> dsData.getDigest() != null && dsData.getDigest().length > 0)
.map(dsData -> DomainDsDataHistory.createFrom(domainHistory.id, dsData))
.collect(toImmutableSet());
domainHistory.gracePeriodHistories =
nullToEmptyImmutableCopy(domainHistory.domainContent.getGracePeriods()).stream()
nullToEmptyImmutableCopy(domainHistory.domainBase.getGracePeriods()).stream()
.map(gracePeriod -> GracePeriodHistory.createFrom(domainHistory.id, gracePeriod))
.collect(toImmutableSet());
} else {
@@ -372,42 +371,42 @@ public class DomainHistory extends HistoryEntry {
super(instance);
}
public Builder setDomain(@Nullable DomainContent domainContent) {
public Builder setDomain(@Nullable DomainBase domainBase) {
// Nullable for the sake of pre-Registry-3.0 history objects
if (domainContent == null) {
if (domainBase == null) {
return this;
}
// TODO(b/203609982): if actual type of domainContent is DomainBase, convert to DomainContent
// Note: a DomainHistory fetched by JPA has DomainContent in this field. Allowing DomainBase
// TODO(b/203609982): if actual type of domainBase is Domain, convert to DomainBase
// Note: a DomainHistory fetched by JPA has DomainBase in this field. Allowing Domain
// in the setter makes equality checks messy.
getInstance().domainContent = domainContent;
if (domainContent instanceof DomainBase) {
super.setParent(domainContent);
getInstance().domainBase = domainBase;
if (domainBase instanceof Domain) {
super.setParent(domainBase);
} else {
super.setParent(Key.create(DomainBase.class, domainContent.getRepoId()));
super.setParent(Key.create(Domain.class, domainBase.getRepoId()));
}
return this;
}
public Builder setDomainRepoId(String domainRepoId) {
getInstance().parent = Key.create(DomainBase.class, domainRepoId);
getInstance().parent = Key.create(Domain.class, domainRepoId);
return this;
}
@Override
public DomainHistory build() {
DomainHistory instance = super.build();
// TODO(b/171990736): Assert instance.domainContent is not null after database migration.
// Note that we cannot assert that instance.domainContent is not null here because this
// TODO(b/171990736): Assert instance.domainBase is not null after database migration.
// Note that we cannot assert that instance.domainBase is not null here because this
// builder is also used to convert legacy HistoryEntry objects to DomainHistory, when
// domainContent is not available.
// domainBase is not available.
fillAuxiliaryFieldsFromDomain(instance);
return instance;
}
public DomainHistory buildAndAssemble(
ImmutableSet<DomainDsDataHistory> dsDataHistories,
ImmutableSet<VKey<HostResource>> domainHistoryHosts,
ImmutableSet<VKey<Host>> domainHistoryHosts,
ImmutableSet<GracePeriodHistory> gracePeriodHistories,
ImmutableSet<DomainTransactionRecord> transactionRecords) {
DomainHistory instance = super.build();
@@ -37,8 +37,8 @@ import org.joda.time.DateTime;
/**
* A domain grace period with an expiration time.
*
* <p>When a grace period expires, it is lazily removed from the {@link DomainBase} the next time
* the resource is loaded from Datastore.
* <p>When a grace period expires, it is lazily removed from the {@link Domain} the next time the
* resource is loaded from Datastore.
*/
@Embed
@Entity
@@ -65,6 +65,7 @@ public class GracePeriodBase extends ImmutableObject implements UnsafeSerializab
// NB: Would @IgnoreSave(IfNull.class), but not allowed for @Embed collections.
@Access(AccessType.FIELD)
@Column(name = "billing_event_id")
@Ignore
VKey<BillingEvent.OneTime> billingEventOneTime = null;
/**
@@ -74,6 +75,7 @@ public class GracePeriodBase extends ImmutableObject implements UnsafeSerializab
// NB: Would @IgnoreSave(IfNull.class), but not allowed for @Embed collections.
@Access(AccessType.FIELD)
@Column(name = "billing_recurrence_id")
@Ignore
VKey<BillingEvent.Recurring> billingEventRecurring = null;
public long getGracePeriodId() {
@@ -103,8 +103,12 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
ANCHOR_TENANT
}
/** Single-use tokens are invalid after use. Infinite-use tokens, predictably, are not. */
/**
* Single-use tokens are invalid after use. Infinite-use tokens, predictably, are not. Package
* tokens are used in package promotions.
*/
public enum TokenType {
PACKAGE,
SINGLE_USE,
UNLIMITED_USE
}
@@ -168,6 +172,7 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
@Enumerated(EnumType.STRING)
@Column(name = "renewalPriceBehavior", nullable = false)
@Ignore
RenewalPriceBehavior renewalPriceBehavior = RenewalPriceBehavior.DEFAULT;
@Enumerated(EnumType.STRING)
@@ -273,6 +278,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
public AllocationToken build() {
checkArgumentNotNull(getInstance().tokenType, "Token type must be specified");
checkArgument(!Strings.isNullOrEmpty(getInstance().token), "Token must not be null or empty");
checkArgument(
!getInstance().tokenType.equals(TokenType.PACKAGE)
|| getInstance().renewalPriceBehavior.equals(RenewalPriceBehavior.SPECIFIED),
"Package tokens must have renewalPriceBehavior set to SPECIFIED");
checkArgument(
getInstance().domainName == null || TokenType.SINGLE_USE.equals(getInstance().tokenType),
"Domain name can only be specified for SINGLE_USE tokens");
@@ -280,6 +289,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
getInstance().redemptionHistoryEntry == null
|| TokenType.SINGLE_USE.equals(getInstance().tokenType),
"Redemption history entry can only be specified for SINGLE_USE tokens");
checkArgument(
getInstance().tokenType != TokenType.PACKAGE
|| getInstance().allowedClientIds.size() == 1,
"PACKAGE tokens must have exactly one allowed client registrar");
checkArgument(
getInstance().discountFraction > 0 || !getInstance().discountPremiums,
"Discount premiums can only be specified along with a discount fraction");
@@ -0,0 +1,157 @@
// Copyright 2022 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.token;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.persistence.VKey;
import google.registry.persistence.converter.JodaMoneyType;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.annotations.Columns;
import org.hibernate.annotations.Type;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** An entity representing a package promotion. */
@Entity
@javax.persistence.Table(indexes = {@javax.persistence.Index(columnList = "token")})
public class PackagePromotion extends ImmutableObject implements Buildable {
/** An autogenerated identifier for the package promotion. */
@Id long packagePromotionId;
/** The allocation token string for the package. */
@Column(nullable = false)
VKey<AllocationToken> token;
/** The maximum number of active domains the package allows at any given time. */
@Column(nullable = false)
int maxDomains;
/** The maximum number of domains that can be created in the package each year. */
@Column(nullable = false)
int maxCreates;
/** The annual price of the package. */
@Type(type = JodaMoneyType.TYPE_NAME)
@Columns(
columns = {
@Column(name = "package_price_amount", nullable = false),
@Column(name = "package_price_currency", nullable = false)
})
Money packagePrice;
/** The next billing date of the package. */
@Column(nullable = false)
DateTime nextBillingDate = END_OF_TIME;
/** Date the last warning email was sent that the package has exceeded the maxDomains limit. */
@Nullable DateTime lastNotificationSent;
public VKey<AllocationToken> getToken() {
return token;
}
public int getMaxDomains() {
return maxDomains;
}
public int getMaxCreates() {
return maxCreates;
}
public Money getPackagePrice() {
return packagePrice;
}
public DateTime getNextBillingDate() {
return nextBillingDate;
}
public Optional<DateTime> getLastNotificationSent() {
return Optional.ofNullable(lastNotificationSent);
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
/** A builder for constructing {@link PackagePromotion} objects, since they are immutable. */
public static class Builder extends Buildable.Builder<PackagePromotion> {
public Builder() {}
private Builder(PackagePromotion instance) {
super(instance);
}
@Override
public PackagePromotion build() {
checkArgumentNotNull(getInstance().token, "Allocation token must be specified");
AllocationToken allocationToken = tm().transact(() -> tm().loadByKey(getInstance().token));
checkArgument(
allocationToken.tokenType == TokenType.PACKAGE,
"Allocation token must be a PACKAGE type");
return super.build();
}
public Builder setToken(AllocationToken token) {
checkArgumentNotNull(token, "Allocation token must not be null");
checkArgument(
token.tokenType == TokenType.PACKAGE, "Allocation token must be a PACKAGE type");
getInstance().token = token.createVKey();
return this;
}
public Builder setMaxDomains(int maxDomains) {
checkArgumentNotNull(maxDomains, "maxDomains must not be null");
getInstance().maxDomains = maxDomains;
return this;
}
public Builder setMaxCreates(int maxCreates) {
checkArgumentNotNull(maxCreates, "maxCreates must not be null");
getInstance().maxCreates = maxCreates;
return this;
}
public Builder setPackagePrice(Money packagePrice) {
checkArgumentNotNull(packagePrice, "Package price must not be null");
getInstance().packagePrice = packagePrice;
return this;
}
public Builder setNextBillingDate(@Nullable DateTime nextBillingDate) {
checkArgumentNotNull(nextBillingDate, "Next billing date must not be null");
getInstance().nextBillingDate = nextBillingDate;
return this;
}
public Builder setLastNotificationSent(@Nullable DateTime lastNotificationSent) {
getInstance().lastNotificationSent = lastNotificationSent;
return this;
}
}
}
@@ -15,14 +15,11 @@
package google.registry.model.eppcommon;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.annotation.AlsoLoad;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.JsonMapBuilder;
@@ -59,40 +56,62 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@MappedSuperclass
public class Address extends ImmutableObject implements Jsonifiable, UnsafeSerializable {
/** The schema validation will enforce that this has 3 lines at most. */
// TODO(b/177569726): Remove this field after migration. We need to figure out how to generate
// same XML from streetLine[1,2,3].
/**
* At most three lines of addresses parsed from XML elements.
*
* <p>This field is used to marshal to/unmarshal from XML elements. When persisting to/from SQL,
* the next three separate fields are used. Those lines are <em>only</em> used for persistence
* purpose and should not be directly used in Java.
*
* <p>We need to keep the list and the three fields in sync in all the following scenarios when an
* entity containing a {@link Address} is created:
*
* <ul>
* <li>When creating an {@link Address} directly in java, using the {@link Builder}: The {@link
* Builder#setStreet(ImmutableList)} sets both the list and the fields.
* <li>When unmarshalling from XML: The list will be set based on the content of the XML.
* Afterwards, {@link #afterUnmarshal(Unmarshaller, Object)}} will be called to set the
* fields.
* <li>When loading from the database: The fields will be set by the values in SQL. Afterwards,
* {@link #postLoad()} will be called to set the list.
* </ul>
*
* The syncing is especially important because when merging a detached entity into a session, JPA
* provides no guarantee that transient fields will be preserved. In fact, Hibernate chooses to
* discard them when returning a newly merged entity. This means that it is not enough to provide
* callbacks to populate the fields based on the list before persistence, as merging does not
* invoke the callbacks, and we would lose the address fields if only the list is set. Instead,
* the fields must be populated when the list is.
*
* <p>Schema validation will enforce the 3-line limit.
*/
@XmlJavaTypeAdapter(NormalizedStringAdapter.class)
@Transient
List<String> street;
protected List<String> street;
@Ignore @XmlTransient @IgnoredInDiffableMap String streetLine1;
@XmlTransient @IgnoredInDiffableMap protected String streetLine1;
@Ignore @XmlTransient @IgnoredInDiffableMap String streetLine2;
@XmlTransient @IgnoredInDiffableMap protected String streetLine2;
@Ignore @XmlTransient @IgnoredInDiffableMap String streetLine3;
@XmlTransient @IgnoredInDiffableMap protected String streetLine3;
@XmlJavaTypeAdapter(NormalizedStringAdapter.class)
String city;
protected String city;
@XmlElement(name = "sp")
@XmlJavaTypeAdapter(NormalizedStringAdapter.class)
String state;
protected String state;
@XmlElement(name = "pc")
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
String zip;
protected String zip;
@XmlElement(name = "cc")
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
String countryCode;
protected String countryCode;
public ImmutableList<String> getStreet() {
if (street == null && streetLine1 != null) {
return ImmutableList.of(streetLine1, nullToEmpty(streetLine2), nullToEmpty(streetLine3));
} else {
return nullToEmptyImmutableCopy(street);
}
return nullToEmptyImmutableCopy(street);
}
public String getCity() {
@@ -139,7 +158,13 @@ public class Address extends ImmutableObject implements Jsonifiable, UnsafeSeria
public Builder<T> setStreet(ImmutableList<String> street) {
checkArgument(
street == null || (!street.isEmpty() && street.size() <= 3),
"Street address must have [1-3] lines: %s", street);
"Street address must have [1-3] lines: %s",
street);
//noinspection ConstantConditions
checkArgument(
street.stream().noneMatch(String::isEmpty),
"Street address cannot contain empty string: %s",
street);
getInstance().street = street;
getInstance().streetLine1 = street.get(0);
getInstance().streetLine2 = street.size() >= 2 ? street.get(1) : null;
@@ -171,24 +196,13 @@ public class Address extends ImmutableObject implements Jsonifiable, UnsafeSeria
}
}
/**
* Sets {@link #streetLine1}, {@link #streetLine2} and {@link #streetLine3} after loading the
* entity from Datastore.
*
* <p>This callback method is used by Objectify to set streetLine[1,2,3] fields as they are not
* persisted in the Datastore.
*/
void onLoad(@AlsoLoad("street") List<String> street) {
mapStreetListToIndividualFields(street);
}
/**
* Sets {@link #street} after loading the entity from Cloud SQL.
*
* <p>This callback method is used by Hibernate to set {@link #street} field as it is not
* persisted in Cloud SQL. We are doing this because the street list field is exposed by Address
* class and is used everywhere in our code base. Also, setting/reading a list of strings is more
* convenient.
* <p>This callback method is used by Hibernate to set the {@link #street} field as it is not
* persisted in Cloud SQL. We are doing this because this field is exposed and used everywhere in
* our code base, whereas the individual {@code streetLine} fields are only used by Hibernate for
* persistence. Also, setting/reading a list of strings is more convenient.
*/
@PostLoad
void postLoad() {
@@ -206,12 +220,9 @@ public class Address extends ImmutableObject implements Jsonifiable, UnsafeSeria
*
* <p>This is a callback function that JAXB invokes after unmarshalling the XML message.
*/
@SuppressWarnings("unused")
void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
mapStreetListToIndividualFields(street);
}
private void mapStreetListToIndividualFields(List<String> street) {
if (street == null || street.size() == 0) {
if (street == null || street.isEmpty()) {
return;
}
streetLine1 = street.get(0);
@@ -22,10 +22,10 @@ import com.google.common.collect.ImmutableSet;
import google.registry.model.EppResource;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.model.host.Host;
import google.registry.model.host.HostBase;
import google.registry.model.host.HostResource;
import google.registry.model.translators.EnumToAttributeAdapter.EppEnum;
import google.registry.model.translators.StatusValueAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@@ -132,12 +132,12 @@ public enum StatusValue implements EppEnum {
ALL(
ContactBase.class,
ContactResource.class,
DomainContent.class,
DomainBase.class,
Domain.class,
HostBase.class,
HostResource.class),
Host.class),
NONE,
DOMAINS(DomainContent.class, DomainBase.class);
DOMAINS(DomainBase.class, Domain.class);
private final ImmutableSet<Class<? extends EppResource>> classes;
@@ -27,7 +27,7 @@ import javax.persistence.AccessType;
/**
* A persistable Host resource including mutable and non-mutable fields.
*
* <p>The {@link javax.persistence.Id} of the HostResource is the repoId.
* <p>The {@link javax.persistence.Id} of the Host is the repoId.
*/
@ReportedOn
@Entity
@@ -54,7 +54,7 @@ import javax.persistence.AccessType;
@ExternalMessagingName("host")
@WithStringVKey
@Access(AccessType.FIELD) // otherwise it'll use the default if the repoId (property)
public class HostResource extends HostBase implements ForeignKeyedEppResource {
public class Host extends HostBase implements ForeignKeyedEppResource {
@Override
@javax.persistence.Id
@@ -64,8 +64,8 @@ public class HostResource extends HostBase implements ForeignKeyedEppResource {
}
@Override
public VKey<HostResource> createVKey() {
return VKey.create(HostResource.class, getRepoId(), Key.create(this));
public VKey<Host> createVKey() {
return VKey.create(Host.class, getRepoId(), Key.create(this));
}
@Override
@@ -73,11 +73,11 @@ public class HostResource extends HostBase implements ForeignKeyedEppResource {
return new Builder(clone(this));
}
/** A builder for constructing {@link HostResource}, since it is immutable. */
public static class Builder extends HostBase.Builder<HostResource, Builder> {
/** A builder for constructing {@link Host}, since it is immutable. */
public static class Builder extends HostBase.Builder<Host, Builder> {
public Builder() {}
private Builder(HostResource instance) {
private Builder(Host instance) {
super(instance);
}
@@ -26,7 +26,7 @@ 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.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.transfer.TransferData;
import google.registry.persistence.VKey;
import java.net.InetAddress;
@@ -76,7 +76,7 @@ public class HostBase extends EppResource {
@Index
@IgnoreSave(IfNull.class)
@DoNotHydrate
VKey<DomainBase> superordinateDomain;
VKey<Domain> superordinateDomain;
/**
* The time that this resource was last transferred.
@@ -98,7 +98,7 @@ public class HostBase extends EppResource {
return fullyQualifiedHostName;
}
public VKey<DomainBase> getSuperordinateDomain() {
public VKey<Domain> getSuperordinateDomain() {
return superordinateDomain;
}
@@ -126,8 +126,7 @@ public class HostBase extends EppResource {
@Override
public VKey<? extends HostBase> createVKey() {
throw new UnsupportedOperationException(
"HostBase is not an actual persisted entity you can create a key to;"
+ " use HostResource instead");
"HostBase is not an actual persisted entity you can create a key to; use Host instead");
}
@Deprecated
@@ -155,7 +154,7 @@ public class HostBase extends EppResource {
* {@link #superordinateDomain} field. Passing it as a parameter allows the caller to control
* the degree of consistency used to load it.
*/
public DateTime computeLastTransferTime(@Nullable DomainBase superordinateDomain) {
public DateTime computeLastTransferTime(@Nullable Domain superordinateDomain) {
if (!isSubordinate()) {
checkArgument(superordinateDomain == null);
return getLastTransferTime();
@@ -222,7 +221,7 @@ public class HostBase extends EppResource {
ImmutableSet.copyOf(difference(getInstance().getInetAddresses(), inetAddresses)));
}
public B setSuperordinateDomain(VKey<DomainBase> superordinateDomain) {
public B setSuperordinateDomain(VKey<Domain> superordinateDomain) {
getInstance().superordinateDomain = superordinateDomain;
return thisCastToDerived();
}
@@ -29,26 +29,26 @@ import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
/** A collection of {@link HostResource} commands. */
/** A collection of {@link Host} commands. */
public class HostCommand {
/** The fields on "chgType" from <a href="http://tools.ietf.org/html/rfc5732">RFC5732</a>. */
@XmlTransient
abstract static class HostCreateOrChange extends AbstractSingleResourceCommand
implements ResourceCreateOrChange<HostResource.Builder> {
implements ResourceCreateOrChange<Host.Builder> {
public String getFullyQualifiedHostName() {
return getTargetId();
}
}
/**
* A create command for a {@link HostResource}, mapping "createType" from <a
* A create command for a {@link Host}, mapping "createType" from <a
* href="http://tools.ietf.org/html/rfc5732">RFC5732</a>.
*/
@XmlType(propOrder = {"targetId", "inetAddresses"})
@XmlRootElement
public static class Create extends HostCreateOrChange
implements ResourceCreateOrChange<HostResource.Builder> {
implements ResourceCreateOrChange<Host.Builder> {
/** IP Addresses for this host. Can be null if this is an external host. */
@XmlElement(name = "addr")
Set<InetAddress> inetAddresses;
@@ -58,23 +58,22 @@ public class HostCommand {
}
}
/** A delete command for a {@link HostResource}. */
/** A delete command for a {@link Host}. */
@XmlRootElement
public static class Delete extends AbstractSingleResourceCommand {}
/** An info request for a {@link HostResource}. */
/** An info request for a {@link Host}. */
@XmlRootElement
public static class Info extends AbstractSingleResourceCommand {}
/** A check request for {@link HostResource}. */
/** A check request for {@link Host}. */
@XmlRootElement
public static class Check extends ResourceCheck {}
/** An update to a {@link HostResource}. */
/** An update to a {@link Host}. */
@XmlRootElement
@XmlType(propOrder = {"targetId", "innerAdd", "innerRemove", "innerChange"})
public static class Update extends ResourceUpdate
<Update.AddRemove, HostResource.Builder, Update.Change> {
public static class Update extends ResourceUpdate<Update.AddRemove, Host.Builder, Update.Change> {
@XmlElement(name = "chg")
protected Change innerChange;
@@ -59,7 +59,7 @@ import javax.persistence.PostLoad;
@IdClass(HostHistoryId.class)
public class HostHistory extends HistoryEntry implements UnsafeSerializable {
// Store HostBase instead of HostResource so we don't pick up its @Id
// Store HostBase instead of Host so we don't pick up its @Id
// Nullable for the sake of pre-Registry-3.0 history objects
@DoNotCompare @Nullable HostBase hostBase;
@@ -74,7 +74,7 @@ public class HostHistory extends HistoryEntry implements UnsafeSerializable {
/** This method is private because it is only used by Hibernate. */
@SuppressWarnings("unused")
private void setHostRepoId(String hostRepoId) {
parent = Key.create(HostResource.class, hostRepoId);
parent = Key.create(Host.class, hostRepoId);
}
@Id
@@ -99,9 +99,9 @@ public class HostHistory extends HistoryEntry implements UnsafeSerializable {
return Optional.ofNullable(hostBase);
}
/** The key to the {@link google.registry.model.host.HostResource} this is based off of. */
public VKey<HostResource> getParentVKey() {
return VKey.create(HostResource.class, getHostRepoId());
/** The key to the {@link Host} this is based off of. */
public VKey<Host> getParentVKey() {
return VKey.create(Host.class, getHostRepoId());
}
/** Creates a {@link VKey} instance for this entity. */
@@ -113,7 +113,7 @@ public class HostHistory extends HistoryEntry implements UnsafeSerializable {
@Override
public Optional<? extends EppResource> getResourceAtPointInTime() {
return getHostBase().map(hostBase -> new HostResource.Builder().copyFrom(hostBase).build());
return getHostBase().map(hostBase -> new Host.Builder().copyFrom(hostBase).build());
}
@PostLoad
@@ -210,7 +210,7 @@ public class HostHistory extends HistoryEntry implements UnsafeSerializable {
}
public Builder setHostRepoId(String hostRepoId) {
getInstance().parent = Key.create(HostResource.class, hostRepoId);
getInstance().parent = Key.create(Host.class, hostRepoId);
return this;
}
}
@@ -48,8 +48,8 @@ import google.registry.model.EppResource;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.HostResource;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
import google.registry.persistence.transaction.JpaTransactionManager;
@@ -75,30 +75,30 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
@Entity
public static class ForeignKeyContactIndex extends ForeignKeyIndex<ContactResource> {}
/** The {@link ForeignKeyIndex} type for {@link DomainBase} entities. */
/** The {@link ForeignKeyIndex} type for {@link Domain} entities. */
@ReportedOn
@Entity
public static class ForeignKeyDomainIndex extends ForeignKeyIndex<DomainBase> {}
public static class ForeignKeyDomainIndex extends ForeignKeyIndex<Domain> {}
/** The {@link ForeignKeyIndex} type for {@link HostResource} entities. */
/** The {@link ForeignKeyIndex} type for {@link Host} entities. */
@ReportedOn
@Entity
public static class ForeignKeyHostIndex extends ForeignKeyIndex<HostResource> {}
public static class ForeignKeyHostIndex extends ForeignKeyIndex<Host> {}
private static final ImmutableBiMap<
Class<? extends EppResource>, Class<? extends ForeignKeyIndex<?>>>
RESOURCE_CLASS_TO_FKI_CLASS =
ImmutableBiMap.of(
ContactResource.class, ForeignKeyContactIndex.class,
DomainBase.class, ForeignKeyDomainIndex.class,
HostResource.class, ForeignKeyHostIndex.class);
Domain.class, ForeignKeyDomainIndex.class,
Host.class, ForeignKeyHostIndex.class);
private static final ImmutableMap<Class<? extends EppResource>, String>
RESOURCE_CLASS_TO_FKI_PROPERTY =
ImmutableMap.of(
ContactResource.class, "contactId",
DomainBase.class, "fullyQualifiedDomainName",
HostResource.class, "fullyQualifiedHostName");
Domain.class, "fullyQualifiedDomainName",
Host.class, "fullyQualifiedHostName");
@Id String foreignKey;
@@ -106,7 +106,7 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
* The deletion time of this {@link ForeignKeyIndex}.
*
* <p>This will generally be equal to the deletion time of {@link #topReference}. However, in the
* case of a {@link HostResource} that was renamed, this field will hold the time of the rename.
* case of a {@link Host} that was renamed, this field will hold the time of the rename.
*/
@Index DateTime deletionTime;
@@ -33,7 +33,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
* via Objectify.
*
* <p>All first class entities are represented as a resource class - {@link
* google.registry.model.domain.DomainBase}, {@link google.registry.model.host.HostResource}, {@link
* google.registry.model.domain.Domain}, {@link google.registry.model.host.Host}, {@link
* google.registry.model.contact.ContactResource}, and {@link
* google.registry.model.registrar.Registrar}. Resource objects are written in a single shared
* entity group per TLD. All commands that operate on those entities are grouped in a "Command"
@@ -20,29 +20,23 @@ import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.EntitySubclass;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.Index;
import google.registry.model.Buildable;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.annotations.OfyIdAllocation;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactHistory.ContactHistoryId;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.DomainRenewData;
import google.registry.model.eppoutput.EppResponse.ResponseData;
import google.registry.model.host.Host;
import google.registry.model.host.HostHistory;
import google.registry.model.host.HostHistory.HostHistoryId;
import google.registry.model.host.HostResource;
import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import google.registry.model.poll.PendingActionNotificationResponse.HostPendingActionNotificationResponse;
@@ -55,16 +49,19 @@ import google.registry.persistence.VKey;
import google.registry.persistence.WithLongVKey;
import google.registry.util.NullIgnoringCollectionBuilder;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.PostLoad;
import javax.persistence.Table;
import org.joda.time.DateTime;
/**
@@ -76,10 +73,10 @@ import org.joda.time.DateTime;
* retained for historical purposes, poll messages are truly deleted once they have been delivered
* and ACKed.
*
* <p>Poll messages are parented off of the {@link HistoryEntry} that resulted in their creation.
* This means that poll messages are contained in the Datastore entity group of the parent {@link
* EppResource} (which can be a domain, contact, or host). It is thus possible to perform a strongly
* consistent query to find all poll messages associated with a given EPP resource.
* <p>Poll messages have a reference to the revision id of the {@link HistoryEntry} that resulted in
* their creation or re-creation, in case of transfer cancellation/rejection. The revision ids are
* not used directly by the Java code, but their presence in the database makes it easier to
* diagnose potential issues related to poll messages.
*
* <p>Poll messages are identified externally by registrars using the format defined in {@link
* PollMessageExternalKeyConverter}.
@@ -88,29 +85,23 @@ import org.joda.time.DateTime;
* Command</a>
*/
@Entity
@ReportedOn
@ExternalMessagingName("message")
@javax.persistence.Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type")
@javax.persistence.Table(
indexes = {
@javax.persistence.Index(columnList = "registrar_id"),
@javax.persistence.Index(columnList = "eventTime")
})
@Table(indexes = {@Index(columnList = "registrar_id"), @Index(columnList = "eventTime")})
public abstract class PollMessage extends ImmutableObject
implements Buildable, TransferServerApproveEntity, UnsafeSerializable {
/** Indicates the type of entity the poll message is for. */
public enum Type {
DOMAIN(1L, DomainBase.class),
DOMAIN(1L, Domain.class),
CONTACT(2L, ContactResource.class),
HOST(3L, HostResource.class);
HOST(3L, Host.class);
private final long id;
private final Class<?> clazz;
private final Class<? extends EppResource> clazz;
Type(long id, Class<?> clazz) {
Type(long id, Class<? extends EppResource> clazz) {
this.id = id;
this.clazz = clazz;
}
@@ -124,58 +115,40 @@ public abstract class PollMessage extends ImmutableObject
}
/** Returns the class of the underlying resource for the poll message type. */
public Class<?> getResourceClass() {
public Class<? extends EppResource> getResourceClass() {
return clazz;
}
/**
* Returns the type specified by the identifer, {@code Optional.empty()} if the id is out of
* range.
*/
public static Optional<Type> fromId(long id) {
for (Type val : values()) {
if (val.id == id) {
return Optional.of(val);
}
}
return Optional.empty();
}
}
/** Entity id. */
@Id
@javax.persistence.Id
@OfyIdAllocation
@Column(name = "poll_message_id")
Long id;
/** The registrar that this poll message will be delivered to. */
@Index
@Column(name = "registrar_id", nullable = false)
String clientId;
/** The time when the poll message should be delivered. May be in the future. */
@Index
@Column(nullable = false)
DateTime eventTime;
/** Human readable message that will be returned with this poll message. */
/** Human-readable message that will be returned with this poll message. */
@Column(name = "message")
String msg;
// TODO(b/456803336): Replace these fields with {Domain,Contact,Host}HistoryId objects.
String domainRepoId;
@Ignore String domainRepoId;
String contactRepoId;
@Ignore String contactRepoId;
String hostRepoId;
@Ignore String hostRepoId;
Long domainHistoryRevisionId;
@Ignore Long domainHistoryRevisionId;
Long contactHistoryRevisionId;
@Ignore Long contactHistoryRevisionId;
@Ignore Long hostHistoryRevisionId;
Long hostHistoryRevisionId;
public Long getId() {
return id;
@@ -216,7 +189,7 @@ public abstract class PollMessage extends ImmutableObject
/**
* Returns the host repo id.
*
* <p>This may only be used on a HostResource poll event.
* <p>This may only be used on a Host poll event.
*/
public String getHostRepoId() {
checkArgument(getType() == Type.DOMAIN);
@@ -228,32 +201,22 @@ public abstract class PollMessage extends ImmutableObject
* the resource.
*/
public String getResourceName() {
return domainRepoId != null
? domainRepoId
: (contactRepoId != null ? contactRepoId : hostRepoId);
return domainRepoId != null ? domainRepoId : contactRepoId != null ? contactRepoId : hostRepoId;
}
/** Gets the underlying history revision id, regardless of the type of the resource. */
public Long getHistoryRevisionId() {
return domainHistoryRevisionId != null
? domainHistoryRevisionId
: (contactHistoryRevisionId != null ? contactHistoryRevisionId : hostHistoryRevisionId);
: contactHistoryRevisionId != null ? contactHistoryRevisionId : hostHistoryRevisionId;
}
public Type getType() {
return domainRepoId != null ? Type.DOMAIN : (contactRepoId != null ? Type.CONTACT : Type.HOST);
return domainRepoId != null ? Type.DOMAIN : contactRepoId != null ? Type.CONTACT : Type.HOST;
}
public abstract ImmutableList<ResponseData> getResponseData();
@Override
public abstract VKey<? extends PollMessage> createVKey();
/** Static VKey factory method for use by VKeyTranslatorFactory. */
public static VKey<PollMessage> createVKey(Key<PollMessage> key) {
return VKey.create(PollMessage.class, key.getId(), key);
}
/** Override Buildable.asBuilder() to give this method stronger typing. */
@Override
public abstract Builder<?, ?> asBuilder();
@@ -318,9 +281,11 @@ public abstract class PollMessage extends ImmutableObject
// Set the appropriate field based on the history entry type.
if (history instanceof DomainHistory) {
return setDomainHistoryId(((DomainHistory) history).getDomainHistoryId());
} else if (history instanceof ContactHistory) {
}
if (history instanceof ContactHistory) {
return setContactHistoryId(((ContactHistory) history).getContactHistoryId());
} else if (history instanceof HostHistory) {
}
if (history instanceof HostHistory) {
return setHostHistoryId(((HostHistory) history).getHostHistoryId());
}
return thisCastToDerived();
@@ -330,7 +295,7 @@ public abstract class PollMessage extends ImmutableObject
* Given an array containing pairs of objects, verifies that both members of exactly one of the
* pairs is non-null.
*/
private boolean exactlyOnePairNonNull(Object... pairs) {
private static boolean exactlyOnePairNonNull(Object... pairs) {
int count = 0;
checkArgument(pairs.length % 2 == 0, "Odd number of arguments provided.");
for (int i = 0; i < pairs.length; i += 2) {
@@ -360,9 +325,9 @@ public abstract class PollMessage extends ImmutableObject
instance.contactHistoryRevisionId,
instance.hostRepoId,
instance.hostHistoryRevisionId),
"the repo id and history revision id must be defined for exactly one of domain, "
+ "contact or host: "
+ instance);
"The repo id and history revision id must be defined for exactly one of domain, "
+ "contact or host: %s",
instance);
return super.build();
}
}
@@ -372,13 +337,11 @@ public abstract class PollMessage extends ImmutableObject
*
* <p>One-time poll messages are deleted from Datastore once they have been delivered and ACKed.
*/
@EntitySubclass(index = false)
@javax.persistence.Entity
@Entity
@DiscriminatorValue("ONE_TIME")
@WithLongVKey(compositeKey = true)
public static class OneTime extends PollMessage {
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(
@@ -399,7 +362,6 @@ public abstract class PollMessage extends ImmutableObject
})
PendingActionNotificationResponse pendingActionNotificationResponse;
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(
@@ -420,35 +382,21 @@ public abstract class PollMessage extends ImmutableObject
})
TransferResponse transferResponse;
@Ignore
@Column(name = "transfer_response_domain_name")
String fullyQualifiedDomainName;
@Ignore
@Column(name = "transfer_response_domain_expiration_time")
DateTime extendedRegistrationExpirationTime;
@Ignore
@Column(name = "transfer_response_contact_id")
String contactId;
@Ignore
@Column(name = "transfer_response_host_id")
String hostId;
@Override
public VKey<OneTime> createVKey() {
return VKey.create(OneTime.class, getId(), Key.create(this));
}
/** Converts an unspecialized VKey&lt;PollMessage&gt; to a VKey of the derived class. */
public static @Nullable VKey<OneTime> convertVKey(@Nullable VKey<? extends PollMessage> key) {
if (key == null) {
return null;
}
Key<OneTime> ofyKey =
Key.create(key.getOfyKey().getParent(), OneTime.class, key.getOfyKey().getId());
return VKey.create(OneTime.class, key.getSqlKey(), ofyKey);
return VKey.createSql(OneTime.class, getId());
}
@Override
@@ -525,8 +473,7 @@ public abstract class PollMessage extends ImmutableObject
/** A builder for {@link OneTime} since it is immutable. */
public static class Builder extends PollMessage.Builder<OneTime, Builder> {
public Builder() {
}
public Builder() {}
private Builder(OneTime instance) {
super(instance);
@@ -587,14 +534,13 @@ public abstract class PollMessage extends ImmutableObject
}
/**
* An auto-renew poll message which recurs annually.
* An autorenew poll message which recurs annually.
*
* <p>Auto-renew poll messages are not deleted until the registration of their parent domain has
* been canceled, because there will always be a speculative renewal for next year until that
* happens.
*/
@EntitySubclass(index = false)
@javax.persistence.Entity
@Entity
@DiscriminatorValue("AUTORENEW")
@WithLongVKey(compositeKey = true)
public static class Autorenew extends PollMessage {
@@ -604,7 +550,6 @@ public abstract class PollMessage extends ImmutableObject
String targetId;
/** The autorenew recurs annually between {@link #eventTime} and this time. */
@Index
DateTime autorenewEndTime;
public String getTargetId() {
@@ -617,25 +562,14 @@ public abstract class PollMessage extends ImmutableObject
@Override
public VKey<Autorenew> createVKey() {
return VKey.create(Autorenew.class, getId(), Key.create(this));
}
/** Converts an unspecialized VKey&lt;PollMessage&gt; to a VKey of the derived class. */
public static @Nullable VKey<Autorenew> convertVKey(VKey<? extends PollMessage> key) {
if (key == null) {
return null;
}
Key<Autorenew> ofyKey =
Key.create(key.getOfyKey().getParent(), Autorenew.class, key.getOfyKey().getId());
return VKey.create(Autorenew.class, key.getSqlKey(), ofyKey);
return VKey.createSql(Autorenew.class, getId());
}
@Override
public ImmutableList<ResponseData> getResponseData() {
// Note that the event time is when the auto-renew occured, so the expiration time in the
// Note that the event time is when the autorenew occurred, so the expiration time in the
// response should be 1 year past that, since it denotes the new expiration time.
return ImmutableList.of(
DomainRenewData.create(getTargetId(), getEventTime().plusYears(1)));
return ImmutableList.of(DomainRenewData.create(getTargetId(), getEventTime().plusYears(1)));
}
@Override
@@ -14,48 +14,38 @@
package google.registry.model.poll;
import com.google.common.base.Splitter;
import com.googlecode.objectify.Key;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import java.util.List;
/**
* A converter between external key strings for {@link PollMessage}s (i.e. what registrars use to
* identify and ACK them) and Datastore keys to the resource.
* identify and ACK them) and {@link VKey}s to the resource.
*
* <p>The format of the key string is A-B-C-D-E-F as follows:
* <p>The format of the key string is A-B as follows:
*
* <pre>
* A = EppResource.typeId (decimal)
* B = EppResource.repoId prefix (STRING)
* C = EppResource.repoId suffix (STRING)
* D = HistoryEntry.id (decimal)
* E = PollMessage.id (decimal)
* F = PollMessage.eventTime (decimal, year only)
* A = PollMessage.id (decimal)
* B = PollMessage.eventTime (decimal, year only)
* </pre>
*
* <p>A typical poll message ID might thus look like: 1-FE0F22-TLD-10071463070-10072612074-2018
* <p>A typical poll message ID might thus look like: 10072612074-2018
*/
public class PollMessageExternalKeyConverter {
public final class PollMessageExternalKeyConverter {
/** An exception thrown when an external key cannot be parsed. */
public static class PollMessageExternalKeyParseException extends RuntimeException {}
public static class PollMessageExternalKeyParseException extends RuntimeException {
private static final long serialVersionUID = 9026397649048831777L;
}
/** Returns an external poll message ID for the given poll message. */
public static String makePollMessageExternalId(PollMessage pollMessage) {
return String.format(
"%d-%s-%d-%d-%d",
pollMessage.getType().getId(),
pollMessage.getResourceName(),
pollMessage.getHistoryRevisionId(),
pollMessage.getId(),
pollMessage.getEventTime().getYear());
return String.format("%d-%d", pollMessage.getId(), pollMessage.getEventTime().getYear());
}
/**
* Returns an Objectify Key to a PollMessage corresponding with the external ID.
* Returns a {@link VKey} to a {@link PollMessage} corresponding with the external ID.
*
* <p>Note that the year field that is included at the end of the poll message isn't actually used
* for anything; it exists solely to create unique externally visible IDs for autorenews. We thus
@@ -66,26 +56,13 @@ public class PollMessageExternalKeyConverter {
*/
public static VKey<PollMessage> parsePollMessageExternalId(String externalKey) {
List<String> idComponents = Splitter.on('-').splitToList(externalKey);
if (idComponents.size() != 6) {
if (idComponents.size() != 2) {
throw new PollMessageExternalKeyParseException();
}
try {
Class<?> resourceClazz =
PollMessage.Type.fromId(Long.parseLong(idComponents.get(0)))
.orElseThrow(() -> new PollMessageExternalKeyParseException())
.getResourceClass();
return VKey.from(
Key.create(
Key.create(
Key.create(
null,
resourceClazz,
String.format("%s-%s", idComponents.get(1), idComponents.get(2))),
HistoryEntry.class,
Long.parseLong(idComponents.get(3))),
PollMessage.class,
Long.parseLong(idComponents.get(4))));
// Note that idComponents.get(5) is entirely ignored; we never use the year field internally.
Long id = Long.parseLong(idComponents.get(0));
return VKey.createSql(PollMessage.class, id);
// Note that idComponents.get(1) is entirely ignored; we never use the year field internally.
} catch (NumberFormatException e) {
throw new PollMessageExternalKeyParseException();
}
@@ -59,10 +59,8 @@ import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.condition.IfNull;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
@@ -315,7 +313,7 @@ public class Registrar extends ImmutableObject
* Localized {@link RegistrarAddress} for this registrar. Contents can be represented in
* unrestricted UTF-8.
*/
@IgnoreSave(IfNull.class)
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(
@@ -340,7 +338,7 @@ public class Registrar extends ImmutableObject
* Internationalized {@link RegistrarAddress} for this registrar. All contained values must be
* representable in the 7-bit US-ASCII character set.
*/
@IgnoreSave(IfNull.class)
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "streetLine1", column = @Column(name = "i18n_address_street_line1")),
@@ -964,7 +962,7 @@ public class Registrar extends ImmutableObject
}
/** Verifies that the email address in question is not null and has a valid format. */
static String checkValidEmail(String email) {
public static String checkValidEmail(String email) {
checkNotNull(email, "Provided email was null");
try {
InternetAddress internetAddress = new InternetAddress(email, true);
@@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
import com.google.common.annotations.VisibleForTesting;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.eppcommon.Address;
import javax.persistence.Embeddable;
@@ -29,7 +28,6 @@ import javax.persistence.Embeddable;
* all defined in parent class {@link Address} so that it can share it with other similar address
* classes.
*/
@Embed
@Embeddable
public class RegistrarAddress extends Address {
@@ -38,16 +38,16 @@ import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactHistory.ContactHistoryId;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainContent;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.Period;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.Host;
import google.registry.model.host.HostBase;
import google.registry.model.host.HostHistory;
import google.registry.model.host.HostHistory.HostHistoryId;
import google.registry.model.host.HostResource;
import google.registry.persistence.VKey;
import java.util.Optional;
import java.util.Set;
@@ -386,10 +386,10 @@ public class HistoryEntry extends ImmutableObject implements Buildable, UnsafeSe
String parentKind = getParent().getKind();
final HistoryEntry resultEntity;
// can't use a switch statement since we're calling getKind()
if (parentKind.equals(getKind(DomainBase.class))) {
if (parentKind.equals(getKind(Domain.class))) {
resultEntity =
new DomainHistory.Builder().copyFrom(this).setDomainRepoId(parent.getName()).build();
} else if (parentKind.equals(getKind(HostResource.class))) {
} else if (parentKind.equals(getKind(Host.class))) {
resultEntity =
new HostHistory.Builder().copyFrom(this).setHostRepoId(parent.getName()).build();
} else if (parentKind.equals(getKind(ContactResource.class))) {
@@ -408,12 +408,12 @@ public class HistoryEntry extends ImmutableObject implements Buildable, UnsafeSe
long id = key.getId();
Key<EppResource> parent = key.getParent();
String parentKind = parent.getKind();
if (parentKind.equals(getKind(DomainBase.class))) {
if (parentKind.equals(getKind(Domain.class))) {
return VKey.create(
DomainHistory.class,
new DomainHistoryId(repoId, id),
Key.create(parent, DomainHistory.class, id));
} else if (parentKind.equals(getKind(HostResource.class))) {
} else if (parentKind.equals(getKind(Host.class))) {
return VKey.create(
HostHistory.class,
new HostHistoryId(repoId, id),
@@ -537,8 +537,8 @@ public class HistoryEntry extends ImmutableObject implements Buildable, UnsafeSe
public static <E extends EppResource>
HistoryEntry.Builder<? extends HistoryEntry, ?> createBuilderForResource(E parent) {
if (parent instanceof DomainContent) {
return new DomainHistory.Builder().setDomain((DomainContent) parent);
if (parent instanceof DomainBase) {
return new DomainHistory.Builder().setDomain((DomainBase) parent);
} else if (parent instanceof ContactBase) {
return new ContactHistory.Builder().setContact((ContactBase) parent);
} else if (parent instanceof HostBase) {
@@ -27,10 +27,10 @@ import com.google.common.collect.Streams;
import google.registry.model.EppResource;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.host.Host;
import google.registry.model.host.HostHistory;
import google.registry.model.host.HostResource;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
import java.util.Comparator;
@@ -53,9 +53,9 @@ public class HistoryEntryDao {
ImmutableMap.of(
ContactResource.class,
ContactHistory.class,
DomainBase.class,
Domain.class,
DomainHistory.class,
HostResource.class,
Host.class,
HostHistory.class);
public static ImmutableMap<Class<? extends HistoryEntry>, String> REPO_ID_FIELD_NAMES =
@@ -73,7 +73,7 @@ public class PremiumListDao {
}
/**
* In-memory price cache for for a given premium list revision and domain label.
* In-memory price cache for a given premium list revision and domain label.
*
* <p>Note that premium list revision ids are globally unique, so this cache is specific to a
* given premium list. Premium list entries might not be present, as indicated by the Optional
@@ -90,12 +90,13 @@ public final class ReservedList
*
* <p>We need to persist the list entries, but only on the initial insert (not on update) since
* the entries themselves never get changed, so we only annotate it with {@link PostPersist}, not
* {@link PostUpdate}.
* PostUpdate.
*/
@PostPersist
void postPersist() {
if (reservedListMap != null) {
reservedListMap.values().stream()
reservedListMap
.values()
.forEach(
entry -> {
// We can safely change the revision id since it's "Insignificant".

Some files were not shown because too many files have changed in this diff Show More