1
0
mirror of https://github.com/google/nomulus synced 2026-07-06 08:06:33 +00:00

Compare commits

..

33 Commits

Author SHA1 Message Date
Pavlo Tkach f6e9dae58d Add REMOVEPACKAGE token functionality to domain transfer flow (#1792) 2022-09-19 15:11:36 -04:00
Lai Jiang c4c1c72306 Refactor ForeignKeyIndex into ForeignKeyUtils (#1783)
The old class is modeled after datastore with some logic jammed in for it to work with SQL as well. As of #1777, the ofy related logic is deleted, however the general structure of the class remained datastore oriented.

This PR refactors the existing class into a ForeignKeyUtils helper class that does away wit the index subclasses and provides static helper methods to do the same, in a SQL-idiomatic fashion.

Some minor changes are made to the EPP resource classes to make it possible to create them in a SQL only environment in tests.
2022-09-19 14:41:19 -04:00
Pavlo Tkach 775f672f2a Do not enqueue DNS updates when flow doesn't affect nameservers (#1785) 2022-09-16 16:59:04 -04:00
gbrodman 372c854268 Create a scrap command to cancel OneTime billing events by ID (#1790)
This allows us to correct situations where we have erroneously charged
registrars for an action, without explicitly issuing a refund.
2022-09-16 16:17:31 -04:00
Lai Jiang edbca15bf4 Remove generics from TransferData (#1787)
`TransferData` is currently a generic class with a complicated type parameter that designate the `Builder` class of its concrete subclass, on order to facilitate returning the said `Builder` from an instance loosely typed to the superclass (`TransferData`) itself.

While this works, in most all places that a `TransferData` is used, the raw, un-generic type is declared, resulting a lot of warnings, not to mention the fact that type safety not actually checked when raw type is used.

In this PR, we make it so that the concrete `Builder` is returned through a protected abstract method that is implemented by the subclasses. The type information therefore no longer needs to be embedded in the superclass type signature, and reflection is not necessary to create the `Builder` either. Overall, it makes `TransferData` a much cleaner class without the messiness of generics.
2022-09-15 14:07:38 -04:00
sarahcaseybot 5f41adf843 Flyway file for autogenerated PackagePromotion id fix (#1789)
* Flyway file for autogenerated PackagePromotion id fix

* Actually include the flyway file
2022-09-15 13:28:46 -04:00
Lai Jiang e21f64b745 Delete EppResourceIndex and EppResourceIndexBucket (#1774) 2022-09-15 10:50:22 -04:00
sarahcaseybot 0dee97934a Prevent creation of package domains for more than 1 year (#1786)
* Prevent creation of package domains for more than 1 year

* Fix docs test
2022-09-14 14:49:56 -04:00
gbrodman 1070173264 Load, project, and save in one txn in ResaveAERP (#1780) 2022-09-13 15:59:49 -04:00
Pavlo Tkach b9a3c0cd96 Add dry run test for remove package token (#1782) 2022-09-13 11:20:53 -04:00
sarahcaseybot 120456d138 Increase dns update failure max retry count (#1781) 2022-09-12 16:17:31 -04:00
gbrodman 66736d52f0 Add a cookie-based OAuth2 authenticator (#1761)
This uses the GoogleIdTokenVerifier to verify ID tokens passed in
(presumably from a front end) via cookies. This isn't used anywhere yet
but it will be used for front-end API calls for the new console.
2022-09-12 15:03:05 -04:00
Lai Jiang b159541278 Remove ofy support from ServerSecret (#1773) 2022-09-09 10:38:12 -04:00
Lai Jiang 335b229ce8 Remove ofy support from TransferData (#1775)
Also makes some changes to eliminate the use of raw types.
2022-09-08 19:25:41 -04:00
Lai Jiang 8ee0a85531 Remove ofy embedded classes (#1778) 2022-09-08 16:12:57 -04:00
gbrodman 5cbc307cd1 Add a DAO for User objects and fix up the User DB object (#1765)
First, we create a sequence of User IDs in Postgres and assign it to the
User ID field, meaning that Hibernate can autogenerate IDs.

Next, add an update timestamp.

Next, add a constraint that we can't have multiple Users with the same
email address.

Finally, create a DAO since we'll usually want to query by that email
address (at least for now).
2022-09-08 15:21:56 -04:00
Lai Jiang bd37541b49 Remove ofy support from ForeignKeyIndex (#1777)
FKI used to be persisted in datastore to help speed up loading by foreign key.
Now it is just a helper class to do the same thing in SQL because
indexing is natively supported in SQL.
2022-09-08 13:12:02 -04:00
Lai Jiang 312bc143d5 Delete EntityGroupRoot (#1776) 2022-09-08 12:54:10 -04:00
Lai Jiang 49ade014ab Remove ofy from Lock (#1771)
<!-- 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/1771)
<!-- Reviewable:end -->
2022-09-07 17:32:03 -04:00
Lai Jiang b8d901effe Remove ofy support from registrar (#1762)
Also fixes some warnings about the use of raw types.
2022-09-07 14:24:42 -04:00
Lai Jiang 23520048dc Remove ofy support from AllocationToken (#1770) 2022-09-07 14:22:42 -04:00
Lai Jiang 37ed6c925c Remove ofy support from RdeRevision (#1772) 2022-09-07 13:30:38 -04:00
Pavlo Tkach 17a21f3326 Update renew flow to accept and process REMOVEPACKAGE token (#1768) 2022-09-02 17:32:59 -04:00
Pavlo Tkach f623da9948 Prohibit renewals of package domains unless REMOVEPACKAGE token is included (#1758) 2022-08-31 18:58:31 -04:00
gbrodman ddc4a615db Fix a few DB issues with the User class (#1766)
- Create a sequence to generate IDs for the user (this allows us to have
  Long ID types so that Hibernate can autogenerate IDs)
- Add an update timestamp column so we can extend BackupGroupRoot
- Add a restriction that there can't be multiple users with the same
  email address
2022-08-31 16:09:07 -04:00
sarahcaseybot 06a1fc0022 Add a packageToken EPP extension for use in the DomainInfo flow (#1760)
* Add a packageToken EPP extension for use in the DomainInfo flow

* small fixes

* Change namespace
2022-08-30 17:50:42 -04:00
sarahcaseybot eec272b6ba Increase max backoff seconds for dns-publish queue (#1764) 2022-08-29 16:30:56 -04:00
Ben McIlwain 3d5b52b853 Rename ContactResource -> Contact (#1763)
* Rename ContactResource -> Contact

This is a follow-up to PR #1725 and #1733. Now all EPP resource entity class
names have been rationalized to match with their SQL table names.
2022-08-29 14:48:32 -04:00
Lai Jiang bd4af052a6 Remove ofy support from Address (#1759) 2022-08-26 12:35:48 -04:00
Pavlo Tkach 78249e1329 Replace PubApi master calls with replica (#1742) 2022-08-26 10:15:30 -04:00
gbrodman 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 Jiang 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 Tkach b0e4e86586 Add registry email to bcc for outgoing DNS failure emails (#1755) 2022-08-25 14:15:20 -04:00
262 changed files with 6268 additions and 5967 deletions
-6
View File
@@ -61,12 +61,6 @@ by Joshua Bloch in his book Effective Java -->
<property name="message" value="Use assertThrows and expectThrows from JUnitBackports instead of the deprecated methods on ExpectedException."/>
</module>
<!-- Checks that the deprecated MockitoJUnitRunner is not used. -->
<module name="RegexpSingleline">
<property name="format" value="MockitoJUnitRunner"/>
<property name="message" value="MockitoJUnitRunner is deprecated. Use @RunWith(JUnit4.class) and MockitoRule instead."/>
</module>
<module name="LineLength">
<!-- Checks if a line is too long. -->
<property name="max" value="${com.puppycrawl.tools.checkstyle.checks.sizes.LineLength.max}" default="100"/>
@@ -29,7 +29,7 @@ import google.registry.config.RegistryEnvironment;
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.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.poll.PollMessage;
@@ -43,8 +43,8 @@ import google.registry.util.Clock;
import javax.inject.Inject;
/**
* Hard deletes load-test ContactResources, Hosts, their subordinate history entries, and the
* associated ForeignKey and EppResourceIndex entities.
* Hard deletes load-test Contacts, Hosts, their subordinate history entries, and the associated
* ForeignKey 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
@@ -92,7 +92,7 @@ public class DeleteLoadTestDataAction implements Runnable {
tm().transact(
() -> {
LOAD_TEST_REGISTRARS.forEach(this::deletePollMessages);
tm().loadAllOfStream(ContactResource.class).forEach(this::deleteContact);
tm().loadAllOfStream(Contact.class).forEach(this::deleteContact);
tm().loadAllOfStream(Host.class).forEach(this::deleteHost);
});
}
@@ -108,7 +108,7 @@ public class DeleteLoadTestDataAction implements Runnable {
}
}
private void deleteContact(ContactResource contact) {
private void deleteContact(Contact contact) {
if (!LOAD_TEST_REGISTRARS.contains(contact.getPersistedCurrentSponsorRegistrarId())) {
return;
}
@@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.batch.BatchModule.PARAM_DRY_RUN;
import static google.registry.config.RegistryEnvironment.PRODUCTION;
import static google.registry.model.ResourceTransferUtils.updateForeignKeyIndexDeletionTime;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_DELETE;
import static google.registry.model.tld.Registries.getTldsOfType;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
@@ -54,7 +53,7 @@ import org.joda.time.Duration;
/**
* Deletes all prober {@link Domain}s and their subordinate history entries, poll messages, and
* billing events, along with their ForeignKeyDomainIndex and EppResourceIndex entities.
* billing events, along with their ForeignKeyDomainIndex entities.
*/
@Action(
service = Action.Service.BACKEND,
@@ -267,8 +266,6 @@ public class DeleteProberDataAction implements Runnable {
// messages, or auto-renews because those will all be hard-deleted the next time the job runs
// anyway.
tm().putAll(ImmutableList.of(deletedDomain, historyEntry));
// updating foreign keys is a no-op in SQL
updateForeignKeyIndexDeletionTime(deletedDomain);
dnsQueue.addDomainRefreshTask(deletedDomain.getDomainName());
}
}
@@ -17,7 +17,7 @@ package google.registry.beam.common;
import static com.google.common.base.Verify.verify;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
import google.registry.persistence.transaction.JpaTransactionManager;
import java.io.Serializable;
@@ -46,8 +46,7 @@ public class JpaDemoPipeline implements Serializable {
.apply(
"Read contacts",
RegistryJpaIO.read(
() -> CriteriaQueryBuilder.create(ContactResource.class).build(),
ContactResource::getRepoId))
() -> CriteriaQueryBuilder.create(Contact.class).build(), Contact::getRepoId))
.apply(
"Count Contacts",
ParDo.of(
@@ -45,8 +45,8 @@ import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.gcs.GcsUtils;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.host.Host;
@@ -140,7 +140,7 @@ import org.joda.time.DateTime;
* pairs of (contact/host repo ID: pending deposit) for all RDE pending deposits for further
* processing.
*
* <h3>{@link ContactResource}</h3>
* <h3>{@link Contact}</h3>
*
* We first join most recent contact histories, represented by (contact repo ID: contact history
* revision ID) pairs, with referenced contacts, represented by (contact repo ID: pending deposit)
@@ -150,13 +150,13 @@ import org.joda.time.DateTime;
*
* <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
* contacts and obtain the (pending deposit: deposit fragment) pairs. For subordinate hosts, we need
* to find the superordinate domain in order to properly handle pending transfer in the deposit as
* well. So we first find the superordinate domain repo ID from the host and join the (superordinate
* domain repo ID: (subordinate host repo ID: (pending deposit: revision ID))) pair with the (domain
* repo ID: revision ID) pair obtained from the domain history query in order to map the host at
* Similar to {@link Contact}, 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 contacts
* and obtain the (pending deposit: deposit fragment) pairs. For subordinate hosts, we need to find
* the superordinate domain in order to properly handle pending transfer in the deposit as well. So
* we first find the superordinate domain repo ID from the host and join the (superordinate domain
* repo ID: (subordinate host repo ID: (pending deposit: revision ID))) pair with the (domain repo
* ID: revision ID) pair obtained from the domain history query in order to map the host at
* watermark to the domain at watermark. We then proceed to create the (pending deposit: deposit
* fragment) pair for subordinate hosts using the added domain information.
*
@@ -466,10 +466,10 @@ public class RdePipeline implements Serializable {
private PCollectionTuple processDomainHistories(PCollection<KV<String, Long>> domainHistories) {
Counter activeDomainCounter = Metrics.counter("RDE", "ActiveDomainBase");
Counter domainFragmentCounter = Metrics.counter("RDE", "DomainFragment");
Counter referencedContactCounter = Metrics.counter("RDE", "ReferencedContactResource");
Counter referencedContactCounter = Metrics.counter("RDE", "ReferencedContact");
Counter referencedHostCounter = Metrics.counter("RDE", "ReferencedHost");
return domainHistories.apply(
"Map DomainHistory to DepositFragment " + "and emit referenced ContactResource and Host",
"Map DomainHistory to DepositFragment " + "and emit referenced Contact and Host",
ParDo.of(
new DoFn<KV<String, Long>, KV<PendingDeposit, DepositFragment>>() {
@ProcessElement
@@ -533,17 +533,17 @@ public class RdePipeline implements Serializable {
PCollection<KV<String, PendingDeposit>> referencedContacts,
PCollection<KV<String, Long>> contactHistories) {
Counter contactFragmentCounter = Metrics.counter("RDE", "ContactFragment");
return removeUnreferencedResource(referencedContacts, contactHistories, ContactResource.class)
return removeUnreferencedResource(referencedContacts, contactHistories, Contact.class)
.apply(
"Map ContactResource to DepositFragment",
"Map Contact to DepositFragment",
FlatMapElements.into(
kvs(
TypeDescriptor.of(PendingDeposit.class),
TypeDescriptor.of(DepositFragment.class)))
.via(
(KV<String, CoGbkResult> kv) -> {
ContactResource contact =
(ContactResource)
Contact contact =
(Contact)
loadResourceByHistoryEntryId(
ContactHistory.class,
kv.getKey(),
@@ -14,20 +14,23 @@
package google.registry.beam.resave;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static org.apache.beam.sdk.values.TypeDescriptors.integers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
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.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.Host;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
import google.registry.persistence.VKey;
import google.registry.util.DateTimeUtils;
import java.io.Serializable;
import java.util.concurrent.ThreadLocalRandom;
@@ -53,7 +56,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, Domain.class, Host.class);
ImmutableSet.of(Contact.class, Domain.class, Host.class);
/**
* There exist three possible situations where we know we'll want to project domains to the
@@ -69,7 +72,7 @@ public class ResaveAllEppResourcesPipeline implements Serializable {
* multiple times, and to avoid projecting and resaving the same domain multiple times.
*/
private static final String DOMAINS_TO_PROJECT_QUERY =
"FROM Domain d WHERE (d.transferData.transferStatus = 'PENDING' AND"
"SELECT repoId FROM Domain d WHERE (d.transferData.transferStatus = 'PENDING' AND"
+ " d.transferData.pendingTransferExpirationTime < current_timestamp()) OR"
+ " (d.registrationExpirationTime < current_timestamp() AND d.deletionTime ="
+ " (:END_OF_TIME)) OR (EXISTS (SELECT 1 FROM GracePeriod gp WHERE gp.domainRepoId ="
@@ -99,13 +102,13 @@ public class ResaveAllEppResourcesPipeline implements Serializable {
/** Projects to the current time and saves any contacts with expired transfers. */
private void fastResaveContacts(Pipeline pipeline) {
Read<ContactResource, ContactResource> read =
Read<String, String> repoIdRead =
RegistryJpaIO.read(
"FROM Contact WHERE transferData.transferStatus = 'PENDING' AND"
"SELECT repoId FROM Contact WHERE transferData.transferStatus = 'PENDING' AND"
+ " transferData.pendingTransferExpirationTime < current_timestamp()",
ContactResource.class,
c -> c);
projectAndResaveResources(pipeline, ContactResource.class, read);
String.class,
r -> r);
projectAndResaveResources(pipeline, Contact.class, repoIdRead);
}
/**
@@ -116,61 +119,72 @@ public class ResaveAllEppResourcesPipeline implements Serializable {
* DomainBase#cloneProjectedAtTime(DateTime)}.
*/
private void fastResaveDomains(Pipeline pipeline) {
Read<Domain, Domain> read =
Read<String, String> repoIdRead =
RegistryJpaIO.read(
DOMAINS_TO_PROJECT_QUERY,
ImmutableMap.of("END_OF_TIME", DateTimeUtils.END_OF_TIME),
Domain.class,
d -> d);
projectAndResaveResources(pipeline, Domain.class, read);
String.class,
r -> r);
projectAndResaveResources(pipeline, Domain.class, repoIdRead);
}
/** Projects all resources to the current time and saves them. */
private <T extends EppResource> void forceResaveAllResources(Pipeline pipeline, Class<T> clazz) {
Read<T, T> read = RegistryJpaIO.read(() -> CriteriaQueryBuilder.create(clazz).build());
projectAndResaveResources(pipeline, clazz, read);
Read<String, String> repoIdRead =
RegistryJpaIO.read(
// Note: cannot use SQL parameters for the table name
String.format("SELECT repoId FROM %s", clazz.getSimpleName()), String.class, r -> r);
projectAndResaveResources(pipeline, clazz, repoIdRead);
}
/** Projects and re-saves the result of the provided {@link Read}. */
/** Projects and re-saves all resources with repo IDs provided by the {@link Read}. */
private <T extends EppResource> void projectAndResaveResources(
Pipeline pipeline, Class<T> clazz, Read<?, T> read) {
Pipeline pipeline, Class<T> clazz, Read<?, String> repoIdRead) {
int numShards = options.getSqlWriteShards();
int batchSize = options.getSqlWriteBatchSize();
String className = clazz.getSimpleName();
pipeline
.apply("Read " + className, read)
.apply("Read " + className, repoIdRead)
.apply(
"Shard data for class" + className,
WithKeys.<Integer, T>of(e -> ThreadLocalRandom.current().nextInt(numShards))
WithKeys.<Integer, String>of(e -> ThreadLocalRandom.current().nextInt(numShards))
.withKeyType(integers()))
.apply(
"Group into batches for class" + className,
GroupIntoBatches.<Integer, T>ofSize(batchSize).withShardedKey())
.apply("Map " + className + " to now", ParDo.of(new BatchedProjectionFunction<>()))
GroupIntoBatches.<Integer, String>ofSize(batchSize).withShardedKey())
.apply(
"Write transformed " + className,
RegistryJpaIO.<EppResource>write()
.withName("Write transformed " + className)
.withBatchSize(batchSize)
.withShards(numShards));
"Load, map, and save " + className,
ParDo.of(new BatchedLoadProjectAndSaveFunction(clazz)));
}
private static class BatchedProjectionFunction<T extends EppResource>
extends DoFn<KV<ShardedKey<Integer>, Iterable<T>>, EppResource> {
/** Function that loads, projects, and saves resources all in the same transaction. */
private static class BatchedLoadProjectAndSaveFunction
extends DoFn<KV<ShardedKey<Integer>, Iterable<String>>, Void> {
private final Class<? extends EppResource> clazz;
private BatchedLoadProjectAndSaveFunction(Class<? extends EppResource> clazz) {
this.clazz = clazz;
}
@ProcessElement
public void processElement(
@Element KV<ShardedKey<Integer>, Iterable<T>> element,
OutputReceiver<EppResource> outputReceiver) {
@Element KV<ShardedKey<Integer>, Iterable<String>> element,
OutputReceiver<Void> outputReceiver) {
jpaTm()
.transact(
() ->
element
.getValue()
.forEach(
resource ->
outputReceiver.output(
resource.cloneProjectedAtTime(jpaTm().getTransactionTime()))));
() -> {
DateTime now = jpaTm().getTransactionTime();
ImmutableList<VKey<? extends EppResource>> keys =
Streams.stream(element.getValue())
.map(repoId -> VKey.create(clazz, repoId))
.collect(toImmutableList());
ImmutableList<EppResource> mappedResources =
jpaTm().loadByKeys(keys).values().stream()
.map(r -> r.cloneProjectedAtTime(now))
.collect(toImmutableList());
jpaTm().putAll(mappedResources);
});
}
}
@@ -1320,8 +1320,14 @@ public final class RegistryConfig {
@Provides
@Config("registrySupportEmail")
public static String provideRegistrySupportEmail(RegistryConfigSettings config) {
return config.dnsUpdate.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
@@ -1476,11 +1482,6 @@ public final class RegistryConfig {
return CONFIG_SETTINGS.get().registryPolicy.defaultRegistrarWhoisServer;
}
/** Returns the number of {@code EppResourceIndex} buckets to be used. */
public static int getEppResourceIndexBucketCount() {
return CONFIG_SETTINGS.get().datastore.eppResourceIndexBucketsNum;
}
/** Returns the base retry duration that gets doubled after each failure within {@code Ofy}. */
public static Duration getBaseOfyRetryDuration() {
return Duration.millis(CONFIG_SETTINGS.get().datastore.baseOfyRetryMillis);
@@ -108,7 +108,6 @@ public class RegistryConfigSettings {
/** Configuration for Cloud Datastore. */
public static class Datastore {
public int eppResourceIndexBucketsNum;
public int baseOfyRetryMillis;
}
@@ -254,5 +253,6 @@ public class RegistryConfigSettings {
public String dnsUpdateFailEmailBodyText;
public String dnsUpdateFailRegistryName;
public String registrySupportEmail;
public String registryCcEmail;
}
}
@@ -183,10 +183,6 @@ registryPolicy:
requireSslCertificates: true
datastore:
# Number of EPP resource index buckets in Datastore. Dont change after
# initial install.
eppResourceIndexBucketsNum: 997
# Milliseconds that Objectify waits to retry a Datastore transaction (this
# doubles after each failure).
baseOfyRetryMillis: 100
@@ -481,6 +477,7 @@ contactHistory:
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:
@@ -34,6 +34,7 @@ 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;
@@ -52,14 +53,16 @@ import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.lock.LockHandler;
import google.registry.ui.server.SendEmailUtils;
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;
@@ -78,7 +81,7 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
// tasks.
public static final String APP_ENGINE_RETRY_HEADER = "X-AppEngine-TaskRetryCount";
public static final String CLOUD_TASKS_RETRY_HEADER = "X-CloudTasks-TaskRetryCount";
public static final int RETRIES_BEFORE_PERMANENT_FAILURE = 10;
public static final int RETRIES_BEFORE_PERMANENT_FAILURE = 20;
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -109,11 +112,13 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
private final Clock clock;
private final CloudTasksUtils cloudTasksUtils;
private final Response response;
private final SendEmailUtils sendEmailUtils;
private final SendEmailService sendEmailService;
private final String dnsUpdateFailEmailSubjectText;
private final String dnsUpdateFailEmailBodyText;
private final String dnsUpdateFailRegistryName;
private final String registrySupportEmail;
private final Lazy<InternetAddress> registrySupportEmail;
private final Lazy<InternetAddress> registryCcEmail;
private final InternetAddress gSuiteOutgoingEmailAddress;
@Inject
public PublishDnsUpdatesAction(
@@ -129,7 +134,9 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
@Config("dnsUpdateFailEmailSubjectText") String dnsUpdateFailEmailSubjectText,
@Config("dnsUpdateFailEmailBodyText") String dnsUpdateFailEmailBodyText,
@Config("dnsUpdateFailRegistryName") String dnsUpdateFailRegistryName,
@Config("registrySupportEmail") String registrySupportEmail,
@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,
@@ -138,13 +145,13 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
LockHandler lockHandler,
Clock clock,
CloudTasksUtils cloudTasksUtils,
SendEmailUtils sendEmailUtils,
SendEmailService sendEmailService,
Response response) {
this.dnsQueue = dnsQueue;
this.dnsWriterProxy = dnsWriterProxy;
this.dnsMetrics = dnsMetrics;
this.timeout = timeout;
this.sendEmailUtils = sendEmailUtils;
this.sendEmailService = sendEmailService;
this.retryCount =
cloudTasksRetryCount.orElse(
appEngineRetryCount.orElseThrow(
@@ -165,6 +172,8 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
this.dnsUpdateFailEmailSubjectText = dnsUpdateFailEmailSubjectText;
this.dnsUpdateFailRegistryName = dnsUpdateFailRegistryName;
this.registrySupportEmail = registrySupportEmail;
this.registryCcEmail = registryCcEmail;
this.gSuiteOutgoingEmailAddress = gSuiteOutgoingEmailAddress;
}
private void recordActionResult(ActionStatus status) {
@@ -267,25 +276,48 @@ 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()) {
sendEmailUtils.sendEmail(
dnsUpdateFailEmailSubjectText,
String body =
String.format(
dnsUpdateFailEmailBodyText,
registrar.get().getRegistrarName(),
hostOrDomainName,
isHost ? "host" : "domain",
registrySupportEmail,
dnsUpdateFailRegistryName),
registrySupportEmail.get().getAddress(),
dnsUpdateFailRegistryName);
ImmutableList<InternetAddress> recipients =
registrar.get().getContacts().stream()
.filter(c -> c.getTypes().contains(RegistrarPoc.Type.ADMIN))
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableList()));
.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));
}
@@ -1,6 +1,6 @@
<datastore-indexes autoGenerate="false">
<!-- For finding contact resources by registrar. -->
<datastore-index kind="ContactResource" ancestor="false" source="manual">
<datastore-index kind="Contact" ancestor="false" source="manual">
<property name="currentSponsorClientId" direction="asc"/>
<property name="deletionTime" direction="asc"/>
<property name="searchName" direction="asc"/>
@@ -91,7 +91,7 @@
<property name="deletionTime" direction="asc"/>
<property name="fullyQualifiedHostName" direction="asc"/>
</datastore-index>
<datastore-index kind="ContactResource" ancestor="false" source="manual">
<datastore-index kind="Contact" ancestor="false" source="manual">
<property name="deletionTime" direction="asc"/>
<property name="searchName" direction="asc"/>
</datastore-index>
@@ -10,10 +10,10 @@
<name>dns-publish</name>
<rate>100/s</rate>
<bucket-size>100</bucket-size>
<!-- 30 sec backoff increasing linearly up to 10 minutes. -->
<!-- 30 sec backoff increasing linearly up to 30 minutes. -->
<retry-parameters>
<min-backoff-seconds>30</min-backoff-seconds>
<max-backoff-seconds>600</max-backoff-seconds>
<max-backoff-seconds>1800</max-backoff-seconds>
<max-doublings>0</max-doublings>
</retry-parameters>
</queue>
@@ -44,8 +44,8 @@ import dagger.Module;
import dagger.Provides;
import google.registry.flows.domain.DomainFlowUtils.BadCommandForRegistryPhaseException;
import google.registry.flows.domain.DomainFlowUtils.InvalidIdnDomainLabelException;
import google.registry.model.ForeignKeyUtils;
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;
import google.registry.monitoring.whitebox.CheckApiMetric;
@@ -156,7 +156,7 @@ public class CheckApiAction implements Runnable {
}
private boolean checkExists(String domainString, DateTime now) {
return !ForeignKeyIndex.loadCached(Domain.class, ImmutableList.of(domainString), now).isEmpty();
return !ForeignKeyUtils.loadCached(Domain.class, ImmutableList.of(domainString), now).isEmpty();
}
private Optional<String> checkReserved(InternetDomainName domainName) {
@@ -17,7 +17,6 @@ package google.registry.flows;
import static com.google.common.collect.Sets.intersection;
import static google.registry.model.EppResourceUtils.isLinked;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
@@ -38,14 +37,14 @@ import google.registry.flows.exceptions.TooManyResourceChecksException;
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.ForeignKeyUtils;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Period;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import java.util.List;
@@ -70,22 +69,17 @@ public final class ResourceFlowUtils {
/**
* Check whether if there are domains linked to the resource to be deleted. Throws an exception if
* so.
*
* <p>Note that in datastore this is a smoke test as the query for linked domains is eventually
* consistent, so we only check a few domains to fail fast.
*/
public static <R extends EppResource> void checkLinkedDomains(
final String targetId, final DateTime now, final Class<R> resourceClass) throws EppException {
EppException failfastException =
tm().transact(
() -> {
final ForeignKeyIndex<R> fki = ForeignKeyIndex.load(resourceClass, targetId, now);
if (fki == null) {
VKey<R> key = ForeignKeyUtils.load(resourceClass, targetId, now);
if (key == null) {
return new ResourceDoesNotExistException(resourceClass, targetId);
}
return isLinked(fki.getResourceKey(), now)
? new ResourceToDeleteIsReferencedException()
: null;
return isLinked(key, now) ? new ResourceToDeleteIsReferencedException() : null;
});
if (failfastException != null) {
throw failfastException;
@@ -118,7 +112,7 @@ public final class ResourceFlowUtils {
public static <R extends EppResource> void verifyResourceDoesNotExist(
Class<R> clazz, String targetId, DateTime now, String registrarId) throws EppException {
VKey<R> key = loadAndGetKey(clazz, targetId, now);
VKey<R> key = ForeignKeyUtils.load(clazz, targetId, now);
if (key != null) {
R resource = tm().loadByKey(key);
// These are similar exceptions, but we can track them internally as log-based metrics.
@@ -139,7 +133,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, ContactResource contact)
public static void verifyOptionalAuthInfo(Optional<AuthInfo> authInfo, Contact contact)
throws EppException {
if (authInfo.isPresent()) {
verifyAuthInfo(authInfo.get(), contact);
@@ -167,7 +161,7 @@ public final class ResourceFlowUtils {
return;
}
// The roid should match one of the contacts.
Optional<VKey<ContactResource>> foundContact =
Optional<VKey<Contact>> foundContact =
domain.getReferencedContacts().stream()
.filter(key -> key.getSqlKey().equals(authRepoId))
.findFirst();
@@ -179,8 +173,7 @@ public final class ResourceFlowUtils {
}
/** Check that the given {@link AuthInfo} is valid for the given contact. */
public static void verifyAuthInfo(AuthInfo authInfo, ContactResource contact)
throws EppException {
public static void verifyAuthInfo(AuthInfo authInfo, Contact contact) throws EppException {
String authRepoId = authInfo.getPw().getRepoId();
String authPassword = authInfo.getPw().getValue();
String contactPassword = contact.getAuthInfo().getPw().getValue();
@@ -26,8 +26,8 @@ import google.registry.flows.ExtensionManager;
import google.registry.flows.Flow;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactCommand.Check;
import google.registry.model.contact.ContactResource;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.CheckData.ContactCheck;
import google.registry.model.eppoutput.CheckData.ContactCheckData;
@@ -62,7 +62,7 @@ public final class ContactCheckFlow implements Flow {
ImmutableList<String> targetIds = ((Check) resourceCommand).getTargetIds();
verifyTargetIdCount(targetIds, maxChecks);
ImmutableSet<String> existingIds =
checkResourcesExist(ContactResource.class, targetIds, clock.nowUtc());
checkResourcesExist(Contact.class, targetIds, clock.nowUtc());
ImmutableList.Builder<ContactCheck> checks = new ImmutableList.Builder<>();
for (String id : targetIds) {
boolean unused = !existingIds.contains(id);
@@ -23,7 +23,6 @@ import static google.registry.model.IdService.allocateId;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
@@ -33,15 +32,13 @@ import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
import google.registry.flows.exceptions.ResourceCreateContentionException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactCommand.Create;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.CreateData.ContactCreateData;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.index.EppResourceIndex;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import javax.inject.Inject;
@@ -75,9 +72,9 @@ public final class ContactCreateFlow implements TransactionalFlow {
extensionManager.validate();
Create command = (Create) resourceCommand;
DateTime now = tm().getTransactionTime();
verifyResourceDoesNotExist(ContactResource.class, targetId, now, registrarId);
ContactResource newContact =
new ContactResource.Builder()
verifyResourceDoesNotExist(Contact.class, targetId, now, registrarId);
Contact newContact =
new Contact.Builder()
.setContactId(targetId)
.setAuthInfo(command.getAuthInfo())
.setCreationRegistrarId(registrarId)
@@ -96,12 +93,7 @@ public final class ContactCreateFlow implements TransactionalFlow {
.setType(HistoryEntry.Type.CONTACT_CREATE)
.setXmlBytes(null) // We don't want to store contact details in the history entry.
.setContact(newContact);
tm().insertAll(
ImmutableSet.of(
newContact,
historyBuilder.build(),
ForeignKeyIndex.create(newContact, newContact.getDeletionTime()),
EppResourceIndex.create(Key.create(newContact))));
tm().insertAll(ImmutableSet.of(newContact, historyBuilder.build()));
return responseBuilder
.setResData(ContactCreateData.create(newContact.getContactId(), now))
.build();
@@ -35,8 +35,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.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
@@ -91,15 +91,15 @@ public final class ContactDeleteFlow implements TransactionalFlow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
checkLinkedDomains(targetId, now, ContactResource.class);
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
checkLinkedDomains(targetId, now, Contact.class);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES);
verifyOptionalAuthInfo(authInfo, existingContact);
if (!isSuperuser) {
verifyResourceOwnership(registrarId, existingContact);
}
// Handle pending transfers on contact deletion.
ContactResource newContact =
Contact newContact =
existingContact.getStatusValues().contains(StatusValue.PENDING_TRANSFER)
? denyPendingTransfer(existingContact, SERVER_CANCELLED, now, registrarId)
: existingContact;
@@ -24,10 +24,10 @@ import com.googlecode.objectify.Key;
import google.registry.flows.EppException;
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactHistory.ContactHistoryId;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.PostalInfo;
import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
@@ -61,7 +61,7 @@ public class ContactFlowUtils {
}
/** Check contact's state against server policy. */
static void validateContactAgainstPolicy(ContactResource contact) throws EppException {
static void validateContactAgainstPolicy(Contact contact) throws EppException {
if (contact.getDisclose() != null && !contact.getDisclose().getFlag()) {
throw new DeclineContactDisclosureFieldDisallowedPolicyException();
}
@@ -27,8 +27,8 @@ import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactInfoData;
import google.registry.model.contact.ContactResource;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppoutput.EppResponse;
@@ -69,7 +69,7 @@ public final class ContactInfoFlow implements Flow {
DateTime now = clock.nowUtc();
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate(); // There are no legal extensions for this flow.
ContactResource contact = loadAndVerifyExistence(ContactResource.class, targetId, now);
Contact contact = loadAndVerifyExistence(Contact.class, targetId, now);
if (!isSuperuser) {
verifyResourceOwnership(registrarId, contact);
}
@@ -33,8 +33,8 @@ import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppinput.ResourceCommand;
@@ -74,7 +74,7 @@ public final class ContactTransferApproveFlow implements TransactionalFlow {
/**
* The logic in this flow, which handles client approvals, very closely parallels the logic in
* {@link ContactResource#cloneProjectedAtTime} which handles implicit server approvals.
* {@link Contact#cloneProjectedAtTime} which handles implicit server approvals.
*/
@Override
public EppResponse run() throws EppException {
@@ -82,11 +82,11 @@ public final class ContactTransferApproveFlow implements TransactionalFlow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyHasPendingTransfer(existingContact);
verifyResourceOwnership(registrarId, existingContact);
ContactResource newContact =
Contact newContact =
approvePendingTransfer(existingContact, TransferStatus.CLIENT_APPROVED, now);
ContactHistory contactHistory =
historyBuilder.setType(CONTACT_TRANSFER_APPROVE).setContact(newContact).build();
@@ -33,8 +33,8 @@ import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppinput.ResourceCommand;
@@ -78,11 +78,11 @@ public final class ContactTransferCancelFlow implements TransactionalFlow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyHasPendingTransfer(existingContact);
verifyTransferInitiator(registrarId, existingContact);
ContactResource newContact =
Contact newContact =
denyPendingTransfer(existingContact, TransferStatus.CLIENT_CANCELLED, now, registrarId);
ContactHistory contactHistory =
historyBuilder.setType(CONTACT_TRANSFER_CANCEL).setContact(newContact).build();
@@ -27,7 +27,7 @@ import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.NoTransferHistoryToQueryException;
import google.registry.flows.exceptions.NotAuthorizedToViewTransferException;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
@@ -66,8 +66,7 @@ public final class ContactTransferQueryFlow implements Flow {
public EppResponse run() throws EppException {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate(); // There are no legal extensions for this flow.
ContactResource contact =
loadAndVerifyExistence(ContactResource.class, targetId, clock.nowUtc());
Contact contact = loadAndVerifyExistence(Contact.class, targetId, clock.nowUtc());
verifyOptionalAuthInfo(authInfo, contact);
// 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).
@@ -33,8 +33,8 @@ import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppResponse;
@@ -76,11 +76,11 @@ public final class ContactTransferRejectFlow implements TransactionalFlow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyHasPendingTransfer(existingContact);
verifyResourceOwnership(registrarId, existingContact);
ContactResource newContact =
Contact newContact =
denyPendingTransfer(existingContact, TransferStatus.CLIENT_REJECTED, now, registrarId);
ContactHistory contactHistory =
historyBuilder.setType(CONTACT_TRANSFER_REJECT).setContact(newContact).build();
@@ -38,8 +38,8 @@ import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
@@ -96,7 +96,7 @@ public final class ContactTransferRequestFlow implements TransactionalFlow {
validateRegistrarIsLoggedIn(gainingClientId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyAuthInfoPresentForResourceTransfer(authInfo);
verifyAuthInfo(authInfo.get(), existingContact);
// Verify that the resource does not already have a pending transfer.
@@ -146,10 +146,12 @@ public final class ContactTransferRequestFlow implements TransactionalFlow {
.asBuilder()
.setEventTime(now) // Unlike the serverApprove messages, this applies immediately.
.build();
ContactResource newContact = existingContact.asBuilder()
.setTransferData(pendingTransferData)
.addStatusValue(StatusValue.PENDING_TRANSFER)
.build();
Contact newContact =
existingContact
.asBuilder()
.setTransferData(pendingTransferData)
.addStatusValue(StatusValue.PENDING_TRANSFER)
.build();
tm().update(newContact);
tm().insertAll(
ImmutableSet.of(
@@ -36,10 +36,10 @@ import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactCommand.Update;
import google.registry.model.contact.ContactCommand.Update.Change;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
@@ -94,7 +94,7 @@ public final class ContactUpdateFlow implements TransactionalFlow {
extensionManager.validate();
Update command = (Update) resourceCommand;
DateTime now = tm().getTransactionTime();
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
ImmutableSet<StatusValue> statusToRemove = command.getInnerRemove().getStatusValues();
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
@@ -104,7 +104,7 @@ public final class ContactUpdateFlow implements TransactionalFlow {
}
verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES);
checkSameValuesNotAddedAndRemoved(statusesToAdd, statusToRemove);
ContactResource.Builder builder = existingContact.asBuilder();
Contact.Builder builder = existingContact.asBuilder();
Change change = command.getInnerChange();
// The spec requires the following behaviors:
// * If you update part of a postal info, the fields that you didn't update are unchanged.
@@ -126,7 +126,7 @@ public final class ContactUpdateFlow implements TransactionalFlow {
builder.setInternationalizedPostalInfo(null);
}
}
ContactResource newContact =
Contact newContact =
builder
.setLastEppUpdateTime(now)
.setLastEppUpdateRegistrarId(registrarId)
@@ -53,6 +53,7 @@ import google.registry.flows.custom.DomainCheckFlowCustomLogic.BeforeResponseRet
import google.registry.flows.domain.token.AllocationTokenDomainCheckResults;
import google.registry.flows.domain.token.AllocationTokenFlowUtils;
import google.registry.model.EppResource;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.billing.BillingEvent;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Check;
@@ -70,7 +71,6 @@ import google.registry.model.eppoutput.CheckData.DomainCheck;
import google.registry.model.eppoutput.CheckData.DomainCheckData;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.tld.Registry;
import google.registry.model.tld.Registry.TldState;
@@ -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<Domain>> existingDomains =
ForeignKeyIndex.load(Domain.class, domainNames, now);
ImmutableMap<String, VKey<Domain>> existingDomains =
ForeignKeyUtils.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<Domain>> existingDomains,
ImmutableMap<String, VKey<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<Domain>> existingDomains,
ImmutableMap<String, VKey<Domain>> existingDomains,
ImmutableSet<String> availableDomains,
DateTime now,
Optional<AllocationToken> allocationToken)
@@ -297,14 +297,14 @@ public final class DomainCheckFlow implements Flow {
* renewal is part of the cost of a restore.
*
* <p>This may be resource-intensive for large checks of many restore fees, but those are
* comparatively rare, and we are at least using an in-memory cache. Also this will get a lot
* comparatively rare, and we are at least using an in-memory cache. Also, this will get a lot
* nicer in Cloud SQL when we can SELECT just the fields we want rather than having to load the
* entire entity.
*/
private ImmutableMap<String, Domain> loadDomainsForRestoreChecks(
FeeCheckCommandExtension<?, ?> feeCheck,
ImmutableMap<String, InternetDomainName> domainNames,
ImmutableMap<String, ForeignKeyIndex<Domain>> existingDomains) {
ImmutableMap<String, VKey<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.
@@ -329,7 +329,7 @@ public final class DomainCheckFlow implements Flow {
ImmutableMap<String, VKey<Domain>> existingDomainsToLoad =
restoreCheckDomains.stream()
.filter(existingDomains::containsKey)
.collect(toImmutableMap(d -> d, d -> existingDomains.get(d).getResourceKey()));
.collect(toImmutableMap(d -> d, existingDomains::get));
ImmutableMap<VKey<? extends EppResource>, EppResource> loadedDomains =
EppResource.loadCached(ImmutableList.copyOf(existingDomainsToLoad.values()));
return ImmutableMap.copyOf(
@@ -59,7 +59,6 @@ import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InternetDomainName;
import com.googlecode.objectify.Key;
import google.registry.dns.DnsQueue;
import google.registry.flows.EppException;
import google.registry.flows.EppException.CommandUseErrorException;
@@ -105,8 +104,6 @@ import google.registry.model.eppinput.EppInput;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.CreateData.DomainCreateData;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.index.EppResourceIndex;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.poll.PollMessage.Autorenew;
@@ -154,6 +151,7 @@ import org.joda.time.Duration;
* @error {@link DomainCreateFlow.MustHaveSignedMarksInCurrentPhaseException}
* @error {@link DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException}
* @error {@link DomainCreateFlow.NoTrademarkedRegistrationsBeforeSunriseException}
* @error {@link DomainCreateFlow.PackageDomainRegisteredForTooManyYearsException}
* @error {@link DomainCreateFlow.SignedMarksOnlyDuringSunriseException}
* @error {@link DomainFlowTmchUtils.NoMarksFoundMatchingDomainException}
* @error {@link DomainFlowTmchUtils.FoundMarkNotYetValidException}
@@ -392,6 +390,9 @@ public final class DomainCreateFlow implements TransactionalFlow {
.build();
if (allocationToken.isPresent()
&& allocationToken.get().getTokenType().equals(TokenType.PACKAGE)) {
if (years > 1) {
throw new PackageDomainRegisteredForTooManyYearsException(allocationToken.get().getToken());
}
domain =
domain.asBuilder().setCurrentPackageToken(allocationToken.get().createVKey()).build();
}
@@ -401,11 +402,7 @@ public final class DomainCreateFlow implements TransactionalFlow {
entitiesToSave.add(
createNameCollisionOneTimePollMessage(targetId, domainHistory, registrarId, now));
}
entitiesToSave.add(
domain,
domainHistory,
ForeignKeyIndex.create(domain, domain.getDeletionTime()),
EppResourceIndex.create(Key.create(domain)));
entitiesToSave.add(domain, domainHistory);
if (allocationToken.isPresent()
&& TokenType.SINGLE_USE.equals(allocationToken.get().getTokenType())) {
entitiesToSave.add(
@@ -757,4 +754,14 @@ public final class DomainCreateFlow implements TransactionalFlow {
ANCHOR_TENANT_CREATE_VALID_YEARS, invalidYears));
}
}
/** Package domain registered for too many years. */
static class PackageDomainRegisteredForTooManyYearsException extends CommandUseErrorException {
public PackageDomainRegisteredForTooManyYearsException(String token) {
super(
String.format(
"The package token %s cannot be used to register names for longer than 1 year.",
token));
}
}
}
@@ -29,7 +29,6 @@ import static google.registry.flows.domain.DomainFlowUtils.updateAutorenewRecurr
import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPredelegation;
import static google.registry.model.ResourceTransferUtils.denyPendingTransfer;
import static google.registry.model.ResourceTransferUtils.handlePendingTransferOnDelete;
import static google.registry.model.ResourceTransferUtils.updateForeignKeyIndexDeletionTime;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.reporting.DomainTransactionRecord.TransactionReportField.ADD_FIELDS;
@@ -257,7 +256,6 @@ public final class DomainDeleteFlow implements TransactionalFlow {
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.
@@ -79,7 +79,7 @@ import google.registry.model.billing.BillingEvent;
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.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.Domain;
@@ -380,9 +380,7 @@ public class DomainFlowUtils {
/** Verify that no linked resources have disallowed statuses. */
static void verifyNotInPendingDelete(
Set<DesignatedContact> contacts,
VKey<ContactResource> registrant,
Set<VKey<Host>> nameservers)
Set<DesignatedContact> contacts, VKey<Contact> registrant, Set<VKey<Host>> nameservers)
throws EppException {
ImmutableList.Builder<VKey<? extends EppResource>> keysToLoad = new ImmutableList.Builder<>();
contacts.stream().map(DesignatedContact::getContactKey).forEach(keysToLoad::add);
@@ -427,7 +425,7 @@ public class DomainFlowUtils {
static void validateNoDuplicateContacts(Set<DesignatedContact> contacts)
throws ParameterValuePolicyErrorException {
ImmutableMultimap<Type, VKey<ContactResource>> contactsByType =
ImmutableMultimap<Type, VKey<Contact>> contactsByType =
contacts.stream()
.collect(
toImmutableSetMultimap(
@@ -436,15 +434,13 @@ public class DomainFlowUtils {
// If any contact type has multiple contacts:
if (contactsByType.asMap().values().stream().anyMatch(v -> v.size() > 1)) {
// Find the duplicates.
Map<Type, Collection<VKey<ContactResource>>> dupeKeysMap =
Map<Type, Collection<VKey<Contact>>> dupeKeysMap =
Maps.filterEntries(contactsByType.asMap(), e -> e.getValue().size() > 1);
ImmutableList<VKey<ContactResource>> dupeKeys =
ImmutableList<VKey<Contact>> dupeKeys =
dupeKeysMap.values().stream().flatMap(Collection::stream).collect(toImmutableList());
// Load the duplicates in one batch.
Map<VKey<? extends ContactResource>, ContactResource> dupeContacts =
tm().loadByKeys(dupeKeys);
ImmutableMultimap.Builder<Type, VKey<ContactResource>> typesMap =
new ImmutableMultimap.Builder<>();
Map<VKey<? extends Contact>, Contact> dupeContacts = tm().loadByKeys(dupeKeys);
ImmutableMultimap.Builder<Type, VKey<Contact>> typesMap = new ImmutableMultimap.Builder<>();
dupeKeysMap.forEach(typesMap::putAll);
// Create an error message showing the type and contact IDs of the duplicates.
throw new DuplicateContactForRoleException(
@@ -453,7 +449,7 @@ public class DomainFlowUtils {
}
static void validateRequiredContactsPresent(
@Nullable VKey<ContactResource> registrant, Set<DesignatedContact> contacts)
@Nullable VKey<Contact> registrant, Set<DesignatedContact> contacts)
throws RequiredParameterMissingException {
if (registrant == null) {
throw new MissingRegistrantException();
@@ -30,6 +30,7 @@ import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.Flow;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.custom.DomainInfoFlowCustomLogic;
@@ -42,6 +43,8 @@ import google.registry.model.domain.DomainCommand.Info.HostsRequest;
import google.registry.model.domain.DomainInfoData;
import google.registry.model.domain.fee06.FeeInfoCommandExtensionV06;
import google.registry.model.domain.fee06.FeeInfoResponseExtensionV06;
import google.registry.model.domain.packagetoken.PackageTokenExtension;
import google.registry.model.domain.packagetoken.PackageTokenResponseExtension;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.rgp.RgpInfoExtension;
import google.registry.model.eppcommon.AuthInfo;
@@ -85,13 +88,14 @@ public final class DomainInfoFlow implements Flow {
@Inject EppResponse.Builder responseBuilder;
@Inject DomainInfoFlowCustomLogic flowCustomLogic;
@Inject DomainPricingLogic pricingLogic;
@Inject @Superuser boolean isSuperuser;
@Inject
DomainInfoFlow() {}
@Override
public EppResponse run() throws EppException {
extensionManager.register(FeeInfoCommandExtensionV06.class);
extensionManager.register(FeeInfoCommandExtensionV06.class, PackageTokenExtension.class);
flowCustomLogic.beforeValidation();
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
@@ -150,6 +154,15 @@ public final class DomainInfoFlow implements Flow {
if (!gracePeriodStatuses.isEmpty()) {
extensions.add(RgpInfoExtension.create(gracePeriodStatuses));
}
Optional<PackageTokenExtension> packageInfo =
eppInput.getSingleExtension(PackageTokenExtension.class);
if (packageInfo.isPresent()) {
// Package info was requested.
if (isSuperuser || registrarId.equals(domain.getCurrentSponsorRegistrarId())) {
// Only show package info to owning registrar or superusers
extensions.add(PackageTokenResponseExtension.create(domain.getCurrentPackageToken()));
}
}
Optional<FeeInfoCommandExtensionV06> feeInfo =
eppInput.getSingleExtension(FeeInfoCommandExtensionV06.class);
if (feeInfo.isPresent()) { // Fee check was requested.
@@ -30,6 +30,8 @@ import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge;
import static google.registry.flows.domain.DomainFlowUtils.validateRegistrationPeriod;
import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive;
import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.maybeApplyPackageRemovalToken;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyTokenAllowedOnDomain;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_RENEW;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.leapSafeAddYears;
@@ -119,6 +121,10 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link DomainRenewFlow.IncorrectCurrentExpirationDateException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemovePackageTokenOnPackageDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.RemovePackageTokenOnNonPackageDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
@@ -174,7 +180,11 @@ public final class DomainRenewFlow implements TransactionalFlow {
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
verifyRenewAllowed(authInfo, existingDomain, command);
verifyRenewAllowed(authInfo, existingDomain, command, allocationToken);
// If client passed an applicable static token this updates the domain
existingDomain = maybeApplyPackageRemovalToken(existingDomain, allocationToken);
int years = command.getPeriod().getValue();
DateTime newExpirationTime =
leapSafeAddYears(existingDomain.getRegistrationExpirationTime(), years); // Uncapped
@@ -302,7 +312,11 @@ public final class DomainRenewFlow implements TransactionalFlow {
.build();
}
private void verifyRenewAllowed(Optional<AuthInfo> authInfo, Domain existingDomain, Renew command)
private void verifyRenewAllowed(
Optional<AuthInfo> authInfo,
Domain existingDomain,
Renew command,
Optional<AllocationToken> allocationToken)
throws EppException {
verifyOptionalAuthInfo(authInfo, existingDomain);
verifyNoDisallowedStatuses(existingDomain, RENEW_DISALLOWED_STATUSES);
@@ -312,6 +326,8 @@ public final class DomainRenewFlow implements TransactionalFlow {
checkHasBillingAccount(registrarId, existingDomain.getTld());
}
verifyUnitIsYears(command.getPeriod());
// We only allow __REMOVE_PACKAGE__ token on promo package domains for now
verifyTokenAllowedOnDomain(existingDomain, allocationToken);
// If the date they specify doesn't match the expiration, fail. (This is an idempotence check).
if (!command.getCurrentExpirationDate().equals(
existingDomain.getRegistrationExpirationTime().toLocalDate())) {
@@ -333,7 +349,11 @@ public final class DomainRenewFlow implements TransactionalFlow {
.setPeriodYears(years)
.setCost(renewCost)
.setEventTime(now)
.setAllocationToken(allocationToken.map(AllocationToken::createVKey).orElse(null))
.setAllocationToken(
allocationToken
.filter(t -> AllocationToken.TokenBehavior.DEFAULT.equals(t.getTokenBehavior()))
.map(AllocationToken::createVKey)
.orElse(null))
.setBillingTime(now.plus(Registry.get(tld).getRenewGracePeriodLength()))
.setDomainHistoryId(
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))
@@ -27,7 +27,6 @@ import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge;
import static google.registry.flows.domain.DomainFlowUtils.verifyNotReserved;
import static google.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked;
import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive;
import static google.registry.model.ResourceTransferUtils.updateForeignKeyIndexDeletionTime;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_RESTORE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
@@ -188,7 +187,6 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
autorenewPollMessage,
now,
registrarId);
updateForeignKeyIndexDeletionTime(newDomain);
DomainHistory domainHistory = buildDomainHistory(newDomain, now);
entitiesToSave.add(newDomain, domainHistory, autorenewEvent, autorenewPollMessage);
tm().putAll(entitiesToSave.build());
@@ -32,6 +32,8 @@ import static google.registry.flows.domain.DomainTransferUtils.createLosingTrans
import static google.registry.flows.domain.DomainTransferUtils.createPendingTransferData;
import static google.registry.flows.domain.DomainTransferUtils.createTransferResponse;
import static google.registry.flows.domain.DomainTransferUtils.createTransferServerApproveEntities;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.maybeApplyPackageRemovalToken;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyTokenAllowedOnDomain;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_TRANSFER_REQUEST;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -63,6 +65,7 @@ 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.AllocationToken;
import google.registry.model.domain.token.AllocationTokenExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
@@ -132,6 +135,8 @@ import org.joda.time.DateTime;
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForTldException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AlreadyRedeemedAllocationTokenException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemovePackageTokenOnPackageDomainException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_TRANSFER_REQUEST)
public final class DomainTransferRequestFlow implements TransactionalFlow {
@@ -169,19 +174,24 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Registry.get(existingDomain.getTld()),
gainingClientId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Registry.get(existingDomain.getTld()),
gainingClientId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
Optional<DomainTransferRequestSuperuserExtension> superuserExtension =
eppInput.getSingleExtension(DomainTransferRequestSuperuserExtension.class);
Period period =
superuserExtension.isPresent()
? superuserExtension.get().getRenewalPeriod()
: ((Transfer) resourceCommand).getPeriod();
verifyTransferAllowed(existingDomain, period, now, superuserExtension);
verifyTransferAllowed(existingDomain, period, now, superuserExtension, allocationToken);
// If client passed an applicable static token this updates the domain
existingDomain = maybeApplyPackageRemovalToken(existingDomain, allocationToken);
String tld = existingDomain.getTld();
Registry registry = Registry.get(tld);
// An optional extension from the client specifying what they think the transfer should cost.
@@ -293,9 +303,11 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
Domain existingDomain,
Period period,
DateTime now,
Optional<DomainTransferRequestSuperuserExtension> superuserExtension)
Optional<DomainTransferRequestSuperuserExtension> superuserExtension,
Optional<AllocationToken> allocationToken)
throws EppException {
verifyNoDisallowedStatuses(existingDomain, DISALLOWED_STATUSES);
verifyTokenAllowedOnDomain(existingDomain, allocationToken);
if (!isSuperuser) {
verifyAuthInfoPresentForResourceTransfer(authInfo);
verifyAuthInfo(authInfo.get(), existingDomain);
@@ -274,7 +274,7 @@ public final class DomainTransferUtils {
* renewal, we must issue a cancellation for the autorenew, so that the losing registrar will not
* be charged (essentially, the gaining registrar takes on the cost of the year of registration
* that the autorenew just added). 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
* without an additional year 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.
*
* <p>For details on the policy justification, see b/19430703#comment17 and <a
@@ -181,7 +181,10 @@ public final class DomainUpdateFlow implements TransactionalFlow {
DomainHistory domainHistory =
historyBuilder.setType(DOMAIN_UPDATE).setDomain(newDomain).build();
validateNewState(newDomain);
dnsQueue.addDomainRefreshTask(targetId);
if (newDomain.getDsData() != existingDomain.getDsData()
|| newDomain.getNsHosts() != existingDomain.getNsHosts()) {
dnsQueue.addDomainRefreshTask(targetId);
}
ImmutableSet.Builder<ImmutableObject> entitiesToSave = new ImmutableSet.Builder<>();
entitiesToSave.add(newDomain, domainHistory);
Optional<BillingEvent.OneTime> statusUpdateBillingEvent =
@@ -268,12 +271,18 @@ public final class DomainUpdateFlow implements TransactionalFlow {
.setLastEppUpdateRegistrarId(registrarId)
.addStatusValues(add.getStatusValues())
.removeStatusValues(remove.getStatusValues())
.addNameservers(add.getNameservers().stream().collect(toImmutableSet()))
.removeNameservers(remove.getNameservers().stream().collect(toImmutableSet()))
.removeContacts(remove.getContacts())
.addContacts(add.getContacts())
.setRegistrant(firstNonNull(change.getRegistrant(), domain.getRegistrant()))
.setAuthInfo(firstNonNull(change.getAuthInfo(), domain.getAuthInfo()));
if (!add.getNameservers().isEmpty()) {
domainBuilder.addNameservers(add.getNameservers().stream().collect(toImmutableSet()));
}
if (!remove.getNameservers().isEmpty()) {
domainBuilder.removeNameservers(remove.getNameservers().stream().collect(toImmutableSet()));
}
Optional<DomainUpdateSuperuserExtension> superuserExt =
eppInput.getSingleExtension(DomainUpdateSuperuserExtension.class);
if (superuserExt.isPresent()) {
@@ -15,6 +15,7 @@
package google.registry.flows.domain.token;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.base.Strings;
@@ -26,9 +27,12 @@ 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.billing.BillingEvent.Recurring;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
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.TokenBehavior;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.model.domain.token.AllocationTokenExtension;
@@ -109,23 +113,27 @@ public class AllocationTokenFlowUtils {
private void validateToken(
InternetDomainName domainName, AllocationToken token, String registrarId, DateTime now)
throws EppException {
if (!token.getAllowedRegistrarIds().isEmpty()
&& !token.getAllowedRegistrarIds().contains(registrarId)) {
throw new AllocationTokenNotValidForRegistrarException();
}
if (!token.getAllowedTlds().isEmpty()
&& !token.getAllowedTlds().contains(domainName.parent().toString())) {
throw new AllocationTokenNotValidForTldException();
}
if (token.getDomainName().isPresent()
&& !token.getDomainName().get().equals(domainName.toString())) {
throw new AllocationTokenNotValidForDomainException();
}
// Tokens without status transitions will just have a single-entry NOT_STARTED map, so only
// check the status transitions map if it's non-trivial.
if (token.getTokenStatusTransitions().size() > 1
&& !TokenStatus.VALID.equals(token.getTokenStatusTransitions().getValueAtTime(now))) {
throw new AllocationTokenNotInPromotionException();
// Only tokens with default behavior require validation
if (TokenBehavior.DEFAULT.equals(token.getTokenBehavior())) {
if (!token.getAllowedRegistrarIds().isEmpty()
&& !token.getAllowedRegistrarIds().contains(registrarId)) {
throw new AllocationTokenNotValidForRegistrarException();
}
if (!token.getAllowedTlds().isEmpty()
&& !token.getAllowedTlds().contains(domainName.parent().toString())) {
throw new AllocationTokenNotValidForTldException();
}
if (token.getDomainName().isPresent()
&& !token.getDomainName().get().equals(domainName.toString())) {
throw new AllocationTokenNotValidForDomainException();
}
// Tokens without status transitions will just have a single-entry NOT_STARTED map, so only
// check the status transitions map if it's non-trivial.
if (token.getTokenStatusTransitions().size() > 1
&& !TokenStatus.VALID.equals(token.getTokenStatusTransitions().getValueAtTime(now))) {
throw new AllocationTokenNotInPromotionException();
}
}
}
@@ -137,8 +145,15 @@ public class AllocationTokenFlowUtils {
// See https://tools.ietf.org/html/draft-ietf-regext-allocation-token-04#section-2.1
throw new InvalidAllocationTokenException();
}
Optional<AllocationToken> maybeTokenEntity =
tm().transact(() -> tm().loadByKeyIfPresent(VKey.create(AllocationToken.class, token)));
Optional<AllocationToken> maybeTokenEntity = AllocationToken.maybeGetStaticTokenInstance(token);
if (maybeTokenEntity.isPresent()) {
return maybeTokenEntity.get();
}
maybeTokenEntity =
tm().transact(() -> tm().loadByKeyIfPresent(VKey.createSql(AllocationToken.class, token)));
if (!maybeTokenEntity.isPresent()) {
throw new InvalidAllocationTokenException();
}
@@ -187,6 +202,49 @@ public class AllocationTokenFlowUtils {
tokenCustomLogic.validateToken(existingDomain, tokenEntity, registry, registrarId, now));
}
public static void verifyTokenAllowedOnDomain(
Domain domain, Optional<AllocationToken> allocationToken) throws EppException {
boolean domainHasPackageToken = domain.getCurrentPackageToken().isPresent();
boolean hasRemovePackageToken =
allocationToken.isPresent()
&& TokenBehavior.REMOVE_PACKAGE.equals(allocationToken.get().getTokenBehavior());
if (hasRemovePackageToken && !domainHasPackageToken) {
throw new RemovePackageTokenOnNonPackageDomainException();
} else if (!hasRemovePackageToken && domainHasPackageToken) {
throw new MissingRemovePackageTokenOnPackageDomainException();
}
}
public static Domain maybeApplyPackageRemovalToken(
Domain domain, Optional<AllocationToken> allocationToken) {
if (!allocationToken.isPresent()
|| !TokenBehavior.REMOVE_PACKAGE.equals(allocationToken.get().getTokenBehavior())) {
return domain;
}
Recurring newRecurringBillingEvent =
tm().loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
.setRenewalPrice(null)
.build();
// the Recurring billing event is reloaded later in the renew flow, so we synchronize changed
// RecurringBillingEvent with storage manually
tm().put(newRecurringBillingEvent);
jpaTm().getEntityManager().flush();
jpaTm().getEntityManager().clear();
// Remove current package token
return domain
.asBuilder()
.setCurrentPackageToken(null)
.setAutorenewBillingEvent(newRecurringBillingEvent.createVKey())
.build();
}
// Note: exception messages should be <= 32 characters long for domain check results
/** The allocation token is not currently valid. */
@@ -234,4 +292,20 @@ public class AllocationTokenFlowUtils {
super("The allocation token is invalid");
}
}
/** The __REMOVEPACKAGE__ token is missing on a package domain command */
public static class MissingRemovePackageTokenOnPackageDomainException
extends AssociationProhibitsOperationException {
MissingRemovePackageTokenOnPackageDomainException() {
super("Domains that are inside packages cannot be explicitly renewed or transferred");
}
}
/** The __REMOVEPACKAGE__ token is not allowed on non package domains */
public static class RemovePackageTokenOnNonPackageDomainException
extends AssociationProhibitsOperationException {
RemovePackageTokenOnNonPackageDomainException() {
super("__REMOVEPACKAGE__ token is not allowed on non package domains");
}
}
}
@@ -27,7 +27,6 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import google.registry.config.RegistryConfig.Config;
import google.registry.dns.DnsQueue;
import google.registry.flows.EppException;
@@ -49,8 +48,6 @@ 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.index.EppResourceIndex;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import java.util.Optional;
import javax.inject.Inject;
@@ -107,7 +104,7 @@ public final class HostCreateFlow implements TransactionalFlow {
DateTime now = tm().getTransactionTime();
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
// creating an external host. This is looked up before we actually create the Host object, so
// we can detect error conditions earlier.
Optional<Domain> superordinateDomain =
lookupSuperordinateDomain(validateHostName(targetId), now);
@@ -131,12 +128,7 @@ public final class HostCreateFlow implements TransactionalFlow {
.setSuperordinateDomain(superordinateDomain.map(Domain::createVKey).orElse(null))
.build();
historyBuilder.setType(HOST_CREATE).setHost(newHost);
ImmutableSet<ImmutableObject> entitiesToSave =
ImmutableSet.of(
newHost,
historyBuilder.build(),
ForeignKeyIndex.create(newHost, newHost.getDeletionTime()),
EppResourceIndex.create(Key.create(newHost)));
ImmutableSet<ImmutableObject> entitiesToSave = ImmutableSet.of(newHost, historyBuilder.build());
if (superordinateDomain.isPresent()) {
tm().update(
superordinateDomain
@@ -154,14 +146,14 @@ public final class HostCreateFlow implements TransactionalFlow {
/** Subordinate hosts must have an ip address. */
static class SubordinateHostMustHaveIpException extends RequiredParameterMissingException {
public SubordinateHostMustHaveIpException() {
SubordinateHostMustHaveIpException() {
super("Subordinate hosts must have an ip address");
}
}
/** External hosts must not have ip addresses. */
static class UnexpectedExternalHostIpException extends ParameterValueRangeErrorException {
public UnexpectedExternalHostIpException() {
UnexpectedExternalHostIpException() {
super("External hosts must not have ip addresses");
}
}
@@ -26,7 +26,6 @@ import static google.registry.flows.host.HostFlowUtils.lookupSuperordinateDomain
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotInPendingDelete;
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership;
import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey;
import static google.registry.model.reporting.HistoryEntry.Type.HOST_UPDATE;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
@@ -46,6 +45,7 @@ import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.model.EppResource;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.Domain;
import google.registry.model.domain.metadata.MetadataExtension;
@@ -57,7 +57,6 @@ 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.index.ForeignKeyIndex;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.persistence.VKey;
import java.util.Objects;
@@ -148,7 +147,7 @@ public final class HostUpdateFlow implements TransactionalFlow {
EppResource owningResource = firstNonNull(oldSuperordinateDomain, existingHost);
verifyUpdateAllowed(
command, existingHost, newSuperordinateDomain.orElse(null), owningResource, isHostRename);
if (isHostRename && loadAndGetKey(Host.class, newHostName, now) != null) {
if (isHostRename && ForeignKeyUtils.load(Host.class, newHostName, now) != null) {
throw new HostAlreadyExistsException(newHostName);
}
AddRemove add = command.getInnerAdd();
@@ -194,11 +193,7 @@ public final class HostUpdateFlow implements TransactionalFlow {
ImmutableSet.Builder<ImmutableObject> entitiesToInsert = new ImmutableSet.Builder<>();
ImmutableSet.Builder<ImmutableObject> entitiesToUpdate = new ImmutableSet.Builder<>();
entitiesToUpdate.add(newHost);
// Keep the {@link ForeignKeyIndex} for this host up to date.
if (isHostRename) {
// Update the foreign key for the old host name and save one for the new host name.
entitiesToUpdate.add(ForeignKeyIndex.create(existingHost, now));
entitiesToUpdate.add(ForeignKeyIndex.create(newHost, newHost.getDeletionTime()));
updateSuperordinateDomains(existingHost, newHost);
}
enqueueTasks(existingHost, newHost);
@@ -16,23 +16,14 @@ package google.registry.model;
import com.google.common.collect.ImmutableSet;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.common.GaeUserIdConverter;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
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.index.EppResourceIndex;
import google.registry.model.index.EppResourceIndexBucket;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.rde.RdeRevision;
import google.registry.model.registrar.Registrar;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.server.Lock;
import google.registry.model.server.ServerSecret;
/** Sets of classes of the Objectify-registered entities in use throughout the model. */
@DeleteAfterMigration
@@ -41,25 +32,14 @@ public final class EntityClasses {
/** Set of entity classes. */
public static final ImmutableSet<Class<? extends ImmutableObject>> ALL_CLASSES =
ImmutableSet.of(
AllocationToken.class,
Contact.class,
ContactHistory.class,
ContactResource.class,
Domain.class,
DomainHistory.class,
EntityGroupRoot.class,
EppResourceIndex.class,
EppResourceIndexBucket.class,
ForeignKeyIndex.ForeignKeyContactIndex.class,
ForeignKeyIndex.ForeignKeyDomainIndex.class,
ForeignKeyIndex.ForeignKeyHostIndex.class,
GaeUserIdConverter.class,
HistoryEntry.class,
Host.class,
HostHistory.class,
Lock.class,
RdeRevision.class,
Registrar.class,
ServerSecret.class);
HostHistory.class);
private EntityClasses() {}
}
@@ -140,6 +140,7 @@ public abstract class EppResource extends BackupGroupRoot implements Buildable {
DateTime lastEppUpdateTime;
/** Status values associated with this resource. */
@Ignore
@Column(name = "statuses")
// TODO(b/177567432): rename to "statuses" once we're off datastore.
Set<StatusValue> status;
@@ -32,11 +32,10 @@ import google.registry.config.RegistryConfig;
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.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.Host;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntryDao;
import google.registry.model.tld.Registry;
@@ -117,20 +116,19 @@ public final class EppResourceUtils {
}
/**
* Loads the last created version of an {@link EppResource} from Datastore by foreign key, using a
* cache.
* Loads the last created version of an {@link EppResource} from the database by foreign key,
* using a cache.
*
* <p>Returns null if no resource with this foreign key was ever created, or if the most recently
* created resource was deleted before time "now".
*
* <p>Loading an {@link EppResource} by itself is not sufficient to know its current state since
* it may have various expirable conditions and status values that might implicitly change its
* state as time progresses even if it has not been updated in Datastore. Rather, the resource
* state as time progresses even if it has not been updated in the database. Rather, the resource
* must be combined with a timestamp to view its current state. We use a global last updated
* timestamp on the resource's entity group (which is essentially free since all writes to the
* entity group must be serialized anyways) to guarantee monotonically increasing write times, and
* forward our projected time to the greater of this timestamp or "now". This guarantees that
* we're not projecting into the past.
* timestamp to guarantee monotonically increasing write times, and forward our projected time to
* the greater of this timestamp or "now". This guarantees that we're not projecting into the
* past.
*
* <p>Do not call this cached version for anything that needs transactional consistency. It should
* only be used when it's OK if the data is potentially being out of date, e.g. WHOIS.
@@ -150,19 +148,18 @@ public final class EppResourceUtils {
checkArgument(
ForeignKeyedEppResource.class.isAssignableFrom(clazz),
"loadByForeignKey may only be called for foreign keyed EPP resources");
ForeignKeyIndex<T> fki =
VKey<T> key =
useCache
? ForeignKeyIndex.loadCached(clazz, ImmutableList.of(foreignKey), now)
.getOrDefault(foreignKey, null)
: ForeignKeyIndex.load(clazz, foreignKey, now);
// The value of fki.getResourceKey() might be null for hard-deleted prober data.
if (fki == null || isAtOrAfter(now, fki.getDeletionTime()) || fki.getResourceKey() == null) {
? ForeignKeyUtils.loadCached(clazz, ImmutableList.of(foreignKey), now).get(foreignKey)
: ForeignKeyUtils.load(clazz, foreignKey, now);
// The returned key is null if the resource is hard deleted or soft deleted by the given time.
if (key == null) {
return Optional.empty();
}
T resource =
useCache
? EppResource.loadCached(fki.getResourceKey())
: tm().transact(() -> tm().loadByKeyIfPresent(fki.getResourceKey()).orElse(null));
? EppResource.loadCached(key)
: tm().transact(() -> tm().loadByKeyIfPresent(key).orElse(null));
if (resource == null || isAtOrAfter(now, resource.getDeletionTime())) {
return Optional.empty();
}
@@ -178,7 +175,7 @@ public final class EppResourceUtils {
}
/**
* Checks multiple {@link EppResource} objects from Datastore by unique ids.
* Checks multiple {@link EppResource} objects from the database by unique ids.
*
* <p>There are currently no resources that support checks and do not use foreign keys. If we need
* to support that case in the future, we can loosen the type to allow any {@link EppResource} and
@@ -190,7 +187,7 @@ public final class EppResourceUtils {
*/
public static <T extends EppResource> ImmutableSet<String> checkResourcesExist(
Class<T> clazz, List<String> uniqueIds, final DateTime now) {
return ForeignKeyIndex.load(clazz, uniqueIds, now).keySet();
return ForeignKeyUtils.load(clazz, uniqueIds, now).keySet();
}
/**
@@ -345,10 +342,10 @@ public final class EppResourceUtils {
public static ImmutableSet<VKey<Domain>> getLinkedDomainKeys(
VKey<? extends EppResource> key, DateTime now, @Nullable Integer limit) {
checkArgument(
key.getKind().equals(ContactResource.class) || key.getKind().equals(Host.class),
"key must be either VKey<ContactResource> or VKey<Host>, but it is %s",
key.getKind().equals(Contact.class) || key.getKind().equals(Host.class),
"key must be either VKey<Contact> or VKey<Host>, but it is %s",
key);
boolean isContactKey = key.getKind().equals(ContactResource.class);
boolean isContactKey = key.getKind().equals(Contact.class);
if (tm().isOfy()) {
com.googlecode.objectify.cmd.Query<Domain> query =
auditedOfy()
@@ -0,0 +1,236 @@
// 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;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.config.RegistryConfig.getEppResourceCachingDuration;
import static google.registry.config.RegistryConfig.getEppResourceMaxCachedEntries;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaJpaTm;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Streams;
import google.registry.config.RegistryConfig;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.util.NonFinalForTesting;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/**
* Util class to map a foreign key to the {@link VKey} to the active instance of {@link EppResource}
* whose unique repoId matches the foreign key string at a given time. The instance is never
* deleted, but it is updated if a newer entity becomes the active entity.
*/
public final class ForeignKeyUtils {
private ForeignKeyUtils() {}
private static final ImmutableMap<Class<? extends EppResource>, String>
RESOURCE_TYPE_TO_FK_PROPERTY =
ImmutableMap.of(
Contact.class, "contactId",
Domain.class, "fullyQualifiedDomainName",
Host.class, "fullyQualifiedHostName");
/**
* Loads a {@link VKey} to an {@link EppResource} from the database by foreign key.
*
* <p>Returns null if no resource with this foreign key was ever created, or if the most recently
* created resource was deleted before time "now".
*
* @param clazz the resource type to load
* @param foreignKey foreign key to match
* @param now the current logical time to use when checking for soft deletion of the foreign key
* index
*/
@Nullable
public static <E extends EppResource> VKey<E> load(
Class<E> clazz, String foreignKey, DateTime now) {
return load(clazz, ImmutableList.of(foreignKey), now).get(foreignKey);
}
/**
* Load a map of {@link String} foreign keys to {@link VKey}s to {@link EppResource} that are
* active at or after the specified moment in time.
*
* <p>The returned map will omit any foreign keys for which the {@link EppResource} doesn't exist
* or has been soft deleted.
*/
public static <E extends EppResource> ImmutableMap<String, VKey<E>> load(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
return load(clazz, foreignKeys, false).entrySet().stream()
.filter(e -> now.isBefore(e.getValue().deletionTime()))
.collect(toImmutableMap(Entry::getKey, e -> VKey.createSql(clazz, e.getValue().repoId())));
}
/**
* Helper method to load {@link VKey}s to all the most recent {@link EppResource}s for the given
* foreign keys, regardless of whether or not they have been soft-deleted.
*
* <p>Used by both the cached (w/o deletion check) and the non-cached (with deletion check) calls.
*
* <p>Note that in production, the {@code deletionTime} for entities with the same foreign key
* should monotonically increase as one cannot create a domain/host/contact with the same foreign
* key without soft deleting the existing resource first. However, in test, there's no such
* guarantee and one must make sure that no two resources with the same foreign key exist with the
* same max {@code deleteTime}, usually {@code END_OF_TIME}, lest this method throws an error due
* to duplicate keys.
*/
private static <E extends EppResource> ImmutableMap<String, MostRecentResource> load(
Class<E> clazz, Collection<String> foreignKeys, boolean useReplicaJpaTm) {
String fkProperty = RESOURCE_TYPE_TO_FK_PROPERTY.get(clazz);
JpaTransactionManager jpaTmToUse = useReplicaJpaTm ? replicaJpaTm() : jpaTm();
return jpaTmToUse.transact(
() ->
jpaTmToUse
.query(
("SELECT %fkProperty%, repoId, deletionTime FROM %entity% WHERE (%fkProperty%,"
+ " deletionTime) IN (SELECT %fkProperty%, MAX(deletionTime) FROM"
+ " %entity% WHERE %fkProperty% IN (:fks) GROUP BY %fkProperty%)")
.replace("%fkProperty%", fkProperty)
.replace("%entity%", clazz.getSimpleName()),
Object[].class)
.setParameter("fks", foreignKeys)
.getResultStream()
.collect(
toImmutableMap(
row -> (String) row[0],
row -> MostRecentResource.create((String) row[1], (DateTime) row[2]))));
}
private static final CacheLoader<VKey<? extends EppResource>, Optional<MostRecentResource>>
CACHE_LOADER =
new CacheLoader<VKey<? extends EppResource>, Optional<MostRecentResource>>() {
@Override
public Optional<MostRecentResource> load(VKey<? extends EppResource> key) {
return loadAll(ImmutableList.of(key)).get(key);
}
@Override
public Map<VKey<? extends EppResource>, Optional<MostRecentResource>> loadAll(
Iterable<? extends VKey<? extends EppResource>> keys) {
if (!keys.iterator().hasNext()) {
return ImmutableMap.of();
}
// It is safe to use the resource type of first element because when this function is
// called, it is always passed with a list of VKeys with the same type.
Class<? extends EppResource> clazz = keys.iterator().next().getKind();
ImmutableList<String> foreignKeys =
Streams.stream(keys)
.map(key -> (String) key.getSqlKey())
.collect(toImmutableList());
ImmutableMap<String, MostRecentResource> existingKeys =
ForeignKeyUtils.load(clazz, foreignKeys, true);
// The above map only contains keys that exist in the database, so we re-add the
// missing ones with Optional.empty() values for caching.
return Maps.asMap(
ImmutableSet.copyOf(keys),
key -> Optional.ofNullable(existingKeys.get((String) key.getSqlKey())));
}
};
/**
* A limited size, limited time cache for foreign-keyed entities.
*
* <p>This is only used to cache foreign-keyed entities for the purposes of checking whether they
* exist (and if so, what entity they point to) during a few domain flows. Any other operations on
* foreign keys should not use this cache.
*
* <p>Note that here the key of the {@link LoadingCache} is of type {@code VKey<? extends
* EppResource>}, but they are not legal {VKey}s to {@link EppResource}s, whose keys are the SQL
* primary keys, i.e. the {@code repoId}s. Instead, their keys are the foreign keys used to query
* the database. We use {@link VKey} here because it is a convenient composite class that contains
* both the resource type and the foreign key, which are needed to for the query and caching.
*
* <p>Also note that the value type of this cache is {@link Optional} because the foreign keys in
* question are coming from external commands, and thus don't necessarily represent entities in
* our system that actually exist. So we cache the fact that they *don't* exist by using
* Optional.empty(), then several layers up the EPP command will fail with an error message like
* "The contact with given IDs (blah) don't exist."
*/
@NonFinalForTesting
private static LoadingCache<VKey<? extends EppResource>, Optional<MostRecentResource>>
foreignKeyCache = createForeignKeyMapCache(getEppResourceCachingDuration());
private static LoadingCache<VKey<? extends EppResource>, Optional<MostRecentResource>>
createForeignKeyMapCache(Duration expiry) {
return CacheUtils.newCacheBuilder(expiry)
.maximumSize(getEppResourceMaxCachedEntries())
.build(CACHE_LOADER);
}
@VisibleForTesting
public static void setCacheForTest(Optional<Duration> expiry) {
Duration effectiveExpiry = expiry.orElse(getEppResourceCachingDuration());
foreignKeyCache = createForeignKeyMapCache(effectiveExpiry);
}
/**
* Load a list of {@link VKey} to {@link EppResource} instances by class and foreign key strings
* that are active at or after the specified moment in time, using the cache if enabled.
*
* <p>The returned map will omit any keys for which the {@link EppResource} doesn't exist or has
* been soft deleted.
*
* <p>Don't use the cached version of this method unless you really need it for performance
* reasons, and are OK with the trade-offs in loss of transactional consistency.
*/
public static <E extends EppResource> ImmutableMap<String, VKey<E>> loadCached(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
if (!RegistryConfig.isEppResourceCachingEnabled()) {
return load(clazz, foreignKeys, now);
}
return foreignKeyCache
.getAll(
foreignKeys.stream().map(fk -> VKey.createSql(clazz, fk)).collect(toImmutableList()))
.entrySet()
.stream()
.filter(e -> e.getValue().isPresent() && now.isBefore(e.getValue().get().deletionTime()))
.collect(
toImmutableMap(
e -> (String) e.getKey().getSqlKey(),
e -> VKey.createSql(clazz, e.getValue().get().repoId())));
}
@AutoValue
abstract static class MostRecentResource {
abstract String repoId();
abstract DateTime deletionTime();
static MostRecentResource create(String repoId, DateTime deletionTime) {
return new AutoValue_ForeignKeyUtils_MostRecentResource(repoId, deletionTime);
}
}
}
@@ -257,7 +257,7 @@ public abstract class ImmutableObject implements Cloneable {
return (Map<String, Object>) toMapRecursive(this);
}
public VKey createVKey() {
public VKey<? extends ImmutableObject> createVKey() {
throw new UnsupportedOperationException("VKey creation is not supported for this entity");
}
}
@@ -23,13 +23,11 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
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.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.poll.PendingActionNotificationResponse;
import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
@@ -60,7 +58,7 @@ public final class ResourceTransferUtils {
EppResource eppResource, TransferData transferData) {
assertIsContactOrDomain(eppResource);
TransferResponse.Builder<? extends TransferResponse, ?> builder;
if (eppResource instanceof ContactResource) {
if (eppResource instanceof Contact) {
builder = new ContactTransferResponse.Builder().setContactId(eppResource.getForeignKey());
} else {
DomainTransferData domainTransferData = (DomainTransferData) transferData;
@@ -93,7 +91,7 @@ public final class ResourceTransferUtils {
boolean actionResult,
DateTime processedDate) {
assertIsContactOrDomain(eppResource);
return eppResource instanceof ContactResource
return eppResource instanceof Contact
? ContactPendingActionNotificationResponse.create(
eppResource.getForeignKey(), actionResult, transferRequestTrid, processedDate)
: DomainPendingActionNotificationResponse.create(
@@ -101,14 +99,7 @@ public final class ResourceTransferUtils {
}
private static void assertIsContactOrDomain(EppResource eppResource) {
checkState(eppResource instanceof ContactResource || eppResource instanceof Domain);
}
/** Update the relevant {@link ForeignKeyIndex} to cache the new deletion time. */
public static <R extends EppResource> void updateForeignKeyIndexDeletionTime(R resource) {
if (resource instanceof ForeignKeyedEppResource) {
tm().insert(ForeignKeyIndex.create(resource, resource.getDeletionTime()));
}
checkState(eppResource instanceof Contact || eppResource instanceof Domain);
}
/** If there is a transfer out, delete the server-approve entities and enqueue a poll message. */
@@ -1,35 +0,0 @@
// Copyright 2021 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 com.googlecode.objectify.annotation.Entity;
import google.registry.model.common.EntityGroupRoot;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation for an Objectify {@link Entity} to indicate that it is in the cross-TLD entity group.
*
* <p>This means that the entity's <code>@Parent</code> field has to have the value of {@link
* EntityGroupRoot#getCrossTldKey}.
*/
@DeleteAfterMigration
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Inherited
public @interface InCrossTld {}
@@ -212,6 +212,9 @@ public abstract class BillingEvent extends ImmutableObject
return nullToEmptyImmutableCopy(flags);
}
@Override
public abstract VKey<? extends BillingEvent> createVKey();
/** Override Buildable.asBuilder() to give this method stronger typing. */
@Override
public abstract Builder<?, ?> asBuilder();
@@ -14,26 +14,22 @@
package google.registry.model.common;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.annotations.InCrossTld;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;
/** A singleton entity in Datastore. */
/**
* A singleton entity in the database.
*
* <p>This class should not be deleted after the migration, because there is still a concept of
* singleton in SQL. We should remove the ofy @Id annotation after all of its subclass are Ofy-free.
*/
@DeleteAfterMigration
@MappedSuperclass
@InCrossTld
public abstract class CrossTldSingleton extends ImmutableObject {
public static final long SINGLETON_ID = 1; // There is always exactly one of these.
@Id @javax.persistence.Id long id = SINGLETON_ID;
@Transient @Parent Key<EntityGroupRoot> parent = getCrossTldKey();
}
@@ -1,57 +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.common;
import com.google.apphosting.api.ApiProxy;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import google.registry.model.BackupGroupRoot;
import google.registry.model.annotations.DeleteAfterMigration;
import javax.annotation.Nullable;
/**
* The root key for the entity group which is known as the cross-tld entity group for historical
* reasons.
*
* <p>This exists as a storage place for common configuration options and global settings that
* aren't updated too frequently. Entities in this entity group are usually cached upon load. The
* reason this common entity group exists is because it enables strongly consistent queries and
* updates across this seldomly updated data. This shared entity group also helps cut down on a
* potential ballooning in the number of entity groups enlisted in transactions.
*
* <p>Historically, each TLD used to have a separate namespace, and all entities for a TLD were in a
* single EntityGroupRoot for that TLD. Hence why there was a "cross-tld" entity group -- it was the
* entity group for the single namespace where global data applicable for all TLDs lived.
*/
@Entity
@DeleteAfterMigration
public class EntityGroupRoot extends BackupGroupRoot {
@SuppressWarnings("unused")
@Id
private String id;
/** The root key for cross-tld resources such as registrars. */
public static @Nullable Key<EntityGroupRoot> getCrossTldKey() {
// If we cannot get a current environment, calling Key.create() will fail. Instead we return a
// null in cases where this key is not actually needed (for example when loading an entity from
// SQL) to initialize an object, to avoid having to register a DatastoreEntityExtension in
// tests.
return ApiProxy.getCurrentEnvironment() == null
? null
: Key.create(EntityGroupRoot.class, "cross-tld");
}
}
@@ -25,7 +25,6 @@ import static org.joda.time.DateTimeZone.UTC;
import com.google.common.base.Splitter;
import com.google.common.collect.ContiguousSet;
import com.google.common.collect.Range;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Index;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
@@ -44,7 +43,6 @@ import org.joda.time.DateTime;
* allows it to be embeddable with no translation needed and also delays parsing of the string on
* load until it's actually needed.
*/
@Embed
@Embeddable
public class TimeOfYear extends ImmutableObject implements UnsafeSerializable {
@@ -22,22 +22,40 @@ 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.BackupGroupRoot;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
/** A console user, either a registry employee or a registrar partner. */
public class User extends ImmutableObject implements Buildable {
@Entity
@Table(
indexes = {
@Index(columnList = "gaiaId", name = "user_gaia_id_idx"),
@Index(columnList = "emailAddress", name = "user_email_address_idx")
})
public class User extends BackupGroupRoot implements Buildable {
/** Autogenerated unique ID of this user. */
private long id;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
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;
/**
@@ -49,7 +67,7 @@ public class User extends ImmutableObject implements Buildable {
/** Randomly generated hash salt. */
String registryLockPasswordSalt;
public long getId() {
public Long getId() {
return id;
}
@@ -0,0 +1,57 @@
// 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.persistence.transaction.TransactionManagerFactory.jpaTm;
import java.util.Optional;
/** Data access object for {@link User} objects to simplify saving and retrieval. */
public class UserDao {
/** Retrieves the one user with this email address if it exists. */
public static Optional<User> loadUser(String emailAddress) {
return jpaTm()
.transact(
() ->
jpaTm()
.query("FROM User WHERE emailAddress = :emailAddress", User.class)
.setParameter("emailAddress", emailAddress)
.getResultStream()
.findFirst());
}
/** Saves the given user, checking that no existing user already exists with this email. */
public static void saveUser(User user) {
jpaTm()
.transact(
() -> {
// Check for an existing user (the unique constraint protects us, but this gives a
// nicer exception)
Optional<User> maybeSavedUser = loadUser(user.getEmailAddress());
if (maybeSavedUser.isPresent()) {
User savedUser = maybeSavedUser.get();
checkArgument(
savedUser.getId().equals(user.getId()),
String.format(
"Attempted save of User with email address %s and ID %s, user with that"
+ " email already exists with ID %s",
user.getEmailAddress(), user.getId(), savedUser.getId()));
}
jpaTm().put(user);
});
}
}
@@ -21,6 +21,10 @@ 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.
@@ -28,15 +32,19 @@ import java.util.Map;
* <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. */
@@ -14,8 +14,6 @@
package google.registry.model.contact;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.annotations.ReportedOn;
@@ -23,6 +21,10 @@ import google.registry.persistence.VKey;
import google.registry.persistence.WithStringVKey;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
import org.joda.time.DateTime;
/**
@@ -32,35 +34,35 @@ import org.joda.time.DateTime;
*/
@ReportedOn
@Entity
@javax.persistence.Entity(name = "Contact")
@javax.persistence.Table(
@com.googlecode.objectify.annotation.Entity
@Table(
name = "Contact",
indexes = {
@javax.persistence.Index(columnList = "creationTime"),
@javax.persistence.Index(columnList = "currentSponsorRegistrarId"),
@javax.persistence.Index(columnList = "deletionTime"),
@javax.persistence.Index(columnList = "contactId"),
@javax.persistence.Index(columnList = "searchName")
@Index(columnList = "creationTime"),
@Index(columnList = "currentSponsorRegistrarId"),
@Index(columnList = "deletionTime"),
@Index(columnList = "contactId"),
@Index(columnList = "searchName")
})
@ExternalMessagingName("contact")
@WithStringVKey
@WithStringVKey(compositeKey = true)
@Access(AccessType.FIELD)
public class ContactResource extends ContactBase implements ForeignKeyedEppResource {
public class Contact extends ContactBase implements ForeignKeyedEppResource {
@Override
public VKey<ContactResource> createVKey() {
return VKey.create(ContactResource.class, getRepoId(), Key.create(this));
public VKey<Contact> createVKey() {
return VKey.createSql(Contact.class, getRepoId());
}
@Override
@javax.persistence.Id
@Id
@Access(AccessType.PROPERTY)
public String getRepoId() {
return super.getRepoId();
}
@Override
public ContactResource cloneProjectedAtTime(DateTime now) {
public Contact cloneProjectedAtTime(DateTime now) {
return ContactBase.cloneContactProjectedAtTime(this, now);
}
@@ -69,12 +71,12 @@ public class ContactResource extends ContactBase implements ForeignKeyedEppResou
return new Builder(clone(this));
}
/** A builder for constructing {@link ContactResource}, since it is immutable. */
public static class Builder extends ContactBase.Builder<ContactResource, Builder> {
/** A builder for constructing {@link Contact}, since it is immutable. */
public static class Builder extends ContactBase.Builder<Contact, Builder> {
public Builder() {}
private Builder(ContactResource instance) {
private Builder(Contact instance) {
super(instance);
}
@@ -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 {
@@ -14,13 +14,12 @@
package google.registry.model.contact;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.eppcommon.AuthInfo;
import javax.persistence.Embeddable;
import javax.xml.bind.annotation.XmlType;
/** A version of authInfo specifically for contacts. */
@Embed
@javax.persistence.Embeddable
@Embeddable
@XmlType(namespace = "urn:ietf:params:xml:ns:contact-1.0")
public class ContactAuthInfo extends AuthInfo {
public static ContactAuthInfo create(PasswordAuth pw) {
@@ -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;
@@ -66,9 +67,9 @@ 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}.
* US-ASCII character set. Personal info; cleared by {@link Contact.Builder#wipeOut}.
*/
@IgnoreSave(IfNull.class)
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "name", column = @Column(name = "addr_local_name")),
@@ -94,9 +95,9 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
/**
* Internationalized postal info for the contact. Personal info; cleared by {@link
* ContactResource.Builder#wipeOut}.
* Contact.Builder#wipeOut}.
*/
@IgnoreSave(IfNull.class)
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "name", column = @Column(name = "addr_i18n_name")),
@@ -123,12 +124,12 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
/**
* Contact name used for name searches. This is set automatically to be the internationalized
* postal name, or if null, the localized postal name, or if that is null as well, null. Personal
* info; cleared by {@link ContactResource.Builder#wipeOut}.
* info; cleared by {@link Contact.Builder#wipeOut}.
*/
@Index String searchName;
/** Contacts voice number. Personal info; cleared by {@link ContactResource.Builder#wipeOut}. */
@IgnoreSave(IfNull.class)
/** Contacts voice number. Personal info; cleared by {@link Contact.Builder#wipeOut}. */
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "phoneNumber", column = @Column(name = "voice_phone_number")),
@@ -136,8 +137,8 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
})
ContactPhoneNumber voice;
/** Contacts fax number. Personal info; cleared by {@link ContactResource.Builder#wipeOut}. */
@IgnoreSave(IfNull.class)
/** Contacts fax number. Personal info; cleared by {@link Contact.Builder#wipeOut}. */
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "phoneNumber", column = @Column(name = "fax_phone_number")),
@@ -145,11 +146,12 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
})
ContactPhoneNumber fax;
/** Contacts email address. Personal info; cleared by {@link ContactResource.Builder#wipeOut}. */
/** Contacts email address. Personal info; cleared by {@link Contact.Builder#wipeOut}. */
@IgnoreSave(IfNull.class)
String email;
/** Authorization info (aka transfer secret) of the contact. */
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "pw.value", column = @Column(name = "auth_info_value")),
@@ -158,7 +160,7 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
ContactAuthInfo authInfo;
/** Data about any pending or past transfers on this contact. */
ContactTransferData transferData;
@Ignore ContactTransferData transferData;
/**
* The time that this resource was last transferred.
@@ -171,6 +173,7 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
// the wipeOut() function, so that data is not kept around for deleted contacts.
/** Disclosure policy. */
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "name", column = @Column(name = "disclose_types_name")),
@@ -187,7 +190,7 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
public VKey<? extends ContactBase> createVKey() {
throw new UnsupportedOperationException(
"ContactBase is not an actual persisted entity you can create a key to;"
+ " use ContactResource instead");
+ " use Contact instead");
}
@OnLoad
@@ -291,7 +294,7 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
return new Builder<>(clone(this));
}
/** A builder for constructing {@link ContactResource}, since it is immutable. */
/** A builder for constructing {@link Contact}, since it is immutable. */
public static class Builder<T extends ContactBase, B extends Builder<T, B>>
extends EppResource.Builder<T, B> implements BuilderWithTransferData<ContactTransferData, B> {
@@ -34,13 +34,13 @@ import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.CollapsedStringAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/** A collection of {@link ContactResource} commands. */
/** A collection of {@link Contact} commands. */
public class ContactCommand {
/** The fields on "chgType" from <a href="http://tools.ietf.org/html/rfc5733">RFC5733</a>. */
@XmlTransient
public static class ContactCreateOrChange extends ImmutableObject
implements ResourceCreateOrChange<ContactResource.Builder> {
implements ResourceCreateOrChange<Contact.Builder> {
/** Postal info for the contact. */
List<PostalInfo> postalInfo;
@@ -111,13 +111,13 @@ public class ContactCommand {
}
/**
* A create command for a {@link ContactResource}, mapping "createType" from <a
* A create command for a {@link Contact}, mapping "createType" from <a
* href="http://tools.ietf.org/html/rfc5733">RFC5733</a>}.
*/
@XmlType(propOrder = {"contactId", "postalInfo", "voice", "fax", "email", "authInfo", "disclose"})
@XmlRootElement
public static class Create extends ContactCreateOrChange
implements SingleResourceCommand, ResourceCreateOrChange<ContactResource.Builder> {
implements SingleResourceCommand, ResourceCreateOrChange<Contact.Builder> {
/**
* Unique identifier for this contact.
*
@@ -139,29 +139,29 @@ public class ContactCommand {
}
}
/** A delete command for a {@link ContactResource}. */
/** A delete command for a {@link Contact}. */
@XmlRootElement
public static class Delete extends AbstractSingleResourceCommand {}
/** An info request for a {@link ContactResource}. */
/** An info request for a {@link Contact}. */
@XmlRootElement
@XmlType(propOrder = {"targetId", "authInfo"})
public static class Info extends AbstractContactAuthCommand {}
/** A check request for {@link ContactResource}. */
/** A check request for {@link Contact}. */
@XmlRootElement
public static class Check extends ResourceCheck {}
/** A transfer operation for a {@link ContactResource}. */
/** A transfer operation for a {@link Contact}. */
@XmlRootElement
@XmlType(propOrder = {"targetId", "authInfo"})
public static class Transfer extends AbstractContactAuthCommand {}
/** An update to a {@link ContactResource}. */
/** An update to a {@link Contact}. */
@XmlRootElement
@XmlType(propOrder = {"targetId", "innerAdd", "innerRemove", "innerChange"})
public static class Update
extends ResourceUpdate<Update.AddRemove, ContactResource.Builder, Update.Change> {
extends ResourceUpdate<Update.AddRemove, Contact.Builder, Update.Change> {
@XmlElement(name = "chg")
protected Change innerChange;
@@ -58,7 +58,7 @@ import javax.persistence.PostLoad;
@IdClass(ContactHistoryId.class)
public class ContactHistory extends HistoryEntry implements UnsafeSerializable {
// Store ContactBase instead of ContactResource so we don't pick up its @Id
// Store ContactBase instead of Contact so we don't pick up its @Id
// Nullable for the sake of pre-Registry-3.0 history objects
@DoNotCompare @Nullable ContactBase contactBase;
@@ -73,7 +73,7 @@ public class ContactHistory extends HistoryEntry implements UnsafeSerializable {
/** This method is private because it is only used by Hibernate. */
@SuppressWarnings("unused")
private void setContactRepoId(String contactRepoId) {
parent = Key.create(ContactResource.class, contactRepoId);
parent = Key.create(Contact.class, contactRepoId);
}
@Id
@@ -98,9 +98,9 @@ public class ContactHistory extends HistoryEntry implements UnsafeSerializable {
return Optional.ofNullable(contactBase);
}
/** The key to the {@link ContactResource} this is based off of. */
public VKey<ContactResource> getParentVKey() {
return VKey.create(ContactResource.class, getContactRepoId());
/** The key to the {@link Contact} this is based off of. */
public VKey<Contact> getParentVKey() {
return VKey.create(Contact.class, getContactRepoId());
}
/** Creates a {@link VKey} instance for this entity. */
@@ -112,8 +112,7 @@ public class ContactHistory extends HistoryEntry implements UnsafeSerializable {
@Override
public Optional<? extends EppResource> getResourceAtPointInTime() {
return getContactBase()
.map(contactBase -> new ContactResource.Builder().copyFrom(contactBase).build());
return getContactBase().map(contactBase -> new Contact.Builder().copyFrom(contactBase).build());
}
@PostLoad
@@ -210,7 +209,7 @@ public class ContactHistory extends HistoryEntry implements UnsafeSerializable {
}
public Builder setContactRepoId(String contactRepoId) {
getInstance().parent = Key.create(ContactResource.class, contactRepoId);
getInstance().parent = Key.create(Contact.class, contactRepoId);
return this;
}
@@ -14,20 +14,18 @@
package google.registry.model.contact;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.eppcommon.PhoneNumber;
import javax.persistence.Embeddable;
/**
* EPP Contact Phone Number
*
* <p>This class is embedded inside a {@link ContactResource} hold the phone number of an EPP
* contact. The fields are all defined in the parent class {@link PhoneNumber}, but the subclass is
* still necessary to pick up the contact namespace.
* <p>This class is embedded inside a {@link Contact} hold the phone number of an EPP contact. The
* fields are all defined in the parent class {@link PhoneNumber}, but the subclass is still
* necessary to pick up the contact namespace.
*
* @see ContactResource
* @see Contact
*/
@Embed
@Embeddable
public class ContactPhoneNumber extends PhoneNumber {
@@ -17,7 +17,6 @@ package google.registry.model.contact;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
@@ -30,7 +29,6 @@ import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlType;
/** The "discloseType" from <a href="http://tools.ietf.org/html/rfc5733">RFC5733</a>. */
@Embed
@Embeddable
@XmlType(propOrder = {"name", "org", "addr", "voice", "fax", "email"})
public class Disclose extends ImmutableObject implements UnsafeSerializable {
@@ -79,7 +77,6 @@ public class Disclose extends ImmutableObject implements UnsafeSerializable {
}
/** The "intLocType" from <a href="http://tools.ietf.org/html/rfc5733">RFC5733</a>. */
@Embed
public static class PostalInfoChoice extends ImmutableObject implements Serializable {
@XmlAttribute
PostalInfo.Type type;
@@ -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
@@ -17,12 +17,11 @@ package google.registry.model.domain;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.Index;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.persistence.VKey;
import javax.persistence.Embeddable;
import javax.xml.bind.annotation.XmlEnumValue;
@@ -45,7 +44,6 @@ import javax.xml.bind.annotation.XmlEnumValue;
* @see <a href="http://tools.ietf.org/html/rfc5731#section-2.2">RFC 5731 - EPP Domain Name Mapping
* - Contact and Client Identifiers</a>
*/
@Embed
@Embeddable
public class DesignatedContact extends ImmutableObject implements UnsafeSerializable {
@@ -64,7 +62,7 @@ public class DesignatedContact extends ImmutableObject implements UnsafeSerializ
REGISTRANT
}
public static DesignatedContact create(Type type, VKey<ContactResource> contact) {
public static DesignatedContact create(Type type, VKey<Contact> contact) {
DesignatedContact instance = new DesignatedContact();
instance.type = type;
instance.contactVKey = checkArgumentNotNull(contact, "Must specify contact key");
@@ -74,14 +72,14 @@ public class DesignatedContact extends ImmutableObject implements UnsafeSerializ
Type type;
@Index Key<ContactResource> contact;
@Ignore VKey<ContactResource> contactVKey;
@Index Key<Contact> contact;
@Ignore VKey<Contact> contactVKey;
public Type getType() {
return type;
}
public VKey<ContactResource> getContactKey() {
public VKey<Contact> getContactKey() {
return contactVKey;
}
@@ -66,7 +66,7 @@ import org.joda.time.DateTime;
@Index(columnList = "transfer_billing_event_id"),
@Index(columnList = "transfer_billing_recurrence_id")
})
@WithStringVKey
@WithStringVKey(compositeKey = true)
@ExternalMessagingName("domain")
@Access(AccessType.FIELD)
public class Domain extends DomainBase implements ForeignKeyedEppResource {
@@ -148,7 +148,7 @@ public class Domain extends DomainBase implements ForeignKeyedEppResource {
@Override
public VKey<Domain> createVKey() {
return VKey.create(Domain.class, getRepoId(), Key.create(this));
return VKey.createSql(Domain.class, getRepoId());
}
@Override
@@ -14,12 +14,11 @@
package google.registry.model.domain;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.eppcommon.AuthInfo;
import javax.persistence.Embeddable;
/** A version of authInfo specifically for domains. */
@Embed
@javax.persistence.Embeddable
@Embeddable
public class DomainAuthInfo extends AuthInfo {
public static DomainAuthInfo create(PasswordAuth pw) {
DomainAuthInfo instance = new DomainAuthInfo();
@@ -41,7 +41,6 @@ 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;
@@ -51,8 +50,7 @@ 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.contact.Contact;
import google.registry.model.domain.launch.LaunchNotice;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.secdns.DelegationSignerData;
@@ -60,7 +58,6 @@ import google.registry.model.domain.token.AllocationToken;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.Host;
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;
@@ -132,13 +129,14 @@ public class DomainBase extends EppResource
@EmptySetToNull @Index @Transient Set<VKey<Host>> nsHosts;
/** Contacts. */
VKey<ContactResource> adminContact;
VKey<Contact> adminContact;
VKey<ContactResource> billingContact;
VKey<ContactResource> techContact;
VKey<ContactResource> registrantContact;
VKey<Contact> billingContact;
VKey<Contact> techContact;
VKey<Contact> registrantContact;
/** Authorization info (aka transfer secret) of the domain. */
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "pw.value", column = @Column(name = "auth_info_value")),
@@ -147,13 +145,14 @@ public class DomainBase extends EppResource
DomainAuthInfo authInfo;
/** Data used to construct DS records for this domain. */
@Transient Set<DelegationSignerData> dsData;
@Ignore @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.
*/
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "noticeId.tcnId", column = @Column(name = "launch_notice_tcn_id")),
@@ -217,7 +216,7 @@ public class DomainBase extends EppResource
VKey<PollMessage.Autorenew> autorenewPollMessage;
/** The unexpired grace periods for this domain (some of which may not be active yet). */
@Transient Set<GracePeriod> gracePeriods;
@Ignore @Transient Set<GracePeriod> gracePeriods;
/**
* The id of the signed mark that was used to create this domain in sunrise.
@@ -228,7 +227,7 @@ public class DomainBase extends EppResource
String smdId;
/** Data about any pending or past transfers on this domain. */
DomainTransferData transferData;
@Ignore DomainTransferData transferData;
/**
* The time that this resource was last transferred.
@@ -277,7 +276,7 @@ public class DomainBase extends EppResource
@Ignore DateTime dnsRefreshRequestTime;
/** The {@link AllocationToken} for the package this domain is currently a part of. */
@Nullable VKey<AllocationToken> currentPackageToken;
@Ignore @Nullable VKey<AllocationToken> currentPackageToken;
/**
* Returns the DNS refresh request time iff this domain's DNS needs refreshing, otherwise absent.
@@ -286,15 +285,6 @@ public class DomainBase extends EppResource
return Optional.ofNullable(dnsRefreshRequestTime);
}
public static <T> VKey<T> restoreOfyFrom(Key<Domain> 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);
}
@@ -602,19 +592,19 @@ public class DomainBase extends EppResource
}
/** A key to the registrant who registered this domain. */
public VKey<ContactResource> getRegistrant() {
public VKey<Contact> getRegistrant() {
return registrantContact;
}
public VKey<ContactResource> getAdminContact() {
public VKey<Contact> getAdminContact() {
return adminContact;
}
public VKey<ContactResource> getBillingContact() {
public VKey<Contact> getBillingContact() {
return billingContact;
}
public VKey<ContactResource> getTechContact() {
public VKey<Contact> getTechContact() {
return techContact;
}
@@ -628,7 +618,7 @@ public class DomainBase extends EppResource
}
/** Returns all referenced contacts from this domain. */
public ImmutableSet<VKey<ContactResource>> getReferencedContacts() {
public ImmutableSet<VKey<Contact>> getReferencedContacts() {
return nullToEmptyImmutableCopy(getAllContacts(true)).stream()
.map(DesignatedContact::getContactKey)
.filter(Objects::nonNull)
@@ -771,7 +761,7 @@ public class DomainBase extends EppResource
return thisCastToDerived();
}
public B setRegistrant(VKey<ContactResource> registrant) {
public B setRegistrant(VKey<Contact> registrant) {
// Set the registrant field specifically.
getInstance().registrantContact = registrant;
return thisCastToDerived();
@@ -17,7 +17,6 @@ package google.registry.model.domain;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.collect.Maps.transformValues;
import static com.google.common.collect.Sets.difference;
import static google.registry.util.CollectionUtils.difference;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
@@ -31,15 +30,15 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.model.EppResource;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.ImmutableObject;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.eppinput.ResourceCommand.AbstractSingleResourceCommand;
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.Host;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.persistence.VKey;
import java.util.Set;
import javax.annotation.Nullable;
@@ -80,7 +79,7 @@ public class DomainCommand {
String registrantContactId;
/** A resolved key to the registrant who registered this domain. */
@XmlTransient VKey<ContactResource> registrant;
@XmlTransient VKey<Contact> registrant;
/** Authorization info (aka transfer secret) of the domain. */
DomainAuthInfo authInfo;
@@ -90,7 +89,7 @@ public class DomainCommand {
}
@Nullable
public VKey<ContactResource> getRegistrant() {
public VKey<Contact> getRegistrant() {
return registrant;
}
@@ -391,7 +390,7 @@ public class DomainCommand {
? null
: getOnlyElement(
loadByForeignKeysCached(
ImmutableSet.of(clone.registrantContactId), ContactResource.class, now)
ImmutableSet.of(clone.registrantContactId), Contact.class, now)
.values());
return clone;
}
@@ -431,8 +430,8 @@ public class DomainCommand {
for (ForeignKeyedDesignatedContact contact : contacts) {
foreignKeys.add(contact.contactId);
}
ImmutableMap<String, VKey<ContactResource>> loadedContacts =
loadByForeignKeysCached(foreignKeys.build(), ContactResource.class, now);
ImmutableMap<String, VKey<Contact>> loadedContacts =
loadByForeignKeysCached(foreignKeys.build(), Contact.class, now);
ImmutableSet.Builder<DesignatedContact> linkedContacts = new ImmutableSet.Builder<>();
for (ForeignKeyedDesignatedContact contact : contacts) {
linkedContacts.add(
@@ -445,13 +444,12 @@ public class DomainCommand {
private static <T extends EppResource> ImmutableMap<String, VKey<T>> loadByForeignKeysCached(
final Set<String> foreignKeys, final Class<T> clazz, final DateTime now)
throws InvalidReferencesException {
ImmutableMap<String, ForeignKeyIndex<T>> fkis =
ForeignKeyIndex.loadCached(clazz, foreignKeys, now);
if (!fkis.keySet().equals(foreignKeys)) {
ImmutableMap<String, VKey<T>> fks = ForeignKeyUtils.loadCached(clazz, foreignKeys, now);
if (!fks.keySet().equals(foreignKeys)) {
throw new InvalidReferencesException(
clazz, ImmutableSet.copyOf(difference(foreignKeys, fkis.keySet())));
clazz, ImmutableSet.copyOf(difference(foreignKeys, fks.keySet())));
}
return ImmutableMap.copyOf(transformValues(fkis, ForeignKeyIndex::getResourceKey));
return fks;
}
/** Exception to throw when referenced objects don't exist. */
@@ -20,6 +20,7 @@ import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.EntitySubclass;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
@@ -133,6 +134,7 @@ public class DomainHistory extends HistoryEntry {
updatable = false)
})
// HashSet rather than ImmutableSet so that Hibernate can fill them out lazily on request
@Ignore
Set<DomainDsDataHistory> dsDataHistories = new HashSet<>();
@DoNotCompare
@@ -153,6 +155,7 @@ public class DomainHistory extends HistoryEntry {
updatable = false)
})
// HashSet rather than ImmutableSet so that Hibernate can fill them out lazily on request
@Ignore
Set<GracePeriodHistory> gracePeriodHistories = new HashSet<>();
@Override
@@ -19,7 +19,6 @@ import static google.registry.model.IdService.allocateId;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
@@ -40,7 +39,6 @@ import org.joda.time.DateTime;
* <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
@Table(
indexes = {
@@ -14,8 +14,6 @@
package google.registry.model.domain;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.billing.BillingEvent;
@@ -31,7 +29,6 @@ import javax.persistence.Transient;
import org.joda.time.DateTime;
/** Base class containing common fields and methods for {@link GracePeriod}. */
@Embed
@MappedSuperclass
@Access(AccessType.FIELD)
public class GracePeriodBase extends ImmutableObject implements UnsafeSerializable {
@@ -40,7 +37,6 @@ public class GracePeriodBase extends ImmutableObject implements UnsafeSerializab
@Transient long gracePeriodId;
/** Repository id for the domain which this grace period belongs to. */
@Ignore
@Column(nullable = false)
String domainRepoId;
@@ -65,7 +61,6 @@ 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;
/**
@@ -75,7 +70,6 @@ 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() {
@@ -14,9 +14,9 @@
package google.registry.model.domain;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import javax.persistence.Embeddable;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.xml.bind.annotation.XmlAttribute;
@@ -24,8 +24,7 @@ import javax.xml.bind.annotation.XmlEnumValue;
import javax.xml.bind.annotation.XmlValue;
/** The "periodType" from <a href="http://tools.ietf.org/html/rfc5731">RFC5731</a>. */
@Embed
@javax.persistence.Embeddable
@Embeddable
public class Period extends ImmutableObject implements UnsafeSerializable {
@Enumerated(EnumType.STRING)
@@ -23,12 +23,10 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.google.common.base.Ascii;
import com.google.common.base.CharMatcher;
import com.google.common.primitives.Ints;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.condition.IfNull;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import java.util.Optional;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
@@ -37,17 +35,15 @@ import javax.xml.bind.annotation.XmlValue;
import org.joda.time.DateTime;
/** The claims notice id from the claims phase. */
@Embed
@XmlType(propOrder = {"noticeId", "expirationTime", "acceptedTime"})
@javax.persistence.Embeddable
@Embeddable
public class LaunchNotice extends ImmutableObject implements UnsafeSerializable {
/** An empty instance to use in place of null. */
private static final NoticeIdType EMPTY_NOTICE_ID = new NoticeIdType();
/** An id with a validator-id attribute. */
@Embed
@javax.persistence.Embeddable
@Embeddable
public static class NoticeIdType extends ImmutableObject implements UnsafeSerializable {
/**
@@ -57,7 +53,6 @@ public class LaunchNotice extends ImmutableObject implements UnsafeSerializable
@XmlValue String tcnId;
/** The identifier of the TMDB provider to use, defaulting to the TMCH. */
@IgnoreSave(IfNull.class)
@XmlAttribute(name = "validatorID")
String validatorId;
@@ -16,7 +16,6 @@ package google.registry.model.domain.launch;
import static java.util.Objects.hash;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.ImmutableObject;
import java.util.Objects;
import javax.xml.bind.annotation.XmlAttribute;
@@ -47,7 +46,6 @@ import javax.xml.bind.annotation.XmlValue;
* sets it is the one that needs to make sure the domain isn't a trademark and that the fields are
* correct.
*/
@Embed
public class LaunchPhase extends ImmutableObject {
/**
@@ -0,0 +1,23 @@
// 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.packagetoken;
import google.registry.model.ImmutableObject;
import google.registry.model.eppinput.EppInput.CommandExtension;
import javax.xml.bind.annotation.XmlRootElement;
/** A package token extension that may be present on EPP domain commands. */
@XmlRootElement(name = "info")
public class PackageTokenExtension extends ImmutableObject implements CommandExtension {}
@@ -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.domain.packagetoken;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.persistence.VKey;
import google.registry.xml.TrimWhitespaceAdapter;
import java.util.Optional;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/**
* An XML data object that represents a package token extension that may be present on the response
* to EPP domain info commands.
*/
@XmlRootElement(name = "packageData")
public class PackageTokenResponseExtension extends ImmutableObject implements ResponseExtension {
/** Token string of the PACKAGE token the name belongs to. */
@XmlJavaTypeAdapter(TrimWhitespaceAdapter.class)
String token;
public static PackageTokenResponseExtension create(Optional<VKey<AllocationToken>> tokenKey) {
PackageTokenResponseExtension instance = new PackageTokenResponseExtension();
instance.token = "";
if (tokenKey.isPresent()) {
instance.token = tokenKey.get().getSqlKey().toString();
}
return instance;
}
}
@@ -0,0 +1,27 @@
// 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.
@XmlSchema(
namespace = "urn:google:params:xml:ns:packageToken-1.0",
xmlns =
@XmlNs(prefix = "packageToken", namespaceURI = "urn:google:params:xml:ns:packageToken-1.0"),
elementFormDefault = XmlNsForm.QUALIFIED)
@XmlAccessorType(XmlAccessType.FIELD)
package google.registry.model.domain.packagetoken;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlNs;
import javax.xml.bind.annotation.XmlNsForm;
import javax.xml.bind.annotation.XmlSchema;
@@ -16,7 +16,6 @@ package google.registry.model.domain.secdns;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.secdns.DelegationSignerData.DomainDsDataId;
import java.io.Serializable;
@@ -37,7 +36,6 @@ import javax.xml.bind.annotation.XmlType;
* @see <a href="http://tools.ietf.org/html/rfc4034">RFC 4034</a>
* <p>TODO(b/177567432): Rename this class to DomainDsData.
*/
@Embed
@XmlType(name = "dsData")
@Entity
@IdClass(DomainDsDataId.class)
@@ -14,8 +14,6 @@
package google.registry.model.domain.secdns;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import javax.persistence.Access;
@@ -29,12 +27,11 @@ import javax.xml.bind.annotation.adapters.HexBinaryAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/** Base class for {@link DelegationSignerData} and {@link DomainDsDataHistory}. */
@Embed
@MappedSuperclass
@Access(AccessType.FIELD)
public abstract class DomainDsDataBase extends ImmutableObject implements UnsafeSerializable {
@Ignore @XmlTransient @Transient String domainRepoId;
@XmlTransient @Transient String domainRepoId;
/** The identifier for this particular key in the domain. */
@Transient int keyTag;
@@ -15,14 +15,12 @@
package google.registry.model.domain.secdns;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.ImmutableObject;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import javax.xml.bind.annotation.XmlRootElement;
/** The EPP secDNS extension to be returned with domain info commands. */
@XmlRootElement(name = "infData")
@Embed
public class SecDnsInfoExtension extends ImmutableObject implements ResponseExtension {
/** Signatures for this domain. */
@@ -26,22 +26,16 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Range;
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.Index;
import com.googlecode.objectify.annotation.OnLoad;
import google.registry.flows.EppException;
import google.registry.flows.domain.DomainFlowUtils;
import google.registry.model.BackupGroupRoot;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.reporting.HistoryEntry;
@@ -54,31 +48,31 @@ import javax.annotation.Nullable;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
import org.joda.time.DateTime;
/** An entity representing an allocation token. */
@ReportedOn
@Entity
@WithStringVKey
@javax.persistence.Entity
@WithStringVKey(compositeKey = true)
@Table(
indexes = {
@javax.persistence.Index(
columnList = "token",
name = "allocation_token_token_idx",
unique = true),
@javax.persistence.Index(
columnList = "domainName",
name = "allocation_token_domain_name_idx"),
@javax.persistence.Index(columnList = "tokenType"),
@javax.persistence.Index(columnList = "redemption_domain_repo_id")
@Index(columnList = "token", name = "allocation_token_token_idx", unique = true),
@Index(columnList = "domainName", name = "allocation_token_domain_name_idx"),
@Index(columnList = "tokenType"),
@Index(columnList = "redemption_domain_repo_id")
})
public class AllocationToken extends BackupGroupRoot implements Buildable {
private static final long serialVersionUID = -3954475393220876903L;
private static final String REMOVE_PACKAGE = "__REMOVEPACKAGE__";
private static final ImmutableMap<String, TokenBehavior> STATIC_TOKEN_BEHAVIORS =
ImmutableMap.of(REMOVE_PACKAGE, TokenBehavior.REMOVE_PACKAGE);
// Promotions should only move forward, and ENDED / CANCELLED are terminal states.
private static final ImmutableMultimap<TokenStatus, TokenStatus> VALID_TOKEN_STATUS_TRANSITIONS =
@@ -87,6 +81,18 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
.putAll(VALID, ENDED, CANCELLED)
.build();
private static final ImmutableMap<String, AllocationToken> BEHAVIORAL_TOKENS =
ImmutableMap.of(
REMOVE_PACKAGE,
new AllocationToken.Builder()
.setTokenType(TokenType.UNLIMITED_USE)
.setToken(REMOVE_PACKAGE)
.build());
public static Optional<AllocationToken> maybeGetStaticTokenInstance(String name) {
return Optional.ofNullable(BEHAVIORAL_TOKENS.get(name));
}
/** Any special behavior that should be used when registering domains using this token. */
public enum RegistrationBehavior {
/** No special behavior */
@@ -110,7 +116,21 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
public enum TokenType {
PACKAGE,
SINGLE_USE,
UNLIMITED_USE
UNLIMITED_USE,
}
/**
* System behaves differently based on a token it gets inside a command. This enumerates different
* types of behaviors we support.
*/
public enum TokenBehavior {
/** No special behavior */
DEFAULT,
/**
* REMOVE_PACKAGE triggers domain removal from promotional package, bypasses DEFAULT token
* validations.
*/
REMOVE_PACKAGE
}
/** The status of this token with regard to any potential promotion. */
@@ -126,11 +146,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
}
/** The allocation token string. */
@javax.persistence.Id @Id String token;
@Id String token;
/** The key of the history entry for which the token was used. Null if not yet used. */
@Nullable
@Index
@AttributeOverrides({
@AttributeOverride(name = "repoId", column = @Column(name = "redemption_domain_repo_id")),
@AttributeOverride(
@@ -140,10 +159,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
DomainHistoryVKey redemptionHistoryEntry;
/** The fully-qualified domain name that this token is limited to, if any. */
@Nullable @Index String domainName;
@Nullable String domainName;
/** When this token was created. */
@Ignore CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
/** Allowed registrar client IDs for this token, or null if all registrars are allowed. */
@Column(name = "allowedRegistrarIds")
@@ -172,21 +191,12 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
@Enumerated(EnumType.STRING)
@Column(name = "renewalPriceBehavior", nullable = false)
@Ignore
RenewalPriceBehavior renewalPriceBehavior = RenewalPriceBehavior.DEFAULT;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
RegistrationBehavior registrationBehavior = RegistrationBehavior.DEFAULT;
// TODO: Remove onLoad once all allocation tokens are migrated to have a discountYears of 1.
@OnLoad
void onLoad() {
if (discountYears == 0) {
discountYears = 1;
}
}
/**
* Promotional token validity periods.
*
@@ -255,9 +265,17 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
return registrationBehavior;
}
public TokenBehavior getTokenBehavior() {
return STATIC_TOKEN_BEHAVIORS.getOrDefault(token, TokenBehavior.DEFAULT);
}
@Override
public VKey<AllocationToken> createVKey() {
return VKey.create(AllocationToken.class, getToken(), Key.create(this));
if (!AllocationToken.TokenBehavior.DEFAULT.equals(getTokenBehavior())) {
throw new IllegalArgumentException(
String.format("%s tokens are not stored in the database", getTokenBehavior()));
}
return VKey.createSql(AllocationToken.class, getToken());
}
@Override
@@ -291,7 +309,8 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
"Redemption history entry can only be specified for SINGLE_USE tokens");
checkArgument(
getInstance().tokenType != TokenType.PACKAGE
|| getInstance().allowedClientIds.size() == 1,
|| (getInstance().allowedClientIds != null
&& getInstance().allowedClientIds.size() == 1),
"PACKAGE tokens must have exactly one allowed client registrar");
checkArgument(
getInstance().discountFraction > 0 || !getInstance().discountPremiums,
@@ -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);
@@ -14,7 +14,6 @@
package google.registry.model.eppcommon;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import javax.persistence.Embeddable;
@@ -45,7 +44,6 @@ public abstract class AuthInfo extends ImmutableObject implements UnsafeSerializ
}
/** The "pwAuthInfoType" complex type. */
@Embed
@XmlType(namespace = "urn:ietf:params:xml:ns:eppcom-1.0")
@Embeddable
public static class PasswordAuth extends ImmutableObject implements UnsafeSerializable {
@@ -31,25 +31,27 @@ import java.io.ByteArrayOutputStream;
public class EppXmlTransformer {
// Hardcoded XML schemas, ordered with respect to dependency.
private static final ImmutableList<String> SCHEMAS = ImmutableList.of(
"eppcom.xsd",
"epp.xsd",
"contact.xsd",
"host.xsd",
"domain.xsd",
"rgp.xsd",
"secdns.xsd",
"fee06.xsd",
"fee11.xsd",
"fee12.xsd",
"metadata.xsd",
"mark.xsd",
"dsig.xsd",
"smd.xsd",
"launch.xsd",
"allocate.xsd",
"superuser.xsd",
"allocationToken-1.0.xsd");
private static final ImmutableList<String> SCHEMAS =
ImmutableList.of(
"eppcom.xsd",
"epp.xsd",
"contact.xsd",
"host.xsd",
"domain.xsd",
"rgp.xsd",
"secdns.xsd",
"fee06.xsd",
"fee11.xsd",
"fee12.xsd",
"metadata.xsd",
"mark.xsd",
"dsig.xsd",
"smd.xsd",
"launch.xsd",
"allocate.xsd",
"superuser.xsd",
"allocationToken-1.0.xsd",
"packageToken.xsd");
private static final XmlTransformer INPUT_TRANSFORMER =
new XmlTransformer(SCHEMAS, EppInput.class);
@@ -14,7 +14,6 @@
package google.registry.model.eppcommon;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.ImmutableObject;
import java.io.Serializable;
import javax.persistence.Embeddable;
@@ -26,7 +25,6 @@ import javax.xml.bind.annotation.XmlTransient;
* <p>When placed in a field "foo", this will correctly unmarshal from both {@code <foo/>} and
* {@code <foo></foo>}, and will unmarshal always to {@code <foo/>}.
*/
@Embed
@Embeddable
public class PresenceMarker extends ImmutableObject implements Serializable {
@XmlTransient
@@ -20,8 +20,8 @@ import static com.google.common.base.Strings.nullToEmpty;
import com.google.common.collect.ImmutableSet;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
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.host.Host;
@@ -130,12 +130,12 @@ public enum StatusValue implements EppEnum {
/** Enum to help clearly list which resource types a status value is allowed to be present on. */
private enum AllowedOn {
ALL(
Contact.class,
ContactBase.class,
ContactResource.class,
DomainBase.class,
Domain.class,
HostBase.class,
Host.class),
DomainBase.class,
Host.class,
HostBase.class),
NONE,
DOMAINS(DomainBase.class, Domain.class);
@@ -16,11 +16,11 @@ package google.registry.model.eppcommon;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.persistence.Embeddable;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
@@ -30,9 +30,8 @@ import javax.xml.bind.annotation.XmlType;
* is formed using the {@code clTRID} associated with the command if supplied by the client and a
* {@code svTRID} (server transaction identifier) that is assigned by and unique to the server."
*/
@Embed
@XmlType(propOrder = {"clientTransactionId", "serverTransactionId"})
@javax.persistence.Embeddable
@Embeddable
public class Trid extends ImmutableObject implements UnsafeSerializable {
/** The server transaction id. */
@@ -45,6 +45,7 @@ import google.registry.model.domain.launch.LaunchDeleteExtension;
import google.registry.model.domain.launch.LaunchInfoExtension;
import google.registry.model.domain.launch.LaunchUpdateExtension;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.packagetoken.PackageTokenExtension;
import google.registry.model.domain.rgp.RgpUpdateExtension;
import google.registry.model.domain.secdns.SecDnsCreateExtension;
import google.registry.model.domain.secdns.SecDnsUpdateExtension;
@@ -362,6 +363,7 @@ public class EppInput extends ImmutableObject {
// Other extensions
@XmlElementRef(type = AllocationTokenExtension.class),
@XmlElementRef(type = MetadataExtension.class),
@XmlElementRef(type = PackageTokenExtension.class),
@XmlElementRef(type = RgpUpdateExtension.class),
@XmlElementRef(type = SecDnsCreateExtension.class),
@XmlElementRef(type = SecDnsUpdateExtension.class)
@@ -43,6 +43,7 @@ import google.registry.model.domain.fee12.FeeRenewResponseExtensionV12;
import google.registry.model.domain.fee12.FeeTransferResponseExtensionV12;
import google.registry.model.domain.fee12.FeeUpdateResponseExtensionV12;
import google.registry.model.domain.launch.LaunchCheckResponseExtension;
import google.registry.model.domain.packagetoken.PackageTokenResponseExtension;
import google.registry.model.domain.rgp.RgpInfoExtension;
import google.registry.model.domain.secdns.SecDnsInfoExtension;
import google.registry.model.eppcommon.Trid;
@@ -121,28 +122,30 @@ public class EppResponse extends ImmutableObject implements ResponseOrGreeting {
/** Zero or more response extensions. */
@XmlElementRefs({
@XmlElementRef(type = FeeCheckResponseExtensionV06.class),
@XmlElementRef(type = FeeInfoResponseExtensionV06.class),
@XmlElementRef(type = FeeCreateResponseExtensionV06.class),
@XmlElementRef(type = FeeDeleteResponseExtensionV06.class),
@XmlElementRef(type = FeeRenewResponseExtensionV06.class),
@XmlElementRef(type = FeeTransferResponseExtensionV06.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV06.class),
@XmlElementRef(type = FeeCheckResponseExtensionV11.class),
@XmlElementRef(type = FeeCreateResponseExtensionV11.class),
@XmlElementRef(type = FeeDeleteResponseExtensionV11.class),
@XmlElementRef(type = FeeRenewResponseExtensionV11.class),
@XmlElementRef(type = FeeTransferResponseExtensionV11.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV11.class),
@XmlElementRef(type = FeeCheckResponseExtensionV12.class),
@XmlElementRef(type = FeeCreateResponseExtensionV12.class),
@XmlElementRef(type = FeeDeleteResponseExtensionV12.class),
@XmlElementRef(type = FeeRenewResponseExtensionV12.class),
@XmlElementRef(type = FeeTransferResponseExtensionV12.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV12.class),
@XmlElementRef(type = LaunchCheckResponseExtension.class),
@XmlElementRef(type = RgpInfoExtension.class),
@XmlElementRef(type = SecDnsInfoExtension.class) })
@XmlElementRef(type = FeeCheckResponseExtensionV06.class),
@XmlElementRef(type = FeeInfoResponseExtensionV06.class),
@XmlElementRef(type = FeeCreateResponseExtensionV06.class),
@XmlElementRef(type = FeeDeleteResponseExtensionV06.class),
@XmlElementRef(type = FeeRenewResponseExtensionV06.class),
@XmlElementRef(type = FeeTransferResponseExtensionV06.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV06.class),
@XmlElementRef(type = FeeCheckResponseExtensionV11.class),
@XmlElementRef(type = FeeCreateResponseExtensionV11.class),
@XmlElementRef(type = FeeDeleteResponseExtensionV11.class),
@XmlElementRef(type = FeeRenewResponseExtensionV11.class),
@XmlElementRef(type = FeeTransferResponseExtensionV11.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV11.class),
@XmlElementRef(type = FeeCheckResponseExtensionV12.class),
@XmlElementRef(type = FeeCreateResponseExtensionV12.class),
@XmlElementRef(type = FeeDeleteResponseExtensionV12.class),
@XmlElementRef(type = FeeRenewResponseExtensionV12.class),
@XmlElementRef(type = FeeTransferResponseExtensionV12.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV12.class),
@XmlElementRef(type = LaunchCheckResponseExtension.class),
@XmlElementRef(type = PackageTokenResponseExtension.class),
@XmlElementRef(type = RgpInfoExtension.class),
@XmlElementRef(type = SecDnsInfoExtension.class)
})
@XmlElementWrapper(name = "extension")
ImmutableList<? extends ResponseExtension> extensions;
@@ -14,7 +14,6 @@
package google.registry.model.host;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.annotations.ExternalMessagingName;
@@ -52,7 +51,7 @@ import javax.persistence.AccessType;
@javax.persistence.Index(columnList = "currentSponsorRegistrarId")
})
@ExternalMessagingName("host")
@WithStringVKey
@WithStringVKey(compositeKey = true)
@Access(AccessType.FIELD) // otherwise it'll use the default if the repoId (property)
public class Host extends HostBase implements ForeignKeyedEppResource {
@@ -65,7 +64,7 @@ public class Host extends HostBase implements ForeignKeyedEppResource {
@Override
public VKey<Host> createVKey() {
return VKey.create(Host.class, getRepoId(), Key.create(this));
return VKey.createSql(Host.class, getRepoId());
}
@Override
@@ -1,79 +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.index;
import static google.registry.util.TypeUtils.instantiate;
import com.google.common.annotations.VisibleForTesting;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.annotation.Parent;
import google.registry.model.BackupGroupRoot;
import google.registry.model.EppResource;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.annotations.ReportedOn;
import google.registry.persistence.VKey;
/** An index that allows for quick enumeration of all EppResource entities (e.g. via map reduce). */
@ReportedOn
@Entity
@DeleteAfterMigration
public class EppResourceIndex extends BackupGroupRoot {
@Id String id;
@Parent Key<EppResourceIndexBucket> bucket;
/** Although this field holds a {@link Key} it is named "reference" for historical reasons. */
Key<? extends EppResource> reference;
@Index String kind;
public String getId() {
return id;
}
public String getKind() {
return kind;
}
public Key<? extends EppResource> getKey() {
return reference;
}
@VisibleForTesting
public Key<EppResourceIndexBucket> getBucket() {
return bucket;
}
@VisibleForTesting
public static <T extends EppResource> EppResourceIndex create(
Key<EppResourceIndexBucket> bucket, Key<T> resourceKey) {
EppResourceIndex instance = instantiate(EppResourceIndex.class);
instance.reference = resourceKey;
instance.kind = resourceKey.getKind();
// creates a web-safe key string, this value is never used
// TODO(b/211785379): remove unused id
instance.id = VKey.from(resourceKey).stringify();
instance.bucket = bucket;
return instance;
}
public static <T extends EppResource> EppResourceIndex create(Key<T> resourceKey) {
return create(EppResourceIndexBucket.getBucketKey(resourceKey), resourceKey);
}
}
@@ -1,68 +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.index;
import static google.registry.config.RegistryConfig.getEppResourceIndexBucketCount;
import com.google.common.collect.ImmutableList;
import com.google.common.hash.Hashing;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.annotations.VirtualEntity;
/** A virtual entity to represent buckets to which EppResourceIndex objects are randomly added. */
@Entity
@VirtualEntity
@DeleteAfterMigration
public class EppResourceIndexBucket extends ImmutableObject {
@SuppressWarnings("unused")
@Id
private long bucketId;
/**
* Deterministic function that returns a bucket id based on the resource's roid.
* NB: At the moment, nothing depends on this being deterministic, so we have the ability to
* change the number of buckets and utilize a random distribution once we do.
*/
private static long getBucketIdFromEppResource(Key<? extends EppResource> resourceKey) {
int numBuckets = getEppResourceIndexBucketCount();
// IDs can't be 0, so add 1 to the hash.
return Hashing.consistentHash(resourceKey.getName().hashCode(), numBuckets) + 1L;
}
/** Gets a bucket key as a function of an EppResource to be indexed. */
public static Key<EppResourceIndexBucket> getBucketKey(Key<? extends EppResource> resourceKey) {
return Key.create(EppResourceIndexBucket.class, getBucketIdFromEppResource(resourceKey));
}
/** Gets the specified numbered bucket key. */
public static Key<EppResourceIndexBucket> getBucketKey(int bucketId) {
return Key.create(EppResourceIndexBucket.class, bucketId);
}
/** Returns the keys to all buckets. */
public static Iterable<Key<EppResourceIndexBucket>> getAllBuckets() {
ImmutableList.Builder<Key<EppResourceIndexBucket>> builder = new ImmutableList.Builder<>();
for (int bucketId = 1; bucketId <= getEppResourceIndexBucketCount(); bucketId++) {
builder.add(getBucketKey(bucketId));
}
return builder.build();
}
}
@@ -1,358 +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.index;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.config.RegistryConfig.getEppResourceCachingDuration;
import static google.registry.config.RegistryConfig.getEppResourceMaxCachedEntries;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaJpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.entriesToImmutableMap;
import static google.registry.util.TypeUtils.instantiate;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Index;
import google.registry.config.RegistryConfig;
import google.registry.model.BackupGroupRoot;
import google.registry.model.CacheUtils;
import google.registry.model.CacheUtils.AppEngineEnvironmentCacheLoader;
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.Domain;
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.util.NonFinalForTesting;
import java.time.Duration;
import java.util.Collection;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/**
* Class to map a foreign key to the active instance of {@link EppResource} whose unique id matches
* the foreign key string. The instance is never deleted, but it is updated if a newer entity
* becomes the active entity.
*/
@DeleteAfterMigration
public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroupRoot {
/** The {@link ForeignKeyIndex} type for {@link ContactResource} entities. */
@ReportedOn
@Entity
public static class ForeignKeyContactIndex extends ForeignKeyIndex<ContactResource> {}
/** The {@link ForeignKeyIndex} type for {@link Domain} entities. */
@ReportedOn
@Entity
public static class ForeignKeyDomainIndex extends ForeignKeyIndex<Domain> {}
/** The {@link ForeignKeyIndex} type for {@link Host} entities. */
@ReportedOn
@Entity
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,
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",
Domain.class, "fullyQualifiedDomainName",
Host.class, "fullyQualifiedHostName");
@Id String foreignKey;
/**
* 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 Host} that was renamed, this field will hold the time of the rename.
*/
@Index DateTime deletionTime;
/**
* The referenced resource.
*
* <p>This field holds a key to the only referenced resource. It is named "topReference" for
* historical reasons.
*/
VKey<E> topReference;
public String getForeignKey() {
return foreignKey;
}
public DateTime getDeletionTime() {
return deletionTime;
}
public VKey<E> getResourceKey() {
return topReference;
}
@SuppressWarnings("unchecked")
public static <T extends EppResource> Class<ForeignKeyIndex<T>> mapToFkiClass(
Class<T> resourceClass) {
return (Class<ForeignKeyIndex<T>>) RESOURCE_CLASS_TO_FKI_CLASS.get(resourceClass);
}
/** Create a {@link ForeignKeyIndex} instance for a resource, expiring at a specified time. */
public static <E extends EppResource> ForeignKeyIndex<E> create(
E resource, DateTime deletionTime) {
@SuppressWarnings("unchecked")
Class<E> resourceClass = (Class<E>) resource.getClass();
ForeignKeyIndex<E> instance = instantiate(mapToFkiClass(resourceClass));
instance.topReference = (VKey<E>) resource.createVKey();
instance.foreignKey = resource.getForeignKey();
instance.deletionTime = deletionTime;
return instance;
}
/** Create a {@link ForeignKeyIndex} key for a resource. */
public static <E extends EppResource> Key<ForeignKeyIndex<E>> createKey(E resource) {
@SuppressWarnings("unchecked")
Class<E> resourceClass = (Class<E>) resource.getClass();
return Key.create(mapToFkiClass(resourceClass), resource.getForeignKey());
}
/**
* Loads a {@link Key} to an {@link EppResource} from Datastore by foreign key.
*
* <p>Returns null if no foreign key index with this foreign key was ever created, or if the most
* recently created foreign key index was deleted before time "now". This method does not actually
* check that the referenced resource actually exists. However, for normal epp resources, it is
* safe to assume that the referenced resource exists if the foreign key index does.
*
* @param clazz the resource type to load
* @param foreignKey id to match
* @param now the current logical time to use when checking for soft deletion of the foreign key
* index
*/
@Nullable
public static <E extends EppResource> VKey<E> loadAndGetKey(
Class<E> clazz, String foreignKey, DateTime now) {
ForeignKeyIndex<E> index = load(clazz, foreignKey, now);
return (index == null) ? null : index.getResourceKey();
}
/**
* Load a {@link ForeignKeyIndex} by class and id string that is active at or after the specified
* moment in time.
*
* <p>This will return null if the {@link ForeignKeyIndex} doesn't exist or has been soft deleted.
*/
@Nullable
public static <E extends EppResource> ForeignKeyIndex<E> load(
Class<E> clazz, String foreignKey, DateTime now) {
return load(clazz, ImmutableList.of(foreignKey), now).get(foreignKey);
}
/**
* Load a map of {@link ForeignKeyIndex} instances by class and id strings that are active at or
* after the specified moment in time.
*
* <p>The returned map will omit any keys for which the {@link ForeignKeyIndex} doesn't exist or
* has been soft deleted.
*/
public static <E extends EppResource> ImmutableMap<String, ForeignKeyIndex<E>> load(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
return loadIndexesFromStore(clazz, foreignKeys, true, false).entrySet().stream()
.filter(e -> now.isBefore(e.getValue().getDeletionTime()))
.collect(entriesToImmutableMap());
}
/**
* Helper method to load all of the most recent {@link ForeignKeyIndex}es for the given foreign
* keys, regardless of whether or not they have been soft-deleted.
*
* <p>Used by both the cached (w/o deletion check) and the non-cached (with deletion check) calls.
*
* <p>Note that in the cached case, we wish to run this outside of any transaction because we may
* be loading many entities, going over the Datastore limit on the number of enrolled entity
* groups per transaction (25). If we require consistency, however, we must use a transaction.
*
* @param inTransaction whether or not to use an Objectify transaction
*/
private static <E extends EppResource>
ImmutableMap<String, ForeignKeyIndex<E>> loadIndexesFromStore(
Class<E> clazz,
Collection<String> foreignKeys,
boolean inTransaction,
boolean useReplicaJpaTm) {
if (tm().isOfy()) {
Class<ForeignKeyIndex<E>> fkiClass = mapToFkiClass(clazz);
return ImmutableMap.copyOf(
inTransaction
? auditedOfy().load().type(fkiClass).ids(foreignKeys)
: tm().doTransactionless(() -> auditedOfy().load().type(fkiClass).ids(foreignKeys)));
} else {
String property = RESOURCE_CLASS_TO_FKI_PROPERTY.get(clazz);
JpaTransactionManager jpaTmToUse = useReplicaJpaTm ? replicaJpaTm() : jpaTm();
ImmutableList<ForeignKeyIndex<E>> indexes =
jpaTmToUse.transact(
() ->
jpaTmToUse
.criteriaQuery(
CriteriaQueryBuilder.create(clazz)
.whereFieldIsIn(property, foreignKeys)
.build())
.getResultStream()
.map(e -> ForeignKeyIndex.create(e, e.getDeletionTime()))
.collect(toImmutableList()));
// We need to find and return the entities with the maximum deletionTime for each foreign key.
return Multimaps.index(indexes, ForeignKeyIndex::getForeignKey).asMap().entrySet().stream()
.map(
entry ->
Maps.immutableEntry(
entry.getKey(),
entry.getValue().stream()
.max(Comparator.comparing(ForeignKeyIndex::getDeletionTime))
.get()))
.collect(entriesToImmutableMap());
}
}
static final CacheLoader<VKey<ForeignKeyIndex<?>>, Optional<ForeignKeyIndex<?>>> CACHE_LOADER =
new AppEngineEnvironmentCacheLoader<
VKey<ForeignKeyIndex<?>>, Optional<ForeignKeyIndex<?>>>() {
@Override
public Optional<ForeignKeyIndex<?>> load(VKey<ForeignKeyIndex<?>> key) {
String foreignKey = key.getSqlKey().toString();
return Optional.ofNullable(
loadIndexesFromStore(
RESOURCE_CLASS_TO_FKI_CLASS.inverse().get(key.getKind()),
ImmutableSet.of(foreignKey),
false,
true)
.get(foreignKey));
}
@Override
public Map<VKey<ForeignKeyIndex<?>>, Optional<ForeignKeyIndex<?>>> loadAll(
Iterable<? extends VKey<ForeignKeyIndex<?>>> keys) {
if (!keys.iterator().hasNext()) {
return ImmutableMap.of();
}
Class<? extends EppResource> resourceClass =
RESOURCE_CLASS_TO_FKI_CLASS.inverse().get(keys.iterator().next().getKind());
ImmutableSet<String> foreignKeys =
Streams.stream(keys).map(v -> v.getSqlKey().toString()).collect(toImmutableSet());
ImmutableSet<VKey<ForeignKeyIndex<?>>> typedKeys = ImmutableSet.copyOf(keys);
ImmutableMap<String, ? extends ForeignKeyIndex<? extends EppResource>> existingFkis =
loadIndexesFromStore(resourceClass, foreignKeys, false, true);
// ofy omits keys that don't have values in Datastore, so re-add them in
// here with Optional.empty() values.
return Maps.asMap(
typedKeys,
(VKey<ForeignKeyIndex<?>> key) ->
Optional.ofNullable(existingFkis.getOrDefault(key.getSqlKey().toString(), null)));
}
};
/**
* A limited size, limited time cache for foreign key entities.
*
* <p>This is only used to cache foreign key entities for the purposes of checking whether they
* exist (and if so, what entity they point to) during a few domain flows. Any other operations on
* foreign keys should not use this cache.
*
* <p>Note that the value type of this cache is Optional because the foreign keys in question are
* coming from external commands, and thus don't necessarily represent entities in our system that
* actually exist. So we cache the fact that they *don't* exist by using Optional.empty(), and
* then several layers up the EPP command will fail with an error message like "The contact with
* given IDs (blah) don't exist."
*/
@NonFinalForTesting
private static LoadingCache<VKey<ForeignKeyIndex<?>>, Optional<ForeignKeyIndex<?>>>
cacheForeignKeyIndexes = createForeignKeyIndexesCache(getEppResourceCachingDuration());
private static LoadingCache<VKey<ForeignKeyIndex<?>>, Optional<ForeignKeyIndex<?>>>
createForeignKeyIndexesCache(Duration expiry) {
return CacheUtils.newCacheBuilder(expiry)
.maximumSize(getEppResourceMaxCachedEntries())
.build(CACHE_LOADER);
}
@VisibleForTesting
public static void setCacheForTest(Optional<Duration> expiry) {
Duration effectiveExpiry = expiry.orElse(getEppResourceCachingDuration());
cacheForeignKeyIndexes = createForeignKeyIndexesCache(effectiveExpiry);
}
/**
* Load a list of {@link ForeignKeyIndex} instances by class and id strings that are active at or
* after the specified moment in time, using the cache if enabled.
*
* <p>The returned map will omit any keys for which the {@link ForeignKeyIndex} doesn't exist or
* has been soft deleted.
*
* <p>Don't use the cached version of this method unless you really need it for performance
* reasons, and are OK with the trade-offs in loss of transactional consistency.
*/
public static <E extends EppResource> ImmutableMap<String, ForeignKeyIndex<E>> loadCached(
Class<E> clazz, Collection<String> foreignKeys, final DateTime now) {
if (!RegistryConfig.isEppResourceCachingEnabled()) {
return tm().doTransactionless(() -> load(clazz, foreignKeys, now));
}
Class<? extends ForeignKeyIndex<?>> fkiClass = mapToFkiClass(clazz);
// Safe to cast VKey<FKI<E>> to VKey<FKI<?>>
@SuppressWarnings("unchecked")
ImmutableList<VKey<ForeignKeyIndex<?>>> fkiVKeys =
foreignKeys.stream()
.map(fk -> (VKey<ForeignKeyIndex<?>>) VKey.create(fkiClass, fk))
.collect(toImmutableList());
// This cast is safe because when we loaded ForeignKeyIndexes above we used type clazz, which
// is scoped to E.
@SuppressWarnings("unchecked")
ImmutableMap<String, ForeignKeyIndex<E>> fkisFromCache =
cacheForeignKeyIndexes.getAll(fkiVKeys).entrySet().stream()
.filter(entry -> entry.getValue().isPresent())
.filter(entry -> now.isBefore(entry.getValue().get().getDeletionTime()))
.collect(
toImmutableMap(
entry -> entry.getKey().getSqlKey().toString(),
entry -> (ForeignKeyIndex<E>) entry.getValue().get()));
return fkisFromCache;
}
}
@@ -34,7 +34,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
*
* <p>All first class entities are represented as a resource class - {@link
* google.registry.model.domain.Domain}, {@link google.registry.model.host.Host}, {@link
* google.registry.model.contact.ContactResource}, and {@link
* google.registry.model.contact.Contact}, 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"
* class- {@link google.registry.model.domain.DomainCommand}, {@link
@@ -15,7 +15,6 @@
package google.registry.model.poll;
import com.google.common.annotations.VisibleForTesting;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.eppcommon.Trid;
@@ -36,7 +35,6 @@ public class PendingActionNotificationResponse extends ImmutableObject
implements ResponseData, UnsafeSerializable {
/** The inner name type that contains a name and the result boolean. */
@Embed
@Embeddable
static class NameOrId extends ImmutableObject implements UnsafeSerializable {
@XmlValue
@@ -80,7 +78,6 @@ public class PendingActionNotificationResponse extends ImmutableObject
}
/** An adapter to output the XML in response to resolving a pending command on a domain. */
@Embed
@XmlRootElement(name = "panData", namespace = "urn:ietf:params:xml:ns:domain-1.0")
@XmlType(
propOrder = {"name", "trid", "processedDate"},
@@ -105,7 +102,6 @@ public class PendingActionNotificationResponse extends ImmutableObject
}
/** An adapter to output the XML in response to resolving a pending command on a contact. */
@Embed
@XmlRootElement(name = "panData", namespace = "urn:ietf:params:xml:ns:contact-1.0")
@XmlType(
propOrder = {"id", "trid", "processedDate"},
@@ -130,7 +126,6 @@ public class PendingActionNotificationResponse extends ImmutableObject
}
/** An adapter to output the XML in response to resolving a pending command on a host. */
@Embed
@XmlRootElement(name = "panData", namespace = "urn:ietf:params:xml:ns:domain-1.0")
@XmlType(
propOrder = {"name", "trid", "processedDate"},
@@ -26,9 +26,9 @@ import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.annotations.OfyIdAllocation;
import google.registry.model.contact.Contact;
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.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
@@ -95,7 +95,7 @@ public abstract class PollMessage extends ImmutableObject
/** Indicates the type of entity the poll message is for. */
public enum Type {
DOMAIN(1L, Domain.class),
CONTACT(2L, ContactResource.class),
CONTACT(2L, Contact.class),
HOST(3L, Host.class);
private final long id;
@@ -179,7 +179,7 @@ public abstract class PollMessage extends ImmutableObject
/**
* Returns the contact repo id.
*
* <p>This may only be used on a ContactResource poll event.
* <p>This may only be used on a {@link Contact} poll event.
*/
public String getContactRepoId() {
checkArgument(getType() == Type.CONTACT);
@@ -215,6 +215,9 @@ public abstract class PollMessage extends ImmutableObject
return domainRepoId != null ? Type.DOMAIN : contactRepoId != null ? Type.CONTACT : Type.HOST;
}
@Override
public abstract VKey<? extends PollMessage> createVKey();
public abstract ImmutableList<ResponseData> getResponseData();
/** Override Buildable.asBuilder() to give this method stronger typing. */
@@ -19,10 +19,6 @@ import static google.registry.model.rde.RdeNamingUtils.makePartialName;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.base.VerifyException;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.BackupGroupRoot;
import google.registry.model.ImmutableObject;
import google.registry.model.rde.RdeRevision.RdeRevisionId;
@@ -32,10 +28,11 @@ import java.io.Serializable;
import java.util.Optional;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Transient;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
@@ -47,18 +44,14 @@ import org.joda.time.LocalDate;
* flag is included in the generated XML.
*/
@Entity
@javax.persistence.Entity
@IdClass(RdeRevisionId.class)
public final class RdeRevision extends BackupGroupRoot {
/** String triplet of tld, date, and mode, e.g. {@code soy_2015-09-01_full}. */
@Id @Transient String id;
@Id String tld;
@javax.persistence.Id @Ignore String tld;
@Id LocalDate date;
@javax.persistence.Id @Ignore LocalDate date;
@javax.persistence.Id @Ignore RdeMode mode;
@Id RdeMode mode;
/**
* Number of last revision successfully staged to GCS.
@@ -71,10 +64,8 @@ public final class RdeRevision extends BackupGroupRoot {
/** Hibernate requires an empty constructor. */
private RdeRevision() {}
public static RdeRevision create(
String id, String tld, LocalDate date, RdeMode mode, int revision) {
public static RdeRevision create(String tld, LocalDate date, RdeMode mode, int revision) {
RdeRevision instance = new RdeRevision();
instance.id = id;
instance.tld = tld;
instance.date = date;
instance.mode = mode;
@@ -92,12 +83,9 @@ public final class RdeRevision extends BackupGroupRoot {
* @return {@code 0} for first deposit generation and {@code >0} for resends
*/
public static int getNextRevision(String tld, DateTime date, RdeMode mode) {
String id = makePartialName(tld, date, mode);
RdeRevisionId sqlKey = RdeRevisionId.create(tld, date.toLocalDate(), mode);
Key<RdeRevision> ofyKey = Key.create(RdeRevision.class, id);
Optional<RdeRevision> revisionOptional =
tm().transact(
() -> tm().loadByKeyIfPresent(VKey.create(RdeRevision.class, sqlKey, ofyKey)));
tm().transact(() -> tm().loadByKeyIfPresent(VKey.createSql(RdeRevision.class, sqlKey)));
return revisionOptional.map(rdeRevision -> rdeRevision.revision + 1).orElse(0);
}
@@ -121,12 +109,10 @@ public final class RdeRevision extends BackupGroupRoot {
*/
public static void saveRevision(String tld, DateTime date, RdeMode mode, int revision) {
checkArgument(revision >= 0, "Negative revision: %s", revision);
String triplet = makePartialName(tld, date, mode);
tm().assertInTransaction();
RdeRevisionId sqlKey = RdeRevisionId.create(tld, date.toLocalDate(), mode);
Key<RdeRevision> ofyKey = Key.create(RdeRevision.class, triplet);
Optional<RdeRevision> revisionOptional =
tm().loadByKeyIfPresent(VKey.create(RdeRevision.class, sqlKey, ofyKey));
tm().loadByKeyIfPresent(VKey.createSql(RdeRevision.class, sqlKey));
if (revision == 0) {
revisionOptional.ifPresent(
rdeRevision -> {
@@ -139,7 +125,7 @@ public final class RdeRevision extends BackupGroupRoot {
checkArgument(
revisionOptional.isPresent(),
"Couldn't find existing RDE revision %s when trying to save new revision %s",
triplet,
makePartialName(tld, date, mode),
revision);
checkArgument(
revisionOptional.get().revision == revision - 1,
@@ -147,7 +133,7 @@ public final class RdeRevision extends BackupGroupRoot {
revision - 1,
revisionOptional.get());
}
RdeRevision object = RdeRevision.create(triplet, tld, date.toLocalDate(), mode, revision);
RdeRevision object = create(tld, date.toLocalDate(), mode, revision);
tm().put(object);
}
@@ -26,8 +26,6 @@ import static com.google.common.collect.Sets.immutableEnumSet;
import static com.google.common.io.BaseEncoding.base64;
import static google.registry.config.RegistryConfig.getDefaultRegistrarWhoisServer;
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.model.tld.Registries.assertTldsExist;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -44,7 +42,6 @@ import static java.util.function.Predicate.isEqual;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -55,14 +52,6 @@ import com.google.common.collect.Range;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.re2j.Pattern;
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;
@@ -70,9 +59,6 @@ import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import google.registry.model.UpdateAutoTimestamp;
import google.registry.model.annotations.InCrossTld;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.tld.Registry;
import google.registry.model.tld.Registry.TldType;
import google.registry.persistence.VKey;
@@ -85,6 +71,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
@@ -92,25 +79,22 @@ 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.Table;
import javax.persistence.Transient;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime;
/** Information about a registrar. */
@ReportedOn
@Entity
@javax.persistence.Entity
@Table(
indexes = {
@javax.persistence.Index(columnList = "registrarName", name = "registrar_name_idx"),
@javax.persistence.Index(
columnList = "ianaIdentifier",
name = "registrar_iana_identifier_idx"),
@Index(columnList = "registrarName", name = "registrar_name_idx"),
@Index(columnList = "ianaIdentifier", name = "registrar_iana_identifier_idx"),
})
@InCrossTld
public class Registrar extends ImmutableObject
implements Buildable, Jsonifiable, UnsafeSerializable {
@@ -199,7 +183,7 @@ public class Registrar extends ImmutableObject
/** The states in which a {@link Registrar} is considered {@link #isLive live}. */
private static final ImmutableSet<State> LIVE_STATES =
Sets.immutableEnumSet(State.ACTIVE, State.SUSPENDED);
immutableEnumSet(State.ACTIVE, State.SUSPENDED);
/**
* The types for which a {@link Registrar} should be included in WHOIS and RDAP output. We exclude
@@ -215,18 +199,9 @@ public class Registrar extends ImmutableObject
private static final Comparator<RegistrarPoc> CONTACT_EMAIL_COMPARATOR =
comparing(RegistrarPoc::getEmailAddress, String::compareTo);
/**
* A caching {@link Supplier} of a registrarId to {@link Registrar} map.
*
* <p>The supplier's get() method enters a transactionless context briefly to avoid enrolling the
* query inside an unrelated client-affecting transaction.
*/
/** A caching {@link Supplier} of a registrarId to {@link Registrar} map. */
private static final Supplier<ImmutableMap<String, Registrar>> CACHE_BY_REGISTRAR_ID =
memoizeWithShortExpiration(
() ->
tm().doTransactionless(() -> Maps.uniqueIndex(loadAll(), Registrar::getRegistrarId)));
@Parent @Transient Key<EntityGroupRoot> parent = getCrossTldKey();
memoizeWithShortExpiration(() -> Maps.uniqueIndex(loadAll(), Registrar::getRegistrarId));
/**
* Unique registrar client id. Must conform to "clIDType" as defined in RFC5730.
@@ -235,7 +210,6 @@ public class Registrar extends ImmutableObject
* <p>TODO(b/177567432): Rename this field to registrarId.
*/
@Id
@javax.persistence.Id
@Column(name = "registrarId", nullable = false)
String clientIdentifier;
@@ -250,7 +224,6 @@ public class Registrar extends ImmutableObject
* @see <a href="http://www.icann.org/registrar-reports/accredited-list.html">ICANN-Accredited
* Registrars</a>
*/
@Index
@Column(nullable = false)
String registrarName;
@@ -315,7 +288,6 @@ public class Registrar extends ImmutableObject
* Localized {@link RegistrarAddress} for this registrar. Contents can be represented in
* unrestricted UTF-8.
*/
@IgnoreSave(IfNull.class)
@Embedded
@AttributeOverrides({
@AttributeOverride(
@@ -340,7 +312,6 @@ 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)
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "streetLine1", column = @Column(name = "i18n_address_street_line1")),
@@ -369,14 +340,14 @@ public class Registrar extends ImmutableObject
*
* <ul>
* <li>8 is used for Testing Registrar.
* <li>9997 is used by ICAAN for SLA monitoring.
* <li>9997 is used by ICANN for SLA monitoring.
* <li>9999 is used for cases when the registry operator acts as registrar.
* </ul>
*
* @see <a href="http://www.iana.org/assignments/registrar-ids/registrar-ids.txt">Registrar
* IDs</a>
*/
@Index @Nullable Long ianaIdentifier;
@Nullable Long ianaIdentifier;
/** Purchase Order number used for invoices in external billing system, if applicable. */
@Nullable String poNumber;
@@ -397,7 +368,7 @@ public class Registrar extends ImmutableObject
/**
* ICANN referral email address.
*
* <p>This value is specified in the initial registrar contact. It can't be edited in the web GUI
* <p>This value is specified in the initial registrar contact. It can't be edited in the web GUI,
* and it must be specified when the registrar account is created.
*/
String icannReferralEmail;
@@ -408,10 +379,10 @@ public class Registrar extends ImmutableObject
// Metadata.
/** The time when this registrar was created. */
@Ignore CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
/** An automatically managed last-saved timestamp. */
@Ignore UpdateAutoTimestamp lastUpdateTime = UpdateAutoTimestamp.create(null);
UpdateAutoTimestamp lastUpdateTime = UpdateAutoTimestamp.create(null);
/** The time that the certificate was last updated. */
DateTime lastCertificateUpdateTime;
@@ -612,18 +583,13 @@ public class Registrar extends ImmutableObject
}
private Iterable<RegistrarPoc> getContactsIterable() {
if (tm().isOfy()) {
return auditedOfy().load().type(RegistrarPoc.class).ancestor(Registrar.this);
} else {
return tm().transact(
() ->
jpaTm()
.query(
"FROM RegistrarPoc WHERE registrarId = :registrarId", RegistrarPoc.class)
.setParameter("registrarId", clientIdentifier)
.getResultStream()
.collect(toImmutableList()));
}
return tm().transact(
() ->
jpaTm()
.query("FROM RegistrarPoc WHERE registrarId = :registrarId", RegistrarPoc.class)
.setParameter("registrarId", clientIdentifier)
.getResultStream()
.collect(toImmutableList()));
}
@Override
@@ -689,18 +655,13 @@ public class Registrar extends ImmutableObject
/** Creates a {@link VKey} for this instance. */
@Override
public VKey<Registrar> createVKey() {
return createVKey(Key.create(this));
return createVKey(clientIdentifier);
}
/** Creates a {@link VKey} for the given {@code registrarId}. */
public static VKey<Registrar> createVKey(String registrarId) {
checkArgumentNotNull(registrarId, "registrarId must be specified");
return createVKey(Key.create(getCrossTldKey(), Registrar.class, registrarId));
}
/** Creates a {@link VKey} instance from a {@link Key} instance. */
public static VKey<Registrar> createVKey(Key<Registrar> key) {
return VKey.create(Registrar.class, key.getName(), key);
return VKey.createSql(Registrar.class, registrarId);
}
/** A builder for constructing {@link Registrar}, since it is immutable. */
@@ -776,7 +737,7 @@ public class Registrar extends ImmutableObject
Set<VKey<Registry>> missingTldKeys =
Sets.difference(
newTldKeys, tm().transact(() -> tm().loadByKeysIfPresent(newTldKeys)).keySet());
checkArgument(missingTldKeys.isEmpty(), "Trying to set nonexisting TLDs: %s", missingTldKeys);
checkArgument(missingTldKeys.isEmpty(), "Trying to set nonexistent TLDs: %s", missingTldKeys);
getInstance().allowedTlds = ImmutableSortedSet.copyOf(allowedTlds);
return this;
}
@@ -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 {
@@ -18,8 +18,6 @@ import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableSet;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
@@ -43,15 +41,13 @@ import org.joda.time.DateTime;
* only exception is for reportField = TRANSFER_LOSING_SUCCESSFUL or TRANSFER_LOSING_NACKED, which
* uses HistoryEntry.otherClientId because the losing party in a transfer is always the otherClient.
*/
@Embed
@Entity
public class DomainTransactionRecord extends ImmutableObject
implements Buildable, UnsafeSerializable {
@Id
@Ignore
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ImmutableObject.Insignificant
@Insignificant
Long id;
/** The TLD this record operates on. */
@@ -62,9 +58,9 @@ public class DomainTransactionRecord extends ImmutableObject
// Datastore-SQL validation. They are excluded from equality check since they are not set in
// Datastore.
// TODO(b/203609782): post migration, decide whether to keep these two fields.
@Ignore @ImmutableObject.Insignificant String domainRepoId;
@Insignificant String domainRepoId;
@Ignore @ImmutableObject.Insignificant Long historyRevisionId;
@Insignificant Long historyRevisionId;
/**
* The time this Transaction takes effect (counting grace periods and other nuances).
@@ -25,19 +25,18 @@ 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.IgnoreSave;
import com.googlecode.objectify.annotation.Ignore;
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.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.contact.Contact;
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.DomainHistory;
@@ -155,8 +154,7 @@ public class HistoryEntry extends ImmutableObject implements Buildable, UnsafeSe
* The length of time that a create, allocate, renewal, or transfer request was issued for. Will
* be null for all other types.
*/
@IgnoreSave(IfNull.class)
@Transient // domain-specific
@Ignore @Transient // domain-specific
Period period;
/**
@@ -189,6 +187,7 @@ public class HistoryEntry extends ImmutableObject implements Buildable, UnsafeSe
/** Transaction id that made this change, or null if the entry was not created by a flow. */
@Nullable
@Ignore
@AttributeOverrides({
@AttributeOverride(
name = "clientTransactionId",
@@ -218,6 +217,7 @@ public class HistoryEntry extends ImmutableObject implements Buildable, UnsafeSe
* also be empty if the HistoryEntry refers to an EPP mutation that does not affect domain
* transaction counts (such as contact or host mutations).
*/
@Ignore
@Transient // domain-specific
@ImmutableObject.EmptySetToNull
protected Set<DomainTransactionRecord> domainTransactionRecords;
@@ -392,7 +392,7 @@ public class HistoryEntry extends ImmutableObject implements Buildable, UnsafeSe
} else if (parentKind.equals(getKind(Host.class))) {
resultEntity =
new HostHistory.Builder().copyFrom(this).setHostRepoId(parent.getName()).build();
} else if (parentKind.equals(getKind(ContactResource.class))) {
} else if (parentKind.equals(getKind(Contact.class))) {
resultEntity =
new ContactHistory.Builder().copyFrom(this).setContactRepoId(parent.getName()).build();
} else {
@@ -418,7 +418,7 @@ public class HistoryEntry extends ImmutableObject implements Buildable, UnsafeSe
HostHistory.class,
new HostHistoryId(repoId, id),
Key.create(parent, HostHistory.class, id));
} else if (parentKind.equals(getKind(ContactResource.class))) {
} else if (parentKind.equals(getKind(Contact.class))) {
return VKey.create(
ContactHistory.class,
new ContactHistoryId(repoId, id),
@@ -25,8 +25,8 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.host.Host;
@@ -51,7 +51,7 @@ public class HistoryEntryDao {
public static ImmutableMap<Class<? extends EppResource>, Class<? extends HistoryEntry>>
RESOURCE_TYPES_TO_HISTORY_TYPES =
ImmutableMap.of(
ContactResource.class,
Contact.class,
ContactHistory.class,
Domain.class,
DomainHistory.class,

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