From 8059ab5c90e43fdf118f24f5b26f63948aab1b37 Mon Sep 17 00:00:00 2001 From: cgoldfeder Date: Mon, 29 Aug 2016 11:56:35 -0700 Subject: [PATCH] A @DoNotHydrate annotation for toHydratedString This is needed for a soon-to-be-submitted CL that changes all Refs to Keys and therefore removes the logic in toHydratedString that doesn't expand Keys. We need to be able to tag types as unexpandable to avoid cycles. It would be better to tag fields, not types, but that is a much harder change and not currently needed by the use case of the following CL, so for now this suffices. While I am in here, add tests for all of the features of toHydratedString. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=131618634 --- .../registry/model/ImmutableObject.java | 16 ++- .../registry/model/ImmutableObjectTest.java | 114 ++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/java/google/registry/model/ImmutableObject.java b/java/google/registry/model/ImmutableObject.java index 849f4ae05..7837d6cf6 100644 --- a/java/google/registry/model/ImmutableObject.java +++ b/java/google/registry/model/ImmutableObject.java @@ -17,6 +17,8 @@ package google.registry.model; import static com.google.common.base.Functions.identity; import static com.google.common.collect.Iterables.transform; import static com.google.common.collect.Maps.transformValues; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.base.Function; import com.google.common.base.Joiner; @@ -25,6 +27,9 @@ import com.google.common.collect.Maps; import com.googlecode.objectify.Ref; import com.googlecode.objectify.annotation.Ignore; import google.registry.model.domain.ReferenceUnion; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.util.Arrays; import java.util.Collection; import java.util.Map; @@ -37,6 +42,12 @@ import javax.xml.bind.annotation.XmlTransient; @XmlTransient public abstract class ImmutableObject implements Cloneable { + /** Marker to indicate that {@link #toHydratedString} should not hydrate a field of this type. */ + @Documented + @Retention(RUNTIME) + @Target(TYPE) + public static @interface DoNotHydrate {} + @Ignore @XmlTransient Integer hashCode; @@ -109,7 +120,10 @@ public abstract class ImmutableObject implements Cloneable { } else if (input instanceof Ref) { // Only follow references of type Ref, not of type Key (the latter deliberately used for // references that should not be followed) - return apply(((Ref) input).get()); + Object target = ((Ref) input).get(); + return target != null && target.getClass().isAnnotationPresent(DoNotHydrate.class) + ? input + : apply(target); } else if (input instanceof Map) { return transformValues((Map) input, this); } else if (input instanceof Collection) { diff --git a/javatests/google/registry/model/ImmutableObjectTest.java b/javatests/google/registry/model/ImmutableObjectTest.java index 307fc3a88..0b474446c 100644 --- a/javatests/google/registry/model/ImmutableObjectTest.java +++ b/javatests/google/registry/model/ImmutableObjectTest.java @@ -19,12 +19,22 @@ import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Sets.newHashSet; import static com.google.common.truth.Truth.assertThat; import static google.registry.model.ImmutableObject.cloneEmptyToNull; +import static google.registry.testing.DatastoreHelper.persistActiveContact; +import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.util.DateTimeUtils.START_OF_TIME; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import com.googlecode.objectify.Key; +import com.googlecode.objectify.ObjectifyService; +import com.googlecode.objectify.Ref; +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.Id; +import google.registry.model.ImmutableObject.DoNotHydrate; +import google.registry.model.domain.ReferenceUnion; +import google.registry.testing.AppEngineRule; import google.registry.util.CidrAddressBlock; import java.util.ArrayDeque; import java.util.Arrays; @@ -33,6 +43,8 @@ import java.util.List; import java.util.Map; import java.util.Set; import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -41,6 +53,17 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class ImmutableObjectTest { + @Rule + public final AppEngineRule appEngine = AppEngineRule.builder() + .withDatastore() + .build(); + + @Before + public void register() { + ObjectifyService.register(HydratableObject.class); + ObjectifyService.register(UnhydratableObject.class); + } + /** Simple subclass of ImmutableObject. */ public static class SimpleObject extends ImmutableObject { String a; @@ -232,4 +255,95 @@ public class ImmutableObjectTest { assertThat(cloned.heterogenousMap).containsEntry("a", new SimpleObject("", "")); assertThat(cloned.heterogenousMap).containsEntry("b", ""); } + + /** Subclass of ImmutableObject with keys to other objects. */ + public static class RootObject extends ImmutableObject { + Ref hydratable; + + Ref unhydratable; + + Key key; + + Map> map; + + Set> set; + + ReferenceUnion referenceUnion; + } + + /** Hydratable subclass of ImmutableObject. */ + @Entity + public static class HydratableObject extends ImmutableObject { + @Id + long id = 1; + + String value; + } + + /** Unhydratable subclass of SimpleObject. */ + @Entity + @DoNotHydrate + public static class UnhydratableObject extends ImmutableObject { + @Id + long id = 1; + + String value; + } + + @Test + public void testToHydratedString_skipsDoNotHydrate() { + HydratableObject hydratable = new HydratableObject(); + hydratable.value = "expected"; + UnhydratableObject unhydratable = new UnhydratableObject(); + unhydratable.value = "unexpected"; + RootObject root = new RootObject(); + root.hydratable = Ref.create(persistResource(hydratable)); + root.unhydratable = Ref.create(persistResource(unhydratable)); + assertThat(root.toHydratedString()).contains("expected"); + assertThat(root.toHydratedString()).doesNotContain("unexpected"); + } + + @Test + public void testToHydratedString_skipsKeys() { + HydratableObject hydratable = new HydratableObject(); + hydratable.value = "unexpected"; + RootObject root = new RootObject(); + root.key = Key.create(persistResource(hydratable)); + assertThat(root.toHydratedString()).doesNotContain("unexpected"); + } + + @Test + public void testToHydratedString_expandsMaps() { + HydratableObject hydratable = new HydratableObject(); + hydratable.value = "expected"; + UnhydratableObject unhydratable = new UnhydratableObject(); + unhydratable.value = "unexpected"; + RootObject root = new RootObject(); + root.map = ImmutableMap.>of( + "hydratable", Ref.create(persistResource(hydratable)), + "unhydratable", Ref.create(persistResource(unhydratable))); + assertThat(root.toHydratedString()).contains("expected"); + assertThat(root.toHydratedString()).doesNotContain("unexpected"); + } + + @Test + public void testToHydratedString_expandsCollections() { + HydratableObject hydratable = new HydratableObject(); + hydratable.value = "expected"; + UnhydratableObject unhydratable = new UnhydratableObject(); + unhydratable.value = "unexpected"; + RootObject root = new RootObject(); + root.set = ImmutableSet.>of( + Ref.create(persistResource(hydratable)), + Ref.create(persistResource(unhydratable))); + assertThat(root.toHydratedString()).contains("expected"); + assertThat(root.toHydratedString()).doesNotContain("unexpected"); + } + + @Test + public void testToHydratedString_expandsReferenceUnions() { + RootObject root = new RootObject(); + root.referenceUnion = ReferenceUnion.create(Ref.create(persistActiveContact("expected"))); + assertThat(root.toHydratedString()).contains("expected"); + } }