1
0
mirror of https://github.com/google/nomulus synced 2026-06-09 16:33:02 +00:00

Compare commits

...

20 Commits

Author SHA1 Message Date
Weimin Yu 2b794347e6 Refactor LevelDbFileBuilder to accept DS Entity (#599)
* Refactor LevelDbFileBuilder to accept DS Entity

Builder now can directly work with Datastore Entity objects.
No need to wrap data in ComparableEntity.
2020-05-28 13:38:00 -04:00
Shicong Huang 26fb5388a4 Generate sql schema for BillingEvent (#565)
* Generate sql schema for BillingEvent

* Change to use sequence

* Address comments

* Resolve warnings and remove duplicate cost related fields

* Increase the flayway file version to V25

* Remove extra space

* Split to 3 tables, merge VKey

* Rename talbes

* Rename repoId to domainRepoId

* Exclude VKey in schema.txt

* Rename target_id to domain_name

* Fix javadoc

* Resolve comments
2020-05-27 15:59:19 -04:00
Lai Jiang bd443633f6 Add a task to compile javadoc across all packages (#597)
Also fixes various issues that prevent javadoc compliation.
2020-05-27 10:33:46 -04:00
Weimin Yu d87f119b36 Add a test for SQL logging config (#598)
* Add a test for SQL logging config

Verifies that SQL statements are logged by Hibernate when
configured to do so.
2020-05-26 16:25:33 -04:00
Weimin Yu 54f1357d83 Fix show-sql which stopped working (#596)
* Fix show-sql which stopped working

Made show-sql property configurable in JpaUnitTestRules.

Added a few comments on foreign key constraint behavior.
2020-05-21 12:20:56 -04:00
Lai Jiang c73d154084 Do not enqueue update snapshot task if import fails (#578)
If the import from Datastore to BigQuery fails, there is no point
enqueuing a job to update the snapshot view.

Also when there's an error updating the snapshot view, log it at severe
level. The HTTP exception thrown is logged at info and triggers a retry
implicitly. I'm not sure if we want this behavior though. Do we want to
retry upon snapshot updating failures? Unless the failurs are transient,
retrying doesn't help. In our case the failure (End of time out of range
in Standard SQL) is not transient.
2020-05-21 11:40:45 -04:00
gbrodman 259d2e2cdc Run "npm audit fix" to fix a vulnerability (#592) 2020-05-20 15:12:27 -04:00
Shicong Huang 0f174d9ce0 Add all existing entities to VKeyTranslatorFactory (#595)
EntityClasses.ALL_CLASSES has all of our registered entities so
we can use it to initialize VKeyTranslatorFactory.classRegistry
to avoid adding them one by one.

Also, this PR changed to use Key.getKind() to get the kind of
the entity to solve the problem that when the entity class
is an inner class, its kind should still be the class name
instead of OuterClass$InnerClass.
2020-05-20 14:24:45 -04:00
Weimin Yu ca2edb6a17 Close input channel in LevelDbLogReader (#594)
* Close input channel in LevelDbLogReader

Input channel should be closed when all data has been read.
2020-05-20 12:54:13 -04:00
Weimin Yu 3947ac6ef7 Read LevelDb incrementally (#593)
* Read LevelDb incrementally

Made LevelDbLogReader an iterator over a LevelDb data stream,
Reducing memory footprint which is important when used in a
Dataflow pipeline.
2020-05-20 10:26:34 -04:00
Michael Muller 579a3d0ac1 Make VKey persist to datastore as a key (#591)
* Make VKey persist to datastore as a key

Convert nsHosts entirely to VKey as a proof-of-concept.

Tested as follows:
    1) Deployed to crash, verified that nameservers were visible for several
       domains (indicating that we are able to load a set of Keys as VKeys)
    2) Updated the set of nameservers for a domain (removing some initial
       hosts) and verified that the changes went through.
    3) Deployed the old version to crash, verified that I was able to retrieve
       the newly saved VKeys as Keys.
    4) Modified the hosts for the same domain (adding back one of the hosts)
       and verified that the change took effect.
    5) Redeployed this change to crash, again updated the nameservers to add
       another host.
    6) Again restored the old version, verified that the new hosts were
       visible.

* Changes in response to review

* Convert to a single VKeyTranslatorFactory instance

* Moved vkey field rename to V25
2020-05-19 14:10:28 -04:00
Lai Jiang 5fe929b027 Log InternalServerErrorException at SEVERE (#585)
Normal HttpException logs at INFO because they usual do not indicate
anything out of the ordinary and is meant to convey to the client that
there is some expected error. However InternalServerErrorException is
something that we do care about being alerted for so we log it at SEVERE.
2020-05-18 22:55:13 -04:00
Lai Jiang fb335b7d89 Upgrade to Gradle 6.4.1 (#590) 2020-05-18 16:47:02 -04:00
Shicong Huang a0f4013d53 Add JUnit5 extension to run test twice against different databases (#588)
* Add JUnit5 extension to run test against different databases

* Fix typos

* Add some explanation
2020-05-18 11:06:21 -04:00
Lai Jiang 5e596bb389 Upgrade to Gradle 6.4 (#589) 2020-05-14 14:57:24 -04:00
Lai Jiang f62fd82803 Log information about SSL connection from the client (#586) 2020-05-14 09:38:33 -04:00
sarahcaseybot b7353ef338 Add TimedTransitionProperty Converters (#561)
* Add TimedTldStateTransitionMapConverter

* Move timedTransitions to a base class and add BillingCostTransitionConverter

* Add test of TimedTransitionPropertyConverterBase

* clean up tests

* Switch tests to JUnit 5

* Make JpaUnitTestRule an extension
2020-05-12 11:46:19 -04:00
Michael Muller 832e1ce047 Implement all DatastoreTransactionManager methods (#581)
* Implement all DatastoreTransactionManager methods

In the course of this:

- Make assertDelete() specific to JpaTransactionManager, remove the return
  value from delete()
- Converter "in transaction" assertion to IllegalStateException, which is less
  JPA specific.

* Upgraded DatastoreTransactionManagerTest to junit5
2020-05-11 17:17:57 -04:00
0xflotus 8087f5bbca (docs): fixed small errors (#572)
* Update first-steps-tutorial.md

* Update proxy-setup.md (#1)

* Update registrar-faq.md (#2)

* Update first-steps-tutorial.md
2020-05-11 10:01:47 -04:00
Ben McIlwain 7f3dbfb62f Reflect refunded billing events on deletion in expiration time (#579)
* Reflect refunded billing events on deletion in expiration time

This doesn't make any change at the time of the domain deletion itself, but it
will matter if the domain is then undeleted, because we need to know what
expiration date to restore, and if there were any renew or autorenew charges
that were refunded by the deletion because they were in a grace period, they
shouldn't be coming back during the restore.

* Add tests for new expiration date behavior

* Add handling of add/renew grace period overlap
2020-05-08 21:51:20 -04:00
144 changed files with 3219 additions and 874 deletions
+23
View File
@@ -197,6 +197,10 @@ task runPresubmits(type: Exec) {
args('config/presubmits.py')
}
def javadocSource = []
def javadocClasspath = []
def javadocDependentTasks = []
subprojects {
// Skip no-op project
if (project.name == 'services') return
@@ -317,6 +321,10 @@ subprojects {
}
}
}
javadocSource << project.sourceSets.main.allJava
javadocClasspath << project.sourceSets.main.compileClasspath
javadocDependentTasks << project.tasks.compileJava
}
// If "-P verboseTestOutput=true" is passed in, configure all subprojects to dump all of their
@@ -445,3 +453,18 @@ task javaIncrementalFormatApply {
}
tasks.build.dependsOn(tasks.javaIncrementalFormatCheck)
task javadoc(type: Javadoc) {
source javadocSource
classpath = files(javadocClasspath)
destinationDir = file("${buildDir}/docs/javadoc")
// In a lot of places we don't write @return so suppress warnings about that.
options.addBooleanOption('Xdoclint:all,-missing', true)
options.addBooleanOption("-allow-script-in-comments",true)
options.tags = ["type:a:Generic Type",
"error:a:Expected Error"]
}
tasks.build.dependsOn(tasks.javadoc)
javadocDependentTasks.each { tasks.javadoc.shouldRunAfter(it) }
@@ -21,7 +21,6 @@ import org.joda.time.ReadableDuration;
* An object which accepts requests to put the current thread to sleep.
*
* @see SystemSleeper
* @see google.registry.testing.FakeSleeper
*/
@ThreadSafe
public interface Sleeper {
+3
View File
@@ -880,6 +880,9 @@ task standardTest(type: FilteringTest) {
// forkEvery 1
// Sets the maximum number of test executors that may exist at the same time.
// Also, Gradle executes tests in 1 thread and some of our test infrastructures
// depend on that, e.g. DualDatabaseTestInvocationContextProvider injects
// different implementation of TransactionManager into TransactionManagerFactory.
maxParallelForks 5
systemProperty 'test.projectRoot', rootProject.projectRootDir
@@ -155,89 +155,100 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
}
int numBillingEventsSaved = 0;
try {
numBillingEventsSaved = tm().transactNew(() -> {
ImmutableSet.Builder<OneTime> syntheticOneTimesBuilder =
new ImmutableSet.Builder<>();
final Registry tld = Registry.get(getTldFromDomainName(recurring.getTargetId()));
numBillingEventsSaved =
tm().transactNew(
() -> {
ImmutableSet.Builder<OneTime> syntheticOneTimesBuilder =
new ImmutableSet.Builder<>();
final Registry tld =
Registry.get(getTldFromDomainName(recurring.getTargetId()));
// Determine the complete set of times at which this recurring event should occur
// (up to and including the runtime of the mapreduce).
Iterable<DateTime> eventTimes =
recurring.getRecurrenceTimeOfYear().getInstancesInRange(Range.closed(
recurring.getEventTime(),
earliestOf(recurring.getRecurrenceEndTime(), executeTime)));
// Determine the complete set of times at which this recurring event should
// occur (up to and including the runtime of the mapreduce).
Iterable<DateTime> eventTimes =
recurring
.getRecurrenceTimeOfYear()
.getInstancesInRange(
Range.closed(
recurring.getEventTime(),
earliestOf(recurring.getRecurrenceEndTime(), executeTime)));
// Convert these event times to billing times
final ImmutableSet<DateTime> billingTimes =
getBillingTimesInScope(eventTimes, cursorTime, executeTime, tld);
// Convert these event times to billing times
final ImmutableSet<DateTime> billingTimes =
getBillingTimesInScope(eventTimes, cursorTime, executeTime, tld);
Key<? extends EppResource> domainKey = recurring.getParentKey().getParent();
Iterable<OneTime> oneTimesForDomain =
ofy().load().type(OneTime.class).ancestor(domainKey);
Key<? extends EppResource> domainKey = recurring.getParentKey().getParent();
Iterable<OneTime> oneTimesForDomain =
ofy().load().type(OneTime.class).ancestor(domainKey);
// Determine the billing times that already have OneTime events persisted.
ImmutableSet<DateTime> existingBillingTimes =
getExistingBillingTimes(oneTimesForDomain, recurring);
// Determine the billing times that already have OneTime events persisted.
ImmutableSet<DateTime> existingBillingTimes =
getExistingBillingTimes(oneTimesForDomain, recurring);
ImmutableSet.Builder<HistoryEntry> historyEntriesBuilder =
new ImmutableSet.Builder<>();
// Create synthetic OneTime events for all billing times that do not yet have an event
// persisted.
for (DateTime billingTime : difference(billingTimes, existingBillingTimes)) {
// Construct a new HistoryEntry that parents over the OneTime
HistoryEntry historyEntry = new HistoryEntry.Builder()
.setBySuperuser(false)
.setClientId(recurring.getClientId())
.setModificationTime(tm().getTransactionTime())
.setParent(domainKey)
.setPeriod(Period.create(1, YEARS))
.setReason("Domain autorenewal by ExpandRecurringBillingEventsAction")
.setRequestedByRegistrar(false)
.setType(DOMAIN_AUTORENEW)
// Don't write a domain transaction record if the recurrence was ended prior to the
// billing time (i.e. a domain was deleted during the autorenew grace period).
.setDomainTransactionRecords(
recurring.getRecurrenceEndTime().isBefore(billingTime)
? ImmutableSet.of()
: ImmutableSet.of(
DomainTransactionRecord.create(
tld.getTldStr(),
// We report this when the autorenew grace period ends
billingTime,
TransactionReportField.netRenewsFieldFromYears(1),
1)))
.build();
historyEntriesBuilder.add(historyEntry);
ImmutableSet.Builder<HistoryEntry> historyEntriesBuilder =
new ImmutableSet.Builder<>();
// Create synthetic OneTime events for all billing times that do not yet have
// an event persisted.
for (DateTime billingTime : difference(billingTimes, existingBillingTimes)) {
// Construct a new HistoryEntry that parents over the OneTime
HistoryEntry historyEntry =
new HistoryEntry.Builder()
.setBySuperuser(false)
.setClientId(recurring.getClientId())
.setModificationTime(tm().getTransactionTime())
.setParent(domainKey)
.setPeriod(Period.create(1, YEARS))
.setReason(
"Domain autorenewal by ExpandRecurringBillingEventsAction")
.setRequestedByRegistrar(false)
.setType(DOMAIN_AUTORENEW)
// Don't write a domain transaction record if the recurrence was
// ended prior to the billing time (i.e. a domain was deleted
// during the autorenew grace period).
.setDomainTransactionRecords(
recurring.getRecurrenceEndTime().isBefore(billingTime)
? ImmutableSet.of()
: ImmutableSet.of(
DomainTransactionRecord.create(
tld.getTldStr(),
// We report this when the autorenew grace period
// ends
billingTime,
TransactionReportField.netRenewsFieldFromYears(1),
1)))
.build();
historyEntriesBuilder.add(historyEntry);
DateTime eventTime = billingTime.minus(tld.getAutoRenewGracePeriodLength());
// Determine the cost for a one-year renewal.
Money renewCost = getDomainRenewCost(recurring.getTargetId(), eventTime, 1);
syntheticOneTimesBuilder.add(new OneTime.Builder()
.setBillingTime(billingTime)
.setClientId(recurring.getClientId())
.setCost(renewCost)
.setEventTime(eventTime)
.setFlags(union(recurring.getFlags(), Flag.SYNTHETIC))
.setParent(historyEntry)
.setPeriodYears(1)
.setReason(recurring.getReason())
.setSyntheticCreationTime(executeTime)
.setCancellationMatchingBillingEvent(Key.create(recurring))
.setTargetId(recurring.getTargetId())
.build());
}
Set<HistoryEntry> historyEntries = historyEntriesBuilder.build();
Set<OneTime> syntheticOneTimes = syntheticOneTimesBuilder.build();
if (!isDryRun) {
ImmutableSet<ImmutableObject> entitiesToSave =
new ImmutableSet.Builder<ImmutableObject>()
.addAll(historyEntries)
.addAll(syntheticOneTimes)
.build();
ofy().save().entities(entitiesToSave).now();
}
return syntheticOneTimes.size();
});
DateTime eventTime = billingTime.minus(tld.getAutoRenewGracePeriodLength());
// Determine the cost for a one-year renewal.
Money renewCost = getDomainRenewCost(recurring.getTargetId(), eventTime, 1);
syntheticOneTimesBuilder.add(
new OneTime.Builder()
.setBillingTime(billingTime)
.setClientId(recurring.getClientId())
.setCost(renewCost)
.setEventTime(eventTime)
.setFlags(union(recurring.getFlags(), Flag.SYNTHETIC))
.setParent(historyEntry)
.setPeriodYears(1)
.setReason(recurring.getReason())
.setSyntheticCreationTime(executeTime)
.setCancellationMatchingBillingEvent(recurring.createVKey())
.setTargetId(recurring.getTargetId())
.build());
}
Set<HistoryEntry> historyEntries = historyEntriesBuilder.build();
Set<OneTime> syntheticOneTimes = syntheticOneTimesBuilder.build();
if (!isDryRun) {
ImmutableSet<ImmutableObject> entitiesToSave =
new ImmutableSet.Builder<ImmutableObject>()
.addAll(historyEntries)
.addAll(syntheticOneTimes)
.build();
ofy().save().entities(entitiesToSave).now();
}
return syntheticOneTimes.size();
});
} catch (Throwable t) {
getContext().incrementCounter("error: " + t.getClass().getSimpleName());
getContext().incrementCounter(ERROR_COUNTER);
@@ -279,7 +290,8 @@ public class ExpandRecurringBillingEventsAction implements Runnable {
return Streams.stream(oneTimesForDomain)
.filter(
billingEvent ->
Key.create(recurringEvent)
recurringEvent
.createVKey()
.equals(billingEvent.getCancellationMatchingBillingEvent()))
.map(OneTime::getBillingTime)
.collect(toImmutableSet());
@@ -442,10 +442,8 @@ public class BigqueryConnection implements AutoCloseable {
* Returns the result of calling queryToLocalTable, but synchronously to avoid spawning new
* background threads, which App Engine doesn't support.
*
* @see <a href="https://cloud.google.com/appengine/docs/standard/java/runtime#Threads">
* App Engine Runtime</a>
*
* <p>Returns the results of the query in an ImmutableTable on success.
* @see <a href="https://cloud.google.com/appengine/docs/standard/java/runtime#Threads">App Engine
* Runtime</a>
*/
public ImmutableTable<Integer, TableFieldSchema, Object> queryToLocalTableSync(String querySql) {
Job job = new Job()
@@ -576,8 +574,6 @@ public class BigqueryConnection implements AutoCloseable {
/**
* Launch a job, wait for it to complete, but <i>do not</i> check for errors.
*
* @throws BigqueryJobFailureException
*/
public Job runJob(Job job, @Nullable AbstractInputStreamContent data) {
return checkJob(waitForJob(launchJob(job, data)));
@@ -84,7 +84,7 @@ public class DnsMessageTransport {
* @param query a message to send
* @return the response received from the server
* @throws IOException if the Socket input/output streams throws one
* @throws IllegalArgumentException if the query is too large to be sent (> 65535 bytes)
* @throws IllegalArgumentException if the query is too large to be sent (&gt; 65535 bytes)
*/
public Message send(Message query) throws IOException {
try (Socket socket = factory.createSocket(InetAddress.getByName(updateHost), DNS_PORT)) {
@@ -73,8 +73,10 @@ public class BigqueryPollJobAction implements Runnable {
@Override
public void run() {
checkJobOutcome(); // Throws a NotModifiedException if the job hasn't completed.
if (payload == null || payload.length == 0) {
boolean jobOutcome =
checkJobOutcome(); // Throws a NotModifiedException if the job hasn't completed.
// If the job failed, do not enqueue the next step.
if (!jobOutcome || payload == null || payload.length == 0) {
return;
}
// If there is a payload, it's a chained task, so enqueue it.
@@ -42,8 +42,8 @@ import javax.xml.stream.events.XMLEvent;
/**
* Sanitizes sensitive data in incoming/outgoing EPP XML messages.
*
* <p>Current implementation masks user credentials (text following <pw> and <newPW> tags) as
* follows:
* <p>Current implementation masks user credentials (text following &lt;pw&gt; and &lt;newPW&gt;
* tags) as follows:
*
* <ul>
* <li>A control character (in ranges [0 - 1F] and [7F - 9F]) is replaced with 'C'.
@@ -75,7 +75,7 @@ public class DomainCheckFlowCustomLogic extends BaseFlowCustomLogic {
/**
* The time to perform the domain check as of. This defaults to the current time, but can be
* overridden in v>=0.12 of the fee extension.
* overridden in v&gt;=0.12 of the fee extension.
*/
public abstract DateTime asOfDate();
@@ -105,7 +105,7 @@ public class DomainCheckFlowCustomLogic extends BaseFlowCustomLogic {
/**
* The time to perform the domain check as of. This defaults to the current time, but can be
* overridden in v>=0.12 of the fee extension.
* overridden in v&gt;=0.12 of the fee extension.
*/
public abstract DateTime asOfDate();
@@ -534,7 +534,7 @@ public class DomainCreateFlow implements TransactionalFlow {
.setPeriodYears(years)
.setCost(feesAndCredits.getCreateCost())
.setEventTime(now)
.setAllocationToken(allocationToken.map(Key::create).orElse(null))
.setAllocationToken(allocationToken.map(AllocationToken::createVKey).orElse(null))
.setBillingTime(
now.plus(
isAnchorTenant
@@ -212,6 +212,28 @@ public final class DomainDeleteFlow implements TransactionalFlow {
builder.setDeletePollMessage(Key.create(deletePollMessage));
}
// Cancel any grace periods that were still active, and set the expiration time accordingly.
DateTime newExpirationTime = existingDomain.getRegistrationExpirationTime();
for (GracePeriod gracePeriod : existingDomain.getGracePeriods()) {
// No cancellation is written if the grace period was not for a billable event.
if (gracePeriod.hasBillingEvent()) {
entitiesToSave.add(
BillingEvent.Cancellation.forGracePeriod(gracePeriod, historyEntry, targetId));
if (gracePeriod.getOneTimeBillingEvent() != null) {
// Take the amount of amount of registration time being refunded off the expiration time.
// This can be either add grace periods or renew grace periods.
BillingEvent.OneTime oneTime =
ofy().load().key(gracePeriod.getOneTimeBillingEvent()).now();
newExpirationTime = newExpirationTime.minusYears(oneTime.getPeriodYears());
} else if (gracePeriod.getRecurringBillingEvent() != null) {
// Take 1 year off the registration if in the autorenew grace period (no need to load the
// recurring billing event; all autorenews are for 1 year).
newExpirationTime = newExpirationTime.minusYears(1);
}
}
}
builder.setRegistrationExpirationTime(newExpirationTime);
DomainBase newDomain = builder.build();
updateForeignKeyIndexDeletionTime(newDomain);
handlePendingTransferOnDelete(existingDomain, newDomain, now, historyEntry);
@@ -221,14 +243,7 @@ public final class DomainDeleteFlow implements TransactionalFlow {
// event and poll message will already have been deleted in
// ResourceDeleteFlow since it's listed in serverApproveEntities.
dnsQueue.addDomainRefreshTask(existingDomain.getFullyQualifiedDomainName());
// Cancel any grace periods that were still active.
for (GracePeriod gracePeriod : existingDomain.getGracePeriods()) {
// No cancellation is written if the grace period was not for a billable event.
if (gracePeriod.hasBillingEvent()) {
entitiesToSave.add(
BillingEvent.Cancellation.forGracePeriod(gracePeriod, historyEntry, targetId));
}
}
entitiesToSave.add(newDomain, historyEntry);
EntityChanges entityChanges = flowCustomLogic.beforeSave(
BeforeSaveParameters.newBuilder()
@@ -34,6 +34,7 @@ import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.Parent;
import google.registry.persistence.VKey;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
@@ -139,10 +140,13 @@ public class ModelUtils {
// If the field's type is the same as the field's class object, then it's a non-parameterized
// type, and thus we just add it directly. We also don't bother looking at the parameterized
// types of Key objects, since they are just references to other objects and don't actually
// embed themselves in the persisted object anyway.
// types of Key and VKey objects, since they are just references to other objects and don't
// actually embed themselves in the persisted object anyway.
Class<?> fieldClazz = field.getType();
Type fieldType = field.getGenericType();
if (VKey.class.equals(fieldClazz)) {
continue;
}
builder.add(fieldClazz);
if (fieldType.equals(fieldClazz) || Key.class.equals(clazz)) {
continue;
@@ -50,7 +50,8 @@ import java.util.stream.Collectors;
public class OteStats {
/**
* Returns the statistics about the OT&E actions that have been taken by a particular registrar.
* Returns the statistics about the OT&amp;E actions that have been taken by a particular
* registrar.
*/
public static OteStats getFromRegistrar(String registrarName) {
return new OteStats().recordRegistrarHistory(registrarName);
@@ -30,7 +30,7 @@ public class UpdateAutoTimestamp extends ImmutableObject {
DateTime timestamp;
/** Returns the timestamp, or {@link START_OF_TIME} if it's null. */
/** Returns the timestamp, or {@code START_OF_TIME} if it's null. */
public DateTime getTimestamp() {
return Optional.ofNullable(timestamp).orElse(START_OF_TIME);
}
@@ -30,6 +30,7 @@ import com.google.common.collect.Sets;
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;
@@ -43,14 +44,28 @@ import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData.TransferServerApproveEntity;
import google.registry.persistence.VKey;
import google.registry.persistence.WithLongVKey;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** A billable event in a domain's lifecycle. */
@MappedSuperclass
@WithLongVKey
public abstract class BillingEvent extends ImmutableObject
implements Buildable, TransferServerApproveEntity {
@@ -93,24 +108,41 @@ public abstract class BillingEvent extends ImmutableObject
/** Entity id. */
@Id
long id;
@javax.persistence.Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Parent
@DoNotHydrate
Key<HistoryEntry> parent;
@Parent @DoNotHydrate @Transient Key<HistoryEntry> parent;
/** The registrar to bill. */
@Index
@Column(nullable = false)
String clientId;
/** Revision id of the entry in DomainHistory table that ths bill belongs to. */
// TODO(shicong): Add foreign key constraint when DomainHistory table is generated
@Ignore
@Column(nullable = false)
Long domainHistoryRevisionId;
/** ID of the EPP resource that the bill is for. */
// TODO(shicong): Add foreign key constraint when we expand DatastoreHelp for Postgresql
@Ignore
@Column(nullable = false)
String domainRepoId;
/** When this event was created. For recurring events, this is also the recurrence start time. */
@Index
@Column(nullable = false)
DateTime eventTime;
/** The reason for the bill. */
@Enumerated(EnumType.STRING)
@Column(nullable = false)
Reason reason;
/** The fully qualified domain name of the domain that the bill is for. */
@Column(name = "domain_name", nullable = false)
String targetId;
@Nullable
@@ -120,6 +152,14 @@ public abstract class BillingEvent extends ImmutableObject
return clientId;
}
public long getDomainHistoryRevisionId() {
return domainHistoryRevisionId;
}
public String getDomainRepoId() {
return domainRepoId;
}
public DateTime getEventTime() {
return eventTime;
}
@@ -163,7 +203,7 @@ public abstract class BillingEvent extends ImmutableObject
return thisCastToDerived();
}
public B setId(Long id) {
public B setId(long id) {
getInstance().id = id;
return thisCastToDerived();
}
@@ -173,6 +213,16 @@ public abstract class BillingEvent extends ImmutableObject
return thisCastToDerived();
}
public B setDomainHistoryRevisionId(long domainHistoryRevisionId) {
getInstance().domainHistoryRevisionId = domainHistoryRevisionId;
return thisCastToDerived();
}
public B setDomainRepoId(String domainRepoId) {
getInstance().domainRepoId = domainRepoId;
return thisCastToDerived();
}
public B setEventTime(DateTime eventTime) {
getInstance().eventTime = eventTime;
return thisCastToDerived();
@@ -194,6 +244,7 @@ public abstract class BillingEvent extends ImmutableObject
}
public B setParent(Key<HistoryEntry> parentKey) {
// TODO(shicong): Figure out how to set domainHistoryRevisionId and domainRepoId
getInstance().parent = parentKey;
return thisCastToDerived();
}
@@ -213,9 +264,23 @@ public abstract class BillingEvent extends ImmutableObject
/** A one-time billable event. */
@ReportedOn
@Entity
@javax.persistence.Entity(name = "BillingEvent")
@javax.persistence.Table(
indexes = {
@javax.persistence.Index(columnList = "clientId"),
@javax.persistence.Index(columnList = "eventTime"),
@javax.persistence.Index(columnList = "billingTime"),
@javax.persistence.Index(columnList = "syntheticCreationTime"),
@javax.persistence.Index(columnList = "allocation_token_id")
})
@AttributeOverride(name = "id", column = @Column(name = "billing_event_id"))
public static class OneTime extends BillingEvent {
/** The billable value. */
@AttributeOverrides({
@AttributeOverride(name = "money.amount", column = @Column(name = "cost_amount")),
@AttributeOverride(name = "money.currency", column = @Column(name = "cost_currency"))
})
Money cost;
/** When the cost should be billed. */
@@ -223,8 +288,8 @@ public abstract class BillingEvent extends ImmutableObject
DateTime billingTime;
/**
* The period in years of the action being billed for, if applicable, otherwise null.
* Used for financial reporting.
* The period in years of the action being billed for, if applicable, otherwise null. Used for
* financial reporting.
*/
@IgnoreSave(IfNull.class)
Integer periodYears = null;
@@ -240,15 +305,21 @@ public abstract class BillingEvent extends ImmutableObject
/**
* For {@link Flag#SYNTHETIC} events, a {@link Key} to the {@link BillingEvent} from which this
* OneTime was created. This is needed in order to properly match billing events against
* {@link Cancellation}s.
* OneTime was created. This is needed in order to properly match billing events against {@link
* Cancellation}s.
*/
Key<? extends BillingEvent> cancellationMatchingBillingEvent;
@Column(name = "cancellation_matching_billing_recurrence_id")
VKey<? extends BillingEvent> cancellationMatchingBillingEvent;
/**
* The {@link AllocationToken} used in the creation of this event, or null if one was not used.
*
* <p>TODO(shicong): Add foreign key constraint when AllocationToken schema is generated
*/
@Index @Nullable Key<AllocationToken> allocationToken;
@Column(name = "allocation_token_id")
@Index
@Nullable
VKey<AllocationToken> allocationToken;
public Money getCost() {
return cost;
@@ -266,14 +337,18 @@ public abstract class BillingEvent extends ImmutableObject
return syntheticCreationTime;
}
public Key<? extends BillingEvent> getCancellationMatchingBillingEvent() {
public VKey<? extends BillingEvent> getCancellationMatchingBillingEvent() {
return cancellationMatchingBillingEvent;
}
public Optional<Key<AllocationToken>> getAllocationToken() {
public Optional<VKey<AllocationToken>> getAllocationToken() {
return Optional.ofNullable(allocationToken);
}
public VKey<OneTime> createVKey() {
return VKey.createOfy(getClass(), Key.create(this));
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
@@ -311,12 +386,12 @@ public abstract class BillingEvent extends ImmutableObject
}
public Builder setCancellationMatchingBillingEvent(
Key<? extends BillingEvent> cancellationMatchingBillingEvent) {
VKey<? extends BillingEvent> cancellationMatchingBillingEvent) {
getInstance().cancellationMatchingBillingEvent = cancellationMatchingBillingEvent;
return this;
}
public Builder setAllocationToken(@Nullable Key<AllocationToken> allocationToken) {
public Builder setAllocationToken(@Nullable VKey<AllocationToken> allocationToken) {
getInstance().allocationToken = allocationToken;
return this;
}
@@ -361,6 +436,15 @@ public abstract class BillingEvent extends ImmutableObject
*/
@ReportedOn
@Entity
@javax.persistence.Entity(name = "BillingRecurrence")
@javax.persistence.Table(
indexes = {
@javax.persistence.Index(columnList = "clientId"),
@javax.persistence.Index(columnList = "eventTime"),
@javax.persistence.Index(columnList = "recurrenceEndTime"),
@javax.persistence.Index(columnList = "recurrence_time_of_year")
})
@AttributeOverride(name = "id", column = @Column(name = "billing_recurrence_id"))
public static class Recurring extends BillingEvent {
/**
@@ -384,6 +468,10 @@ public abstract class BillingEvent extends ImmutableObject
* model, whereas the billing time is a fixed {@link org.joda.time.Duration} later.
*/
@Index
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "timeString", column = @Column(name = "recurrence_time_of_year"))
})
TimeOfYear recurrenceTimeOfYear;
public DateTime getRecurrenceEndTime() {
@@ -394,6 +482,10 @@ public abstract class BillingEvent extends ImmutableObject
return recurrenceTimeOfYear;
}
public VKey<Recurring> createVKey() {
return VKey.createOfy(getClass(), Key.create(this));
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
@@ -434,6 +526,14 @@ public abstract class BillingEvent extends ImmutableObject
*/
@ReportedOn
@Entity
@javax.persistence.Entity(name = "BillingCancellation")
@javax.persistence.Table(
indexes = {
@javax.persistence.Index(columnList = "clientId"),
@javax.persistence.Index(columnList = "eventTime"),
@javax.persistence.Index(columnList = "billingTime")
})
@AttributeOverride(name = "id", column = @Column(name = "billing_cancellation_id"))
public static class Cancellation extends BillingEvent {
/** The billing time of the charge that is being cancelled. */
@@ -446,7 +546,8 @@ public abstract class BillingEvent extends ImmutableObject
* <p>Although the type is {@link Key} the name "ref" is preserved for historical reasons.
*/
@IgnoreSave(IfNull.class)
Key<BillingEvent.OneTime> refOneTime = null;
@Column(name = "billing_event_id")
VKey<BillingEvent.OneTime> refOneTime = null;
/**
* The recurring billing event to cancel, or null for non-autorenew cancellations.
@@ -454,13 +555,14 @@ public abstract class BillingEvent extends ImmutableObject
* <p>Although the type is {@link Key} the name "ref" is preserved for historical reasons.
*/
@IgnoreSave(IfNull.class)
Key<BillingEvent.Recurring> refRecurring = null;
@Column(name = "billing_recurrence_id")
VKey<BillingEvent.Recurring> refRecurring = null;
public DateTime getBillingTime() {
return billingTime;
}
public Key<? extends BillingEvent> getEventKey() {
public VKey<? extends BillingEvent> getEventKey() {
return firstNonNull(refOneTime, refRecurring);
}
@@ -492,13 +594,19 @@ public abstract class BillingEvent extends ImmutableObject
.setParent(historyEntry);
// Set the grace period's billing event using the appropriate Cancellation builder method.
if (gracePeriod.getOneTimeBillingEvent() != null) {
builder.setOneTimeEventKey(gracePeriod.getOneTimeBillingEvent());
builder.setOneTimeEventKey(
VKey.createOfy(BillingEvent.OneTime.class, gracePeriod.getOneTimeBillingEvent()));
} else if (gracePeriod.getRecurringBillingEvent() != null) {
builder.setRecurringEventKey(gracePeriod.getRecurringBillingEvent());
builder.setRecurringEventKey(
VKey.createOfy(BillingEvent.Recurring.class, gracePeriod.getRecurringBillingEvent()));
}
return builder.build();
}
public VKey<Cancellation> createVKey() {
return VKey.createOfy(getClass(), Key.create(this));
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
@@ -518,12 +626,12 @@ public abstract class BillingEvent extends ImmutableObject
return this;
}
public Builder setOneTimeEventKey(Key<BillingEvent.OneTime> eventKey) {
public Builder setOneTimeEventKey(VKey<BillingEvent.OneTime> eventKey) {
getInstance().refOneTime = eventKey;
return this;
}
public Builder setRecurringEventKey(Key<BillingEvent.Recurring> eventKey) {
public Builder setRecurringEventKey(VKey<BillingEvent.Recurring> eventKey) {
getInstance().refRecurring = eventKey;
return this;
}
@@ -540,9 +648,7 @@ public abstract class BillingEvent extends ImmutableObject
}
}
/**
* An event representing a modification of an existing one-time billing event.
*/
/** An event representing a modification of an existing one-time billing event. */
@ReportedOn
@Entity
public static class Modification extends BillingEvent {
@@ -29,20 +29,22 @@ import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Index;
import google.registry.model.ImmutableObject;
import java.util.List;
import javax.persistence.Embeddable;
import org.joda.time.DateTime;
/**
* A time of year (month, day, millis of day) that can be stored in a sort-friendly format.
*
* <p>This is conceptually similar to {@code MonthDay} in Joda or more generally to Joda's
* {@code Partial}, but the parts we need are too simple to justify a full implementation of
* {@code Partial}.
* <p>This is conceptually similar to {@code MonthDay} in Joda or more generally to Joda's {@code
* Partial}, but the parts we need are too simple to justify a full implementation of {@code
* Partial}.
*
* <p>For simplicity, the native representation of this class's data is its stored format. This
* 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 {
/**
@@ -37,7 +37,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/** A collection of {@link ContactResource} commands. */
public class ContactCommand {
/** The fields on "chgType" from {@link "http://tools.ietf.org/html/rfc5733"}. */
/** 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> {
@@ -111,8 +111,8 @@ public class ContactCommand {
}
/**
* A create command for a {@link ContactResource}, mapping "createType" from
* {@link "http://tools.ietf.org/html/rfc5733"}.
* A create command for a {@link ContactResource}, mapping "createType" from <a
* href="http://tools.ietf.org/html/rfc5733">RFC5733</a>}.
*/
@XmlType(propOrder = {"contactId", "postalInfo", "voice", "fax", "email", "authInfo", "disclose"})
@XmlRootElement
@@ -27,7 +27,7 @@ import javax.persistence.Embedded;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlType;
/** The "discloseType" from {@link "http://tools.ietf.org/html/rfc5733"}. */
/** The "discloseType" from <a href="http://tools.ietf.org/html/rfc5733">RFC5733</a>. */
@Embed
@Embeddable
@XmlType(propOrder = {"name", "org", "addr", "voice", "fax", "email"})
@@ -76,7 +76,7 @@ public class Disclose extends ImmutableObject {
return flag;
}
/** The "intLocType" from {@link "http://tools.ietf.org/html/rfc5733"}. */
/** The "intLocType" from <a href="http://tools.ietf.org/html/rfc5733">RFC5733</a>. */
@Embed
public static class PostalInfoChoice extends ImmutableObject {
@XmlAttribute
@@ -32,8 +32,8 @@ import javax.xml.bind.annotation.adapters.NormalizedStringAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/**
* Implementation of both "postalInfoType" and "chgPostalInfoType" from {@link
* "http://tools.ietf.org/html/rfc5733"}.
* Implementation of both "postalInfoType" and "chgPostalInfoType" from <a href=
* "http://tools.ietf.org/html/rfc5733">RFC5733</a>.
*/
@Embed
@Embeddable
@@ -136,18 +136,11 @@ public class DomainBase extends EppResource
@Index
String tld;
/**
* References to hosts that are the nameservers for the domain.
*
* <p>This is a legacy field: we have to preserve it because it is still persisted and indexed in
* the datastore, but all external references go through nsHostVKeys.
*/
@Index @ElementCollection @Transient Set<Key<HostResource>> nsHosts;
@Ignore
/** References to hosts that are the nameservers for the domain. */
@Index
@ElementCollection
@JoinTable(name = "DomainHost")
Set<VKey<HostResource>> nsHostVKeys;
Set<VKey<HostResource>> nsHosts;
/**
* The union of the contacts visible via {@link #getContacts} and {@link #getRegistrant}.
@@ -269,11 +262,6 @@ public class DomainBase extends EppResource
@OnLoad
void load() {
nsHostVKeys =
nullToEmptyImmutableCopy(nsHosts).stream()
.map(hostKey -> VKey.createOfy(HostResource.class, hostKey))
.collect(toImmutableSet());
// Reconstitute all of the contacts so that they have VKeys.
allContacts =
allContacts.stream().map(contact -> contact.reconstitute()).collect(toImmutableSet());
@@ -363,9 +351,7 @@ public class DomainBase extends EppResource
}
public ImmutableSet<VKey<HostResource>> getNameservers() {
// Since nsHostVKeys gets initialized both from setNameservers() and the OnLoad method, this
// should always be valid.
return nullToEmptyImmutableCopy(nsHostVKeys);
return nullToEmptyImmutableCopy(nsHosts);
}
public final String getCurrentSponsorClientId() {
@@ -645,14 +631,6 @@ public class DomainBase extends EppResource
Builder(DomainBase instance) {
super(instance);
// Convert nsHosts to nsHostVKeys.
if (instance.nsHosts != null) {
instance.nsHostVKeys =
instance.nsHosts.stream()
.map(key -> VKey.createOfy(HostResource.class, key))
.collect(toImmutableSet());
}
}
@Override
@@ -710,27 +688,12 @@ public class DomainBase extends EppResource
}
public Builder setNameservers(VKey<HostResource> nameserver) {
Optional<Key<HostResource>> nsKey = nameserver.maybeGetOfyKey();
if (nsKey.isPresent()) {
getInstance().nsHosts = ImmutableSet.of(nsKey.get());
} else {
getInstance().nsHosts = null;
}
getInstance().nsHostVKeys = ImmutableSet.of(nameserver);
getInstance().nsHosts = ImmutableSet.of(nameserver);
return thisCastToDerived();
}
public Builder setNameservers(ImmutableSet<VKey<HostResource>> nameservers) {
// If we have all of the ofy keys, we can set nsHosts. Otherwise, make it null.
if (nameservers != null
&& nameservers.stream().allMatch(key -> key.maybeGetOfyKey().isPresent())) {
getInstance().nsHosts =
nameservers.stream().map(key -> key.getOfyKey()).collect(toImmutableSet());
} else {
getInstance().nsHosts = null;
}
getInstance().nsHostVKeys = forceEmptyToNull(nameservers);
getInstance().nsHosts = forceEmptyToNull(nameservers);
return thisCastToDerived();
}
@@ -72,10 +72,10 @@ public class DomainCommand {
T cloneAndLinkReferences(DateTime now) throws InvalidReferencesException;
}
/** The fields on "chgType" from {@link "http://tools.ietf.org/html/rfc5731"}. */
/** The fields on "chgType" from <a href="http://tools.ietf.org/html/rfc5731">RFC5731</a>. */
@XmlTransient
public static class DomainCreateOrChange<B extends DomainBase.Builder>
extends ImmutableObject implements ResourceCreateOrChange<B> {
public static class DomainCreateOrChange<B extends DomainBase.Builder> extends ImmutableObject
implements ResourceCreateOrChange<B> {
/** The contactId of the registrant who registered this domain. */
@XmlElement(name = "registrant")
@@ -103,19 +103,20 @@ public class DomainCommand {
}
/**
* A create command for a {@link DomainBase}, mapping "createType" from
* {@link "http://tools.ietf.org/html/rfc5731"}.
* A create command for a {@link DomainBase}, mapping "createType" from <a
* href="http://tools.ietf.org/html/rfc5731">RFC5731</a>.
*/
@XmlRootElement
@XmlType(propOrder = {
"fullyQualifiedDomainName",
"period",
"nameserverFullyQualifiedHostNames",
"registrantContactId",
"foreignKeyedDesignatedContacts",
"authInfo"})
public static class Create
extends DomainCreateOrChange<DomainBase.Builder>
@XmlType(
propOrder = {
"fullyQualifiedDomainName",
"period",
"nameserverFullyQualifiedHostNames",
"registrantContactId",
"foreignKeyedDesignatedContacts",
"authInfo"
})
public static class Create extends DomainCreateOrChange<DomainBase.Builder>
implements CreateOrUpdate<Create> {
/** Fully qualified domain name, which serves as a unique identifier for this domain. */
@@ -20,7 +20,7 @@ import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlEnumValue;
import javax.xml.bind.annotation.XmlValue;
/** The "periodType" from {@link "http://tools.ietf.org/html/rfc5731"}. */
/** The "periodType" from <a href="http://tools.ietf.org/html/rfc5731">RFC5731</a>. */
@Embed
@javax.persistence.Embeddable
public class Period extends ImmutableObject {
@@ -29,11 +29,13 @@ import org.joda.time.DateTime;
* An individual price check item in version 0.12 of the fee extension on domain check commands.
* Items look like:
*
* <pre>{@code
* <fee:command name="renew" phase="sunrise" subphase="hello">
* <fee:period unit="y">1</fee:period>
* <fee:class>premium</fee:class>
* <fee:date>2017-05-17T13:22:21.0Z</fee:date>
* </fee:command>
* }</pre>
*
* In a change from previous versions of the extension, items do not contain domain names; instead,
* the names from the non-extension check element are used.
@@ -95,9 +95,6 @@ public class LaunchNotice extends ImmutableObject {
/**
* Validate the checksum of the notice against the domain label.
*
* @throws IllegalArgumentException
* @throws InvalidChecksumException
*/
public void validate(String domainLabel) throws InvalidChecksumException {
// According to http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.3, a TCNID
@@ -49,9 +49,9 @@ public enum GracePeriodStatus implements EppEnum {
AUTO_RENEW("autoRenewPeriod"),
/**
* This status value is used to describe a domain for which a <delete> command has been received,
* but the domain has not yet been purged because an opportunity exists to restore the domain and
* abort the deletion process.
* This status value is used to describe a domain for which a &lt;delete&gt; command has been
* received, but the domain has not yet been purged because an opportunity exists to restore the
* domain and abort the deletion process.
*/
REDEMPTION("redemptionPeriod"),
@@ -45,6 +45,8 @@ import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.common.TimedTransitionProperty.TimeMapper;
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import google.registry.persistence.WithStringVKey;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
@@ -53,6 +55,7 @@ import org.joda.time.DateTime;
/** An entity representing an allocation token. */
@ReportedOn
@Entity
@WithStringVKey
public class AllocationToken extends BackupGroupRoot implements Buildable {
// Promotions should only move forward, and ENDED / CANCELLED are terminal states.
@@ -179,6 +182,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
return tokenStatusTransitions;
}
public VKey<AllocationToken> createVKey() {
return VKey.createOfy(getClass(), Key.create(this));
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
@@ -25,7 +25,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
* An allocation token extension that may be present on EPP domain commands.
*
* @see <a href="https://tools.ietf.org/html/draft-ietf-regext-allocation-token-04">the IETF
* draft</a> for full details.
* draft</a>
*/
@XmlRootElement(name = "allocationToken")
public class AllocationTokenExtension extends ImmutableObject implements CommandExtension {
@@ -44,8 +44,9 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/**
* Container for generic street address.
*
* <p>This is the "addrType" type from {@link "http://tools.ietf.org/html/rfc5733"}. It also matches
* the "addrType" type from {@link "http://tools.ietf.org/html/draft-lozano-tmch-smd"}.
* <p>This is the "addrType" type from <a href="http://tools.ietf.org/html/rfc5733">RFC5733</a>. It
* also matches the "addrType" type from <a
* href="http://tools.ietf.org/html/draft-lozano-tmch-smd">Mark and Signed Mark Objects Mapping</a>.
*
* @see google.registry.model.contact.ContactAddress
* @see google.registry.model.mark.MarkAddress
@@ -29,8 +29,9 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/**
* Container for generic E164 phone number.
*
* <p>This is the "e164" type from {@link "http://tools.ietf.org/html/rfc5733"}. It also matches the
* "e164Type" type from {@link "http://tools.ietf.org/html/draft-lozano-tmch-smd"}.
* <p>This is the "e164" type from <a href="http://tools.ietf.org/html/rfc5733">RFC5733</a>. It also
* matches the "e164Type" type from <a href="http://tools.ietf.org/html/draft-lozano-tmch-smd">Mark
* and Signed Mark Objects Mapping</a>
*
* <blockquote>
*
@@ -24,7 +24,7 @@ import javax.xml.bind.annotation.XmlElementWrapper;
import org.joda.time.DateTime;
/**
* A greeting, defined in {@link "http://tools.ietf.org/html/rfc5730"}.
* A greeting, defined in <a href="http://tools.ietf.org/html/rfc5730">RFC5730</a>.
*
* <p>It would be nice to make this a singleton, but we need the {@link #svDate} field to stay
* current.
@@ -32,7 +32,7 @@ import javax.xml.bind.annotation.XmlType;
/** A collection of {@link HostResource} commands. */
public class HostCommand {
/** The fields on "chgType" from {@link "http://tools.ietf.org/html/rfc5732"}. */
/** The fields on "chgType" from <a href="http://tools.ietf.org/html/rfc5732">RFC5732</a>. */
@XmlTransient
abstract static class HostCreateOrChange extends AbstractSingleResourceCommand
implements ResourceCreateOrChange<HostResource.Builder> {
@@ -42,13 +42,13 @@ public class HostCommand {
}
/**
* A create command for a {@link HostResource}, mapping "createType" from
* {@link "http://tools.ietf.org/html/rfc5732"}.
* A create command for a {@link HostResource}, mapping "createType" from <a
* href="http://tools.ietf.org/html/rfc5732">RFC5732</a>.
*/
@XmlType(propOrder = {"targetId", "inetAddresses" })
@XmlType(propOrder = {"targetId", "inetAddresses"})
@XmlRootElement
public static class Create
extends HostCreateOrChange implements ResourceCreateOrChange<HostResource.Builder> {
public static class Create extends HostCreateOrChange
implements ResourceCreateOrChange<HostResource.Builder> {
/** IP Addresses for this host. Can be null if this is an external host. */
@XmlElement(name = "addr")
Set<InetAddress> inetAddresses;
@@ -15,6 +15,7 @@
package google.registry.model.ofy;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
@@ -94,42 +95,45 @@ public class DatastoreTransactionManager implements TransactionManager {
@Override
public void saveNew(Object entity) {
throw new UnsupportedOperationException("Not available in the Datastore transaction manager");
checkArgumentNotNull(entity, "entity must be specified");
getOfy().save().entity(entity);
}
@Override
public void saveAllNew(ImmutableCollection<?> entities) {
throw new UnsupportedOperationException("Not available in the Datastore transaction manager");
getOfy().save().entities(entities);
}
@Override
public void saveNewOrUpdate(Object entity) {
throw new UnsupportedOperationException("Not available in the Datastore transaction manager");
checkArgumentNotNull(entity, "entity must be specified");
getOfy().save().entity(entity);
}
@Override
public void saveNewOrUpdateAll(ImmutableCollection<?> entities) {
throw new UnsupportedOperationException("Not available in the Datastore transaction manager");
getOfy().save().entities(entities);
}
@Override
public void update(Object entity) {
throw new UnsupportedOperationException("Not available in the Datastore transaction manager");
checkArgumentNotNull(entity, "entity must be specified");
getOfy().save().entity(entity);
}
@Override
public void updateAll(ImmutableCollection<?> entities) {
throw new UnsupportedOperationException("Not available in the Datastore transaction manager");
getOfy().save().entities(entities);
}
@Override
public boolean checkExists(Object entity) {
throw new UnsupportedOperationException("Not available in the Datastore transaction manager");
return getOfy().load().key(Key.create(entity)).now() != null;
}
@Override
public <T> boolean checkExists(VKey<T> key) {
throw new UnsupportedOperationException("Not available in the Datastore transaction manager");
return getOfy().load().key(key.getOfyKey()).now() != null;
}
// TODO: add tests for these methods. They currently have some degree of test coverage because
@@ -138,7 +142,7 @@ public class DatastoreTransactionManager implements TransactionManager {
// interface tests that are applied to both the datastore and SQL implementations.
@Override
public <T> Optional<T> maybeLoad(VKey<T> key) {
return Optional.of(getOfy().load().key(key.getOfyKey()).now());
return Optional.ofNullable(getOfy().load().key(key.getOfyKey()).now());
}
@Override
@@ -161,16 +165,12 @@ public class DatastoreTransactionManager implements TransactionManager {
@Override
public <T> ImmutableList<T> loadAll(Class<T> clazz) {
// We can do a ofy().load().type(clazz), but this doesn't work in a transaction.
throw new UnsupportedOperationException("Not available in the Datastore transaction manager");
}
@Override
public <T> int delete(VKey<T> key) {
throw new UnsupportedOperationException("Not available in the Datastore transaction manager");
}
@Override
public <T> void assertDelete(VKey<T> key) {
throw new UnsupportedOperationException("Not available in the Datastore transaction manager");
public <T> void delete(VKey<T> key) {
getOfy().delete().key(key.getOfyKey()).now();
}
}
@@ -34,10 +34,9 @@ import com.googlecode.objectify.annotation.EntitySubclass;
import com.googlecode.objectify.impl.translate.TranslatorFactory;
import com.googlecode.objectify.impl.translate.opt.joda.MoneyStringTranslatorFactory;
import google.registry.config.RegistryEnvironment;
import google.registry.model.Buildable;
import google.registry.model.EntityClasses;
import google.registry.model.ImmutableObject;
import google.registry.model.contact.ContactResource;
import google.registry.model.host.HostResource;
import google.registry.model.translators.BloomFilterOfStringTranslatorFactory;
import google.registry.model.translators.CidrAddressBlockTranslatorFactory;
import google.registry.model.translators.CommitLogRevisionsTranslatorFactory;
@@ -131,8 +130,7 @@ public class ObjectifyService {
new InetAddressTranslatorFactory(),
new MoneyStringTranslatorFactory(),
new ReadableInstantUtcTranslatorFactory(),
new VKeyTranslatorFactory<ContactResource>(ContactResource.class),
new VKeyTranslatorFactory<HostResource>(HostResource.class),
new VKeyTranslatorFactory(),
new UpdateAutoTimestampTranslatorFactory())) {
factory().getTranslators().add(translatorFactory);
}
@@ -170,10 +168,16 @@ public class ObjectifyService {
}
com.googlecode.objectify.ObjectifyService.register(clazz);
// Autogenerated ids make the commit log code very difficult since we won't always be able
// to create a key for an entity immediately when requesting a save. Disallow that here.
checkState(
!factory().getMetadata(clazz).getKeyMetadata().isIdGeneratable(),
"Can't register %s: Autogenerated ids (@Id on a Long) are not supported.", kind);
// to create a key for an entity immediately when requesting a save. So, we require such
// entities to implement google.registry.model.Buildable as its build() function allocates the
// id to the entity.
if (factory().getMetadata(clazz).getKeyMetadata().isIdGeneratable()) {
checkState(
Buildable.class.isAssignableFrom(clazz),
"Can't register %s: Entity with autogenerated ids (@Id on a Long) must implement"
+ " google.registry.model.Buildable.",
kind);
}
}
}
@@ -62,7 +62,7 @@ import org.joda.time.DateTime;
* <p>Poll messages are identified externally by registrars using the format defined in {@link
* PollMessageExternalKeyConverter}.
*
* @see <a href="https://tools.ietf.org/html/rfc5730#section-2.9.2.3">RFC5730 - EPP - &ltpoll&gt
* @see <a href="https://tools.ietf.org/html/rfc5730#section-2.9.2.3">RFC5730 - EPP - &lt;poll&gt;
* Command</a>
*/
@Entity
@@ -158,8 +158,8 @@ public final class ReservedList
/**
* Gets a ReservedList by name using the caching layer.
*
* @return An Optional<ReservedList> that has a value if a reserved list exists by the given name,
* or absent if not.
* @return An Optional&lt;ReservedList&gt; that has a value if a reserved list exists by the given
* name, or absent if not.
* @throws UncheckedExecutionException if some other error occurs while trying to load the
* ReservedList from the cache or Datastore.
*/
@@ -22,8 +22,8 @@ public final class IcannReportingTypes {
* Represents the set of possible ICANN Monthly Registry Functions Activity Report fields.
*
* <p>Refer to the <a
* href="https://newgtlds.icann.org/sites/default/files/agreements/agreement-approved-09jan14-en.htm#_DV_M278>ICANN
* registry agreement Specification 3 Section 2</a> for details.
* href="https://newgtlds.icann.org/sites/default/files/agreements/agreement-approved-09jan14-en.htm#_DV_M278">
* ICANN registry agreement Specification 3 Section 2</a> for details.
*/
public enum ActivityReportField {
DOMAIN_CHECK("srs-dom-check"),
@@ -14,13 +14,14 @@
package google.registry.model.translators;
import static java.nio.charset.StandardCharsets.UTF_8;
import static com.google.common.base.Functions.identity;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static google.registry.model.EntityClasses.ALL_CLASSES;
import com.googlecode.objectify.Key;
import com.google.appengine.api.datastore.Key;
import com.google.common.collect.ImmutableMap;
import com.googlecode.objectify.annotation.EntitySubclass;
import google.registry.persistence.VKey;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
* Translator factory for VKey.
@@ -28,57 +29,39 @@ import java.net.URLEncoder;
* <p>These get translated to a string containing the URL safe encoding of the objectify key
* followed by a (url-unsafe) ampersand delimiter and the SQL key.
*/
public class VKeyTranslatorFactory<T> extends AbstractSimpleTranslatorFactory<VKey, String> {
private final Class<T> refClass;
public class VKeyTranslatorFactory extends AbstractSimpleTranslatorFactory<VKey, Key> {
public VKeyTranslatorFactory(Class<T> refClass) {
// Class registry allowing us to restore the original class object from the unqualified class
// name, which is all the datastore key gives us.
// Note that entities annotated with @EntitySubclass are removed because they share the same
// kind of the key with their parent class.
private static final ImmutableMap<String, Class> CLASS_REGISTRY =
ALL_CLASSES.stream()
.filter(clazz -> !clazz.isAnnotationPresent(EntitySubclass.class))
.collect(toImmutableMap(com.googlecode.objectify.Key::getKind, identity()));
;
public VKeyTranslatorFactory() {
super(VKey.class);
this.refClass = refClass;
}
@Override
public SimpleTranslator<VKey, String> createTranslator() {
return new SimpleTranslator<VKey, String>() {
public SimpleTranslator<VKey, Key> createTranslator() {
return new SimpleTranslator<VKey, Key>() {
@Override
public VKey loadValue(String datastoreValue) {
int pos = datastoreValue.indexOf('&');
Key ofyKey = null;
String sqlKey = null;
if (pos > 0) {
// We have an objectify key.
ofyKey = Key.create(datastoreValue.substring(0, pos));
}
if (pos < datastoreValue.length() - 1) {
// We have an SQL key.
sqlKey = decode(datastoreValue.substring(pos + 1));
}
return VKey.create(refClass, sqlKey, ofyKey);
public VKey loadValue(Key datastoreValue) {
// TODO(mmuller): we need to call a method on refClass to also reconstitute the SQL key.
return datastoreValue == null
? null
: VKey.createOfy(
CLASS_REGISTRY.get(datastoreValue.getKind()),
com.googlecode.objectify.Key.create(datastoreValue));
}
@Override
public String saveValue(VKey key) {
return ((key.getOfyKey() == null) ? "" : key.getOfyKey().getString())
+ "&"
+ ((key.getSqlKey() == null) ? "" : encode(key.getSqlKey().toString()));
public Key saveValue(VKey key) {
return key == null ? null : key.getOfyKey().getRaw();
}
};
}
private static String encode(String val) {
try {
return URLEncoder.encode(val, UTF_8.toString());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private static String decode(String encoded) {
try {
return URLDecoder.decode(encoded, UTF_8.toString());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,47 @@
// Copyright 2020 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.persistence.converter;
import avro.shaded.com.google.common.collect.Maps;
import google.registry.model.registry.Registry.BillingCostTransition;
import java.util.Map;
import javax.persistence.Converter;
import org.joda.money.Money;
import org.joda.time.DateTime;
/**
* JPA converter for storing/retrieving {@code TimedTransitionProperty<Money,BillingCostTransition>}
* objects.
*/
@Converter(autoApply = true)
public class BillingCostTransitionConverter
extends TimedTransitionPropertyConverterBase<Money, BillingCostTransition> {
@Override
Map.Entry<String, String> convertToDatabaseMapEntry(
Map.Entry<DateTime, BillingCostTransition> entry) {
return Maps.immutableEntry(entry.getKey().toString(), entry.getValue().getValue().toString());
}
@Override
Map.Entry<DateTime, Money> convertToEntityMapEntry(Map.Entry<String, String> entry) {
return Maps.immutableEntry(DateTime.parse(entry.getKey()), Money.parse(entry.getValue()));
}
@Override
Class<BillingCostTransition> getTimedTransitionSubclass() {
return BillingCostTransition.class;
}
}
@@ -0,0 +1,35 @@
// Copyright 2020 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.persistence.converter;
import google.registry.model.billing.BillingEvent.Flag;
import java.util.Set;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** JPA {@link AttributeConverter} for storing/retrieving {@link Set}. */
@Converter(autoApply = true)
public class BillingEventFlagSetConverter extends StringSetConverterBase<Flag> {
@Override
String toString(Flag element) {
return element.name();
}
@Override
Flag fromString(String value) {
return Flag.valueOf(value);
}
}
@@ -15,12 +15,11 @@
package google.registry.persistence.converter;
import google.registry.util.CidrAddressBlock;
import java.util.List;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/**
* JPA {@link AttributeConverter} for storing/retrieving {@link List<CidrAddressBlock>} objects.
* JPA {@link AttributeConverter} for storing/retrieving {@code List<CidrAddressBlock>} objects.
* TODO(shicong): Investigate if we can have one converter for any List type
*/
@Converter(autoApply = true)
@@ -21,7 +21,7 @@ import java.util.Map;
import javax.persistence.Converter;
import org.joda.money.CurrencyUnit;
/** JPA converter for storing/retrieving {@link Map <CurrencyUnit, BillingAccountEntry>} objects. */
/** JPA converter for storing/retrieving {@code Map<CurrencyUnit, BillingAccountEntry>} objects. */
@Converter(autoApply = true)
public class CurrencyToBillingConverter
extends StringMapConverterBase<CurrencyUnit, BillingAccountEntry> {
@@ -16,11 +16,10 @@ package google.registry.persistence.converter;
import google.registry.model.contact.Disclose.PostalInfoChoice;
import google.registry.model.contact.PostalInfo;
import java.util.List;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** JPA {@link AttributeConverter} for storing/retrieving {@link List < PostalInfoChoice >}. */
/** JPA {@link AttributeConverter} for storing/retrieving {@code List<PostalInfoChoice>}. */
@Converter(autoApply = true)
public class PostalInfoChoiceListConverter extends StringListConverterBase<PostalInfoChoice> {
@@ -15,11 +15,10 @@
package google.registry.persistence.converter;
import google.registry.model.registrar.RegistrarContact.Type;
import java.util.Set;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** JPA {@link AttributeConverter} for storing/retrieving {@link Set<Type>}. */
/** JPA {@link AttributeConverter} for storing/retrieving {@code Set<Type>}. */
@Converter(autoApply = true)
public class RegistrarPocSetConverter extends StringSetConverterBase<Type> {
@Override
@@ -15,11 +15,10 @@
package google.registry.persistence.converter;
import google.registry.model.eppcommon.StatusValue;
import java.util.Set;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** JPA {@link AttributeConverter} for storing/retrieving {@link Set<StatusValue>}. */
/** JPA {@link AttributeConverter} for storing/retrieving {@code Set<StatusValue>}. */
@Converter(autoApply = true)
public class StatusValueSetConverter extends StringSetConverterBase<StatusValue> {
@@ -37,7 +37,7 @@ import org.hibernate.type.descriptor.sql.SqlTypeDescriptor;
/**
* The {@link JavaTypeDescriptor} and {@link SqlTypeDescriptor} for {@link StringCollection}.
*
* <p>A {@link StringCollection} object is a simple wrapper for a {@link Collection<String>} which
* <p>A {@link StringCollection} object is a simple wrapper for a {@code Collection<String>} which
* can be stored as a string array in the database. The {@link JavaTypeDescriptor} and {@link
* SqlTypeDescriptor} is used by JPA/Hibernate to map between the collection and {@link Array} which
* is the actual type that JDBC uses to read from and write to the database.
@@ -14,11 +14,10 @@
package google.registry.persistence.converter;
import java.util.List;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** JPA {@link AttributeConverter} for storing/retrieving {@link List<String>}. */
/** JPA {@link AttributeConverter} for storing/retrieving {@code List<String>}. */
@Converter(autoApply = true)
public class StringListConverter extends StringListConverterBase<String> {
@@ -36,7 +36,7 @@ import org.hibernate.type.descriptor.sql.SqlTypeDescriptor;
/**
* The {@link JavaTypeDescriptor} and {@link SqlTypeDescriptor} for {@link StringMap}.
*
* <p>A {@link StringMap} object is a simple wrapper for a {@link Map <String, String>} which can be
* <p>A {@link StringMap} object is a simple wrapper for a {@code Map<String, String>} which can be
* stored in a column with data type of hstore in the database. The {@link JavaTypeDescriptor} and
* {@link SqlTypeDescriptor} is used by JPA/Hibernate to map between the map and hstore which is the
* actual type that JDBC uses to read from and write to the database.
@@ -156,7 +156,7 @@ public class StringMapDescriptor extends AbstractTypeDescriptor<StringMap>
};
}
/** A simple wrapper class for {@link Map<String, String>}. */
/** A simple wrapper class for {@code Map<String, String>}. */
public static class StringMap {
private Map<String, String> map;
@@ -169,7 +169,7 @@ public class StringMapDescriptor extends AbstractTypeDescriptor<StringMap>
return new StringMap(ImmutableMap.copyOf(map));
}
/** Returns the underlying {@link Map<String, String>} object. */
/** Returns the underlying {@code Map<String, String>} object. */
public Map<String, String> getMap() {
return map;
}
@@ -14,11 +14,10 @@
package google.registry.persistence.converter;
import java.util.Set;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** JPA {@link AttributeConverter} for storing/retrieving {@link Set<String>}. */
/** JPA {@link AttributeConverter} for storing/retrieving {@code Set<String>}. */
@Converter(autoApply = true)
public class StringSetConverter extends StringSetConverterBase<String> {
@@ -0,0 +1,63 @@
// Copyright 2020 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.persistence.converter;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
import google.registry.persistence.converter.StringMapDescriptor.StringMap;
import java.util.Map;
import javax.annotation.Nullable;
import javax.persistence.AttributeConverter;
import org.joda.time.DateTime;
/**
* Base JPA converter for {@link TimedTransitionProperty} objects that are stored in a column with
* data type of hstore in the database.
*/
public abstract class TimedTransitionPropertyConverterBase<K, V extends TimedTransition<K>>
implements AttributeConverter<TimedTransitionProperty<K, V>, StringMap> {
abstract Map.Entry<String, String> convertToDatabaseMapEntry(Map.Entry<DateTime, V> entry);
abstract Map.Entry<DateTime, K> convertToEntityMapEntry(Map.Entry<String, String> entry);
abstract Class<V> getTimedTransitionSubclass();
@Override
public StringMap convertToDatabaseColumn(@Nullable TimedTransitionProperty<K, V> attribute) {
return attribute == null
? null
: StringMap.create(
attribute.entrySet().stream()
.map(this::convertToDatabaseMapEntry)
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)));
}
@Override
public TimedTransitionProperty<K, V> convertToEntityAttribute(@Nullable StringMap dbData) {
if (dbData == null) {
return null;
}
Map<DateTime, K> map =
dbData.getMap().entrySet().stream()
.map(this::convertToEntityMapEntry)
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
return TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.copyOf(map), getTimedTransitionSubclass());
}
}
@@ -0,0 +1,47 @@
// Copyright 2020 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.persistence.converter;
import com.google.common.collect.Maps;
import google.registry.model.registry.Registry.TldState;
import google.registry.model.registry.Registry.TldStateTransition;
import java.util.Map;
import javax.persistence.Converter;
import org.joda.time.DateTime;
/**
* JPA converter for storing/retrieving {@code TimedTransitionProperty<TldState,
* TldStateTransition>} objects.
*/
@Converter(autoApply = true)
public class TldStateTransitionConverter
extends TimedTransitionPropertyConverterBase<TldState, TldStateTransition> {
@Override
Map.Entry<String, String> convertToDatabaseMapEntry(
Map.Entry<DateTime, TldStateTransition> entry) {
return Maps.immutableEntry(entry.getKey().toString(), entry.getValue().getValue().name());
}
@Override
Map.Entry<DateTime, TldState> convertToEntityMapEntry(Map.Entry<String, String> entry) {
return Maps.immutableEntry(DateTime.parse(entry.getKey()), TldState.valueOf(entry.getValue()));
}
@Override
Class<TldStateTransition> getTimedTransitionSubclass() {
return TldStateTransition.class;
}
}
@@ -14,6 +14,7 @@
package google.registry.persistence.transaction;
import google.registry.persistence.VKey;
import javax.persistence.EntityManager;
/** Sub-interface of {@link TransactionManager} which defines JPA related methods. */
@@ -21,4 +22,7 @@ public interface JpaTransactionManager extends TransactionManager {
/** Returns the {@link EntityManager} for the current request. */
EntityManager getEntityManager();
/** Deletes the entity by its id, throws exception if the entity is not deleted. */
public abstract <T> void assertDelete(VKey<T> key);
}
@@ -77,7 +77,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public void assertInTransaction() {
if (!inTransaction()) {
throw new PersistenceException("Not in a transaction");
throw new IllegalStateException("Not in a transaction");
}
}
@@ -278,8 +278,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
.getResultList());
}
@Override
public <T> int delete(VKey<T> key) {
private <T> int internalDelete(VKey<T> key) {
checkArgumentNotNull(key, "key must be specified");
assertInTransaction();
EntityType<?> entityType = getEntityType(key.getKind());
@@ -291,9 +290,14 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return query.executeUpdate();
}
@Override
public <T> void delete(VKey<T> key) {
internalDelete(key);
}
@Override
public <T> void assertDelete(VKey<T> key) {
if (delete(key) != 1) {
if (internalDelete(key) != 1) {
throw new IllegalArgumentException(
String.format("Error deleting the entity of the key: %s", key.getSqlKey()));
}
@@ -17,6 +17,7 @@ package google.registry.persistence.transaction;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import google.registry.persistence.VKey;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.function.Supplier;
import org.joda.time.DateTime;
@@ -123,9 +124,6 @@ public interface TransactionManager {
/** Loads all entities of the given type, returns empty if there is no such entity. */
<T> ImmutableList<T> loadAll(Class<T> clazz);
/** Deletes the entity by its id, returns the number of deleted entity. */
<T> int delete(VKey<T> key);
/** Deletes the entity by its id, throws exception if the entity is not deleted. */
<T> void assertDelete(VKey<T> key);
/** Deletes the entity by its id. */
<T> void delete(VKey<T> key);
}
@@ -16,6 +16,7 @@ package google.registry.persistence.transaction;
import com.google.appengine.api.utils.SystemProperty;
import com.google.appengine.api.utils.SystemProperty.Environment.Value;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Suppliers;
import google.registry.model.ofy.DatastoreTransactionManager;
import google.registry.persistence.DaggerPersistenceComponent;
@@ -26,7 +27,9 @@ import java.util.function.Supplier;
// TODO: Rename this to PersistenceFactory and move to persistence package.
public class TransactionManagerFactory {
private static final TransactionManager TM = createTransactionManager();
private static final DatastoreTransactionManager ofyTm = createTransactionManager();
@NonFinalForTesting private static TransactionManager tm = ofyTm;
/** Supplier for jpaTm so that it is initialized only once, upon first usage. */
@NonFinalForTesting
@@ -45,10 +48,7 @@ public class TransactionManagerFactory {
}
}
private static TransactionManager createTransactionManager() {
// TODO: Determine how to provision TransactionManager after the dual-write. During the
// dual-write transitional phase, we need the TransactionManager for both Datastore and Cloud
// SQL, and this method returns the one for Datastore.
private static DatastoreTransactionManager createTransactionManager() {
return new DatastoreTransactionManager(null);
}
@@ -67,7 +67,7 @@ public class TransactionManagerFactory {
/** Returns {@link TransactionManager} instance. */
public static TransactionManager tm() {
return TM;
return tm;
}
/** Returns {@link JpaTransactionManager} instance. */
@@ -75,8 +75,20 @@ public class TransactionManagerFactory {
return jpaTm.get();
}
/** Returns {@link DatastoreTransactionManager} instance. */
@VisibleForTesting
public static DatastoreTransactionManager ofyTm() {
return ofyTm;
}
/** Sets the return of {@link #jpaTm()} to the given instance of {@link JpaTransactionManager}. */
public static void setJpaTm(JpaTransactionManager newJpaTm) {
jpaTm = Suppliers.ofInstance(newJpaTm);
}
/** Sets the return of {@link #tm()} to the given instance of {@link TransactionManager}. */
@VisibleForTesting
public static void setTm(TransactionManager newTm) {
tm = newTm;
}
}
@@ -61,10 +61,9 @@ import javax.inject.Inject;
* (RDAP) Query Format</a>
* @see <a href="http://tools.ietf.org/html/rfc7483">RFC 7483: JSON Responses for the Registration
* Data Access Protocol (RDAP)</a>
*
* TODO(guyben):This isn't required by the RDAP Technical Implementation Guide, and hence should be
* deleted, at least until it's actually required.
*/
// TODO: This isn't required by the RDAP Technical Implementation Guide, and hence should be
// deleted, at least until it's actually required.
@Action(
service = Action.Service.PUBAPI,
path = "/rdap/domains",
@@ -72,10 +72,9 @@ import javax.inject.Inject;
* (RDAP) Query Format</a>
* @see <a href="http://tools.ietf.org/html/rfc7483">RFC 7483: JSON Responses for the Registration
* Data Access Protocol (RDAP)</a>
*
* TODO(guyben):This isn't required by the RDAP Technical Implementation Guide, and hence should be
* deleted, at least until it's actually required.
*/
// TODO: This isn't required by the RDAP Technical Implementation Guide, and hence should be
// deleted, at least until it's actually required.
@Action(
service = Action.Service.PUBAPI,
path = "/rdap/entities",
@@ -221,10 +221,9 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
*
* <p>This version handles a list of parameter values, all associated with the same name.
*
* <p>Example: If the original parameters were "a=w&a=x&b=y&c=z", and this method is called with
* parameterName = "b" and parameterValues of "p" and "q", the result will be
* "a=w&a=x&c=z&b=p&b=q". The new values of parameter "b" replace the old ones.
*
* <p>Example: If the original parameters were "a=w&amp;a=x&amp;b=y&amp;c=z", and this method is
* called with parameterName = "b" and parameterValues of "p" and "q", the result will be
* "a=w&amp;a=x&amp;c=z&amp;b=p&amp;b=q". The new values of parameter "b" replace the old ones.
*/
protected String getRequestUrlWithExtraParameter(
String parameterName, List<String> parameterValues) {
@@ -120,9 +120,6 @@ public final class Ghostryde {
/**
* Creates a ghostryde file from an in-memory byte array.
*
* @throws PGPException
* @throws IOException
*/
public static byte[] encode(byte[] data, PGPPublicKey key)
throws IOException, PGPException {
@@ -137,9 +134,6 @@ public final class Ghostryde {
/**
* Deciphers a ghostryde file from an in-memory byte array.
*
* @throws PGPException
* @throws IOException
*/
public static byte[] decode(byte[] data, PGPPrivateKey key)
throws IOException, PGPException {
@@ -46,9 +46,6 @@ public final class RdeUtil {
/**
* Look at some bytes from {@code xmlInput} to ensure it appears to be a FULL XML deposit and
* then use a regular expression to extract the watermark timestamp which is returned.
*
* @throws IOException
* @throws XmlException
*/
public static DateTime peekWatermark(BufferedInputStream xmlInput)
throws IOException, XmlException {
@@ -34,7 +34,7 @@ import javax.mail.internet.InternetAddress;
import org.joda.time.YearMonth;
/** Utility functions for sending emails involving monthly invoices. */
class BillingEmailUtils {
public class BillingEmailUtils {
private final SendEmailService emailService;
private final YearMonth yearMonth;
@@ -43,8 +43,8 @@ import org.joda.time.YearMonth;
* PublishInvoicesAction} to publish the subsequent output.
*
* <p>This action runs the {@link google.registry.beam.invoicing.InvoicingPipeline} beam template,
* staged at gs://<projectId>-beam/templates/invoicing. The pipeline then generates invoices for the
* month and stores them on GCS.
* staged at gs://&lt;projectId&gt;-beam/templates/invoicing. The pipeline then generates invoices
* for the month and stores them on GCS.
*/
@Action(
service = Action.Service.BACKEND,
@@ -53,7 +53,7 @@ import org.json.JSONException;
* pipeline accordingly.
*
* <p>This calls {@link Spec11EmailUtils#emailSpec11Reports(LocalDate, SoyTemplateInfo, String,
* Set)} on success or {@link Spec11EmailUtils#sendAlertEmail(String, String)} on failure.
* ImmutableSet)} on success or {@link Spec11EmailUtils#sendAlertEmail(String, String)} on failure.
*/
@Action(
service = Action.Service.BACKEND,
@@ -18,6 +18,7 @@ import static com.google.common.html.HtmlEscapers.htmlEscaper;
import com.google.common.flogger.FluentLogger;
import java.io.IOException;
import java.util.logging.Level;
import javax.servlet.http.HttpServletResponse;
/** Base for exceptions that cause an HTTP error response. */
@@ -28,11 +29,18 @@ public abstract class HttpException extends RuntimeException {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Level logLevel;
private final int responseCode;
protected HttpException(int responseCode, String message, Throwable cause) {
protected HttpException(int responseCode, String message, Throwable cause, Level logLevel) {
super(message, cause);
this.responseCode = responseCode;
this.logLevel = logLevel;
}
protected HttpException(int responseCode, String message, Throwable cause) {
this(responseCode, message, cause, Level.INFO);
}
public final int getResponseCode() {
@@ -57,7 +65,7 @@ public abstract class HttpException extends RuntimeException {
*/
public final void send(HttpServletResponse rsp) throws IOException {
rsp.sendError(getResponseCode(), htmlEscaper().escape(getMessage()));
logger.atInfo().withCause(getCause()).log("%s", this);
logger.at(logLevel).withCause(getCause()).log("%s", this);
}
/**
@@ -196,7 +204,7 @@ public abstract class HttpException extends RuntimeException {
/** Exception that causes a 500 response. */
public static final class InternalServerErrorException extends HttpException {
public InternalServerErrorException(String message) {
super(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message, null);
super(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message, null, Level.SEVERE);
}
public InternalServerErrorException(String message, Throwable cause) {
@@ -45,9 +45,9 @@ public final class RequestParameters {
*
* <ul>
* <li>/foo?bar=hello hello
* <li>/foo?bar=hello&bar=there hello
* <li>/foo?bar=hello&amp;bar=there hello
* <li>/foo?bar= 400 error (empty)
* <li>/foo?bar=&bar=there 400 error (empty)
* <li>/foo?bar=&amp;bar=there 400 error (empty)
* <li>/foo 400 error (absent)
* </ul>
*
@@ -59,7 +59,7 @@ public class CursorDao {
.transact(() -> jpaTm().getEntityManager().find(Cursor.class, new CursorId(type, scope)));
}
/** If no scope is given, use {@link Cursor.GLOBAL} as the scope. */
/** If no scope is given, use {@link Cursor#GLOBAL} as the scope. */
public static Cursor load(CursorType type) {
checkNotNull(type, "The type of the cursor to load must be specified");
return load(type, Cursor.GLOBAL);
@@ -214,13 +214,13 @@ public final class RegistryLock extends ImmutableObject implements Buildable, Sq
return lockCompletionTimestamp != null && unlockCompletionTimestamp == null;
}
/** Returns true iff the lock was requested >= 1 hour ago and has not been verified. */
/** Returns true iff the lock was requested &gt;= 1 hour ago and has not been verified. */
public boolean isLockRequestExpired(DateTime now) {
return !getLockCompletionTimestamp().isPresent()
&& isBeforeOrAt(getLockRequestTimestamp(), now.minusHours(1));
}
/** Returns true iff the unlock was requested >= 1 hour ago and has not been verified. */
/** Returns true iff the unlock was requested &gt;= 1 hour ago and has not been verified. */
public boolean isUnlockRequestExpired(DateTime now) {
Optional<DateTime> unlockRequestTimestamp = getUnlockRequestTimestamp();
return unlockRequestTimestamp.isPresent()
@@ -42,7 +42,7 @@ public class DriveConnection {
/**
* Creates a folder with the given parent.
*
* @returns the folder id.
* @return the folder id.
*/
public String createFolder(String title, String parentFolderId) throws IOException {
return drive.files()
@@ -58,7 +58,7 @@ public class DriveConnection {
* existing file is the desired behavior, use {@link #createOrUpdateFile(String, MediaType,
* String, byte[])} instead.
*
* @returns the file id.
* @return the file id.
*/
public String createFile(String title, MediaType mimeType, String parentFolderId, byte[] bytes)
throws IOException {
@@ -76,13 +76,10 @@ public class DriveConnection {
*
* @throws IllegalStateException if multiple files with that name exist in the given folder.
* @throws IOException if communication with Google Drive fails for any reason.
* @returns the file id.
* @return the file id.
*/
public String createOrUpdateFile(
String title,
MediaType mimeType,
String parentFolderId,
byte[] bytes) throws IOException {
String title, MediaType mimeType, String parentFolderId, byte[] bytes) throws IOException {
List<String> existingFiles = listFiles(parentFolderId, String.format("title = '%s'", title));
if (existingFiles.size() > 1) {
throw new IllegalStateException(String.format(
@@ -97,10 +94,10 @@ public class DriveConnection {
}
/**
* Updates the file with the given id in place, setting the title, content, and mime type to
* the newly specified values.
* Updates the file with the given id in place, setting the title, content, and mime type to the
* newly specified values.
*
* @returns the file id.
* @return the file id.
*/
public String updateFile(String fileId, String title, MediaType mimeType, byte[] bytes)
throws IOException {
@@ -77,10 +77,6 @@ public class TmchXmlSignature {
*
* @throws GeneralSecurityException for unsupported protocols, certs not signed by the TMCH,
* incorrect keys, and for invalid, old, not-yet-valid or revoked certificates.
* @throws IOException
* @throws MarshalException
* @throws ParserConfigurationException
* @throws SAXException
*/
public void verify(byte[] smdXml)
throws GeneralSecurityException, IOException, MarshalException, ParserConfigurationException,
@@ -39,18 +39,14 @@ class CompareDbBackups {
return;
}
ImmutableSet<ComparableEntity> entities1 =
new RecordAccumulator()
.readDirectory(new File(args[0]), DATA_FILE_MATCHER)
.getComparableEntitySet();
ImmutableSet<ComparableEntity> entities2 =
new RecordAccumulator()
.readDirectory(new File(args[1]), DATA_FILE_MATCHER)
.getComparableEntitySet();
ImmutableSet<EntityWrapper> entities1 =
RecordAccumulator.readDirectory(new File(args[0]), DATA_FILE_MATCHER).getEntityWrapperSet();
ImmutableSet<EntityWrapper> entities2 =
RecordAccumulator.readDirectory(new File(args[1]), DATA_FILE_MATCHER).getEntityWrapperSet();
// Calculate the entities added and removed.
SetView<ComparableEntity> added = Sets.difference(entities2, entities1);
SetView<ComparableEntity> removed = Sets.difference(entities1, entities2);
SetView<EntityWrapper> added = Sets.difference(entities2, entities1);
SetView<EntityWrapper> removed = Sets.difference(entities1, entities2);
printHeader(
String.format("First backup: %d records", entities1.size()),
@@ -58,14 +54,14 @@ class CompareDbBackups {
if (!removed.isEmpty()) {
printHeader(removed.size() + " records were removed:");
for (ComparableEntity entity : removed) {
for (EntityWrapper entity : removed) {
System.out.println(entity);
}
}
if (!added.isEmpty()) {
printHeader(added.size() + " records were added:");
for (ComparableEntity entity : added) {
for (EntityWrapper entity : added) {
System.out.println(entity);
}
}
@@ -15,20 +15,32 @@
package google.registry.tools;
import com.google.appengine.api.datastore.Entity;
import com.google.auto.value.AutoValue;
import com.google.common.base.Objects;
/** Wraps {@link Entity} to do hashCode/equals based on both the entity's key and its properties. */
final class ComparableEntity {
/**
* Wraps {@link Entity} for ease of processing in collections.
*
* <p>Note that the {@link #hashCode}/{@link #equals} methods are based on both the entity's key and
* its properties.
*/
final class EntityWrapper {
private static final String TEST_ENTITY_KIND = "TestEntity";
private final Entity entity;
ComparableEntity(Entity entity) {
EntityWrapper(Entity entity) {
this.entity = entity;
}
public Entity getEntity() {
return entity;
}
@Override
public boolean equals(Object that) {
if (that instanceof ComparableEntity) {
ComparableEntity thatEntity = (ComparableEntity) that;
if (that instanceof EntityWrapper) {
EntityWrapper thatEntity = (EntityWrapper) that;
return entity.equals(thatEntity.entity)
&& entity.getProperties().equals(thatEntity.entity.getProperties());
}
@@ -43,6 +55,26 @@ final class ComparableEntity {
@Override
public String toString() {
return "ComparableEntity(" + entity + ")";
return "EntityWrapper(" + entity + ")";
}
public static EntityWrapper from(int id, Property... properties) {
Entity entity = new Entity(TEST_ENTITY_KIND, id);
for (Property prop : properties) {
entity.setProperty(prop.name(), prop.value());
}
return new EntityWrapper(entity);
}
@AutoValue
abstract static class Property {
static Property create(String name, Object value) {
return new AutoValue_EntityWrapper_Property(name, value);
}
abstract String name();
abstract Object value();
}
}
@@ -14,37 +14,101 @@
package google.registry.tools;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Optional;
/**
* Reads records from a set of LevelDB files and builds a gigantic ImmutableList from them.
* Iterator that incrementally parses binary data in LevelDb format into records.
*
* <p>See <a
* href="https://github.com/google/leveldb/blob/master/doc/log_format.md">log_format.md</a> for the
* leveldb log format specification.</a>
* <p>The input source is automatically closed when all data have been read.
*
* <p>There are several other implementations of this, none of which appeared suitable for our use
* case: <a href="https://github.com/google/leveldb">The original C++ implementation</a>. <a
* href="https://cloud.google.com/appengine/docs/standard/java/javadoc/com/google/appengine/api/files/RecordWriteChannel">
* com.google.appengine.api.files.RecordWriteChannel</a> - Exactly what we need but deprecated. The
* href="https://cloud.google.com/appengine/docs/standard/java/javadoc/com/google/appengine/api/files/RecordReadChannel">
* com.google.appengine.api.files.RecordReadChannel</a> - Exactly what we need but deprecated. The
* referenced replacement: <a
* href="https://github.com/GoogleCloudPlatform/appengine-gcs-client.git">The App Engine GCS
* Client</a> - Does not appear to have any support for working with LevelDB.
* Client</a> - Does not appear to have any support for working with LevelDB. *
*
* <p>See <a
* href="https://github.com/google/leveldb/blob/master/doc/log_format.md">log_format.md</a>
*/
public final class LevelDbLogReader {
public final class LevelDbLogReader implements Iterator<byte[]> {
@VisibleForTesting static final int BLOCK_SIZE = 32 * 1024;
@VisibleForTesting static final int HEADER_SIZE = 7;
private final ByteArrayOutputStream recordContents = new ByteArrayOutputStream();
private final ImmutableList.Builder<byte[]> recordListBuilder = new ImmutableList.Builder<>();
private final LinkedList<byte[]> recordList = Lists.newLinkedList();
private final ByteBuffer byteBuffer = ByteBuffer.allocate(BLOCK_SIZE);
private final ReadableByteChannel channel;
LevelDbLogReader(ReadableByteChannel channel) {
this.channel = channel;
}
@Override
public boolean hasNext() {
while (recordList.isEmpty()) {
try {
Optional<byte[]> block = readFromChannel();
if (!block.isPresent()) {
return false;
}
if (block.get().length != BLOCK_SIZE) {
throw new IllegalStateException("Data size is not multiple of " + BLOCK_SIZE);
}
processBlock(block.get());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return true;
}
@Override
public byte[] next() {
checkState(hasNext(), "The next() method called on empty iterator.");
return recordList.removeFirst();
}
/**
* Returns the next {@link #BLOCK_SIZE} bytes from the input channel, or {@link
* Optional#empty()} if there is no more data.
*/
// TODO(weiminyu): use ByteBuffer directly.
private Optional<byte[]> readFromChannel() throws IOException {
while (channel.isOpen()) {
int bytesRead = channel.read(byteBuffer);
if (!byteBuffer.hasRemaining() || bytesRead < 0) {
byteBuffer.flip();
if (!byteBuffer.hasRemaining()) {
channel.close();
return Optional.empty();
}
byte[] result = new byte[byteBuffer.remaining()];
byteBuffer.get(result);
byteBuffer.clear();
return Optional.of(result);
}
}
return Optional.empty();
}
/** Read a complete block, which must be exactly 32 KB. */
private void processBlock(byte[] block) {
@@ -63,7 +127,7 @@ public final class LevelDbLogReader {
// If this is the last (or only) chunk in the record, store the full contents into the List.
if (recordHeader.type == ChunkType.FULL || recordHeader.type == ChunkType.LAST) {
recordListBuilder.add(recordContents.toByteArray());
recordList.add(recordContents.toByteArray());
recordContents.reset();
}
@@ -96,40 +160,24 @@ public final class LevelDbLogReader {
return new RecordHeader(checksum, size, ChunkType.fromCode(type));
}
/** Reads all records in the Reader into the record set. */
public void readFrom(InputStream source) throws IOException {
byte[] block = new byte[BLOCK_SIZE];
// read until we have no more.
while (true) {
int amountRead = source.read(block, 0, BLOCK_SIZE);
if (amountRead <= 0) {
break;
}
assert amountRead == BLOCK_SIZE;
processBlock(block);
}
/** Returns a {@link LevelDbLogReader} over a {@link ReadableByteChannel}. */
public static LevelDbLogReader from(ReadableByteChannel channel) {
return new LevelDbLogReader(channel);
}
/** Reads all records from the file specified by "path" into the record set. */
public void readFrom(Path path) throws IOException {
readFrom(Files.newInputStream(path));
/** Returns a {@link LevelDbLogReader} over an {@link InputStream}. */
public static LevelDbLogReader from(InputStream source) {
return new LevelDbLogReader(Channels.newChannel(source));
}
/** Reads all records from the specified file into the record set. */
public void readFrom(String filename) throws IOException {
readFrom(FileSystems.getDefault().getPath(filename));
/** Returns a {@link LevelDbLogReader} over a file specified by {@link Path}. */
public static LevelDbLogReader from(Path path) throws IOException {
return from(Files.newInputStream(path));
}
/**
* Gets the list of records constructed so far.
*
* <p>Note that this does not invalidate the internal state of the object: we return a copy and
* this can be called multiple times.
*/
ImmutableList<byte[]> getRecords() {
return recordListBuilder.build();
/** Returns a {@link LevelDbLogReader} over a file specified by {@code filename}. */
public static LevelDbLogReader from(String filename) throws IOException {
return from(FileSystems.getDefault().getPath(filename));
}
/** Aggregates the fields in a record header. */
@@ -15,42 +15,47 @@
package google.registry.tools;
import com.google.appengine.api.datastore.EntityTranslator;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.storage.onestore.v3.OnestoreEntity.EntityProto;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.function.Predicate;
/** Accumulates Entity records from level db files under a directory hierarchy. */
class RecordAccumulator {
private final LevelDbLogReader reader = new LevelDbLogReader();
private final ImmutableList<byte[]> records;
RecordAccumulator(ImmutableList<byte[]> records) {
this.records = records;
}
/** Recursively reads all records in the directory. */
public final RecordAccumulator readDirectory(File dir, Predicate<File> fileMatcher) {
public static RecordAccumulator readDirectory(File dir, Predicate<File> fileMatcher) {
ImmutableList.Builder<byte[]> builder = new ImmutableList.Builder<>();
for (File child : dir.listFiles()) {
if (child.isDirectory()) {
readDirectory(child, fileMatcher);
builder.addAll(readDirectory(child, fileMatcher).records);
} else if (fileMatcher.test(child)) {
try {
reader.readFrom(new FileInputStream(child));
builder.addAll(LevelDbLogReader.from(child.getPath()));
} catch (IOException e) {
throw new RuntimeException("IOException reading from file: " + child, e);
}
}
}
return this;
return new RecordAccumulator(builder.build());
}
/** Creates an entity set from the current set of raw records. */
ImmutableSet<ComparableEntity> getComparableEntitySet() {
ImmutableSet.Builder<ComparableEntity> builder = new ImmutableSet.Builder<>();
for (byte[] rawRecord : reader.getRecords()) {
/** Creates an {@link EntityWrapper} set from the current set of raw records. */
ImmutableSet<EntityWrapper> getEntityWrapperSet() {
ImmutableSet.Builder<EntityWrapper> builder = new ImmutableSet.Builder<>();
for (byte[] rawRecord : records) {
// Parse the entity proto and create an Entity object from it.
EntityProto proto = new EntityProto();
proto.parseFrom(rawRecord);
ComparableEntity entity = new ComparableEntity(EntityTranslator.createFromPb(proto));
EntityWrapper entity = new EntityWrapper(EntityTranslator.createFromPb(proto));
builder.add(entity);
}
@@ -44,43 +44,45 @@ import javax.annotation.concurrent.Immutable;
* Declarative functional fluent form field converter / validator.
*
* <p>This class is responsible for converting arbitrary data, sent to us by the web browser, into
* validated data structures that the server-side code can use. For example:<pre>
* validated data structures that the server-side code can use. For example:
*
* private enum Gender { MALE, FEMALE }
* <pre>{@code
* private enum Gender { MALE, FEMALE }
*
* private static final FormField<String, String> NAME_FIELD = FormField.named("name")
* .matches("[a-z]+")
* .range(atMost(16))
* .required()
* .build();
* private static final FormField<String, String> NAME_FIELD = FormField.named("name")
* .matches("[a-z]+")
* .range(atMost(16))
* .required()
* .build();
*
* private static final FormField<String, Gender> GENDER_FIELD = FormField.named("gender")
* .asEnum(Gender.class)
* .required()
* .build();
* private static final FormField<String, Gender> GENDER_FIELD = FormField.named("gender")
* .asEnum(Gender.class)
* .required()
* .build();
*
* public Person makePerson(Map<String, String> params) {
* Person.Builder person = new Person.Builder();
* for (String name : NAME_FIELD.extract(params).asSet()) {
* person.setName(name);
* }
* for (Gender name : GENDER_FIELD.extract(params).asSet()) {
* person.setGender(name);
* }
* return person.build();
* }</pre>
* public Person makePerson(Map<String, String> params) {
* Person.Builder person = new Person.Builder();
* for (String name : NAME_FIELD.extract(params).asSet()) {
* person.setName(name);
* }
* for (Gender name : GENDER_FIELD.extract(params).asSet()) {
* person.setGender(name);
* }
* return person.build();
* }
* }</pre>
*
* <p>This class provides <b>full type-safety</b> <i>if and only if</i> you statically initialize
* your FormField objects and write a unit test that causes the class to be loaded.
*
* <h3>Exception Handling</h3>
*
* <p>When values passed to {@link #convert} or {@link #extract} don't meet the contract,
* {@link FormFieldException} will be thrown, which provides the field name and a short error
* message that's safe to pass along to the client.
* <p>When values passed to {@link #convert} or {@link #extract} don't meet the contract, {@link
* FormFieldException} will be thrown, which provides the field name and a short error message
* that's safe to pass along to the client.
*
* <p>You can safely throw {@code FormFieldException} from within your validator functions, and
* the field name will automatically be propagated into the exception object for you.
* <p>You can safely throw {@code FormFieldException} from within your validator functions, and the
* field name will automatically be propagated into the exception object for you.
*
* <p>In situations when you're validating lists or maps, you'll end up with a hierarchical field
* naming structure. For example, if you were validating a list of maps, an error generated by the
@@ -92,23 +94,25 @@ import javax.annotation.concurrent.Immutable;
* <p>You should never assign a partially constructed {@code FormField.Builder} to a variable or
* constant. Instead, you should use {@link #asBuilder()} or {@link #asBuilderNamed(String)}.
*
* <p>Here is an example of how you might go about defining library definitions:<pre>
* <p>Here is an example of how you might go about defining library definitions:
*
* final class FormFields {
* private static final FormField<String, String> COUNTRY_CODE =
* FormField.named("countryCode")
* .range(Range.singleton(2))
* .uppercased()
* .in(ImmutableSet.copyOf(Locale.getISOCountries()))
* .build();
* }
* <pre>{@code
* final class FormFields {
* private static final FormField<String, String> COUNTRY_CODE =
* FormField.named("countryCode")
* .range(Range.singleton(2))
* .uppercased()
* .in(ImmutableSet.copyOf(Locale.getISOCountries()))
* .build();
* }
*
* final class Form {
* private static final FormField<String, String> COUNTRY_CODE_FIELD =
* FormFields.COUNTRY_CODE.asBuilder()
* .required()
* .build();
* }</pre>
* final class Form {
* private static final FormField<String, String> COUNTRY_CODE_FIELD =
* FormFields.COUNTRY_CODE.asBuilder()
* .required()
* .build();
* }
* }</pre>
*
* @param <I> input value type
* @param <O> output value type
@@ -105,9 +105,10 @@ public class SendEmailUtils {
}
}
/** Sends an email from Nomulus to the registrarChangesNotificationEmailAddresses.
/**
* Sends an email from Nomulus to the registrarChangesNotificationEmailAddresses.
*
* <p>See {@link #sendEmail(String, String, ImmutableList<String>)}.
* <p>See {@link #sendEmail(String, String, ImmutableList)}.
*/
public boolean sendEmail(final String subject, String body) {
return sendEmail(subject, body, ImmutableList.of());
@@ -58,40 +58,38 @@ import org.joda.time.Duration;
* <p>You can send AJAX requests to our WHOIS API from your <em>very own</em> website using the
* following embed code:
*
* <pre>
* <p>
* <input id="query-input" placeholder="Domain, Nameserver, IP, etc." autofocus>
* <button id="search-button">Lookup</button>
* <p>
* <pre id="whois-results"></pre>
* <script>
* (function() {
* var WHOIS_API_URL = 'https://domain-registry-alpha.appspot.com/whois/';
* function OnKeyPressQueryInput(ev) {
* if (typeof ev == 'undefined' && window.event) {
* ev = window.event;
* }
* if (ev.keyCode == 13) {
* document.getElementById('search-button').click();
* }
* <pre>{@code
* <input id="query-input" placeholder="Domain, Nameserver, IP, etc." autofocus>
* <button id="search-button">Lookup</button>
* <pre id="whois-results"></pre>
* <script>
* (function() {
* var WHOIS_API_URL = 'https://domain-registry-alpha.appspot.com/whois/';
* function OnKeyPressQueryInput(ev) {
* if (typeof ev == 'undefined' && window.event) {
* ev = window.event;
* }
* function OnClickSearchButton() {
* var query = document.getElementById('query-input').value;
* var req = new XMLHttpRequest();
* req.onreadystatechange = function() {
* if (req.readyState == 4) {
* var results = document.getElementById('whois-results');
* results.textContent = req.responseText;
* }
* };
* req.open('GET', WHOIS_API_URL + escape(query), true);
* req.send();
* if (ev.keyCode == 13) {
* document.getElementById('search-button').click();
* }
* document.getElementById('search-button').onclick = OnClickSearchButton;
* document.getElementById('query-input').onkeypress = OnKeyPressQueryInput;
* })();
* </script>
* </pre>
* }
* function OnClickSearchButton() {
* var query = document.getElementById('query-input').value;
* var req = new XMLHttpRequest();
* req.onreadystatechange = function() {
* if (req.readyState == 4) {
* var results = document.getElementById('whois-results');
* results.textContent = req.responseText;
* }
* };
* req.open('GET', WHOIS_API_URL + escape(query), true);
* req.send();
* }
* document.getElementById('search-button').onclick = OnClickSearchButton;
* document.getElementById('query-input').onkeypress = OnKeyPressQueryInput;
* })();
* </script>
* }</pre>
*
* @see WhoisAction
*/
@@ -19,6 +19,9 @@
* Move tests to another (sub)project. This is not a big problem, but feels unnatural.
* Use Hibernate's ServiceRegistry for bootstrapping (not JPA-compliant)
-->
<class>google.registry.model.billing.BillingEvent$Cancellation</class>
<class>google.registry.model.billing.BillingEvent$OneTime</class>
<class>google.registry.model.billing.BillingEvent$Recurring</class>
<class>google.registry.model.contact.ContactResource</class>
<class>google.registry.model.domain.DomainBase</class>
<class>google.registry.model.host.HostResource</class>
@@ -35,6 +38,8 @@
<class>google.registry.model.domain.GracePeriod</class>
<!-- Customized type converters -->
<class>google.registry.persistence.converter.BillingCostTransitionConverter</class>
<class>google.registry.persistence.converter.BillingEventFlagSetConverter</class>
<class>google.registry.persistence.converter.BloomFilterConverter</class>
<class>google.registry.persistence.converter.CidrAddressBlockListConverter</class>
<class>google.registry.persistence.converter.CreateAutoTimestampConverter</class>
@@ -47,10 +52,13 @@
<class>google.registry.persistence.converter.StatusValueSetConverter</class>
<class>google.registry.persistence.converter.StringListConverter</class>
<class>google.registry.persistence.converter.StringSetConverter</class>
<class>google.registry.persistence.converter.TldStateTransitionConverter</class>
<class>google.registry.persistence.converter.UpdateAutoTimestampConverter</class>
<class>google.registry.persistence.converter.ZonedDateTimeConverter</class>
<!-- Generated converters for VKey -->
<class>google.registry.model.billing.VKeyConverter_BillingEvent</class>
<class>google.registry.model.domain.token.VKeyConverter_AllocationToken</class>
<class>google.registry.model.host.VKeyConverter_HostResource</class>
<class>google.registry.model.contact.VKeyConverter_ContactResource</class>
@@ -55,7 +55,7 @@
{template .resultSuccess}
{@param baseClientId: string} /** The base clientId used for the OT&amp;E setup. */
{@param contactEmail: string} /** The contact's email added to the registrars. */
{@param clientIdToTld: map<string, string>} /** The created registrars->TLD mapping. */
{@param clientIdToTld: map<string, string>} /** The created registrars-&gt;TLD mapping. */
{@param password: string} /** The password given for the created registrars. */
{@param username: string} /** Arbitrary username to display. */
{@param logoutUrl: string} /** Generated URL for logging out of Google. */
@@ -155,7 +155,7 @@ public class ExpandRecurringBillingEventsActionTest
.setPeriodYears(1)
.setReason(Reason.RENEW)
.setSyntheticCreationTime(beginningOfTest)
.setCancellationMatchingBillingEvent(Key.create(recurring))
.setCancellationMatchingBillingEvent(recurring.createVKey())
.setTargetId(domain.getFullyQualifiedDomainName());
}
@@ -274,10 +274,12 @@ public class ExpandRecurringBillingEventsActionTest
.setParent(persistedEntries.get(0))
.build();
// Persist an otherwise identical billing event that differs only in recurring event key.
BillingEvent.OneTime persisted = expected.asBuilder()
.setParent(persistedEntries.get(1))
.setCancellationMatchingBillingEvent(Key.create(recurring2))
.build();
BillingEvent.OneTime persisted =
expected
.asBuilder()
.setParent(persistedEntries.get(1))
.setCancellationMatchingBillingEvent(recurring2.createVKey())
.build();
assertCursorAt(beginningOfTest);
assertBillingEventsForResource(domain, persisted, expected, recurring, recurring2);
}
@@ -604,19 +606,21 @@ public class ExpandRecurringBillingEventsActionTest
assertHistoryEntryMatches(
domain, persistedEntries.get(0), "TheRegistrar", DateTime.parse("2000-02-19T00:00:00Z"),
true);
BillingEvent.OneTime expected = defaultOneTimeBuilder()
.setParent(persistedEntries.get(0))
.setCancellationMatchingBillingEvent(Key.create(recurring))
.build();
BillingEvent.OneTime expected =
defaultOneTimeBuilder()
.setParent(persistedEntries.get(0))
.setCancellationMatchingBillingEvent(recurring.createVKey())
.build();
assertHistoryEntryMatches(
domain, persistedEntries.get(1), "TheRegistrar", DateTime.parse("2000-05-20T00:00:00Z"),
true);
BillingEvent.OneTime expected2 = defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2000-05-20T00:00:00Z"))
.setEventTime(DateTime.parse("2000-04-05T00:00:00Z"))
.setParent(persistedEntries.get(1))
.setCancellationMatchingBillingEvent(Key.create(recurring2))
.build();
BillingEvent.OneTime expected2 =
defaultOneTimeBuilder()
.setBillingTime(DateTime.parse("2000-05-20T00:00:00Z"))
.setEventTime(DateTime.parse("2000-04-05T00:00:00Z"))
.setParent(persistedEntries.get(1))
.setCancellationMatchingBillingEvent(recurring2.createVKey())
.build();
assertBillingEventsForResource(domain, expected, expected2, recurring, recurring2);
assertCursorAt(beginningOfTest);
}
@@ -17,6 +17,7 @@ package google.registry.export;
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.TaskQueueHelper.assertNoTasksEnqueued;
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
import static google.registry.testing.TestLogHandlerUtils.assertLogMessage;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -174,6 +175,7 @@ public class BigqueryPollJobActionTest {
action.run();
assertLogMessage(
logHandler, SEVERE, String.format("Bigquery job failed - %s:%s", PROJECT_ID, JOB_ID));
assertNoTasksEnqueued(CHAINED_QUEUE_NAME);
}
@Test
@@ -18,6 +18,7 @@ import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_AND_CLOSE;
import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.registry.Registry.TldState.GENERAL_AVAILABILITY;
import static google.registry.model.registry.Registry.TldState.PREDELEGATION;
import static google.registry.model.registry.Registry.TldState.START_DATE_SUNRISE;
@@ -26,6 +27,7 @@ import static google.registry.testing.DatastoreHelper.createTld;
import static google.registry.testing.DatastoreHelper.createTlds;
import static google.registry.testing.DatastoreHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatastoreHelper.persistResource;
import static google.registry.testing.DomainBaseSubject.assertAboutDomains;
import static google.registry.testing.EppMetricSubject.assertThat;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.USD;
@@ -84,19 +86,311 @@ public class EppLifecycleDomainTest extends EppTestCase {
"domain_create_response.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00.0Z",
"EXDATE", "2002-06-01T00:02:00.0Z"));
"CRDATE", "2000-06-01T00:02:00Z",
"EXDATE", "2002-06-01T00:02:00Z"));
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-07T00:02:00Z")
.hasResponse(
"domain_info_response_inactive.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
"EXDATE", "2002-06-01T00:02:00Z",
"UPDATE", "2000-06-06T00:02:00Z"));
// Delete domain example.tld after its add grace period has expired.
assertThatCommand("domain_delete.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-07-01T00:02:00Z")
.hasResponse("generic_success_action_pending_response.xml");
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-07-03T00:02:00Z")
.hasResponse(
"domain_info_response_redemptionperiod_wildcard.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
// The exp. date doesn't change because the deletion didn't cancel any charges.
"EXDATE", "2002-06-01T00:02:00Z",
"UPDATE", "2000-07-01T00:02:00Z"));
// Restore the domain.
assertThatCommand("domain_update_restore_request.xml")
.atTime("2000-07-01T00:03:00Z")
.hasResponse("generic_success_response.xml");
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-07-02T00:03:00Z")
.hasResponse(
"domain_info_response_inactive.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
// TODO(mcilwain): The exp. date should be restored back to 2002-06-01T00:02:00Z,
// but this is old behavior of being 1 year after the moment of the restore.
"EXDATE", "2001-07-01T00:03:00Z",
"UPDATE", "2000-07-01T00:03:00Z"));
assertThatLogoutSucceeds();
}
@Test
public void testDomainDeleteRestore_duringAutorenewGracePeriod() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
// Create domain example.tld
assertThatCommand(
"domain_create_no_hosts_or_dsdata.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-01T00:02:00Z")
.hasResponse(
"domain_create_response.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
"EXDATE", "2002-06-01T00:02:00Z"));
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-07T00:02:00Z")
.hasResponse(
"domain_info_response_inactive.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
"EXDATE", "2002-06-01T00:02:00Z",
"UPDATE", "2000-06-06T00:02:00Z"));
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2002-06-07T00:02:00Z")
.hasResponse(
"domain_info_response_graceperiod.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
// The exp. date has advanced 1 year because of autorenew.
"EXDATE", "2003-06-01T00:02:00Z",
// This is the time of the autorenew.
"UPDATE", "2002-06-01T00:02:00Z",
"GRACEPERIOD", "autoRenewPeriod"));
// Delete domain example.tld during its autorenew grace period.
assertThatCommand("domain_delete.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2002-07-01T00:02:00Z")
.hasResponse("generic_success_action_pending_response.xml");
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2002-07-03T00:02:00Z")
.hasResponse(
"domain_info_response_redemptionperiod_wildcard.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
// The exp. date reverts back to what it was originally because the deletion
// canceled out the autorenew.
"EXDATE", "2002-06-01T00:02:00Z",
"UPDATE", "2002-07-01T00:02:00Z"));
// Restore the domain.
assertThatCommand("domain_update_restore_request.xml")
.atTime("2002-07-05T00:03:00Z")
.hasResponse("generic_success_response.xml");
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2002-07-07T00:03:00Z")
.hasResponse(
"domain_info_response_inactive.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
// TODO(mcilwain): The exp. date should be 2003-06-01T00:02:00Z, the same as its
// value prior to the deletion, because the year that was taken off when the
// autorenew was canceled will be re-added in renewal during the restore.
// For now though, the current behavior is 1 year after restore.
"EXDATE", "2003-07-05T00:03:00Z",
"UPDATE", "2002-07-05T00:03:00Z"));
assertThatLogoutSucceeds();
}
@Test
public void testDomainDeleteRestore_duringRenewalGracePeriod() throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
// Create domain example.tld
assertThatCommand(
"domain_create_no_hosts_or_dsdata.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-01T00:02:00Z")
.hasResponse(
"domain_create_response.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
"EXDATE", "2002-06-01T00:02:00Z"));
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-07T00:02:00Z")
.hasResponse(
"domain_info_response_inactive.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
"EXDATE", "2002-06-01T00:02:00Z",
"UPDATE", "2000-06-06T00:02:00Z"));
assertThatCommand(
"domain_renew.xml",
ImmutableMap.of("DOMAIN", "example.tld", "EXPDATE", "2002-06-01", "YEARS", "3"))
.atTime("2000-06-08T00:00:00Z")
.hasResponse(
"domain_renew_response.xml",
ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2005-06-01T00:02:00Z"));
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-10T00:02:00Z")
.hasResponse(
"domain_info_response_graceperiod.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
// The exp. date is 5 years in total after the create.
"EXDATE", "2005-06-01T00:02:00Z",
// This is the time of the renew.
"UPDATE", "2000-06-08T00:00:00Z",
"GRACEPERIOD", "renewPeriod"));
// Delete domain example.tld during its renew grace period.
assertThatCommand("domain_delete.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-12T00:00:00Z")
.hasResponse("generic_success_action_pending_response.xml");
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-13T00:00:00Z")
.hasResponse(
"domain_info_response_redemptionperiod_wildcard.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
// The exp. date reverts back to what it was originally because the deletion
// canceled out the 3-year renewal.
"EXDATE", "2002-06-01T00:02:00Z",
"UPDATE", "2000-06-12T00:00:00Z"));
// Restore the domain.
assertThatCommand("domain_update_restore_request.xml")
.atTime("2000-06-20T00:00:00Z")
.hasResponse("generic_success_response.xml");
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-21T00:00:00Z")
.hasResponse(
"domain_info_response_inactive.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
// TODO(mcilwain): The exp. date should be 2002-06-01T00:02:00Z, which is the
// current registration expiration time on the (deleted) domain, but for now is
// 1 year after restore.
"EXDATE", "2001-06-20T00:00:00Z",
"UPDATE", "2000-06-20T00:00:00Z"));
assertThatLogoutSucceeds();
}
@Test
public void testDomainDelete_duringAddAndRenewalGracePeriod_deletesImmediately()
throws Exception {
assertThatLoginSucceeds("NewRegistrar", "foo-BAR2");
createContacts(DateTime.parse("2000-06-01T00:00:00Z"));
DateTime createTime = DateTime.parse("2000-06-01T00:02:00Z");
// Create domain example.tld
assertThatCommand(
"domain_create_no_hosts_or_dsdata.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime(createTime)
.hasResponse(
"domain_create_response.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
"EXDATE", "2002-06-01T00:02:00Z"));
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-02T00:02:00Z")
.hasResponse(
"domain_info_response_addperiod_wildcard.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
"EXDATE", "2002-06-01T00:02:00Z"));
DateTime renewTime = DateTime.parse("2000-06-03T00:00:00Z");
assertThatCommand(
"domain_renew.xml",
ImmutableMap.of("DOMAIN", "example.tld", "EXPDATE", "2002-06-01", "YEARS", "3"))
.atTime(renewTime)
.hasResponse(
"domain_renew_response.xml",
ImmutableMap.of("DOMAIN", "example.tld", "EXDATE", "2005-06-01T00:02:00Z"));
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-03T03:00:00Z")
.hasResponse(
"domain_info_response_graceperiod_add_and_renew.xml",
ImmutableMap.of(
"DOMAIN", "example.tld",
"CRDATE", "2000-06-01T00:02:00Z",
// The exp. date is 5 years in total after the create.
"EXDATE", "2005-06-01T00:02:00Z",
// This is the time of the renew.
"UPDATE", "2000-06-03T00:00:00Z"));
DomainBase domain =
loadByForeignKey(DomainBase.class, "example.tld", DateTime.parse("2000-06-03T04:00:00Z"))
.get();
DateTime deleteTime = DateTime.parse("2000-06-04T00:00:00Z");
// Delete domain example.tld during both grace periods.
assertThatCommand("domain_delete.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-04T00:00:00Z")
.hasResponse("generic_success_response.xml");
// Verify that it is immediately non-existent.
assertThatCommand("domain_info.xml", ImmutableMap.of("DOMAIN", "example.tld"))
.atTime("2000-06-04T00:01:00Z")
.hasResponse(
"response_error.xml",
ImmutableMap.of(
"CODE", "2303", "MSG", "The domain with given ID (example.tld) doesn't exist."));
// The expected one-time billing event, that should have an associated Cancellation.
OneTime oneTimeCreateBillingEvent = makeOneTimeCreateBillingEvent(domain, createTime);
OneTime oneTimeRenewBillingEvent = makeOneTimeRenewBillingEvent(domain, renewTime);
// Verify that the OneTime billing event associated with the domain creation is canceled.
assertBillingEventsForResource(
domain,
// There should be one-time billing events for the create and the renew.
oneTimeCreateBillingEvent,
oneTimeRenewBillingEvent,
// There should be two ended recurring billing events, one each from the create and renew.
// (The former was ended by the renew and the latter was ended by the delete.)
makeRecurringCreateBillingEvent(domain, createTime.plusYears(2), renewTime),
makeRecurringRenewBillingEvent(domain, createTime.plusYears(5), deleteTime),
// There should be Cancellations offsetting both of the one-times.
makeCancellationBillingEventForCreate(
domain, oneTimeCreateBillingEvent, createTime, deleteTime),
makeCancellationBillingEventForRenew(
domain, oneTimeRenewBillingEvent, renewTime, deleteTime));
// Verify that the registration expiration time was set back to the creation time, because the
// entire cost of registration was refunded. We have to do this through the DB instead of EPP
// because domains deleted during the add grace period vanish immediately as far as the world
// outside our system is concerned.
DomainBase deletedDomain = ofy().load().entity(domain).now();
assertAboutDomains().that(deletedDomain).hasRegistrationExpirationTime(createTime);
assertThatLogoutSucceeds();
}
@@ -143,9 +437,16 @@ public class EppLifecycleDomainTest extends EppTestCase {
oneTimeCreateBillingEvent,
makeRecurringCreateBillingEvent(domain, createTime.plusYears(2), deleteTime),
// Check for the existence of a cancellation for the given one-time billing event.
makeCancellationBillingEventFor(
makeCancellationBillingEventForCreate(
domain, oneTimeCreateBillingEvent, createTime, deleteTime));
// Verify that the registration expiration time was set back to the creation time, because the
// entire cost of registration was refunded. We have to do this through the DB instead of EPP
// because domains deleted during the add grace period vanish immediately as far as the world
// outside our system is concerned.
DomainBase deletedDomain = ofy().load().entity(domain).now();
assertAboutDomains().that(deletedDomain).hasRegistrationExpirationTime(createTime);
assertThatLogoutSucceeds();
}
@@ -268,7 +569,7 @@ public class EppLifecycleDomainTest extends EppTestCase {
expectedCreateEapBillingEvent,
makeRecurringCreateBillingEvent(domain, createTime.plusYears(2), deleteTime),
// ... and verify that the create one-time billing event was canceled ...
makeCancellationBillingEventFor(
makeCancellationBillingEventForCreate(
domain, expectedOneTimeCreateBillingEvent, createTime, deleteTime));
// ... but there was NOT a Cancellation for the EAP fee, as this would fail if additional
// billing events were present.
@@ -41,6 +41,7 @@ import google.registry.model.registry.Registry;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.reporting.HistoryEntry.Type;
import google.registry.monitoring.whitebox.EppMetric;
import google.registry.persistence.VKey;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeHttpSession;
import google.registry.testing.FakeResponse;
@@ -284,20 +285,42 @@ public class EppTestCase extends ShardableTestCase {
.setCost(Money.parse("USD 26.00"))
.setPeriodYears(2)
.setEventTime(createTime)
.setBillingTime(createTime.plus(Registry.get(domain.getTld()).getRenewGracePeriodLength()))
.setBillingTime(createTime.plus(Registry.get(domain.getTld()).getAddGracePeriodLength()))
.setParent(getOnlyHistoryEntryOfType(domain, Type.DOMAIN_CREATE))
.build();
}
/** Makes a one-time billing event corresponding to the given domain's renewal. */
protected static BillingEvent.OneTime makeOneTimeRenewBillingEvent(
DomainBase domain, DateTime renewTime) {
return new BillingEvent.OneTime.Builder()
.setReason(Reason.RENEW)
.setTargetId(domain.getFullyQualifiedDomainName())
.setClientId(domain.getCurrentSponsorClientId())
.setCost(Money.parse("USD 33.00"))
.setPeriodYears(3)
.setEventTime(renewTime)
.setBillingTime(renewTime.plus(Registry.get(domain.getTld()).getRenewGracePeriodLength()))
.setParent(getOnlyHistoryEntryOfType(domain, Type.DOMAIN_RENEW))
.build();
}
/** Makes a recurring billing event corresponding to the given domain's creation. */
protected static BillingEvent.Recurring makeRecurringCreateBillingEvent(
DomainBase domain, DateTime eventTime, DateTime endTime) {
return makeRecurringCreateBillingEvent(
return makeRecurringBillingEvent(
domain, getOnlyHistoryEntryOfType(domain, Type.DOMAIN_CREATE), eventTime, endTime);
}
/** Makes a recurring billing event corresponding to the given domain's renewal. */
protected static BillingEvent.Recurring makeRecurringRenewBillingEvent(
DomainBase domain, DateTime eventTime, DateTime endTime) {
return makeRecurringBillingEvent(
domain, getOnlyHistoryEntryOfType(domain, Type.DOMAIN_RENEW), eventTime, endTime);
}
/** Makes a recurring billing event corresponding to the given history entry. */
protected static BillingEvent.Recurring makeRecurringCreateBillingEvent(
protected static BillingEvent.Recurring makeRecurringBillingEvent(
DomainBase domain, HistoryEntry historyEntry, DateTime eventTime, DateTime endTime) {
return new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
@@ -311,22 +334,35 @@ public class EppTestCase extends ShardableTestCase {
}
/** Makes a cancellation billing event cancelling out the given domain create billing event. */
protected static BillingEvent.Cancellation makeCancellationBillingEventFor(
DomainBase domain,
OneTime billingEventToCancel,
DateTime createTime,
DateTime deleteTime) {
protected static BillingEvent.Cancellation makeCancellationBillingEventForCreate(
DomainBase domain, OneTime billingEventToCancel, DateTime createTime, DateTime deleteTime) {
return new BillingEvent.Cancellation.Builder()
.setTargetId(domain.getFullyQualifiedDomainName())
.setClientId(domain.getCurrentSponsorClientId())
.setEventTime(deleteTime)
.setOneTimeEventKey(findKeyToActualOneTimeBillingEvent(billingEventToCancel))
.setBillingTime(createTime.plus(Registry.get(domain.getTld()).getRenewGracePeriodLength()))
.setOneTimeEventKey(
VKey.createOfy(OneTime.class, findKeyToActualOneTimeBillingEvent(billingEventToCancel)))
.setBillingTime(createTime.plus(Registry.get(domain.getTld()).getAddGracePeriodLength()))
.setReason(Reason.CREATE)
.setParent(getOnlyHistoryEntryOfType(domain, Type.DOMAIN_DELETE))
.build();
}
/** Makes a cancellation billing event cancelling out the given domain renew billing event. */
protected static BillingEvent.Cancellation makeCancellationBillingEventForRenew(
DomainBase domain, OneTime billingEventToCancel, DateTime renewTime, DateTime deleteTime) {
return new BillingEvent.Cancellation.Builder()
.setTargetId(domain.getFullyQualifiedDomainName())
.setClientId(domain.getCurrentSponsorClientId())
.setEventTime(deleteTime)
.setOneTimeEventKey(
VKey.createOfy(OneTime.class, findKeyToActualOneTimeBillingEvent(billingEventToCancel)))
.setBillingTime(renewTime.plus(Registry.get(domain.getTld()).getRenewGracePeriodLength()))
.setReason(Reason.RENEW)
.setParent(getOnlyHistoryEntryOfType(domain, Type.DOMAIN_DELETE))
.build();
}
/**
* Finds the Key to the actual one-time create billing event associated with a domain's creation.
*
@@ -276,7 +276,7 @@ public class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow,
.setBillingTime(billingTime)
.setFlags(expectedBillingFlags)
.setParent(historyEntry)
.setAllocationToken(allocationToken == null ? null : Key.create(allocationToken))
.setAllocationToken(allocationToken == null ? null : allocationToken.createVKey())
.build();
BillingEvent.Recurring renewBillingEvent =
@@ -214,7 +214,7 @@ public class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow,
.setClientId("TheRegistrar")
.setEventTime(eventTime)
.setBillingTime(TIME_BEFORE_FLOW.plusDays(1))
.setOneTimeEventKey(Key.create(graceBillingEvent))
.setOneTimeEventKey(graceBillingEvent.createVKey())
.setParent(historyEntryDomainDelete)
.build());
}
@@ -391,7 +391,7 @@ public class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow,
GracePeriod.create(GracePeriodStatus.TRANSFER, TIME_BEFORE_FLOW.plusDays(1), "foo", null));
// We should see exactly one poll message, which is for the autorenew 1 month in the future.
assertPollMessages(createAutorenewPollMessage("TheRegistrar").build());
DateTime originalExpirationTime = domain.getRegistrationExpirationTime();
DateTime expectedExpirationTime = domain.getRegistrationExpirationTime().minusYears(2);
clock.advanceOneMilli();
runFlowAssertResponse(loadFile(responseFilename, substitutions));
DomainBase resource = reloadResourceByForeignKey();
@@ -413,7 +413,7 @@ public class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow,
// deletion time, that means once it passes the domain will experience a "phantom autorenew"
// where the expirationTime advances and the grace period appears, but since the delete flow
// closed the autorenew recurrences immediately, there are no other autorenew effects.
assertAboutDomains().that(resource).hasRegistrationExpirationTime(originalExpirationTime);
assertAboutDomains().that(resource).hasRegistrationExpirationTime(expectedExpirationTime);
// All existing grace periods that were for billable actions should cause cancellations.
assertAutorenewClosedAndCancellationCreatedFor(
renewBillingEvent, getOnlyHistoryEntryOfType(resource, DOMAIN_DELETE));
@@ -71,6 +71,7 @@ import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import java.util.Arrays;
import java.util.stream.Stream;
import org.joda.money.Money;
@@ -402,7 +403,8 @@ public class DomainTransferApproveFlowTest
.setEventTime(clock.nowUtc()) // The cancellation happens at the moment of transfer.
.setBillingTime(
oldExpirationTime.plus(Registry.get("tld").getAutoRenewGracePeriodLength()))
.setRecurringEventKey(domain.getAutorenewBillingEvent()));
.setRecurringEventKey(
VKey.createOfy(BillingEvent.Recurring.class, domain.getAutorenewBillingEvent())));
}
@Test
@@ -100,6 +100,7 @@ import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.VKey;
import google.registry.testing.TaskQueueHelper.TaskMatcher;
import java.util.Map;
import java.util.Optional;
@@ -1136,7 +1137,8 @@ public class DomainTransferRequestFlowTest
.setEventTime(clock.nowUtc().plus(Registry.get("tld").getAutomaticTransferLength()))
.setBillingTime(autorenewTime.plus(Registry.get("tld").getAutoRenewGracePeriodLength()))
// The cancellation should refer to the old autorenew billing event.
.setRecurringEventKey(existingAutorenewEvent));
.setRecurringEventKey(
VKey.createOfy(BillingEvent.Recurring.class, existingAutorenewEvent)));
}
@Test
@@ -1164,7 +1166,8 @@ public class DomainTransferRequestFlowTest
.setBillingTime(
expirationTime.plus(Registry.get("tld").getAutoRenewGracePeriodLength()))
// The cancellation should refer to the old autorenew billing event.
.setRecurringEventKey(existingAutorenewEvent));
.setRecurringEventKey(
VKey.createOfy(BillingEvent.Recurring.class, existingAutorenewEvent)));
}
@Test
@@ -28,6 +28,7 @@ import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.annotation.Serialize;
import com.googlecode.objectify.cmd.Query;
import google.registry.model.ofy.Ofy;
import google.registry.persistence.VKey;
import google.registry.testing.AppEngineRule;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectRule;
@@ -164,7 +165,8 @@ public abstract class EntityTestCase {
: (Class<?>) inner;
}
// Descend into persisted ImmutableObject classes, but not anything else.
if (ImmutableObject.class.isAssignableFrom(fieldClass)) {
if (ImmutableObject.class.isAssignableFrom(fieldClass)
&& !VKey.class.isAssignableFrom(fieldClass)) {
getAllPotentiallyIndexedFieldPaths(fieldClass).stream()
.map(subfield -> field.getName() + "." + subfield)
.distinct()
@@ -17,9 +17,11 @@ package google.registry.model.billing;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.domain.token.AllocationToken.TokenType.UNLIMITED_USE;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.DatastoreHelper.createTld;
import static google.registry.testing.DatastoreHelper.persistActiveDomain;
import static google.registry.testing.DatastoreHelper.persistResource;
import static google.registry.testing.SqlHelper.saveRegistrar;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static org.joda.money.CurrencyUnit.USD;
import static org.joda.time.DateTimeZone.UTC;
@@ -37,19 +39,25 @@ import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import google.registry.util.DateTimeUtils;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link BillingEvent}. */
public class BillingEventTest extends EntityTestCase {
private final DateTime now = DateTime.now(UTC);
public BillingEventTest() {
super(true);
}
HistoryEntry historyEntry;
HistoryEntry historyEntry2;
DomainBase domain;
BillingEvent.OneTime sqlOneTime;
BillingEvent.OneTime oneTime;
BillingEvent.OneTime oneTimeSynthetic;
BillingEvent.Recurring recurring;
@@ -57,7 +65,7 @@ public class BillingEventTest extends EntityTestCase {
BillingEvent.Cancellation cancellationRecurring;
BillingEvent.Modification modification;
@Before
@BeforeEach
public void setUp() {
createTld("tld");
domain = persistActiveDomain("foo.tld");
@@ -97,7 +105,18 @@ public class BillingEventTest extends EntityTestCase {
.setCost(Money.of(USD, 1))
.setEventTime(now)
.setBillingTime(now.plusDays(5))
.setAllocationToken(Key.create(allocationToken))));
.setAllocationToken(allocationToken.createVKey())));
sqlOneTime =
oneTime
.asBuilder()
.setDomainRepoId(domain.getRepoId())
.setDomainHistoryRevisionId(1L)
.setAllocationToken(
VKey.create(
AllocationToken.class, allocationToken.getToken(), Key.create(allocationToken)))
.build();
recurring =
persistResource(
commonInit(
@@ -107,31 +126,41 @@ public class BillingEventTest extends EntityTestCase {
.setReason(Reason.RENEW)
.setEventTime(now.plusYears(1))
.setRecurrenceEndTime(END_OF_TIME)));
oneTimeSynthetic = persistResource(commonInit(
new BillingEvent.OneTime.Builder()
.setParent(historyEntry)
.setReason(Reason.CREATE)
.setFlags(ImmutableSet.of(BillingEvent.Flag.ANCHOR_TENANT, BillingEvent.Flag.SYNTHETIC))
.setSyntheticCreationTime(now.plusDays(10))
.setCancellationMatchingBillingEvent(Key.create(recurring))
.setPeriodYears(2)
.setCost(Money.of(USD, 1))
.setEventTime(now)
.setBillingTime(now.plusDays(5))));
cancellationOneTime = persistResource(commonInit(
new BillingEvent.Cancellation.Builder()
.setParent(historyEntry2)
.setReason(Reason.CREATE)
.setEventTime(now.plusDays(1))
.setBillingTime(now.plusDays(5))
.setOneTimeEventKey(Key.create(oneTime))));
cancellationRecurring = persistResource(commonInit(
new BillingEvent.Cancellation.Builder()
.setParent(historyEntry2)
.setReason(Reason.RENEW)
.setEventTime(now.plusDays(1))
.setBillingTime(now.plusYears(1).plusDays(45))
.setRecurringEventKey(Key.create(recurring))));
oneTimeSynthetic =
persistResource(
commonInit(
new BillingEvent.OneTime.Builder()
.setParent(historyEntry)
.setReason(Reason.CREATE)
.setFlags(
ImmutableSet.of(
BillingEvent.Flag.ANCHOR_TENANT, BillingEvent.Flag.SYNTHETIC))
.setSyntheticCreationTime(now.plusDays(10))
.setCancellationMatchingBillingEvent(recurring.createVKey())
.setPeriodYears(2)
.setCost(Money.of(USD, 1))
.setEventTime(now)
.setBillingTime(now.plusDays(5))));
cancellationOneTime =
persistResource(
commonInit(
new BillingEvent.Cancellation.Builder()
.setParent(historyEntry2)
.setReason(Reason.CREATE)
.setEventTime(now.plusDays(1))
.setBillingTime(now.plusDays(5))
.setOneTimeEventKey(oneTime.createVKey())));
cancellationRecurring =
persistResource(
commonInit(
new BillingEvent.Cancellation.Builder()
.setParent(historyEntry2)
.setReason(Reason.RENEW)
.setEventTime(now.plusDays(1))
.setBillingTime(now.plusYears(1).plusDays(45))
.setRecurringEventKey(recurring.createVKey())));
modification = persistResource(commonInit(
new BillingEvent.Modification.Builder()
.setParent(historyEntry2)
@@ -149,6 +178,83 @@ public class BillingEventTest extends EntityTestCase {
.build();
}
private void saveNewBillingEvent(BillingEvent billingEvent) {
billingEvent.id = null;
jpaTm().transact(() -> jpaTm().saveNew(billingEvent));
}
@Test
public void testCloudSqlPersistence_OneTime() {
saveRegistrar("a registrar");
saveNewBillingEvent(sqlOneTime);
BillingEvent.OneTime persisted =
jpaTm()
.transact(
() -> jpaTm().load(VKey.createSql(BillingEvent.OneTime.class, sqlOneTime.id)));
// TODO(shicong): Remove these fixes after the entities are fully compatible
BillingEvent.OneTime fixed =
persisted
.asBuilder()
.setParent(sqlOneTime.getParentKey())
.setAllocationToken(sqlOneTime.getAllocationToken().get())
.build();
assertThat(fixed).isEqualTo(sqlOneTime);
}
@Test
public void testCloudSqlPersistence_Cancellation() {
saveRegistrar("a registrar");
saveNewBillingEvent(sqlOneTime);
VKey<BillingEvent.OneTime> sqlVKey = VKey.createSql(BillingEvent.OneTime.class, sqlOneTime.id);
BillingEvent sqlCancellationOneTime =
cancellationOneTime
.asBuilder()
.setOneTimeEventKey(sqlVKey)
.setDomainRepoId(domain.getRepoId())
.setDomainHistoryRevisionId(1L)
.build();
saveNewBillingEvent(sqlCancellationOneTime);
BillingEvent.Cancellation persisted =
jpaTm()
.transact(
() ->
jpaTm()
.load(
VKey.createSql(
BillingEvent.Cancellation.class, sqlCancellationOneTime.id)));
// TODO(shicong): Remove these fixes after the entities are fully compatible
BillingEvent.Cancellation fixed =
persisted
.asBuilder()
.setParent(sqlCancellationOneTime.getParentKey())
.setOneTimeEventKey(sqlVKey)
.build();
assertThat(fixed).isEqualTo(sqlCancellationOneTime);
}
@Test
public void testCloudSqlPersistence_Recurring() {
saveRegistrar("a registrar");
BillingEvent.Recurring sqlRecurring =
recurring
.asBuilder()
.setDomainRepoId(domain.getRepoId())
.setDomainHistoryRevisionId(1L)
.build();
saveNewBillingEvent(sqlRecurring);
BillingEvent.Recurring persisted =
jpaTm()
.transact(
() -> jpaTm().load(VKey.createSql(BillingEvent.Recurring.class, sqlRecurring.id)));
// TODO(shicong): Remove these fixes after the entities are fully compatible
BillingEvent.Recurring fixed =
persisted.asBuilder().setParent(sqlRecurring.getParentKey()).build();
assertThat(fixed).isEqualTo(sqlRecurring);
}
@Test
public void testPersistence() {
assertThat(ofy().load().entity(oneTime).now()).isEqualTo(oneTime);
@@ -183,8 +289,13 @@ public class BillingEventTest extends EntityTestCase {
@Test
public void testCancellationMatching() {
Key<?> recurringKey = ofy().load().entity(oneTimeSynthetic).now()
.getCancellationMatchingBillingEvent();
Key<?> recurringKey =
ofy()
.load()
.entity(oneTimeSynthetic)
.now()
.getCancellationMatchingBillingEvent()
.getOfyKey();
assertThat(ofy().load().key(recurringKey).now()).isEqualTo(recurring);
}
@@ -219,7 +330,7 @@ public class BillingEventTest extends EntityTestCase {
oneTime
.asBuilder()
.setFlags(ImmutableSet.of(BillingEvent.Flag.SYNTHETIC))
.setCancellationMatchingBillingEvent(Key.create(recurring))
.setCancellationMatchingBillingEvent(recurring.createVKey())
.build());
assertThat(thrown)
.hasMessageThat()
@@ -263,7 +374,7 @@ public class BillingEventTest extends EntityTestCase {
() ->
oneTime
.asBuilder()
.setCancellationMatchingBillingEvent(Key.create(recurring))
.setCancellationMatchingBillingEvent(recurring.createVKey())
.build());
assertThat(thrown)
.hasMessageThat()
@@ -334,8 +445,8 @@ public class BillingEventTest extends EntityTestCase {
() ->
cancellationOneTime
.asBuilder()
.setOneTimeEventKey(Key.create(oneTime))
.setRecurringEventKey(Key.create(recurring))
.setOneTimeEventKey(oneTime.createVKey())
.setRecurringEventKey(recurring.createVKey())
.build());
assertThat(thrown).hasMessageThat().contains("exactly one billing event");
}
@@ -119,7 +119,8 @@ public class DomainBaseSqlTest {
.transact(
() -> {
// Persist the contacts. Note that these need to be persisted before the domain
// otherwise we get a foreign key constraint error.
// otherwise we get a foreign key constraint error. If we ever decide to defer the
// relevant foreign key checks to commit time, then the order would not matter.
jpaTm().saveNew(contact);
jpaTm().saveNew(contact2);
@@ -127,7 +128,8 @@ public class DomainBaseSqlTest {
jpaTm().saveNew(domain);
// Persist the host. This does _not_ need to be persisted before the domain,
// presumably because its relationship is stored in a join table.
// because only the row in the join table (DomainHost) is subject to foreign key
// constraints, and Hibernate knows to insert it after domain and host.
jpaTm().saveNew(host);
});
@@ -771,60 +771,4 @@ public class DomainBaseTest extends EntityTestCase {
assertThat(getOnlyElement(clone.getGracePeriods()).getType())
.isEqualTo(GracePeriodStatus.TRANSFER);
}
private static ImmutableSet<Key<HostResource>> getOfyNameservers(DomainBase domain) {
return domain.getNameservers().stream().map(key -> key.getOfyKey()).collect(toImmutableSet());
}
@Test
public void testNameservers_nsHostsOfyKeys() {
assertThat(domain.nsHosts).isEqualTo(getOfyNameservers(domain));
// Test the setNameserver that functions on a function.
VKey<HostResource> host1Key =
persistResource(
new HostResource.Builder()
.setFullyQualifiedHostName("ns2.example.com")
.setSuperordinateDomain(domainKey)
.setRepoId("2-COM")
.build())
.createKey();
DomainBase dom = new DomainBase.Builder(domain).setNameservers(host1Key).build();
assertThat(dom.getNameservers()).isEqualTo(ImmutableSet.of(host1Key));
assertThat(getOfyNameservers(dom)).isEqualTo(ImmutableSet.of(host1Key.getOfyKey()));
// Test that setting to a single host of null throws an NPE.
assertThrows(
NullPointerException.class,
() -> new DomainBase.Builder(domain).setNameservers((VKey<HostResource>) null));
// Test that setting to a set of values works.
VKey<HostResource> host2Key =
persistResource(
new HostResource.Builder()
.setFullyQualifiedHostName("ns3.example.com")
.setSuperordinateDomain(domainKey)
.setRepoId("3-COM")
.build())
.createKey();
dom =
new DomainBase.Builder(domain).setNameservers(ImmutableSet.of(host1Key, host2Key)).build();
assertThat(dom.getNameservers()).isEqualTo(ImmutableSet.of(host1Key, host2Key));
assertThat(getOfyNameservers(dom))
.isEqualTo(ImmutableSet.of(host1Key.getOfyKey(), host2Key.getOfyKey()));
// Set of values, passing null.
dom =
new DomainBase.Builder(domain)
.setNameservers((ImmutableSet<VKey<HostResource>>) null)
.build();
assertThat(dom.nsHostVKeys).isNull();
assertThat(dom.nsHosts).isNull();
// Empty set of values gets translated to null.
dom = new DomainBase.Builder(domain).setNameservers(ImmutableSet.of()).build();
assertThat(dom.nsHostVKeys).isNull();
assertThat(dom.nsHosts).isNull();
}
}
@@ -0,0 +1,77 @@
// Copyright 2020 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.persistence.converter;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.money.CurrencyUnit.USD;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.ImmutableObject;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.registry.Registry.BillingCostTransition;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link BillingCostTransitionConverter}. */
public class BillingCostTransitionConverterTest {
@RegisterExtension
public final JpaUnitTestRule jpa =
new JpaTestRules.Builder()
.withInitScript("sql/flyway/V14__load_extension_for_hstore.sql")
.withEntityClass(TestEntity.class)
.buildUnitTestRule();
private static final ImmutableSortedMap<DateTime, Money> values =
ImmutableSortedMap.of(
START_OF_TIME,
Money.of(USD, 8),
DateTime.parse("2001-01-01T00:00:00.0Z"),
Money.of(USD, 0));
@Test
void roundTripConversion_returnsSameTimedTransitionProperty() {
TimedTransitionProperty<Money, BillingCostTransition> timedTransitionProperty =
TimedTransitionProperty.fromValueMap(values, BillingCostTransition.class);
TestEntity testEntity = new TestEntity(timedTransitionProperty);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity));
TestEntity persisted =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id"));
assertThat(persisted.timedTransitionProperty).containsExactlyEntriesIn(timedTransitionProperty);
}
@Entity(name = "TestEntity")
private static class TestEntity extends ImmutableObject {
@Id String name = "id";
TimedTransitionProperty<Money, BillingCostTransition> timedTransitionProperty;
private TestEntity() {}
private TestEntity(
TimedTransitionProperty<Money, BillingCostTransition> timedTransitionProperty) {
this.timedTransitionProperty = timedTransitionProperty;
}
}
}
@@ -0,0 +1,181 @@
// Copyright 2020 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.persistence.converter;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Maps;
import google.registry.model.ImmutableObject;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.common.TimedTransitionProperty.TimedTransition;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule;
import java.util.Map;
import javax.persistence.Converter;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NoResultException;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link TimedTransitionPropertyConverterBase}. */
public class TimedTransitionPropertyConverterBaseTest {
@RegisterExtension
public final JpaUnitTestRule jpa =
new JpaTestRules.Builder()
.withInitScript("sql/flyway/V14__load_extension_for_hstore.sql")
.withEntityClass(TestTimedTransitionPropertyConverter.class, TestEntity.class)
.buildUnitTestRule();
private static final DateTime DATE_1 = DateTime.parse("2001-01-01T00:00:00.000Z");
private static final DateTime DATE_2 = DateTime.parse("2002-01-01T00:00:00.000Z");
private static final ImmutableSortedMap<DateTime, String> VALUES =
ImmutableSortedMap.of(
START_OF_TIME, "val1",
DATE_1, "val2",
DATE_2, "val3");
private static final TimedTransitionProperty<String, TestTransition> TIMED_TRANSITION_PROPERTY =
TimedTransitionProperty.fromValueMap(VALUES, TestTransition.class);
@Test
void roundTripConversion_returnsSameTimedTransitionProperty() {
TestEntity testEntity = new TestEntity(TIMED_TRANSITION_PROPERTY);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity));
TestEntity persisted =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id"));
assertThat(persisted.property).containsExactlyEntriesIn(TIMED_TRANSITION_PROPERTY);
}
@Test
void testUpdateColumn_succeeds() {
TestEntity testEntity = new TestEntity(TIMED_TRANSITION_PROPERTY);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity));
TestEntity persisted =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id"));
assertThat(persisted.property).containsExactlyEntriesIn(TIMED_TRANSITION_PROPERTY);
ImmutableSortedMap<DateTime, String> newValues = ImmutableSortedMap.of(START_OF_TIME, "val4");
persisted.property = TimedTransitionProperty.fromValueMap(newValues, TestTransition.class);
jpaTm().transact(() -> jpaTm().getEntityManager().merge(persisted));
TestEntity updated =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id"));
assertThat(updated.property.toValueMap()).isEqualTo(newValues);
}
@Test
void testNullValue_writesAndReadsNullSuccessfully() {
TestEntity testEntity = new TestEntity(null);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity));
TestEntity persisted =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id"));
assertThat(persisted.property).isNull();
}
@Test
void testNativeQuery_succeeds() {
executeNativeQuery(
"INSERT INTO \"TestEntity\" (name, property) VALUES ('id',"
+ " 'val1=>1970-01-01T00:00:00.000Z, val2=>2001-01-01T00:00:00.000Z')");
assertThat(
getSingleResultFromNativeQuery(
"SELECT property -> 'val1' FROM \"TestEntity\" WHERE name = 'id'"))
.isEqualTo(START_OF_TIME.toString());
assertThat(
getSingleResultFromNativeQuery(
"SELECT property -> 'val2' FROM \"TestEntity\" WHERE name = 'id'"))
.isEqualTo(DATE_1.toString());
executeNativeQuery(
"UPDATE \"TestEntity\" SET property = 'val3=>2002-01-01T00:00:00.000Z' WHERE name = 'id'");
assertThat(
getSingleResultFromNativeQuery(
"SELECT property -> 'val3' FROM \"TestEntity\" WHERE name = 'id'"))
.isEqualTo(DATE_2.toString());
executeNativeQuery("DELETE FROM \"TestEntity\" WHERE name = 'id'");
assertThrows(
NoResultException.class,
() ->
getSingleResultFromNativeQuery(
"SELECT property -> 'val3' FROM \"TestEntity\" WHERE name = 'id'"));
}
private static Object getSingleResultFromNativeQuery(String sql) {
return jpaTm()
.transact(() -> jpaTm().getEntityManager().createNativeQuery(sql).getSingleResult());
}
private static void executeNativeQuery(String sql) {
jpaTm().transact(() -> jpaTm().getEntityManager().createNativeQuery(sql).executeUpdate());
}
public static class TestTransition extends TimedTransition<String> {
private String transition;
@Override
public String getValue() {
return transition;
}
@Override
protected void setValue(String transition) {
this.transition = transition;
}
}
@Converter(autoApply = true)
private static class TestTimedTransitionPropertyConverter
extends TimedTransitionPropertyConverterBase<String, TestTransition> {
@Override
Map.Entry<DateTime, String> convertToEntityMapEntry(Map.Entry<String, String> entry) {
return Maps.immutableEntry(DateTime.parse(entry.getKey()), entry.getValue());
}
@Override
Class<TestTransition> getTimedTransitionSubclass() {
return TestTransition.class;
}
@Override
Map.Entry<String, String> convertToDatabaseMapEntry(Map.Entry<DateTime, TestTransition> entry) {
return Maps.immutableEntry(entry.getKey().toString(), entry.getValue().getValue());
}
}
@Entity(name = "TestEntity") // Override entity name to avoid the nested class reference.
private static class TestEntity extends ImmutableObject {
@Id String name = "id";
TimedTransitionProperty<String, TestTransition> property;
private TestEntity() {}
private TestEntity(TimedTransitionProperty<String, TestTransition> timedTransitionProperty) {
this.property = timedTransitionProperty;
}
}
}
@@ -0,0 +1,80 @@
// Copyright 2020 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.persistence.converter;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.model.ImmutableObject;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.model.registry.Registry.TldState;
import google.registry.model.registry.Registry.TldStateTransition;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link TldStateTransitionConverter}. */
public class TldStateTransitionConverterTest {
@RegisterExtension
public final JpaUnitTestRule jpa =
new JpaTestRules.Builder()
.withInitScript("sql/flyway/V14__load_extension_for_hstore.sql")
.withEntityClass(TestEntity.class)
.buildUnitTestRule();
private static final ImmutableSortedMap<DateTime, TldState> values =
ImmutableSortedMap.of(
START_OF_TIME,
TldState.PREDELEGATION,
DateTime.parse("2001-01-01T00:00:00.0Z"),
TldState.QUIET_PERIOD,
DateTime.parse("2002-01-01T00:00:00.0Z"),
TldState.PDT,
DateTime.parse("2003-01-01T00:00:00.0Z"),
TldState.GENERAL_AVAILABILITY);
@Test
void roundTripConversion_returnsSameTimedTransitionProperty() {
TimedTransitionProperty<TldState, TldStateTransition> timedTransitionProperty =
TimedTransitionProperty.fromValueMap(values, TldStateTransition.class);
TestEntity testEntity = new TestEntity(timedTransitionProperty);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity));
TestEntity persisted =
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id"));
assertThat(persisted.timedTransitionProperty).containsExactlyEntriesIn(timedTransitionProperty);
}
@Entity(name = "TestEntity")
private static class TestEntity extends ImmutableObject {
@Id String name = "id";
TimedTransitionProperty<TldState, TldStateTransition> timedTransitionProperty;
private TestEntity() {}
private TestEntity(
TimedTransitionProperty<TldState, TldStateTransition> timedTransitionProperty) {
this.timedTransitionProperty = timedTransitionProperty;
}
}
}
@@ -45,7 +45,7 @@ public class JpaEntityCoverage extends ExternalResource {
private static final ImmutableSet<Class> ALL_JPA_ENTITIES =
PersistenceXmlUtility.getManagedClasses().stream()
.filter(e -> !IGNORE_ENTITIES.contains(e.getSimpleName()))
.filter(e -> e.getAnnotation(Entity.class) != null)
.filter(e -> e.isAnnotationPresent(Entity.class))
.collect(ImmutableSet.toImmutableSet());
private static final Set<Class> allCoveredJpaEntities = Sets.newHashSet();
// Map of test class name to boolean flag indicating if it tests any JPA entities.
@@ -28,6 +28,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.hibernate.cfg.Environment;
import org.joda.time.DateTime;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
@@ -60,10 +61,11 @@ public class JpaTestRules {
/**
* Junit rule for unit tests with JPA framework, when the underlying database is populated by the
* optional init script (which must not be the Nomulus Cloud SQL schema).
* optional init script (which must not be the Nomulus Cloud SQL schema). This rule can also be
* used as am extension for JUnit5 tests.
*/
public static class JpaUnitTestRule extends JpaTransactionManagerRule {
public static class JpaUnitTestRule extends JpaTransactionManagerRule
implements BeforeEachCallback, AfterEachCallback {
private JpaUnitTestRule(
Clock clock,
Optional<String> initScriptPath,
@@ -71,6 +73,16 @@ public class JpaTestRules {
ImmutableMap<String, String> userProperties) {
super(clock, initScriptPath, extraEntityClasses, userProperties);
}
@Override
public void beforeEach(ExtensionContext context) throws Exception {
this.before();
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
this.after();
}
}
/**
@@ -169,6 +181,17 @@ public class JpaTestRules {
return this;
}
/**
* Enables logging of SQL statements.
*
* <p>SQL logging is very noisy and disabled by default. This method maybe useful when
* troubleshooting a specific test.
*/
public Builder withSqlLogging() {
withProperty(Environment.SHOW_SQL, "true");
return this;
}
/** Builds a {@link JpaIntegrationTestRule} instance. */
public JpaIntegrationTestRule buildIntegrationTestRule() {
return new JpaIntegrationTestRule(
@@ -195,7 +218,9 @@ public class JpaTestRules {
return new JpaIntegrationWithCoverageExtension(buildIntegrationTestRule());
}
/** Builds a {@link JpaUnitTestRule} instance. */
/**
* Builds a {@link JpaUnitTestRule} instance that can also be used as an extension for JUnit5.
*/
public JpaUnitTestRule buildUnitTestRule() {
checkState(
!Objects.equals(GOLDEN_SCHEMA_SQL_PATH, initScript),
@@ -0,0 +1,57 @@
// Copyright 2020 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.persistence.transaction;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static java.nio.charset.StandardCharsets.UTF_8;
import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit test for {@link JpaTestRules.Builder#withSqlLogging()}. */
public class JpaTestRulesSqlLoggingTest {
// Entity under test: configured to log SQL statements to Stdout.
@RegisterExtension
JpaUnitTestRule jpaRule = new JpaTestRules.Builder().withSqlLogging().buildUnitTestRule();
private PrintStream orgStdout;
private ByteArrayOutputStream stdoutBuffer;
@BeforeEach
public void beforeEach() {
orgStdout = System.out;
System.setOut(new PrintStream(stdoutBuffer = new ByteArrayOutputStream()));
}
@AfterEach
public void afterEach() {
System.setOut(orgStdout);
}
@Test
void sqlLog_displayed() throws UnsupportedEncodingException {
jpaTm()
.transact(() -> jpaTm().getEntityManager().createNativeQuery("select 1").getSingleResult());
assertThat(stdoutBuffer.toString(UTF_8.name())).contains("select 1");
}
}
@@ -31,14 +31,19 @@ import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.PersistenceException;
import javax.persistence.RollbackException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link JpaTransactionManagerImpl}. */
/**
* Unit tests for SQL only APIs defined in {@link JpaTransactionManagerImpl}. Note that the tests
* for common APIs in {@link TransactionManager} are added in {@link TransactionManagerTest}.
*
* <p>TODO(shicong): Remove duplicate tests that covered by TransactionManagerTest by refactoring
* the test schema.
*/
@RunWith(JUnit4.class)
public class JpaTransactionManagerImplTest {
@@ -63,29 +68,6 @@ public class JpaTransactionManagerImplTest {
.withEntityClass(TestEntity.class, TestCompoundIdEntity.class)
.buildUnitTestRule();
@Test
public void inTransaction_returnsCorrespondingResult() {
assertThat(jpaTm().inTransaction()).isFalse();
jpaTm().transact(() -> assertThat(jpaTm().inTransaction()).isTrue());
assertThat(jpaTm().inTransaction()).isFalse();
}
@Test
public void assertInTransaction_throwsExceptionWhenNotInTransaction() {
assertThrows(PersistenceException.class, () -> jpaTm().assertInTransaction());
jpaTm().transact(() -> jpaTm().assertInTransaction());
assertThrows(PersistenceException.class, () -> jpaTm().assertInTransaction());
}
@Test
public void getTransactionTime_throwsExceptionWhenNotInTransaction() {
FakeClock txnClock = fakeClock;
txnClock.advanceOneMilli();
assertThrows(PersistenceException.class, () -> jpaTm().getTransactionTime());
jpaTm().transact(() -> assertThat(jpaTm().getTransactionTime()).isEqualTo(txnClock.nowUtc()));
assertThrows(PersistenceException.class, () -> jpaTm().getTransactionTime());
}
@Test
public void transact_succeeds() {
assertPersonEmpty();
@@ -333,14 +315,14 @@ public class JpaTransactionManagerImplTest {
public void delete_succeeds() {
jpaTm().transact(() -> jpaTm().saveNew(theEntity));
assertThat(jpaTm().transact(() -> jpaTm().checkExists(theEntity))).isTrue();
assertThat(jpaTm().transact(() -> jpaTm().delete(theEntityKey))).isEqualTo(1);
jpaTm().transact(() -> jpaTm().delete(theEntityKey));
assertThat(jpaTm().transact(() -> jpaTm().checkExists(theEntity))).isFalse();
}
@Test
public void delete_returnsZeroWhenNoEntity() {
assertThat(jpaTm().transact(() -> jpaTm().checkExists(theEntity))).isFalse();
assertThat(jpaTm().transact(() -> jpaTm().delete(theEntityKey))).isEqualTo(0);
jpaTm().transact(() -> jpaTm().delete(theEntityKey));
assertThat(jpaTm().transact(() -> jpaTm().checkExists(theEntity))).isFalse();
}
@@ -41,6 +41,8 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Collectors;
@@ -146,13 +148,15 @@ abstract class JpaTransactionManagerRule extends ExternalResource {
ImmutableMap properties = PersistenceModule.providesDefaultDatabaseConfigs();
if (!userProperties.isEmpty()) {
// If there are user properties, create a new properties object with these added.
ImmutableMap.Builder builder = properties.builder();
builder.putAll(userProperties);
// Forbid Hibernate push to stay consistent with flyway-based schema management.
builder.put(Environment.HBM2DDL_AUTO, "none");
builder.put(Environment.SHOW_SQL, "true");
properties = builder.build();
Map<String, String> mergedProperties = Maps.newHashMap();
mergedProperties.putAll(properties);
mergedProperties.putAll(userProperties);
properties = ImmutableMap.copyOf(mergedProperties);
}
// Forbid Hibernate push to stay consistent with flyway-based schema management.
checkState(
Objects.equals(properties.get(Environment.HBM2DDL_AUTO), "none"),
"The HBM2DDL_AUTO property must be 'none'.");
assertReasonableNumDbConnections();
emf =
createEntityManagerFactory(
@@ -0,0 +1,273 @@
// Copyright 2019 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.persistence.transaction;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import google.registry.model.ImmutableObject;
import google.registry.model.ofy.DatastoreTransactionManager;
import google.registry.model.ofy.Ofy;
import google.registry.persistence.VKey;
import google.registry.testing.AppEngineRule;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.InjectRule;
import java.util.NoSuchElementException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.RegisterExtension;
/**
* Unit tests for common APIs in {@link DatastoreTransactionManager} and {@link
* JpaTransactionManagerImpl}.
*/
@DualDatabaseTest
public class TransactionManagerTest {
private final FakeClock fakeClock = new FakeClock();
private final TestEntity theEntity = new TestEntity("theEntity", "foo");
private final ImmutableList<TestEntity> moreEntities =
ImmutableList.of(
new TestEntity("entity1", "foo"),
new TestEntity("entity2", "bar"),
new TestEntity("entity3", "qux"));
@RegisterExtension public InjectRule inject = new InjectRule();
@RegisterExtension
public final AppEngineRule appEngine =
AppEngineRule.builder()
.withClock(fakeClock)
.withDatastoreAndCloudSql()
.withOfyTestEntities(TestEntity.class)
.withJpaUnitTestEntities(TestEntity.class)
.build();
public TransactionManagerTest() {}
@BeforeEach
public void setUp() {
inject.setStaticField(Ofy.class, "clock", fakeClock);
}
@TestTemplate
public void inTransaction_returnsCorrespondingResult() {
assertThat(tm().inTransaction()).isFalse();
tm().transact(() -> assertThat(tm().inTransaction()).isTrue());
assertThat(tm().inTransaction()).isFalse();
}
@TestTemplate
public void assertInTransaction_throwsExceptionWhenNotInTransaction() {
assertThrows(IllegalStateException.class, () -> tm().assertInTransaction());
tm().transact(() -> tm().assertInTransaction());
assertThrows(IllegalStateException.class, () -> tm().assertInTransaction());
}
@TestTemplate
public void getTransactionTime_throwsExceptionWhenNotInTransaction() {
FakeClock txnClock = fakeClock;
txnClock.advanceOneMilli();
assertThrows(IllegalStateException.class, () -> tm().getTransactionTime());
tm().transact(() -> assertThat(tm().getTransactionTime()).isEqualTo(txnClock.nowUtc()));
assertThrows(IllegalStateException.class, () -> tm().getTransactionTime());
}
@TestTemplate
public void transact_hasNoEffectWithPartialSuccess() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
assertThrows(
RuntimeException.class,
() ->
tm()
.transact(
() -> {
tm().saveNew(theEntity);
throw new RuntimeException();
}));
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
}
@TestTemplate
public void transact_reusesExistingTransaction() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
tm().transact(() -> tm().transact(() -> tm().saveNew(theEntity)));
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isTrue();
}
@TestTemplate
public void saveNew_succeeds() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
tm().transact(() -> tm().saveNew(theEntity));
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isTrue();
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().load(theEntity.key()))).isEqualTo(theEntity);
}
@TestTemplate
public void saveAllNew_succeeds() {
moreEntities.forEach(
entity -> assertThat(tm().transact(() -> tm().checkExists(entity))).isFalse());
fakeClock.advanceOneMilli();
tm().transact(() -> tm().saveAllNew(moreEntities));
fakeClock.advanceOneMilli();
moreEntities.forEach(
entity -> assertThat(tm().transact(() -> tm().checkExists(entity))).isTrue());
}
@TestTemplate
public void saveNewOrUpdate_persistsNewEntity() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
tm().transact(() -> tm().saveNewOrUpdate(theEntity));
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isTrue();
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().load(theEntity.key()))).isEqualTo(theEntity);
}
@TestTemplate
public void saveNewOrUpdate_updatesExistingEntity() {
fakeClock.advanceOneMilli();
tm().transact(() -> tm().saveNew(theEntity));
fakeClock.advanceOneMilli();
TestEntity persisted = tm().transact(() -> tm().load(theEntity.key()));
assertThat(persisted.data).isEqualTo("foo");
theEntity.data = "bar";
tm().transact(() -> tm().saveNewOrUpdate(theEntity));
fakeClock.advanceOneMilli();
persisted = tm().transact(() -> tm().load(theEntity.key()));
fakeClock.advanceOneMilli();
assertThat(persisted.data).isEqualTo("bar");
}
@TestTemplate
public void saveNewOrUpdateAll_succeeds() {
moreEntities.forEach(
entity -> assertThat(tm().transact(() -> tm().checkExists(entity))).isFalse());
fakeClock.advanceOneMilli();
tm().transact(() -> tm().saveNewOrUpdateAll(moreEntities));
fakeClock.advanceOneMilli();
moreEntities.forEach(
entity -> assertThat(tm().transact(() -> tm().checkExists(entity))).isTrue());
}
@TestTemplate
public void update_succeeds() {
fakeClock.advanceOneMilli();
tm().transact(() -> tm().saveNew(theEntity));
fakeClock.advanceOneMilli();
TestEntity persisted =
tm().transact(
() ->
tm().load(
VKey.create(TestEntity.class, theEntity.name, Key.create(theEntity))));
fakeClock.advanceOneMilli();
assertThat(persisted.data).isEqualTo("foo");
theEntity.data = "bar";
tm().transact(() -> tm().update(theEntity));
fakeClock.advanceOneMilli();
persisted = tm().transact(() -> tm().load(theEntity.key()));
assertThat(persisted.data).isEqualTo("bar");
}
@TestTemplate
public void load_succeeds() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
tm().transact(() -> tm().saveNew(theEntity));
fakeClock.advanceOneMilli();
TestEntity persisted = tm().transact(() -> tm().load(theEntity.key()));
assertThat(persisted.name).isEqualTo("theEntity");
assertThat(persisted.data).isEqualTo("foo");
}
@TestTemplate
public void load_throwsOnMissingElement() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
assertThrows(
NoSuchElementException.class, () -> tm().transact(() -> tm().load(theEntity.key())));
}
@TestTemplate
public void maybeLoad_succeeds() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
tm().transact(() -> tm().saveNew(theEntity));
fakeClock.advanceOneMilli();
TestEntity persisted = tm().transact(() -> tm().maybeLoad(theEntity.key()).get());
assertThat(persisted.name).isEqualTo("theEntity");
assertThat(persisted.data).isEqualTo("foo");
}
@TestTemplate
public void maybeLoad_nonExistentObject() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().maybeLoad(theEntity.key())).isPresent()).isFalse();
}
@TestTemplate
public void delete_succeeds() {
fakeClock.advanceOneMilli();
tm().transact(() -> tm().saveNew(theEntity));
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isTrue();
fakeClock.advanceOneMilli();
tm().transact(() -> tm().delete(theEntity.key()));
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
}
@TestTemplate
public void delete_returnsZeroWhenNoEntity() {
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
fakeClock.advanceOneMilli();
tm().transact(() -> tm().delete(theEntity.key()));
fakeClock.advanceOneMilli();
assertThat(tm().transact(() -> tm().checkExists(theEntity))).isFalse();
}
@Entity(name = "TestEntity")
@javax.persistence.Entity(name = "TestEntity")
private static class TestEntity extends ImmutableObject {
@Id @javax.persistence.Id private String name;
private String data;
private TestEntity() {}
private TestEntity(String name, String data) {
this.name = name;
this.data = data;
}
public VKey<TestEntity> key() {
return VKey.create(TestEntity.class, name, Key.create(this));
}
}
}
@@ -16,6 +16,7 @@ package google.registry.schema.integration;
import static com.google.common.truth.Truth.assert_;
import google.registry.model.billing.BillingEventTest;
import google.registry.model.contact.ContactResourceTest;
import google.registry.model.domain.DomainBaseSqlTest;
import google.registry.model.registry.RegistryLockDaoTest;
@@ -68,6 +69,7 @@ import org.junit.runner.RunWith;
@SelectClasses({
// BeforeSuiteTest must be the first entry. See class javadoc for details.
BeforeSuiteTest.class,
BillingEventTest.class,
ClaimsListDaoTest.class,
ContactResourceTest.class,
CursorDaoTest.class,
@@ -17,6 +17,8 @@ package google.registry.testing;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.testing.DatastoreHelper.persistSimpleResources;
import static google.registry.testing.DualDatabaseTestInvocationContextProvider.injectTmForDualDatabaseTest;
import static google.registry.testing.DualDatabaseTestInvocationContextProvider.restoreTmAfterDualDatabaseTest;
import static google.registry.util.ResourceUtils.readResourceUtf8;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.json.XML.toJSONObject;
@@ -45,6 +47,7 @@ import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationTestRule;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageRule;
import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule;
import google.registry.util.Clock;
import java.io.ByteArrayInputStream;
import java.io.File;
@@ -118,8 +121,11 @@ public final class AppEngineRule extends ExternalResource
*/
JpaIntegrationWithCoverageExtension jpaIntegrationWithCoverageExtension = null;
JpaUnitTestRule jpaUnitTestRule;
private boolean withDatastoreAndCloudSql;
private boolean enableJpaEntityCoverageCheck;
private boolean withJpaUnitTest;
private boolean withLocalModules;
private boolean withTaskQueue;
private boolean withUserService;
@@ -131,12 +137,14 @@ public final class AppEngineRule extends ExternalResource
// Test Objectify entity classes to be used with this AppEngineRule instance.
private ImmutableList<Class<?>> ofyTestEntities;
private ImmutableList<Class<?>> jpaTestEntities;
/** Builder for {@link AppEngineRule}. */
public static class Builder {
private AppEngineRule rule = new AppEngineRule();
private ImmutableList.Builder<Class<?>> ofyTestEntities = new ImmutableList.Builder();
private ImmutableList.Builder<Class<?>> ofyTestEntities = new ImmutableList.Builder<>();
private ImmutableList.Builder<Class<?>> jpaTestEntities = new ImmutableList.Builder<>();
/** Turn on the Datastore service and the Cloud SQL service. */
public Builder withDatastoreAndCloudSql() {
@@ -205,11 +213,24 @@ public final class AppEngineRule extends ExternalResource
return this;
}
public Builder withJpaUnitTestEntities(Class<?>... entities) {
jpaTestEntities.add(entities);
rule.withJpaUnitTest = true;
return this;
}
public AppEngineRule build() {
checkState(
!rule.enableJpaEntityCoverageCheck || rule.withDatastoreAndCloudSql,
"withJpaEntityCoverageCheck enabled without Cloud SQL");
checkState(
!rule.withJpaUnitTest || rule.withDatastoreAndCloudSql,
"withJpaUnitTestEntities enabled without Cloud SQL");
checkState(
!rule.withJpaUnitTest || !rule.enableJpaEntityCoverageCheck,
"withJpaUnitTestEntities cannot be set when enableJpaEntityCoverageCheck");
rule.ofyTestEntities = this.ofyTestEntities.build();
rule.jpaTestEntities = this.jpaTestEntities.build();
return rule;
}
}
@@ -328,11 +349,18 @@ public final class AppEngineRule extends ExternalResource
if (enableJpaEntityCoverageCheck) {
jpaIntegrationWithCoverageExtension = builder.buildIntegrationWithCoverageExtension();
jpaIntegrationWithCoverageExtension.beforeEach(context);
} else if (withJpaUnitTest) {
jpaUnitTestRule =
builder
.withEntityClass(jpaTestEntities.toArray(new Class[jpaTestEntities.size()]))
.buildUnitTestRule();
jpaUnitTestRule.before();
} else {
jpaIntegrationTestRule = builder.buildIntegrationTestRule();
jpaIntegrationTestRule.before();
}
}
injectTmForDualDatabaseTest(context);
}
/** Called after each test method. JUnit 5 only. */
@@ -341,11 +369,14 @@ public final class AppEngineRule extends ExternalResource
if (withDatastoreAndCloudSql) {
if (enableJpaEntityCoverageCheck) {
jpaIntegrationWithCoverageExtension.afterEach(context);
} else if (withJpaUnitTest) {
jpaUnitTestRule.after();
} else {
jpaIntegrationTestRule.after();
}
}
after();
restoreTmAfterDualDatabaseTest(context);
}
/**
@@ -560,4 +591,8 @@ public final class AppEngineRule extends ExternalResource
makeRegistrarContact2(),
makeRegistrarContact3()));
}
boolean isWithDatastoreAndCloudSql() {
return withDatastoreAndCloudSql;
}
}

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