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;