content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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 org.apache.beam.sdk.transforms.display;
  
  import static org.apache.beam.sdk.transforms.display.DisplayDataMatchers.hasDisplayItem;
  import static org.apache.beam.sdk.transforms.display.DisplayDataMatchers.hasKey;
  import static org.apache.beam.sdk.transforms.display.DisplayDataMatchers.hasLabel;
  import static org.apache.beam.sdk.transforms.display.DisplayDataMatchers.hasNamespace;
  import static org.apache.beam.sdk.transforms.display.DisplayDataMatchers.hasPath;
  import static org.apache.beam.sdk.transforms.display.DisplayDataMatchers.hasType;
  import static org.apache.beam.sdk.transforms.display.DisplayDataMatchers.hasValue;
  import static org.apache.beam.sdk.transforms.display.DisplayDataMatchers.includesDisplayDataFor;
  import static org.hamcrest.MatcherAssert.assertThat;
  import static org.hamcrest.Matchers.allOf;
  import static org.hamcrest.Matchers.empty;
  import static org.hamcrest.Matchers.everyItem;
  import static org.hamcrest.Matchers.hasItem;
  import static org.hamcrest.Matchers.hasItems;
  import static org.hamcrest.Matchers.hasSize;
  import static org.hamcrest.Matchers.is;
  import static org.hamcrest.Matchers.isA;
  import static org.hamcrest.Matchers.isEmptyOrNullString;
  import static org.hamcrest.Matchers.not;
  import static org.hamcrest.Matchers.notNullValue;
  import static org.hamcrest.Matchers.nullValue;
  import static org.hamcrest.Matchers.startsWith;
  import static org.junit.Assert.assertEquals;
  import static org.junit.Assert.fail;
  
  import com.fasterxml.jackson.databind.JsonNode;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import com.google.auto.value.AutoValue;
  import com.google.common.testing.EqualsTester;
  import java.io.IOException;
  import java.io.Serializable;
  import java.util.Collection;
  import java.util.Map;
  import java.util.regex.Pattern;
  import org.apache.beam.sdk.options.ValueProvider;
  import org.apache.beam.sdk.options.ValueProvider.StaticValueProvider;
  import org.apache.beam.sdk.transforms.PTransform;
  import org.apache.beam.sdk.transforms.display.DisplayData.Item;
  import org.apache.beam.sdk.util.SerializableUtils;
  import org.apache.beam.sdk.values.PCollection;
  import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
  import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMultimap;
  import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables;
  import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Multimap;
  import org.checkerframework.checker.nullness.qual.Nullable;
  import org.hamcrest.CustomTypeSafeMatcher;
  import org.hamcrest.FeatureMatcher;
  import org.hamcrest.Matcher;
  import org.hamcrest.Matchers;
  import org.joda.time.Duration;
  import org.joda.time.Instant;
  import org.joda.time.format.DateTimeFormatter;
  import org.joda.time.format.ISODateTimeFormat;
  import org.junit.Rule;
  import org.junit.Test;
  import org.junit.rules.ExpectedException;
  import org.junit.runner.RunWith;
  import org.junit.runners.JUnit4;
  
  /** Tests for {@link DisplayData} class. */
  @RunWith(JUnit4.class)
  public class DisplayDataTest implements Serializable {
    @Rule public transient ExpectedException thrown = ExpectedException.none();
  
    private static final DateTimeFormatter ISO_FORMATTER = ISODateTimeFormat.dateTime();
    private static final ObjectMapper MAPPER = new ObjectMapper();
  
    @Test
    public void testTypicalUsage() {
      final HasDisplayData subComponent1 =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("ExpectedAnswer", 42));
            }
          };
  
      final HasDisplayData subComponent2 =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder
                  .add(DisplayData.item("Location", "Seattle"))
                  .add(DisplayData.item("Forecast", "Rain"));
            }
          };
  
      PTransform<?, ?> transform =
          new PTransform<PCollection<String>, PCollection<String>>() {
            final Instant defaultStartTime = new Instant(0);
            Instant startTime = defaultStartTime;
  
            @Override
            public PCollection<String> expand(PCollection<String> begin) {
              throw new IllegalArgumentException("Should never be applied");
            }
  
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder
                  .include("p1", subComponent1)
                  .include("p2", subComponent2)
                  .add(DisplayData.item("minSproggles", 200).withLabel("Minimum Required Sproggles"))
                  .add(DisplayData.item("fireLasers", true))
                  .addIfNotDefault(DisplayData.item("startTime", startTime), defaultStartTime)
                  .add(DisplayData.item("timeBomb", Instant.now().plus(Duration.standardDays(1))))
                  .add(DisplayData.item("filterLogic", subComponent1.getClass()))
                  .add(
                      DisplayData.item("serviceUrl", "google.com/fizzbang")
                          .withLinkUrl("http://www.google.com/fizzbang"));
            }
          };
  
      DisplayData data = DisplayData.from(transform);
  
      assertThat(data.items(), not(empty()));
      assertThat(
          data.items(),
          everyItem(
              allOf(
                  hasKey(not(isEmptyOrNullString())),
                  hasNamespace(
                      Matchers.<Class<?>>isOneOf(
                          transform.getClass(), subComponent1.getClass(), subComponent2.getClass())),
                  hasType(notNullValue(DisplayData.Type.class)),
                  hasValue(not(isEmptyOrNullString())))));
    }
  
    @Test
    public void testDefaultInstance() {
      DisplayData none = DisplayData.none();
      assertThat(none.items(), empty());
    }
  
    @Test
    public void testCanBuildDisplayData() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder.add(DisplayData.item("foo", "bar"));
                }
              });
  
      assertThat(data.items(), hasSize(1));
      assertThat(data, hasDisplayItem("foo", "bar"));
    }
  
    @Test
    public void testStaticValueProviderDate() {
      final Instant value = Instant.now();
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder.add(DisplayData.item("foo", StaticValueProvider.of(value)));
                }
              });
  
      @SuppressWarnings("unchecked")
      DisplayData.Item item = (DisplayData.Item) data.items().toArray()[0];
  
      @SuppressWarnings("unchecked")
      Matcher<Item> matchesAllOf =
          allOf(
              hasKey("foo"),
              hasType(DisplayData.Type.TIMESTAMP),
              hasValue(ISO_FORMATTER.print(value)));
  
      assertThat(item, matchesAllOf);
    }
  
    @Test
    public void testStaticValueProviderString() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder.add(DisplayData.item("foo", StaticValueProvider.of("bar")));
                }
              });
  
      assertThat(data.items(), hasSize(1));
      assertThat(data, hasDisplayItem("foo", "bar"));
    }
  
    @Test
    public void testStaticValueProviderInt() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder.add(DisplayData.item("foo", StaticValueProvider.of(1)));
                }
              });
  
      assertThat(data.items(), hasSize(1));
      assertThat(data, hasDisplayItem("foo", 1));
    }
  
    @Test
    public void testInaccessibleValueProvider() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder.add(
                      DisplayData.item(
                          "foo",
                          new ValueProvider<String>() {
                            @Override
                            public boolean isAccessible() {
                              return false;
                            }
  
                            @Override
                            public String get() {
                              return "bar";
                            }
  
                            @Override
                            public String toString() {
                              return "toString";
                            }
                          }));
                }
              });
  
      assertThat(data.items(), hasSize(1));
      assertThat(data, hasDisplayItem("foo", "toString"));
    }
  
    @Test
    public void testAsMap() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder.add(DisplayData.item("foo", "bar"));
                }
              });
  
      Map<DisplayData.Identifier, DisplayData.Item> map = data.asMap();
      assertEquals(1, map.size());
      assertThat(data, hasDisplayItem("foo", "bar"));
      assertEquals(map.values(), data.items());
    }
  
    @Test
    public void testItemProperties() {
      final Instant value = Instant.now();
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder.add(
                      DisplayData.item("now", value)
                          .withLabel("the current instant")
                          .withLinkUrl("http://time.gov")
                          .withNamespace(DisplayDataTest.class));
                }
              });
  
      @SuppressWarnings("unchecked")
      DisplayData.Item item = (DisplayData.Item) data.items().toArray()[0];
  
      @SuppressWarnings("unchecked")
      Matcher<Item> matchesAllOf =
          allOf(
              hasNamespace(DisplayDataTest.class),
              hasKey("now"),
              hasType(DisplayData.Type.TIMESTAMP),
              hasValue(ISO_FORMATTER.print(value)),
              hasShortValue(nullValue(String.class)),
              hasLabel("the current instant"),
              hasUrl(is("http://time.gov")));
  
      assertThat(item, matchesAllOf);
    }
  
    @Test
    public void testUnspecifiedOptionalProperties() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder.add(DisplayData.item("foo", "bar"));
                }
              });
  
      assertThat(
          data,
          hasDisplayItem(allOf(hasLabel(nullValue(String.class)), hasUrl(nullValue(String.class)))));
    }
  
    @Test
    public void testAddIfNotDefault() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder
                      .addIfNotDefault(DisplayData.item("defaultString", "foo"), "foo")
                      .addIfNotDefault(DisplayData.item("notDefaultString", "foo"), "notFoo")
                      .addIfNotDefault(DisplayData.item("defaultInteger", 1), 1)
                      .addIfNotDefault(DisplayData.item("notDefaultInteger", 1), 2)
                      .addIfNotDefault(DisplayData.item("defaultDouble", 123.4), 123.4)
                      .addIfNotDefault(DisplayData.item("notDefaultDouble", 123.4), 234.5)
                      .addIfNotDefault(DisplayData.item("defaultBoolean", true), true)
                      .addIfNotDefault(DisplayData.item("notDefaultBoolean", true), false)
                      .addIfNotDefault(
                          DisplayData.item("defaultInstant", new Instant(0)), new Instant(0))
                      .addIfNotDefault(
                          DisplayData.item("notDefaultInstant", new Instant(0)), Instant.now())
                      .addIfNotDefault(
                          DisplayData.item("defaultDuration", Duration.ZERO), Duration.ZERO)
                      .addIfNotDefault(
                          DisplayData.item("notDefaultDuration", Duration.millis(1234)),
                          Duration.ZERO)
                      .addIfNotDefault(
                          DisplayData.item("defaultClass", DisplayDataTest.class),
                          DisplayDataTest.class)
                      .addIfNotDefault(
                          DisplayData.item("notDefaultClass", DisplayDataTest.class), null);
                }
              });
  
      assertThat(data.items(), hasSize(7));
      assertThat(data.items(), everyItem(hasKey(startsWith("notDefault"))));
    }
  
    @Test
    @SuppressWarnings("UnnecessaryBoxing")
    public void testInterpolatedTypeDefaults() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder
                      .addIfNotDefault(DisplayData.item("integer", 123), 123)
                      .addIfNotDefault(
                          DisplayData.item("Integer", Integer.valueOf(123)), Integer.valueOf(123))
                      .addIfNotDefault(DisplayData.item("long", 123L), 123L)
                      .addIfNotDefault(DisplayData.item("Long", Long.valueOf(123)), Long.valueOf(123))
                      .addIfNotDefault(DisplayData.item("float", 1.23f), 1.23f)
                      .addIfNotDefault(
                          DisplayData.item("Float", Float.valueOf(1.23f)), Float.valueOf(1.23f))
                      .addIfNotDefault(DisplayData.item("double", 1.23), 1.23)
                      .addIfNotDefault(
                          DisplayData.item("Double", Double.valueOf(1.23)), Double.valueOf(1.23))
                      .addIfNotDefault(DisplayData.item("boolean", true), true)
                      .addIfNotDefault(DisplayData.item("Boolean", Boolean.TRUE), Boolean.TRUE);
                }
              });
  
      assertThat(data.items(), empty());
    }
  
    @Test
    public void testAddIfNotNull() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder
                      .addIfNotNull(DisplayData.item("nullString", (String) null))
                      .addIfNotNull(DisplayData.item("nullVPString", (ValueProvider<String>) null))
                      .addIfNotNull(DisplayData.item("nullierVPString", StaticValueProvider.of(null)))
                      .addIfNotNull(DisplayData.item("notNullString", "foo"))
                      .addIfNotNull(DisplayData.item("nullLong", (Long) null))
                      .addIfNotNull(DisplayData.item("notNullLong", 1234L))
                      .addIfNotNull(DisplayData.item("nullDouble", (Double) null))
                      .addIfNotNull(DisplayData.item("notNullDouble", 123.4))
                      .addIfNotNull(DisplayData.item("nullBoolean", (Boolean) null))
                      .addIfNotNull(DisplayData.item("notNullBoolean", true))
                      .addIfNotNull(DisplayData.item("nullInstant", (Instant) null))
                      .addIfNotNull(DisplayData.item("notNullInstant", Instant.now()))
                      .addIfNotNull(DisplayData.item("nullDuration", (Duration) null))
                      .addIfNotNull(DisplayData.item("notNullDuration", Duration.ZERO))
                      .addIfNotNull(DisplayData.item("nullClass", (Class<?>) null))
                      .addIfNotNull(DisplayData.item("notNullClass", DisplayDataTest.class));
                }
              });
  
      assertThat(data.items(), hasSize(7));
      assertThat(data.items(), everyItem(hasKey(startsWith("notNull"))));
    }
  
    @Test
    public void testModifyingConditionalItemIsSafe() {
      HasDisplayData component =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.addIfNotNull(
                  DisplayData.item("nullItem", (Class<?>) null)
                      .withLinkUrl("http://abc")
                      .withNamespace(DisplayDataTest.class)
                      .withLabel("Null item should be safe"));
            }
          };
  
      DisplayData.from(component); // should not throw
    }
  
    @Test
    public void testRootPath() {
      DisplayData.Path root = DisplayData.Path.root();
      assertThat(root.getComponents(), empty());
    }
  
    @Test
    public void testExtendPath() {
      DisplayData.Path a = DisplayData.Path.root().extend("a");
      assertThat(a.getComponents(), hasItems("a"));
  
      DisplayData.Path b = a.extend("b");
      assertThat(b.getComponents(), hasItems("a", "b"));
    }
  
    @Test
    public void testExtendNullPathValidation() {
      DisplayData.Path root = DisplayData.Path.root();
      thrown.expect(NullPointerException.class);
      root.extend(null);
    }
  
    @Test
    public void testExtendEmptyPathValidation() {
      DisplayData.Path root = DisplayData.Path.root();
      thrown.expect(IllegalArgumentException.class);
      root.extend("");
    }
  
    @Test
    public void testAbsolute() {
      DisplayData.Path path = DisplayData.Path.absolute("a", "b", "c");
      assertThat(path.getComponents(), hasItems("a", "b", "c"));
    }
  
    @Test
    public void testAbsoluteValidationNullFirstPath() {
      thrown.expect(NullPointerException.class);
      DisplayData.Path.absolute(null, "foo", "bar");
    }
  
    @Test
    public void testAbsoluteValidationEmptyFirstPath() {
      thrown.expect(IllegalArgumentException.class);
      DisplayData.Path.absolute("", "foo", "bar");
    }
  
    @Test
    public void testAbsoluteValidationNullSubsequentPath() {
      thrown.expect(NullPointerException.class);
      DisplayData.Path.absolute("a", "b", null, "c");
    }
  
    @Test
    public void testAbsoluteValidationEmptySubsequentPath() {
      thrown.expect(IllegalArgumentException.class);
      DisplayData.Path.absolute("a", "b", "", "c");
    }
  
    @Test
    public void testPathToString() {
      assertEquals("root string", "[]", DisplayData.Path.root().toString());
      assertEquals("single component", "[a]", DisplayData.Path.absolute("a").toString());
      assertEquals("hierarchy", "[a/b/c]", DisplayData.Path.absolute("a", "b", "c").toString());
    }
  
    @Test
    public void testPathEquality() {
      new EqualsTester()
          .addEqualityGroup(DisplayData.Path.root(), DisplayData.Path.root())
          .addEqualityGroup(DisplayData.Path.root().extend("a"), DisplayData.Path.absolute("a"))
          .addEqualityGroup(
              DisplayData.Path.root().extend("a").extend("b"), DisplayData.Path.absolute("a", "b"))
          .testEquals();
    }
  
    @Test
    public void testIncludes() {
      final HasDisplayData subComponent =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("foo", "bar"));
            }
          };
  
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder.include("p", subComponent);
                }
              });
  
      assertThat(data, includesDisplayDataFor("p", subComponent));
    }
  
    @Test
    public void testIncludeSameComponentAtDifferentPaths() {
      final HasDisplayData subComponent1 =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("foo", "bar"));
            }
          };
      final HasDisplayData subComponent2 =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("foo2", "bar2"));
            }
          };
  
      HasDisplayData component =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.include("p1", subComponent1).include("p2", subComponent2);
            }
          };
  
      DisplayData data = DisplayData.from(component);
      assertThat(data, includesDisplayDataFor("p1", subComponent1));
      assertThat(data, includesDisplayDataFor("p2", subComponent2));
    }
  
    @Test
    public void testIncludesComponentsAtSamePath() {
      HasDisplayData component =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.include("p", new NoopDisplayData()).include("p", new NoopDisplayData());
            }
          };
  
      thrown.expectCause(isA(IllegalArgumentException.class));
      DisplayData.from(component);
    }
  
    @Test
    public void testNullNamespaceOverride() {
      thrown.expectCause(isA(NullPointerException.class));
  
      DisplayData.from(
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("foo", "bar").withNamespace(null));
            }
          });
    }
  
    @Test
    public void testIdentifierEquality() {
      new EqualsTester()
          .addEqualityGroup(
              DisplayData.Identifier.of(DisplayData.Path.absolute("a"), DisplayDataTest.class, "1"),
              DisplayData.Identifier.of(DisplayData.Path.absolute("a"), DisplayDataTest.class, "1"))
          .addEqualityGroup(
              DisplayData.Identifier.of(DisplayData.Path.absolute("b"), DisplayDataTest.class, "1"))
          .addEqualityGroup(
              DisplayData.Identifier.of(DisplayData.Path.absolute("a"), Object.class, "1"))
          .addEqualityGroup(
              DisplayData.Identifier.of(DisplayData.Path.absolute("a"), DisplayDataTest.class, "2"))
          .testEquals();
    }
  
    @Test
    public void testItemEquality() {
      new EqualsTester()
          .addEqualityGroup(DisplayData.item("foo", "bar"), DisplayData.item("foo", "bar"))
          .addEqualityGroup(DisplayData.item("foo", "barz"))
          .testEquals();
    }
  
    @Test
    public void testDisplayDataEquality() {
      HasDisplayData component1 =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("foo", "bar"));
            }
          };
      HasDisplayData component2 =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("foo", "bar"));
            }
          };
  
      DisplayData component1DisplayData1 = DisplayData.from(component1);
      DisplayData component1DisplayData2 = DisplayData.from(component1);
      DisplayData component2DisplayData = DisplayData.from(component2);
  
      new EqualsTester()
          .addEqualityGroup(component1DisplayData1, component1DisplayData2)
          .addEqualityGroup(component2DisplayData)
          .testEquals();
    }
  
    @Test
    public void testAcceptsKeysWithDifferentNamespaces() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder
                      .add(DisplayData.item("foo", "bar"))
                      .include(
                          "p",
                          new HasDisplayData() {
                            @Override
                            public void populateDisplayData(DisplayData.Builder builder) {
                              builder.add(DisplayData.item("foo", "bar"));
                            }
                          });
                }
              });
  
      assertThat(data.items(), hasSize(2));
    }
  
    @Test
    public void testDuplicateKeyThrowsException() {
      thrown.expectCause(isA(IllegalArgumentException.class));
      DisplayData.from(
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("foo", "bar")).add(DisplayData.item("foo", "baz"));
            }
          });
    }
  
    @Test
    public void testDuplicateKeyWithNamespaceOverrideDoesntThrow() {
      DisplayData displayData =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder
                      .add(DisplayData.item("foo", "bar"))
                      .add(DisplayData.item("foo", "baz").withNamespace(DisplayDataTest.class));
                }
              });
  
      assertThat(displayData.items(), hasSize(2));
    }
  
    @Test
    public void testToString() {
      HasDisplayData component =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("foo", "bar"));
            }
          };
  
      DisplayData data = DisplayData.from(component);
      assertEquals(String.format("[]%s:foo=bar", component.getClass().getName()), data.toString());
    }
  
    @Test
    public void testHandlesIncludeCycles() {
  
      final IncludeSubComponent componentA =
          new IncludeSubComponent() {
            @Override
            String getId() {
              return "componentA";
            }
          };
      final IncludeSubComponent componentB =
          new IncludeSubComponent() {
            @Override
            String getId() {
              return "componentB";
            }
          };
  
      HasDisplayData component =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.include("p", componentA);
            }
          };
  
      componentA.subComponent = componentB;
      componentB.subComponent = componentA;
  
      DisplayData data = DisplayData.from(component);
      assertThat(data.items(), hasSize(2));
    }
  
    @Test
    public void testHandlesIncludeCyclesDifferentInstances() {
      HasDisplayData component =
          new DelegatingDisplayData(new DelegatingDisplayData(new NoopDisplayData()));
  
      DisplayData data = DisplayData.from(component);
      assertThat(data.items(), hasSize(2));
    }
  
    private static class DelegatingDisplayData implements HasDisplayData {
      private final HasDisplayData subComponent;
  
      public DelegatingDisplayData(HasDisplayData subComponent) {
        this.subComponent = subComponent;
      }
  
      @Override
      public void populateDisplayData(DisplayData.Builder builder) {
        builder
            .add(DisplayData.item("subComponent", subComponent.getClass()))
            .include("p", subComponent);
      }
    }
  
    @Test
    public void testIncludesSubcomponentsWithObjectEquality() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder
                      .include("p1", new EqualsEverything("foo1", "bar1"))
                      .include("p2", new EqualsEverything("foo2", "bar2"));
                }
              });
  
      assertThat(data.items(), hasSize(2));
    }
  
    private static class EqualsEverything implements HasDisplayData {
      private final String value;
      private final String key;
  
      EqualsEverything(String key, String value) {
        this.key = key;
        this.value = value;
      }
  
      @Override
      public void populateDisplayData(DisplayData.Builder builder) {
        builder.add(DisplayData.item(key, value));
      }
  
      @Override
      public int hashCode() {
        return 1;
      }
  
      @Override
      @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
      public boolean equals(@Nullable Object obj) {
        return true;
      }
    }
  
    @Test
    public void testDelegate() {
      final HasDisplayData subcomponent =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("subCompKey", "foo"));
            }
          };
  
      final HasDisplayData wrapped =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("wrappedKey", "bar")).include("p", subcomponent);
            }
          };
  
      HasDisplayData wrapper =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.delegate(wrapped);
            }
          };
  
      DisplayData data = DisplayData.from(wrapper);
      assertThat(
          data,
          hasDisplayItem(
              allOf(hasKey("wrappedKey"), hasNamespace(wrapped.getClass()), hasPath(/* root */ ))));
      assertThat(
          data,
          hasDisplayItem(
              allOf(hasKey("subCompKey"), hasNamespace(subcomponent.getClass()), hasPath("p"))));
    }
  
    abstract static class IncludeSubComponent implements HasDisplayData {
      HasDisplayData subComponent;
  
      @Override
      public void populateDisplayData(DisplayData.Builder builder) {
        builder.add(DisplayData.item("id", getId())).include(getId(), subComponent);
      }
  
      abstract String getId();
    }
  
    @Test
    public void testTypeMappings() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder
                      .add(DisplayData.item("string", "foobar"))
                      .add(DisplayData.item("integer", 123))
                      .add(DisplayData.item("float", 2.34))
                      .add(DisplayData.item("boolean", true))
                      .add(DisplayData.item("java_class", DisplayDataTest.class))
                      .add(DisplayData.item("timestamp", Instant.now()))
                      .add(DisplayData.item("duration", Duration.standardHours(1)));
                }
              });
  
      Collection<Item> items = data.items();
      assertThat(items, hasItem(allOf(hasKey("string"), hasType(DisplayData.Type.STRING))));
      assertThat(items, hasItem(allOf(hasKey("integer"), hasType(DisplayData.Type.INTEGER))));
      assertThat(items, hasItem(allOf(hasKey("float"), hasType(DisplayData.Type.FLOAT))));
      assertThat(items, hasItem(allOf(hasKey("boolean"), hasType(DisplayData.Type.BOOLEAN))));
      assertThat(items, hasItem(allOf(hasKey("java_class"), hasType(DisplayData.Type.JAVA_CLASS))));
      assertThat(items, hasItem(allOf(hasKey("timestamp"), hasType(DisplayData.Type.TIMESTAMP))));
      assertThat(items, hasItem(allOf(hasKey("duration"), hasType(DisplayData.Type.DURATION))));
    }
  
    @Test
    public void testExplicitItemType() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder
                      .add(DisplayData.item("integer", DisplayData.Type.INTEGER, 1234L))
                      .add(DisplayData.item("string", DisplayData.Type.STRING, "foobar"));
                }
              });
  
      assertThat(data, hasDisplayItem("integer", 1234L));
      assertThat(data, hasDisplayItem("string", "foobar"));
    }
  
    @Test
    public void testFormatIncompatibleTypes() {
      Map<DisplayData.Type, Object> invalidPairs =
          ImmutableMap.<DisplayData.Type, Object>builder()
              .put(DisplayData.Type.STRING, 1234)
              .put(DisplayData.Type.INTEGER, "string value")
              .put(DisplayData.Type.FLOAT, "string value")
              .put(DisplayData.Type.BOOLEAN, "string value")
              .put(DisplayData.Type.TIMESTAMP, "string value")
              .put(DisplayData.Type.DURATION, "string value")
              .put(DisplayData.Type.JAVA_CLASS, "string value")
              .build();
  
      for (Map.Entry<DisplayData.Type, Object> pair : invalidPairs.entrySet()) {
        try {
          DisplayData.Type type = pair.getKey();
          Object invalidValue = pair.getValue();
  
          type.format(invalidValue);
          fail(
              String.format(
                  "Expected exception not thrown for invalid %s value: %s", type, invalidValue));
        } catch (ClassCastException e) {
          // Expected
        }
      }
    }
  
    @Test
    public void testFormatCompatibleTypes() {
      Multimap<DisplayData.Type, Object> validPairs =
          ImmutableMultimap.<DisplayData.Type, Object>builder()
              .put(DisplayData.Type.INTEGER, 1234)
              .put(DisplayData.Type.INTEGER, 1234L)
              .put(DisplayData.Type.FLOAT, 123.4f)
              .put(DisplayData.Type.FLOAT, 123.4)
              .put(DisplayData.Type.FLOAT, 1234)
              .put(DisplayData.Type.FLOAT, 1234L)
              .build();
  
      for (Map.Entry<DisplayData.Type, Object> pair : validPairs.entries()) {
        DisplayData.Type type = pair.getKey();
        Object value = pair.getValue();
  
        try {
          type.format(value);
        } catch (ClassCastException e) {
          throw new AssertionError(
              String.format(
                  "Failed to format %s for DisplayData.%s", value.getClass().getSimpleName(), type),
              e);
        }
      }
    }
  
    @Test
    public void testInvalidExplicitItemType() {
      HasDisplayData component =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("integer", DisplayData.Type.INTEGER, "foobar"));
            }
          };
  
      thrown.expectCause(isA(ClassCastException.class));
      DisplayData.from(component);
    }
  
    @Test
    public void testKnownTypeInference() {
      assertEquals(DisplayData.Type.INTEGER, DisplayData.inferType(1234));
      assertEquals(DisplayData.Type.INTEGER, DisplayData.inferType(1234L));
      assertEquals(DisplayData.Type.FLOAT, DisplayData.inferType(12.3));
      assertEquals(DisplayData.Type.FLOAT, DisplayData.inferType(12.3f));
      assertEquals(DisplayData.Type.BOOLEAN, DisplayData.inferType(true));
      assertEquals(DisplayData.Type.TIMESTAMP, DisplayData.inferType(Instant.now()));
      assertEquals(DisplayData.Type.DURATION, DisplayData.inferType(Duration.millis(1234)));
      assertEquals(DisplayData.Type.JAVA_CLASS, DisplayData.inferType(DisplayDataTest.class));
      assertEquals(DisplayData.Type.STRING, DisplayData.inferType("hello world"));
  
      assertEquals(null, DisplayData.inferType(null));
      assertEquals(null, DisplayData.inferType(new Object() {}));
    }
  
    @Test
    public void testStringFormatting() throws IOException {
      final Instant now = Instant.now();
      final Duration oneHour = Duration.standardHours(1);
  
      HasDisplayData component =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder
                  .add(DisplayData.item("string", "foobar"))
                  .add(DisplayData.item("integer", 123))
                  .add(DisplayData.item("float", 2.34))
                  .add(DisplayData.item("boolean", true))
                  .add(DisplayData.item("java_class", DisplayDataTest.class))
                  .add(DisplayData.item("timestamp", now))
                  .add(DisplayData.item("duration", oneHour));
            }
          };
      DisplayData data = DisplayData.from(component);
  
      assertThat(data, hasDisplayItem("string", "foobar"));
      assertThat(data, hasDisplayItem("integer", 123));
      assertThat(data, hasDisplayItem("float", 2.34));
      assertThat(data, hasDisplayItem("boolean", true));
      assertThat(data, hasDisplayItem("java_class", DisplayDataTest.class));
      assertThat(data, hasDisplayItem("timestamp", now));
      assertThat(data, hasDisplayItem("duration", oneHour));
    }
  
    @Test
    public void testContextProperlyReset() {
      final HasDisplayData subComponent =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("foo", "bar"));
            }
          };
  
      HasDisplayData component =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.include("p", subComponent).add(DisplayData.item("alpha", "bravo"));
            }
          };
  
      DisplayData data = DisplayData.from(component);
      assertThat(data.items(), hasItem(allOf(hasKey("alpha"), hasNamespace(component.getClass()))));
    }
  
    @Test
    public void testFromNull() {
      thrown.expect(NullPointerException.class);
      DisplayData.from(null);
    }
  
    @Test
    public void testIncludeNull() {
      thrown.expectCause(isA(NullPointerException.class));
      DisplayData.from(
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.include("p", null);
            }
          });
    }
  
    @Test
    public void testIncludeNullPath() {
      thrown.expectCause(isA(NullPointerException.class));
      DisplayData.from(
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.include(null, new NoopDisplayData());
            }
          });
    }
  
    @Test
    public void testIncludeEmptyPath() {
      thrown.expectCause(isA(IllegalArgumentException.class));
      DisplayData.from(
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.include("", new NoopDisplayData());
            }
          });
    }
  
    @Test
    public void testNullKey() {
      thrown.expectCause(isA(NullPointerException.class));
      DisplayData.from(
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item(null, "foo"));
            }
          });
    }
  
    @Test
    public void testRejectsNullValues() {
      DisplayData.from(
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              try {
                builder.add(DisplayData.item("key", (String) null));
                throw new RuntimeException("Should throw on null string value");
              } catch (NullPointerException ex) {
                // Expected
              }
  
              try {
                builder.add(DisplayData.item("key", (Class<?>) null));
                throw new RuntimeException("Should throw on null class value");
              } catch (NullPointerException ex) {
                // Expected
              }
  
              try {
                builder.add(DisplayData.item("key", (Duration) null));
                throw new RuntimeException("Should throw on null duration value");
              } catch (NullPointerException ex) {
                // Expected
              }
  
              try {
                builder.add(DisplayData.item("key", (Instant) null));
                throw new RuntimeException("Should throw on null instant value");
              } catch (NullPointerException ex) {
                // Expected
              }
            }
          });
    }
  
    @Test
    public void testAcceptsNullOptionalValues() {
      DisplayData.from(
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("key", "value").withLabel(null).withLinkUrl(null));
            }
          });
  
      // Should not throw
    }
  
    @Test
    public void testJsonSerialization() throws IOException {
      final String stringValue = "foobar";
      final int intValue = 1234;
      final double floatValue = 123.4;
      final boolean boolValue = true;
      final int durationMillis = 1234;
  
      HasDisplayData component =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder
                  .add(DisplayData.item("string", stringValue))
                  .add(DisplayData.item("long", intValue))
                  .add(DisplayData.item("double", floatValue))
                  .add(DisplayData.item("boolean", boolValue))
                  .add(DisplayData.item("instant", new Instant(0)))
                  .add(DisplayData.item("duration", Duration.millis(durationMillis)))
                  .add(
                      DisplayData.item("class", DisplayDataTest.class)
                          .withLinkUrl("http://abc")
                          .withLabel("baz"));
            }
          };
      DisplayData data = DisplayData.from(component);
  
      JsonNode json = MAPPER.readTree(MAPPER.writeValueAsBytes(data));
      assertThat(json, hasExpectedJson(component, "STRING", "string", quoted(stringValue)));
      assertThat(json, hasExpectedJson(component, "INTEGER", "long", intValue));
      assertThat(json, hasExpectedJson(component, "FLOAT", "double", floatValue));
      assertThat(json, hasExpectedJson(component, "BOOLEAN", "boolean", boolValue));
      assertThat(json, hasExpectedJson(component, "DURATION", "duration", durationMillis));
      assertThat(
          json,
          hasExpectedJson(component, "TIMESTAMP", "instant", quoted("1970-01-01T00:00:00.000Z")));
      assertThat(
          json,
          hasExpectedJson(
              component,
              "JAVA_CLASS",
              "class",
              quoted(DisplayDataTest.class.getName()),
              quoted("DisplayDataTest"),
              "baz",
              "http://abc"));
    }
  
    @Test
    public void testJsonSerializationAnonymousClassNamespace() throws IOException {
      HasDisplayData component =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("foo", "bar"));
            }
          };
      DisplayData data = DisplayData.from(component);
  
      JsonNode json = MAPPER.readTree(MAPPER.writeValueAsBytes(data));
      String namespace = json.elements().next().get("namespace").asText();
      final Pattern anonClassRegex =
          Pattern.compile(Pattern.quote(DisplayDataTest.class.getName()) + "\\$\\d+$");
      assertThat(
          namespace,
          new CustomTypeSafeMatcher<String>("anonymous class regex: " + anonClassRegex) {
            @Override
            protected boolean matchesSafely(String item) {
              java.util.regex.Matcher m = anonClassRegex.matcher(item);
              return m.matches();
            }
          });
    }
  
    @Test
    public void testCanSerializeItemSpecReference() {
      DisplayData.ItemSpec<?> spec = DisplayData.item("clazz", DisplayDataTest.class);
      SerializableUtils.ensureSerializable(new HoldsItemSpecReference(spec));
    }
  
    private static class HoldsItemSpecReference implements Serializable {
      public HoldsItemSpecReference(DisplayData.ItemSpec<?> spec) {}
    }
  
    @Test
    public void testSerializable() {
      DisplayData data =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder.add(DisplayData.item("foo", "bar"));
                }
              });
  
      DisplayData serData = SerializableUtils.clone(data);
      assertEquals(data, serData);
    }
  
    /**
     * Verify that {@link DisplayData.Builder} can recover from exceptions thrown in user code. This
     * is not used within the Beam SDK since we want all code to produce valid DisplayData. This test
     * just ensures it is possible to write custom code that does recover.
     */
    @Test
    public void testCanRecoverFromBuildException() {
      final HasDisplayData safeComponent =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.add(DisplayData.item("a", "a"));
            }
          };
  
      final HasDisplayData failingComponent =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              throw new RuntimeException("oh noes!");
            }
          };
  
      DisplayData displayData =
          DisplayData.from(
              new HasDisplayData() {
                @Override
                public void populateDisplayData(DisplayData.Builder builder) {
                  builder.add(DisplayData.item("b", "b")).add(DisplayData.item("c", "c"));
  
                  try {
                    builder.include("p", failingComponent);
                    fail("Expected exception not thrown");
                  } catch (RuntimeException e) {
                    // Expected
                  }
  
                  builder.include("p", safeComponent).add(DisplayData.item("d", "d"));
                }
              });
  
      assertThat(displayData, hasDisplayItem("a"));
      assertThat(displayData, hasDisplayItem("b"));
      assertThat(displayData, hasDisplayItem("c"));
      assertThat(displayData, hasDisplayItem("d"));
    }
  
    @Test
    public void testExceptionMessage() {
      final RuntimeException cause = new RuntimeException("oh noes!");
      HasDisplayData component =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              throw cause;
            }
          };
  
      thrown.expectMessage(component.getClass().getName());
      thrown.expectCause(is(cause));
  
      DisplayData.from(component);
    }
  
    @Test
    public void testExceptionsNotWrappedRecursively() {
      final RuntimeException cause = new RuntimeException("oh noes!");
      HasDisplayData component =
          new HasDisplayData() {
            @Override
            public void populateDisplayData(DisplayData.Builder builder) {
              builder.include(
                  "p",
                  new HasDisplayData() {
                    @Override
                    public void populateDisplayData(DisplayData.Builder builder) {
                      throw cause;
                    }
                  });
            }
          };
  
      thrown.expectCause(is(cause));
      DisplayData.from(component);
    }
  
    @AutoValue
    abstract static class Foo implements HasDisplayData {
      @Override
      public void populateDisplayData(DisplayData.Builder builder) {
        builder.add(DisplayData.item("someKey", "someValue"));
      }
    }
  
    @Test
    public void testAutoValue() {
      DisplayData data = DisplayData.from(new AutoValue_DisplayDataTest_Foo());
      Item item = Iterables.getOnlyElement(data.asMap().values());
      assertEquals(Foo.class, item.getNamespace());
    }
  
    private String quoted(Object obj) {
      return String.format("\"%s\"", obj);
    }
  
    private Matcher<Iterable<? super JsonNode>> hasExpectedJson(
        HasDisplayData component, String type, String key, Object value) throws IOException {
      return hasExpectedJson(component, type, key, value, null, null, null);
    }
  
    private Matcher<Iterable<? super JsonNode>> hasExpectedJson(
        HasDisplayData component,
        String type,
        String key,
        Object value,
        Object shortValue,
        String label,
        String linkUrl)
        throws IOException {
      Class<?> nsClass = component.getClass();
  
      StringBuilder builder = new StringBuilder();
      builder.append("{");
      builder.append(String.format("\"namespace\":\"%s\",", nsClass.getName()));
      builder.append(String.format("\"type\":\"%s\",", type));
      builder.append(String.format("\"key\":\"%s\",", key));
      builder.append(String.format("\"value\":%s", value));
  
      if (shortValue != null) {
        builder.append(String.format(",\"shortValue\":%s", shortValue));
      }
      if (label != null) {
        builder.append(String.format(",\"label\":\"%s\"", label));
      }
      if (linkUrl != null) {
        builder.append(String.format(",\"linkUrl\":\"%s\"", linkUrl));
      }
  
      builder.append("}");
  
      JsonNode jsonNode = MAPPER.readTree(builder.toString());
      return hasItem(jsonNode);
    }
  
    private static class NoopDisplayData implements HasDisplayData {
      @Override
      public void populateDisplayData(DisplayData.Builder builder) {}
    }
  
    private static Matcher<DisplayData.Item> hasUrl(Matcher<String> urlMatcher) {
      return new FeatureMatcher<DisplayData.Item, String>(
          urlMatcher, "display item with url", "URL") {
        @Override
        protected String featureValueOf(DisplayData.Item actual) {
          return actual.getLinkUrl();
        }
      };
    }
  
    private static <T> Matcher<DisplayData.Item> hasShortValue(Matcher<T> valueStringMatcher) {
      return new FeatureMatcher<DisplayData.Item, T>(
          valueStringMatcher, "display item with short value", "short value") {
        @Override
        protected T featureValueOf(DisplayData.Item actual) {
          @SuppressWarnings("unchecked")
          T shortValue = (T) actual.getShortValue();
          return shortValue;
        }
      };
    }
  }
  