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"); + } }