From 266bd4379205eae54022cb37f9052b57d1ab45f7 Mon Sep 17 00:00:00 2001 From: gbrodman Date: Wed, 26 Aug 2020 11:45:09 -0400 Subject: [PATCH] Persist *History objects as HistoryEntry objects (#749) * Persist *History objects as HistoryEntry objects While Datastore is the primary database, we will store *History objects as HistoryEntry objects and convert to/from the proper objects in the Datastore transaction manager. This means that History objects will not properly store the copy of the EppResource until we move to SQL as primary, but this is the way the world exists anyway so it's not a problem. * Format code and simplify the bulk loading * Add comments with context --- .../google/registry/model/EntityClasses.java | 6 + .../model/contact/ContactHistory.java | 2 + .../registry/model/domain/DomainHistory.java | 2 + .../registry/model/host/HostHistory.java | 2 + .../ofy/DatastoreTransactionManager.java | 58 +++++++-- .../model/reporting/HistoryEntry.java | 69 +++++++++- .../model/history/ContactHistoryTest.java | 59 +++++++-- .../model/history/DomainHistoryTest.java | 80 +++++++++--- .../model/history/HostHistoryTest.java | 57 +++++++-- .../google/registry/model/schema.txt | 120 ++++++++++++++++++ 10 files changed, 401 insertions(+), 54 deletions(-) diff --git a/core/src/main/java/google/registry/model/EntityClasses.java b/core/src/main/java/google/registry/model/EntityClasses.java index 64ac80f30..1bae42463 100644 --- a/core/src/main/java/google/registry/model/EntityClasses.java +++ b/core/src/main/java/google/registry/model/EntityClasses.java @@ -19,9 +19,12 @@ import google.registry.model.billing.BillingEvent; import google.registry.model.common.Cursor; import google.registry.model.common.EntityGroupRoot; import google.registry.model.common.GaeUserIdConverter; +import google.registry.model.contact.ContactHistory; import google.registry.model.contact.ContactResource; import google.registry.model.domain.DomainBase; +import google.registry.model.domain.DomainHistory; import google.registry.model.domain.token.AllocationToken; +import google.registry.model.host.HostHistory; import google.registry.model.host.HostResource; import google.registry.model.index.EppResourceIndex; import google.registry.model.index.EppResourceIndexBucket; @@ -68,9 +71,11 @@ public final class EntityClasses { CommitLogCheckpointRoot.class, CommitLogManifest.class, CommitLogMutation.class, + ContactHistory.class, ContactResource.class, Cursor.class, DomainBase.class, + DomainHistory.class, EntityGroupRoot.class, EppResourceIndex.class, EppResourceIndexBucket.class, @@ -79,6 +84,7 @@ public final class EntityClasses { ForeignKeyIndex.ForeignKeyHostIndex.class, GaeUserIdConverter.class, HistoryEntry.class, + HostHistory.class, HostResource.class, KmsSecret.class, KmsSecretRevision.class, diff --git a/core/src/main/java/google/registry/model/contact/ContactHistory.java b/core/src/main/java/google/registry/model/contact/ContactHistory.java index b6dcab38a..9160061cf 100644 --- a/core/src/main/java/google/registry/model/contact/ContactHistory.java +++ b/core/src/main/java/google/registry/model/contact/ContactHistory.java @@ -15,6 +15,7 @@ package google.registry.model.contact; import com.googlecode.objectify.Key; +import com.googlecode.objectify.annotation.EntitySubclass; import google.registry.model.EppResource; import google.registry.model.reporting.HistoryEntry; import google.registry.persistence.VKey; @@ -36,6 +37,7 @@ import javax.persistence.Entity; @javax.persistence.Index(columnList = "historyType"), @javax.persistence.Index(columnList = "historyModificationTime") }) +@EntitySubclass public class ContactHistory extends HistoryEntry { // Store ContactBase instead of ContactResource so we don't pick up its @Id ContactBase contactBase; diff --git a/core/src/main/java/google/registry/model/domain/DomainHistory.java b/core/src/main/java/google/registry/model/domain/DomainHistory.java index 4718d820e..f37771be9 100644 --- a/core/src/main/java/google/registry/model/domain/DomainHistory.java +++ b/core/src/main/java/google/registry/model/domain/DomainHistory.java @@ -15,6 +15,7 @@ package google.registry.model.domain; import com.googlecode.objectify.Key; +import com.googlecode.objectify.annotation.EntitySubclass; import google.registry.model.EppResource; import google.registry.model.contact.ContactResource; import google.registry.model.host.HostResource; @@ -43,6 +44,7 @@ import javax.persistence.JoinTable; @javax.persistence.Index(columnList = "historyType"), @javax.persistence.Index(columnList = "historyModificationTime") }) +@EntitySubclass public class DomainHistory extends HistoryEntry { // Store DomainContent instead of DomainBase so we don't pick up its @Id DomainContent domainContent; diff --git a/core/src/main/java/google/registry/model/host/HostHistory.java b/core/src/main/java/google/registry/model/host/HostHistory.java index 2a9b7a4dc..e220ca1ad 100644 --- a/core/src/main/java/google/registry/model/host/HostHistory.java +++ b/core/src/main/java/google/registry/model/host/HostHistory.java @@ -15,6 +15,7 @@ package google.registry.model.host; import com.googlecode.objectify.Key; +import com.googlecode.objectify.annotation.EntitySubclass; import google.registry.model.EppResource; import google.registry.model.reporting.HistoryEntry; import google.registry.persistence.VKey; @@ -37,6 +38,7 @@ import javax.persistence.Entity; @javax.persistence.Index(columnList = "historyType"), @javax.persistence.Index(columnList = "historyModificationTime") }) +@EntitySubclass public class HostHistory extends HistoryEntry { // Store HostBase instead of HostResource so we don't pick up its @Id diff --git a/core/src/main/java/google/registry/model/ofy/DatastoreTransactionManager.java b/core/src/main/java/google/registry/model/ofy/DatastoreTransactionManager.java index 723cc0703..a6a4baa47 100644 --- a/core/src/main/java/google/registry/model/ofy/DatastoreTransactionManager.java +++ b/core/src/main/java/google/registry/model/ofy/DatastoreTransactionManager.java @@ -24,13 +24,16 @@ import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.googlecode.objectify.Key; +import google.registry.model.contact.ContactHistory; +import google.registry.model.host.HostHistory; +import google.registry.model.reporting.HistoryEntry; import google.registry.persistence.VKey; import google.registry.persistence.transaction.TransactionManager; -import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Optional; import java.util.function.Supplier; import java.util.stream.StreamSupport; +import javax.annotation.Nullable; import org.joda.time.DateTime; /** Datastore implementation of {@link TransactionManager}. */ @@ -99,8 +102,7 @@ public class DatastoreTransactionManager implements TransactionManager { @Override public void saveNew(Object entity) { - checkArgumentNotNull(entity, "entity must be specified"); - getOfy().save().entity(entity); + saveEntity(entity); } @Override @@ -110,8 +112,7 @@ public class DatastoreTransactionManager implements TransactionManager { @Override public void saveNewOrUpdate(Object entity) { - checkArgumentNotNull(entity, "entity must be specified"); - getOfy().save().entity(entity); + saveEntity(entity); } @Override @@ -121,8 +122,7 @@ public class DatastoreTransactionManager implements TransactionManager { @Override public void update(Object entity) { - checkArgumentNotNull(entity, "entity must be specified"); - getOfy().save().entity(entity); + saveEntity(entity); } @Override @@ -137,7 +137,7 @@ public class DatastoreTransactionManager implements TransactionManager { @Override public boolean checkExists(VKey key) { - return getOfy().load().key(key.getOfyKey()).now() != null; + return loadNullable(key) != null; } // TODO: add tests for these methods. They currently have some degree of test coverage because @@ -146,12 +146,12 @@ public class DatastoreTransactionManager implements TransactionManager { // interface tests that are applied to both the datastore and SQL implementations. @Override public Optional maybeLoad(VKey key) { - return Optional.ofNullable(getOfy().load().key(key.getOfyKey()).now()); + return Optional.ofNullable(loadNullable(key)); } @Override public T load(VKey key) { - T result = getOfy().load().key(key.getOfyKey()).now(); + T result = loadNullable(key); if (result == null) { throw new NoSuchElementException(key.toString()); } @@ -167,7 +167,10 @@ public class DatastoreTransactionManager implements TransactionManager { .collect(toImmutableMap(key -> (Key) key.getOfyKey(), Functions.identity())); return getOfy().load().keys(keyMap.keySet()).entrySet().stream() - .collect(ImmutableMap.toImmutableMap(entry -> keyMap.get(entry.getKey()), Entry::getValue)); + .collect( + toImmutableMap( + entry -> keyMap.get(entry.getKey()), + entry -> toChildHistoryEntryIfPossible(entry.getValue()))); } @Override @@ -191,4 +194,37 @@ public class DatastoreTransactionManager implements TransactionManager { .collect(toImmutableList()); getOfy().delete().keys(list).now(); } + + /** + * The following three methods exist due to the migration to Cloud SQL. + * + *

In Cloud SQL, {@link HistoryEntry} objects are represented instead as {@link DomainHistory}, + * {@link ContactHistory}, and {@link HostHistory} objects. During the migration, we do not wish + * to change the Datastore schema so all of these objects are stored in Datastore as HistoryEntry + * objects. They are converted to/from the appropriate classes upon retrieval, and converted to + * HistoryEntry on save. See go/r3.0-history-objects for more details. + */ + private void saveEntity(Object entity) { + checkArgumentNotNull(entity, "entity must be specified"); + if (entity instanceof HistoryEntry) { + entity = ((HistoryEntry) entity).asHistoryEntry(); + } + getOfy().save().entity(entity); + } + + @SuppressWarnings("unchecked") + private T toChildHistoryEntryIfPossible(@Nullable T obj) { + // NB: The Key of the object in question may not necessarily be the resulting class that we + // wish to have. Because all *History classes are @EntitySubclasses, their Keys will have type + // HistoryEntry -- even if you create them based off the *History class. + if (obj != null && HistoryEntry.class.isAssignableFrom(obj.getClass())) { + return (T) ((HistoryEntry) obj).toChildHistoryEntity(); + } + return obj; + } + + @Nullable + private T loadNullable(VKey key) { + return toChildHistoryEntryIfPossible(getOfy().load().key(key.getOfyKey()).now()); + } } diff --git a/core/src/main/java/google/registry/model/reporting/HistoryEntry.java b/core/src/main/java/google/registry/model/reporting/HistoryEntry.java index 38b1ee948..d1d47f41b 100644 --- a/core/src/main/java/google/registry/model/reporting/HistoryEntry.java +++ b/core/src/main/java/google/registry/model/reporting/HistoryEntry.java @@ -14,8 +14,10 @@ package google.registry.model.reporting; +import static com.googlecode.objectify.Key.getKind; import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import com.googlecode.objectify.Key; import com.googlecode.objectify.annotation.Entity; @@ -28,8 +30,14 @@ import google.registry.model.Buildable; import google.registry.model.EppResource; import google.registry.model.ImmutableObject; import google.registry.model.annotations.ReportedOn; +import google.registry.model.contact.ContactHistory; +import google.registry.model.contact.ContactResource; +import google.registry.model.domain.DomainBase; +import google.registry.model.domain.DomainHistory; import google.registry.model.domain.Period; import google.registry.model.eppcommon.Trid; +import google.registry.model.host.HostHistory; +import google.registry.model.host.HostResource; import google.registry.persistence.VKey; import google.registry.persistence.WithStringVKey; import java.util.Set; @@ -111,7 +119,8 @@ public class HistoryEntry extends ImmutableObject implements Buildable { @Id @javax.persistence.Id @Column(name = "historyRevisionId") - Long id; + @VisibleForTesting + public Long id; /** The resource this event mutated. */ @Parent @Transient protected Key parent; @@ -259,6 +268,39 @@ public class HistoryEntry extends ImmutableObject implements Buildable { return new Builder(clone(this)); } + public HistoryEntry asHistoryEntry() { + return new Builder().copyFrom(this).build(); + } + + public HistoryEntry toChildHistoryEntity() { + String parentKind = getParent().getKind(); + final HistoryEntry resultEntity; + // can't use a switch statement since we're calling getKind() + if (parentKind.equals(getKind(DomainBase.class))) { + resultEntity = + new DomainHistory.Builder() + .copyFrom(this) + .setDomainRepoId(VKey.create(DomainBase.class, parent.getName(), parent)) + .build(); + } else if (parentKind.equals(getKind(HostResource.class))) { + resultEntity = + new HostHistory.Builder() + .copyFrom(this) + .setHostRepoId(VKey.create(HostResource.class, parent.getName(), parent)) + .build(); + } else if (parentKind.equals(getKind(ContactResource.class))) { + resultEntity = + new ContactHistory.Builder() + .copyFrom(this) + .setContactRepoId(VKey.create(ContactResource.class, parent.getName(), parent)) + .build(); + } else { + throw new IllegalStateException( + String.format("Unknown kind of HistoryEntry parent %s", parentKind)); + } + return resultEntity; + } + /** A builder for {@link HistoryEntry} since it is immutable */ public static class Builder> extends GenericBuilder { @@ -268,17 +310,40 @@ public class HistoryEntry extends ImmutableObject implements Buildable { super(instance); } + // Used to fill out the fields in this object from an object which may not be exactly the same + // as the class T, where both classes still subclass HistoryEntry + public B copyFrom(HistoryEntry historyEntry) { + setId(historyEntry.id); + setParent(historyEntry.parent); + setType(historyEntry.type); + setPeriod(historyEntry.period); + setXmlBytes(historyEntry.xmlBytes); + setModificationTime(historyEntry.modificationTime); + setClientId(historyEntry.clientId); + setOtherClientId(historyEntry.otherClientId); + setTrid(historyEntry.trid); + setBySuperuser(historyEntry.bySuperuser); + setReason(historyEntry.reason); + setRequestedByRegistrar(historyEntry.requestedByRegistrar); + setDomainTransactionRecords(nullToEmptyImmutableCopy(historyEntry.domainTransactionRecords)); + return thisCastToDerived(); + } + @Override public T build() { return super.build(); } + public B setId(long id) { + getInstance().id = id; + return thisCastToDerived(); + } + public B setParent(EppResource parent) { getInstance().parent = Key.create(parent); return thisCastToDerived(); } - // Until we move completely to SQL, override this in subclasses (e.g. HostHistory) to set VKeys public B setParent(Key parent) { getInstance().parent = parent; return thisCastToDerived(); diff --git a/core/src/test/java/google/registry/model/history/ContactHistoryTest.java b/core/src/test/java/google/registry/model/history/ContactHistoryTest.java index edee61dae..743dfaaff 100644 --- a/core/src/test/java/google/registry/model/history/ContactHistoryTest.java +++ b/core/src/test/java/google/registry/model/history/ContactHistoryTest.java @@ -17,11 +17,14 @@ package google.registry.model.history; import static com.google.common.truth.Truth.assertThat; import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.testing.DatastoreHelper.newContactResourceWithRoid; import static google.registry.testing.SqlHelper.saveRegistrar; import static java.nio.charset.StandardCharsets.UTF_8; +import com.googlecode.objectify.Key; import google.registry.model.EntityTestCase; +import google.registry.model.contact.ContactBase; import google.registry.model.contact.ContactHistory; import google.registry.model.contact.ContactResource; import google.registry.model.eppcommon.Trid; @@ -44,19 +47,8 @@ public class ContactHistoryTest extends EntityTestCase { jpaTm().transact(() -> jpaTm().saveNew(contact)); VKey contactVKey = contact.createVKey(); ContactResource contactFromDb = jpaTm().transact(() -> jpaTm().load(contactVKey)); - ContactHistory contactHistory = - new ContactHistory.Builder() - .setType(HistoryEntry.Type.HOST_CREATE) - .setXmlBytes("".getBytes(UTF_8)) - .setModificationTime(fakeClock.nowUtc()) - .setClientId("TheRegistrar") - .setTrid(Trid.create("ABC-123", "server-trid")) - .setBySuperuser(false) - .setReason("reason") - .setRequestedByRegistrar(true) - .setContactBase(contactFromDb) - .setContactRepoId(contactVKey) - .build(); + ContactHistory contactHistory = createContactHistory(contactFromDb, contactVKey); + contactHistory.id = null; jpaTm().transact(() -> jpaTm().saveNew(contactHistory)); jpaTm() .transact( @@ -68,6 +60,47 @@ public class ContactHistoryTest extends EntityTestCase { }); } + @Test + void testOfyPersistence() { + saveRegistrar("TheRegistrar"); + + ContactResource contact = newContactResourceWithRoid("contactId", "contact1"); + tm().transact(() -> tm().saveNew(contact)); + VKey contactVKey = contact.createVKey(); + ContactResource contactFromDb = tm().transact(() -> tm().load(contactVKey)); + fakeClock.advanceOneMilli(); + ContactHistory contactHistory = createContactHistory(contactFromDb, contactVKey); + tm().transact(() -> tm().saveNew(contactHistory)); + + // retrieving a HistoryEntry or a ContactHistory with the same key should return the same object + // note: due to the @EntitySubclass annotation. all Keys for ContactHistory objects will have + // type HistoryEntry + VKey contactHistoryVKey = + VKey.createOfy(ContactHistory.class, Key.create(contactHistory)); + VKey historyEntryVKey = + VKey.createOfy(HistoryEntry.class, Key.create(contactHistory.asHistoryEntry())); + ContactHistory hostHistoryFromDb = tm().transact(() -> tm().load(contactHistoryVKey)); + HistoryEntry historyEntryFromDb = tm().transact(() -> tm().load(historyEntryVKey)); + + assertThat(hostHistoryFromDb).isEqualTo(historyEntryFromDb); + } + + private ContactHistory createContactHistory( + ContactBase contact, VKey contactVKey) { + return new ContactHistory.Builder() + .setType(HistoryEntry.Type.HOST_CREATE) + .setXmlBytes("".getBytes(UTF_8)) + .setModificationTime(fakeClock.nowUtc()) + .setClientId("TheRegistrar") + .setTrid(Trid.create("ABC-123", "server-trid")) + .setBySuperuser(false) + .setReason("reason") + .setRequestedByRegistrar(true) + .setContactBase(contact) + .setContactRepoId(contactVKey) + .build(); + } + static void assertContactHistoriesEqual(ContactHistory one, ContactHistory two) { assertAboutImmutableObjects() .that(one) diff --git a/core/src/test/java/google/registry/model/history/DomainHistoryTest.java b/core/src/test/java/google/registry/model/history/DomainHistoryTest.java index a05fc4cd4..78998c22d 100644 --- a/core/src/test/java/google/registry/model/history/DomainHistoryTest.java +++ b/core/src/test/java/google/registry/model/history/DomainHistoryTest.java @@ -17,15 +17,18 @@ package google.registry.model.history; import static com.google.common.truth.Truth.assertThat; import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.testing.DatastoreHelper.newContactResourceWithRoid; import static google.registry.testing.DatastoreHelper.newDomainBase; import static google.registry.testing.DatastoreHelper.newHostResourceWithRoid; import static google.registry.testing.SqlHelper.saveRegistrar; import static java.nio.charset.StandardCharsets.UTF_8; +import com.googlecode.objectify.Key; import google.registry.model.EntityTestCase; import google.registry.model.contact.ContactResource; import google.registry.model.domain.DomainBase; +import google.registry.model.domain.DomainContent; import google.registry.model.domain.DomainHistory; import google.registry.model.eppcommon.Trid; import google.registry.model.host.HostResource; @@ -45,9 +48,14 @@ public class DomainHistoryTest extends EntityTestCase { saveRegistrar("TheRegistrar"); HostResource host = newHostResourceWithRoid("ns1.example.com", "host1"); - jpaTm().transact(() -> jpaTm().saveNew(host)); ContactResource contact = newContactResourceWithRoid("contactId", "contact1"); - jpaTm().transact(() -> jpaTm().saveNew(contact)); + + jpaTm() + .transact( + () -> { + jpaTm().saveNew(host); + jpaTm().saveNew(contact); + }); DomainBase domain = newDomainBase("example.tld", "domainRepoId", contact) @@ -56,19 +64,8 @@ public class DomainHistoryTest extends EntityTestCase { .build(); jpaTm().transact(() -> jpaTm().saveNew(domain)); - DomainHistory domainHistory = - new DomainHistory.Builder() - .setType(HistoryEntry.Type.DOMAIN_CREATE) - .setXmlBytes("".getBytes(UTF_8)) - .setModificationTime(fakeClock.nowUtc()) - .setClientId("TheRegistrar") - .setTrid(Trid.create("ABC-123", "server-trid")) - .setBySuperuser(false) - .setReason("reason") - .setRequestedByRegistrar(true) - .setDomainContent(domain) - .setDomainRepoId(domain.createVKey()) - .build(); + DomainHistory domainHistory = createDomainHistory(domain); + domainHistory.id = null; jpaTm().transact(() -> jpaTm().saveNew(domainHistory)); jpaTm() @@ -82,9 +79,62 @@ public class DomainHistoryTest extends EntityTestCase { }); } + @Test + void testOfyPersistence() { + saveRegistrar("TheRegistrar"); + + HostResource host = newHostResourceWithRoid("ns1.example.com", "host1"); + ContactResource contact = newContactResourceWithRoid("contactId", "contact1"); + + tm().transact( + () -> { + tm().saveNew(host); + tm().saveNew(contact); + }); + fakeClock.advanceOneMilli(); + + DomainBase domain = + newDomainBase("example.tld", "domainRepoId", contact) + .asBuilder() + .setNameservers(host.createVKey()) + .build(); + tm().transact(() -> tm().saveNew(domain)); + + fakeClock.advanceOneMilli(); + DomainHistory domainHistory = createDomainHistory(domain); + tm().transact(() -> tm().saveNew(domainHistory)); + + // retrieving a HistoryEntry or a DomainHistory with the same key should return the same object + // note: due to the @EntitySubclass annotation. all Keys for ContactHistory objects will have + // type HistoryEntry + VKey domainHistoryVKey = + VKey.createOfy(DomainHistory.class, Key.create(domainHistory)); + VKey historyEntryVKey = + VKey.createOfy(HistoryEntry.class, Key.create(domainHistory.asHistoryEntry())); + DomainHistory domainHistoryFromDb = tm().transact(() -> tm().load(domainHistoryVKey)); + HistoryEntry historyEntryFromDb = tm().transact(() -> tm().load(historyEntryVKey)); + + assertThat(domainHistoryFromDb).isEqualTo(historyEntryFromDb); + } + static void assertDomainHistoriesEqual(DomainHistory one, DomainHistory two) { assertAboutImmutableObjects() .that(one) .isEqualExceptFields(two, "domainContent", "domainRepoId", "parent"); } + + private DomainHistory createDomainHistory(DomainContent domain) { + return new DomainHistory.Builder() + .setType(HistoryEntry.Type.DOMAIN_CREATE) + .setXmlBytes("".getBytes(UTF_8)) + .setModificationTime(fakeClock.nowUtc()) + .setClientId("TheRegistrar") + .setTrid(Trid.create("ABC-123", "server-trid")) + .setBySuperuser(false) + .setReason("reason") + .setRequestedByRegistrar(true) + .setDomainContent(domain) + .setDomainRepoId(domain.createVKey()) + .build(); + } } diff --git a/core/src/test/java/google/registry/model/history/HostHistoryTest.java b/core/src/test/java/google/registry/model/history/HostHistoryTest.java index 1b125a5d1..3593ab53b 100644 --- a/core/src/test/java/google/registry/model/history/HostHistoryTest.java +++ b/core/src/test/java/google/registry/model/history/HostHistoryTest.java @@ -17,12 +17,15 @@ package google.registry.model.history; import static com.google.common.truth.Truth.assertThat; import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.testing.DatastoreHelper.newHostResourceWithRoid; import static google.registry.testing.SqlHelper.saveRegistrar; import static java.nio.charset.StandardCharsets.UTF_8; +import com.googlecode.objectify.Key; import google.registry.model.EntityTestCase; import google.registry.model.eppcommon.Trid; +import google.registry.model.host.HostBase; import google.registry.model.host.HostHistory; import google.registry.model.host.HostResource; import google.registry.model.reporting.HistoryEntry; @@ -44,19 +47,8 @@ public class HostHistoryTest extends EntityTestCase { jpaTm().transact(() -> jpaTm().saveNew(host)); VKey hostVKey = VKey.createSql(HostResource.class, "host1"); HostResource hostFromDb = jpaTm().transact(() -> jpaTm().load(hostVKey)); - HostHistory hostHistory = - new HostHistory.Builder() - .setType(HistoryEntry.Type.HOST_CREATE) - .setXmlBytes("".getBytes(UTF_8)) - .setModificationTime(fakeClock.nowUtc()) - .setClientId("TheRegistrar") - .setTrid(Trid.create("ABC-123", "server-trid")) - .setBySuperuser(false) - .setReason("reason") - .setRequestedByRegistrar(true) - .setHostBase(hostFromDb) - .setHostRepoId(hostVKey) - .build(); + HostHistory hostHistory = createHostHistory(hostFromDb, hostVKey); + hostHistory.id = null; jpaTm().transact(() -> jpaTm().saveNew(hostHistory)); jpaTm() .transact( @@ -69,10 +61,49 @@ public class HostHistoryTest extends EntityTestCase { }); } + @Test + public void testOfySave() { + saveRegistrar("registrar1"); + + HostResource host = newHostResourceWithRoid("ns1.example.com", "host1"); + tm().transact(() -> tm().saveNew(host)); + VKey hostVKey = VKey.create(HostResource.class, "host1", Key.create(host)); + HostResource hostFromDb = tm().transact(() -> tm().load(hostVKey)); + HostHistory hostHistory = createHostHistory(hostFromDb, hostVKey); + fakeClock.advanceOneMilli(); + tm().transact(() -> tm().saveNew(hostHistory)); + + // retrieving a HistoryEntry or a HostHistory with the same key should return the same object + // note: due to the @EntitySubclass annotation. all Keys for ContactHistory objects will have + // type HistoryEntry + VKey hostHistoryVKey = VKey.createOfy(HostHistory.class, Key.create(hostHistory)); + VKey historyEntryVKey = + VKey.createOfy(HistoryEntry.class, Key.create(hostHistory.asHistoryEntry())); + HostHistory hostHistoryFromDb = tm().transact(() -> tm().load(hostHistoryVKey)); + HistoryEntry historyEntryFromDb = tm().transact(() -> tm().load(historyEntryVKey)); + + assertThat(hostHistoryFromDb).isEqualTo(historyEntryFromDb); + } + private void assertHostHistoriesEqual(HostHistory one, HostHistory two) { assertAboutImmutableObjects().that(one).isEqualExceptFields(two, "hostBase"); assertAboutImmutableObjects() .that(one.getHostBase()) .isEqualExceptFields(two.getHostBase(), "repoId"); } + + private HostHistory createHostHistory(HostBase hostBase, VKey hostVKey) { + return new HostHistory.Builder() + .setType(HistoryEntry.Type.HOST_CREATE) + .setXmlBytes("".getBytes(UTF_8)) + .setModificationTime(fakeClock.nowUtc()) + .setClientId("TheRegistrar") + .setTrid(Trid.create("ABC-123", "server-trid")) + .setBySuperuser(false) + .setReason("reason") + .setRequestedByRegistrar(true) + .setHostBase(hostBase) + .setHostRepoId(hostVKey) + .build(); + } } diff --git a/core/src/test/resources/google/registry/model/schema.txt b/core/src/test/resources/google/registry/model/schema.txt index 262fcbfee..87253ee33 100644 --- a/core/src/test/resources/google/registry/model/schema.txt +++ b/core/src/test/resources/google/registry/model/schema.txt @@ -99,6 +99,46 @@ class google.registry.model.contact.ContactAddress { class google.registry.model.contact.ContactAuthInfo { google.registry.model.eppcommon.AuthInfo$PasswordAuth pw; } +class google.registry.model.contact.ContactBase { + @Id java.lang.String repoId; + com.google.common.collect.ImmutableSortedMap> revisions; + google.registry.model.CreateAutoTimestamp creationTime; + google.registry.model.UpdateAutoTimestamp updateTimestamp; + google.registry.model.contact.ContactAuthInfo authInfo; + google.registry.model.contact.ContactPhoneNumber fax; + google.registry.model.contact.ContactPhoneNumber voice; + google.registry.model.contact.Disclose disclose; + google.registry.model.contact.PostalInfo internationalizedPostalInfo; + google.registry.model.contact.PostalInfo localizedPostalInfo; + google.registry.model.transfer.ContactTransferData transferData; + java.lang.String contactId; + java.lang.String creationClientId; + java.lang.String currentSponsorClientId; + java.lang.String email; + java.lang.String lastEppUpdateClientId; + java.lang.String searchName; + java.util.Set status; + org.joda.time.DateTime deletionTime; + org.joda.time.DateTime lastEppUpdateTime; + org.joda.time.DateTime lastTransferTime; +} +class google.registry.model.contact.ContactHistory { + @Id java.lang.Long id; + @Parent com.googlecode.objectify.Key parent; + boolean bySuperuser; + byte[] xmlBytes; + google.registry.model.contact.ContactBase contactBase; + google.registry.model.domain.Period period; + google.registry.model.eppcommon.Trid trid; + google.registry.model.reporting.HistoryEntry$Type type; + google.registry.persistence.VKey contactRepoId; + java.lang.Boolean requestedByRegistrar; + java.lang.String clientId; + java.lang.String otherClientId; + java.lang.String reason; + java.util.Set domainTransactionRecords; + org.joda.time.DateTime modificationTime; +} class google.registry.model.contact.ContactPhoneNumber { java.lang.String extension; java.lang.String phoneNumber; @@ -191,6 +231,52 @@ class google.registry.model.domain.DomainBase { org.joda.time.DateTime lastTransferTime; org.joda.time.DateTime registrationExpirationTime; } +class google.registry.model.domain.DomainContent { + @Id java.lang.String repoId; + com.google.common.collect.ImmutableSortedMap> revisions; + google.registry.model.CreateAutoTimestamp creationTime; + google.registry.model.UpdateAutoTimestamp updateTimestamp; + google.registry.model.domain.DomainAuthInfo authInfo; + google.registry.model.domain.launch.LaunchNotice launchNotice; + google.registry.model.transfer.DomainTransferData transferData; + google.registry.persistence.VKey autorenewBillingEvent; + google.registry.persistence.VKey autorenewPollMessage; + google.registry.persistence.VKey deletePollMessage; + java.lang.String creationClientId; + java.lang.String currentSponsorClientId; + java.lang.String fullyQualifiedDomainName; + java.lang.String idnTableName; + java.lang.String lastEppUpdateClientId; + java.lang.String smdId; + java.lang.String tld; + java.util.Set allContacts; + java.util.Set gracePeriods; + java.util.Set dsData; + java.util.Set status; + java.util.Set> nsHosts; + java.util.Set subordinateHosts; + org.joda.time.DateTime deletionTime; + org.joda.time.DateTime lastEppUpdateTime; + org.joda.time.DateTime lastTransferTime; + org.joda.time.DateTime registrationExpirationTime; +} +class google.registry.model.domain.DomainHistory { + @Id java.lang.Long id; + @Parent com.googlecode.objectify.Key parent; + boolean bySuperuser; + byte[] xmlBytes; + google.registry.model.domain.DomainContent domainContent; + google.registry.model.domain.Period period; + google.registry.model.eppcommon.Trid trid; + google.registry.model.reporting.HistoryEntry$Type type; + google.registry.persistence.VKey domainRepoId; + java.lang.Boolean requestedByRegistrar; + java.lang.String clientId; + java.lang.String otherClientId; + java.lang.String reason; + java.util.Set domainTransactionRecords; + org.joda.time.DateTime modificationTime; +} class google.registry.model.domain.GracePeriod { google.registry.model.domain.rgp.GracePeriodStatus type; google.registry.persistence.VKey billingEventOneTime; @@ -288,6 +374,40 @@ class google.registry.model.eppcommon.Trid { java.lang.String clientTransactionId; java.lang.String serverTransactionId; } +class google.registry.model.host.HostBase { + @Id java.lang.String repoId; + com.google.common.collect.ImmutableSortedMap> revisions; + google.registry.model.CreateAutoTimestamp creationTime; + google.registry.model.UpdateAutoTimestamp updateTimestamp; + google.registry.persistence.VKey superordinateDomain; + java.lang.String creationClientId; + java.lang.String currentSponsorClientId; + java.lang.String fullyQualifiedHostName; + java.lang.String lastEppUpdateClientId; + java.util.Set status; + java.util.Set inetAddresses; + org.joda.time.DateTime deletionTime; + org.joda.time.DateTime lastEppUpdateTime; + org.joda.time.DateTime lastSuperordinateChange; + org.joda.time.DateTime lastTransferTime; +} +class google.registry.model.host.HostHistory { + @Id java.lang.Long id; + @Parent com.googlecode.objectify.Key parent; + boolean bySuperuser; + byte[] xmlBytes; + google.registry.model.domain.Period period; + google.registry.model.eppcommon.Trid trid; + google.registry.model.host.HostBase hostBase; + google.registry.model.reporting.HistoryEntry$Type type; + google.registry.persistence.VKey hostRepoId; + java.lang.Boolean requestedByRegistrar; + java.lang.String clientId; + java.lang.String otherClientId; + java.lang.String reason; + java.util.Set domainTransactionRecords; + org.joda.time.DateTime modificationTime; +} class google.registry.model.host.HostResource { @Id java.lang.String repoId; com.google.common.collect.ImmutableSortedMap> revisions;