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.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument;
  import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull;
  
  import com.fasterxml.jackson.annotation.JsonGetter;
  import com.fasterxml.jackson.annotation.JsonIgnore;
  import com.fasterxml.jackson.annotation.JsonInclude;
  import com.fasterxml.jackson.annotation.JsonValue;
  import com.google.auto.value.AutoValue;
  import java.io.Serializable;
  import java.util.Collection;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Set;
  import org.apache.beam.sdk.options.ValueProvider;
  import org.apache.beam.sdk.transforms.PTransform;
  import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Joiner;
  import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
  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.Maps;
  import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets;
  import org.checkerframework.checker.nullness.qual.Nullable;
  import org.joda.time.Duration;
  import org.joda.time.Instant;
  import org.joda.time.format.DateTimeFormatter;
  import org.joda.time.format.ISODateTimeFormat;
  
  /**
   * Static display data associated with a pipeline component. Display data is useful for pipeline
   * runner UIs and diagnostic dashboards to display details about {@link PTransform PTransforms} that
   * make up a pipeline.
   *
   * <p>Components specify their display data by implementing the {@link HasDisplayData} interface.
   */
  @SuppressWarnings({
    "nullness" // TODO(https://github.com/apache/beam/issues/20497)
  })
  public class DisplayData implements Serializable {
    private static final DisplayData EMPTY = new DisplayData(Maps.newHashMap());
    private static final DateTimeFormatter TIMESTAMP_FORMATTER = ISODateTimeFormat.dateTime();
  
    private final ImmutableMap<Identifier, Item> entries;
  
    private DisplayData(Map<Identifier, Item> entries) {
      this.entries = ImmutableMap.copyOf(entries);
    }
  
    /** Default empty {@link DisplayData} instance. */
    public static DisplayData none() {
      return EMPTY;
    }
  
    /**
     * Collect the {@link DisplayData} from a component. This will traverse all subcomponents
     * specified via {@link Builder#include} in the given component. Data in this component will be in
     * a namespace derived from the component.
     */
    public static DisplayData from(HasDisplayData component) {
      checkNotNull(component, "component argument cannot be null");
  
      InternalBuilder builder = new InternalBuilder();
      builder.include(Path.root(), component);
  
      return builder.build();
    }
  
    /**
     * Infer the {@link Type} for the given object.
     *
     * <p>Use this method if the type of metadata is not known at compile time. For example:
     *
     * <pre>{@code @Override
     * public void populateDisplayData(DisplayData.Builder builder) {
     *   Optional<DisplayData.Type> type = DisplayData.inferType(foo);
     *   if (type.isPresent()) {
     *     builder.add(DisplayData.item("foo", type.get(), foo));
     *   }
     * }
     * }</pre>
     *
     * @return The inferred {@link Type}, or null if the type cannot be inferred,
     */
    public static @Nullable Type inferType(@Nullable Object value) {
      return Type.tryInferFrom(value);
    }
  
    @JsonValue
    public Collection<Item> items() {
      return entries.values();
    }
  
    public Map<Identifier, Item> asMap() {
      return entries;
    }
  
    @Override
    public int hashCode() {
      return entries.hashCode();
    }
  
    @Override
    public boolean equals(@Nullable Object obj) {
      if (obj instanceof DisplayData) {
        DisplayData that = (DisplayData) obj;
        return Objects.equals(this.entries, that.entries);
      }
  
      return false;
    }
  
    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      boolean isFirstLine = true;
      for (Item entry : entries.values()) {
        if (isFirstLine) {
          isFirstLine = false;
        } else {
          builder.append("\n");
        }
  
        builder.append(entry);
      }
  
      return builder.toString();
    }
  
    /** Utility to build up display data from a component and its included subcomponents. */
    public interface Builder {
      /**
       * Register display data from the specified subcomponent at the given path. For example, a
       * {@link PTransform} which delegates to a user-provided function can implement {@link
       * HasDisplayData} on the function and include it from the {@link PTransform}:
       *
       * <pre>{@code @Override
       * public void populateDisplayData(DisplayData.Builder builder) {
       *   super.populateDisplayData(builder);
       *
       *   builder
       *     // To register the class name of the userFn
       *     .add(DisplayData.item("userFn", userFn.getClass()))
       *     // To allow the userFn to register additional display data
       *     .include("userFn", userFn);
       * }
       * }</pre>
       *
       * <p>Using {@code include(path, subComponent)} will associate each of the registered items with
       * the namespace of the {@code subComponent} being registered, with the specified path element
       * relative to the current path. To register display data in the current path and namespace,
       * such as from a base class implementation, use {@code
       * subComponent.populateDisplayData(builder)} instead.
       *
       * @see HasDisplayData#populateDisplayData(DisplayData.Builder)
       */
      Builder include(String path, HasDisplayData subComponent);
  
      /**
       * Register display data from the specified component on behalf of the current component.
       * Display data items will be added with the subcomponent namespace but the current component
       * path.
       *
       * <p>This is useful for components which simply wrap other components and wish to retain the
       * display data from the wrapped component. Such components should implement {@code
       * populateDisplayData} as:
       *
       * <pre>{@code @Override
       * public void populateDisplayData(DisplayData.Builder builder) {
       *   builder.delegate(wrapped);
       * }
       * }</pre>
       */
      Builder delegate(HasDisplayData component);
  
      /** Register the given display item. */
      Builder add(ItemSpec<?> item);
  
      /** Register the given display item if the value is not null. */
      Builder addIfNotNull(ItemSpec<?> item);
  
      /** Register the given display item if the value is different than the specified default. */
      <T> Builder addIfNotDefault(ItemSpec<T> item, @Nullable T defaultValue);
    }
  
    /**
     * {@link Item Items} are the unit of display data. Each item is identified by a given path, key,
     * and namespace from the component the display item belongs to.
     *
     * <p>{@link Item Items} are registered via {@link DisplayData.Builder#add} within {@link
     * HasDisplayData#populateDisplayData} implementations.
     */
    @AutoValue
    public abstract static class Item implements Serializable {
  
      /** The path for the display item within a component hierarchy. */
      @JsonIgnore
      @Nullable
      public abstract Path getPath();
  
      /**
       * The namespace for the display item. The namespace defaults to the component which the display
       * item belongs to.
       */
      @JsonGetter("namespace")
      @Nullable
      public abstract Class<?> getNamespace();
  
      /**
       * The key for the display item. Each display item is created with a key and value via {@link
       * DisplayData#item}.
       */
      @JsonGetter("key")
      public abstract String getKey();
  
      /**
       * Retrieve the {@link DisplayData.Type} of display data. All metadata conforms to a predefined
       * set of allowed types.
       */
      @JsonGetter("type")
      public abstract Type getType();
  
      /**
       * Retrieve the value of the display item. The value is translated from the input to {@link
       * DisplayData#item} into a format suitable for display. Translation is based on the item's
       * {@link #getType() type}.
       */
      @JsonGetter("value")
      public abstract @Nullable Object getValue();
  
      /**
       * Return the optional short value for an item, or null if none is provided.
       *
       * <p>The short value is an alternative display representation for items having a long display
       * value. For example, the {@link #getValue() value} for {@link Type#JAVA_CLASS} items contains
       * the full class name with package, while the short value contains just the class name.
       *
       * <p>A {@link #getValue() value} will be provided for each display item, and some types may
       * also provide a short-value. If a short value is provided, display data consumers may choose
       * to display it instead of or in addition to the {@link #getValue() value}.
       */
      @JsonGetter("shortValue")
      @JsonInclude(JsonInclude.Include.NON_NULL)
      public abstract @Nullable Object getShortValue();
  
      /**
       * Retrieve the optional label for an item. The label is a human-readable description of what
       * the metadata represents. UIs may choose to display the label instead of the item key.
       *
       * <p>If no label was specified, this will return {@code null}.
       */
      @JsonGetter("label")
      @JsonInclude(JsonInclude.Include.NON_NULL)
      public abstract @Nullable String getLabel();
  
      /**
       * Retrieve the optional link URL for an item. The URL points to an address where the reader can
       * find additional context for the display data.
       *
       * <p>If no URL was specified, this will return {@code null}.
       */
      @JsonGetter("linkUrl")
      @JsonInclude(JsonInclude.Include.NON_NULL)
      public abstract @Nullable String getLinkUrl();
  
      private static Item create(ItemSpec<?> spec, Path path) {
        checkNotNull(spec, "spec cannot be null");
        checkNotNull(path, "path cannot be null");
        Class<?> ns = checkNotNull(spec.getNamespace(), "namespace must be set");
  
        return new AutoValue_DisplayData_Item(
            path,
            ns,
            spec.getKey(),
            spec.getType(),
            spec.getValue(),
            spec.getShortValue(),
            spec.getLabel(),
            spec.getLinkUrl());
      }
  
      @Override
      public final String toString() {
        return String.format("%s%s:%s=%s", getPath(), getNamespace().getName(), getKey(), getValue());
      }
    }
  
    /**
     * Specifies an {@link Item} to register as display data. Each item is identified by a given path,
     * key, and namespace from the component the display item belongs to.
     *
     * <p>{@link Item Items} are registered via {@link DisplayData.Builder#add} within {@link
     * HasDisplayData#populateDisplayData} implementations.
     */
    @AutoValue
    public abstract static class ItemSpec<T> implements Serializable {
      /**
       * The namespace for the display item. If unset, defaults to the component which the display
       * item is registered to.
       */
      public abstract @Nullable Class<?> getNamespace();
  
      /**
       * The key for the display item. Each display item is created with a key and value via {@link
       * DisplayData#item}.
       */
      public abstract String getKey();
  
      /**
       * The {@link DisplayData.Type} of display data. All display data conforms to a predefined set
       * of allowed types.
       */
      public abstract Type getType();
  
      /**
       * The value of the display item. The value is translated from the input to {@link
       * DisplayData#item} into a format suitable for display. Translation is based on the item's
       * {@link #getType() type}.
       */
      public abstract @Nullable Object getValue();
  
      /**
       * The optional short value for an item, or {@code null} if none is provided.
       *
       * <p>The short value is an alternative display representation for items having a long display
       * value. For example, the {@link #getValue() value} for {@link Type#JAVA_CLASS} items contains
       * the full class name with package, while the short value contains just the class name.
       *
       * <p>A {@link #getValue() value} will be provided for each display item, and some types may
       * also provide a short-value. If a short value is provided, display data consumers may choose
       * to display it instead of or in addition to the {@link #getValue() value}.
       */
      public abstract @Nullable Object getShortValue();
  
      /**
       * The optional label for an item. The label is a human-readable description of what the
       * metadata represents. UIs may choose to display the label instead of the item key.
       */
      public abstract @Nullable String getLabel();
  
      /**
       * The optional link URL for an item. The URL points to an address where the reader can find
       * additional context for the display data.
       */
      public abstract @Nullable String getLinkUrl();
  
      private static <T> ItemSpec<T> create(String key, Type type, @Nullable T value) {
        return ItemSpec.<T>builder().setKey(key).setType(type).setRawValue(value).build();
      }
  
      /**
       * Set the item {@link ItemSpec#getNamespace() namespace} from the given {@link Class}.
       *
       * <p>This method does not alter the current instance, but instead returns a new {@link
       * ItemSpec} with the namespace set.
       */
      public ItemSpec<T> withNamespace(Class<?> namespace) {
        checkNotNull(namespace, "namespace argument cannot be null");
        return toBuilder().setNamespace(namespace).build();
      }
  
      /**
       * Set the item {@link Item#getLabel() label}.
       *
       * <p>Specifying a null value will clear the label if it was previously defined.
       *
       * <p>This method does not alter the current instance, but instead returns a new {@link
       * ItemSpec} with the label set.
       */
      public ItemSpec<T> withLabel(@Nullable String label) {
        return toBuilder().setLabel(label).build();
      }
  
      /**
       * Set the item {@link Item#getLinkUrl() link url}.
       *
       * <p>Specifying a null value will clear the link url if it was previously defined.
       *
       * <p>This method does not alter the current instance, but instead returns a new {@link
       * ItemSpec} with the link url set.
       */
      public ItemSpec<T> withLinkUrl(@Nullable String url) {
        return toBuilder().setLinkUrl(url).build();
      }
  
      /**
       * Creates a similar item to the current instance but with the specified value.
       *
       * <p>This should only be used internally. It is useful to compare the value of a {@link
       * DisplayData.Item} to the value derived from a specified input.
       */
      private ItemSpec<T> withValue(T value) {
        return toBuilder().setRawValue(value).build();
      }
  
      @Override
      public final String toString() {
        return String.format("%s:%s=%s", getNamespace(), getKey(), getValue());
      }
  
      static <T> ItemSpec.Builder<T> builder() {
        return new AutoValue_DisplayData_ItemSpec.Builder<>();
      }
  
      abstract ItemSpec.Builder<T> toBuilder();
  
      @AutoValue.Builder
      abstract static class Builder<T> {
        public abstract ItemSpec.Builder<T> setKey(String key);
  
        public abstract ItemSpec.Builder<T> setNamespace(@Nullable Class<?> namespace);
  
        public abstract ItemSpec.Builder<T> setType(Type type);
  
        public abstract ItemSpec.Builder<T> setValue(@Nullable Object longValue);
  
        public abstract ItemSpec.Builder<T> setShortValue(@Nullable Object shortValue);
  
        public abstract ItemSpec.Builder<T> setLabel(@Nullable String label);
  
        public abstract ItemSpec.Builder<T> setLinkUrl(@Nullable String url);
  
        public abstract ItemSpec<T> build();
  
        abstract Type getType();
  
        ItemSpec.Builder<T> setRawValue(@Nullable T value) {
          FormattedItemValue formatted = getType().safeFormat(value);
          return this.setValue(formatted.getLongValue()).setShortValue(formatted.getShortValue());
        }
      }
    }
  
    /**
     * Unique identifier for a display data item within a component.
     *
     * <p>Identifiers are composed of:
     *
     * <ul>
     *   <li>A {@link #getPath() path} based on the component hierarchy
     *   <li>The {@link #getKey() key} it is registered with
     *   <li>A {@link #getNamespace() namespace} generated from the class of the component which
     *       registered the item.
     * </ul>
     *
     * <p>Display data registered with the same key from different components will have different
     * namespaces and thus will both be represented in the composed {@link DisplayData}. If a single
     * component registers multiple metadata items with the same key, only the most recent item will
     * be retained; previous versions are discarded.
     */
    @AutoValue
    public abstract static class Identifier implements Serializable {
      public abstract Path getPath();
  
      public abstract Class<?> getNamespace();
  
      public abstract String getKey();
  
      public static Identifier of(Path path, Class<?> namespace, String key) {
        return new AutoValue_DisplayData_Identifier(path, namespace, key);
      }
  
      @Override
      public final String toString() {
        return String.format("%s%s:%s", getPath(), getNamespace(), getKey());
      }
    }
  
    /**
     * Structured path of registered display data within a component hierarchy.
     *
     * <p>Display data items registered directly by a component will have the {@link Path#root() root}
     * path. If the component {@link Builder#include includes} a sub-component, its display data will
     * be registered at the path specified. Each sub-component path is created by appending a child
     * element to the path of its parent component, forming a hierarchy.
     */
    public static class Path implements Serializable {
      private final ImmutableList<String> components;
  
      private Path(ImmutableList<String> components) {
        this.components = components;
      }
  
      /** Path for display data registered by a top-level component. */
      public static Path root() {
        return new Path(ImmutableList.of());
      }
  
      /**
       * Construct a path from an absolute component path hierarchy.
       *
       * <p>For the root path, use {@link Path#root()}.
       *
       * @param firstPath Path of the first sub-component.
       * @param paths Additional path components.
       */
      public static Path absolute(String firstPath, String... paths) {
        ImmutableList.Builder<String> builder = ImmutableList.builder();
  
        validatePathElement(firstPath);
        builder.add(firstPath);
        for (String path : paths) {
          validatePathElement(path);
          builder.add(path);
        }
  
        return new Path(builder.build());
      }
  
      /**
       * Hierarchy list of component paths making up the full path, starting with the top-level child
       * component path. For the {@link #root root} path, returns the empty list.
       */
      public List<String> getComponents() {
        return components;
      }
  
      /**
       * Extend the path by appending a sub-component path. The new path element is added to the end
       * of the path hierarchy.
       *
       * <p>Returns a new {@link Path} instance; the originating {@link Path} is not modified.
       */
      public Path extend(String path) {
        validatePathElement(path);
        return new Path(
            ImmutableList.<String>builder().addAll(components.iterator()).add(path).build());
      }
  
      private static void validatePathElement(String path) {
        checkNotNull(path);
        checkArgument(!"".equals(path), "path cannot be empty");
      }
  
      @Override
      public String toString() {
        StringBuilder b = new StringBuilder().append("[");
        Joiner.on("/").appendTo(b, components);
        b.append("]");
        return b.toString();
      }
  
      @Override
      public boolean equals(@Nullable Object obj) {
        return obj instanceof Path && Objects.equals(components, ((Path) obj).components);
      }
  
      @Override
      public int hashCode() {
        return components.hashCode();
      }
    }
  
    /** Display data type. */
    public enum Type {
      STRING {
        @Override
        FormattedItemValue format(Object value) {
          return new FormattedItemValue(checkType(value, String.class, STRING));
        }
      },
      INTEGER {
        @Override
        FormattedItemValue format(Object value) {
          if (value instanceof Integer) {
            long l = ((Integer) value).longValue();
            return format(l);
          }
  
          return new FormattedItemValue(checkType(value, Long.class, INTEGER));
        }
      },
      FLOAT {
        @Override
        FormattedItemValue format(Object value) {
          return new FormattedItemValue(checkType(value, Number.class, FLOAT));
        }
      },
      BOOLEAN() {
        @Override
        FormattedItemValue format(Object value) {
          return new FormattedItemValue(checkType(value, Boolean.class, BOOLEAN));
        }
      },
      TIMESTAMP() {
        @Override
        FormattedItemValue format(Object value) {
          Instant instant = checkType(value, Instant.class, TIMESTAMP);
          return new FormattedItemValue(TIMESTAMP_FORMATTER.print(instant));
        }
      },
      DURATION {
        @Override
        FormattedItemValue format(Object value) {
          Duration duration = checkType(value, Duration.class, DURATION);
          return new FormattedItemValue(duration.getMillis());
        }
      },
      JAVA_CLASS {
        @Override
        FormattedItemValue format(Object value) {
          Class<?> clazz = checkType(value, Class.class, JAVA_CLASS);
          return new FormattedItemValue(clazz.getName(), clazz.getSimpleName());
        }
      };
  
      private static <T> T checkType(Object value, Class<T> clazz, DisplayData.Type expectedType) {
        if (!clazz.isAssignableFrom(value.getClass())) {
          throw new ClassCastException(
              String.format("Value is not valid for DisplayData type %s: %s", expectedType, value));
        }
  
        @SuppressWarnings("unchecked") // type checked above.
        T typedValue = (T) value;
        return typedValue;
      }
  
      /**
       * Format the display data value into a long string representation, and optionally a shorter
       * representation for display.
       *
       * <p>Internal-only. Value objects can be safely cast to the expected Java type.
       */
      abstract FormattedItemValue format(Object value);
  
      /**
       * Safe version of {@link Type#format(Object)}, which checks for null input value and if so
       * returns a {@link FormattedItemValue} with null value properties.
       *
       * @see #format(Object)
       */
      FormattedItemValue safeFormat(@Nullable Object value) {
        if (value == null) {
          return FormattedItemValue.NULL_VALUES;
        }
  
        return format(value);
      }
  
      private static @Nullable Type tryInferFrom(@Nullable Object value) {
        if (value instanceof Integer || value instanceof Long) {
          return INTEGER;
        } else if (value instanceof Double || value instanceof Float) {
          return FLOAT;
        } else if (value instanceof Boolean) {
          return BOOLEAN;
        } else if (value instanceof Instant) {
          return TIMESTAMP;
        } else if (value instanceof Duration) {
          return DURATION;
        } else if (value instanceof Class<?>) {
          return JAVA_CLASS;
        } else if (value instanceof String) {
          return STRING;
        } else {
          return null;
        }
      }
    }
  
    static class FormattedItemValue {
      /** Default instance which contains null values. */
      private static final FormattedItemValue NULL_VALUES = new FormattedItemValue(null);
  
      private final @Nullable Object shortValue;
      private final @Nullable Object longValue;
  
      private FormattedItemValue(@Nullable Object longValue) {
        this(longValue, null);
      }
  
      private FormattedItemValue(@Nullable Object longValue, @Nullable Object shortValue) {
        this.longValue = longValue;
        this.shortValue = shortValue;
      }
  
      Object getLongValue() {
        return this.longValue;
      }
  
      Object getShortValue() {
        return this.shortValue;
      }
    }
  
    private static class InternalBuilder implements Builder {
      private final Map<Identifier, Item> entries;
      private final Set<HasDisplayData> visitedComponents;
      private final Map<Path, HasDisplayData> visitedPathMap;
  
      private @Nullable Path latestPath;
      private @Nullable Class<?> latestNs;
  
      private InternalBuilder() {
        this.entries = Maps.newHashMap();
        this.visitedComponents = Sets.newIdentityHashSet();
        this.visitedPathMap = Maps.newHashMap();
      }
  
      @Override
      public Builder include(String path, HasDisplayData subComponent) {
        checkNotNull(subComponent, "subComponent argument cannot be null");
        checkNotNull(path, "path argument cannot be null");
  
        Path absolutePath = latestPath.extend(path);
  
        HasDisplayData existingComponent = visitedPathMap.get(absolutePath);
        if (existingComponent != null) {
          throw new IllegalArgumentException(
              String.format(
                  "Specified path '%s' already used for "
                      + "subcomponent %s. Subcomponents must be included using unique paths.",
                  path, existingComponent));
        }
  
        return include(absolutePath, subComponent);
      }
  
      @Override
      public Builder delegate(HasDisplayData component) {
        checkNotNull(component);
  
        return include(latestPath, component);
      }
  
      private Builder include(Path path, HasDisplayData subComponent) {
        if (visitedComponents.contains(subComponent)) {
          // Component previously registered; ignore in order to break cyclic dependencies
          return this;
        }
  
        // New component; add it.
        visitedComponents.add(subComponent);
        visitedPathMap.put(path, subComponent);
        Class<?> namespace = subComponent.getClass();
        // Common case: AutoValue classes such as AutoValue_FooIO_Read. It's more useful
        // to show the user the FooIO.Read class, which is the direct superclass of the AutoValue
        // generated class.
        if (namespace.getSimpleName().startsWith("AutoValue_")) {
          namespace = namespace.getSuperclass();
        }
        if (namespace.isSynthetic() && namespace.getSimpleName().contains("$$Lambda")) {
          try {
            String className = namespace.getCanonicalName();
            // A local class, local interface, or anonymous class does not have a canonical name.
            // https://docs.oracle.com/javase/specs/jls/se17/html/jls-6.html#jls-6.7
            if (className == null) {
              className = namespace.getName();
            }
            namespace = Class.forName(className.replaceFirst("\\$\\$Lambda.*", ""));
          } catch (Exception e) {
            throw new PopulateDisplayDataException(
                "Failed to get the enclosing class of lambda " + subComponent, e);
          }
        }
  
        Path prevPath = latestPath;
        Class<?> prevNs = latestNs;
        latestPath = path;
        latestNs = namespace;
  
        try {
          subComponent.populateDisplayData(this);
        } catch (PopulateDisplayDataException e) {
          // Don't re-wrap exceptions recursively.
          throw e;
        } catch (Throwable e) {
          String msg =
              String.format(
                  "Error while populating display data for component '%s': %s",
                  namespace.getName(), e.getMessage());
          throw new PopulateDisplayDataException(msg, e);
        }
  
        latestPath = prevPath;
        latestNs = prevNs;
  
        return this;
      }
  
      /** Marker exception class for exceptions encountered while populating display data. */
      private static class PopulateDisplayDataException extends RuntimeException {
        PopulateDisplayDataException(String message, Throwable cause) {
          super(message, cause);
        }
      }
  
      @Override
      public Builder add(ItemSpec<?> item) {
        checkNotNull(item, "Input display item cannot be null");
        return addItemIf(true, item);
      }
  
      @Override
      public Builder addIfNotNull(ItemSpec<?> item) {
        checkNotNull(item, "Input display item cannot be null");
        return addItemIf(item.getValue() != null, item);
      }
  
      @Override
      public <T> Builder addIfNotDefault(ItemSpec<T> item, @Nullable T defaultValue) {
        checkNotNull(item, "Input display item cannot be null");
        ItemSpec<T> defaultItem = item.withValue(defaultValue);
        return addItemIf(!Objects.equals(item, defaultItem), item);
      }
  
      private Builder addItemIf(boolean condition, ItemSpec<?> spec) {
        if (!condition) {
          return this;
        }
  
        checkNotNull(spec, "Input display item cannot be null");
        checkNotNull(spec.getValue(), "Input display value cannot be null");
  
        if (spec.getNamespace() == null) {
          spec = spec.withNamespace(latestNs);
        }
        Item item = Item.create(spec, latestPath);
  
        Identifier id = Identifier.of(item.getPath(), item.getNamespace(), item.getKey());
        checkArgument(
            !entries.containsKey(id),
            "Display data key (%s) is not unique within the specified path and namespace: %s%s.",
            item.getKey(),
            item.getPath(),
            item.getNamespace());
  
        entries.put(id, item);
        return this;
      }
  
      private DisplayData build() {
        return new DisplayData(this.entries);
      }
    }
  
    /** Create a display item for the specified key and string value. */
    public static ItemSpec<String> item(String key, @Nullable String value) {
      return item(key, Type.STRING, value);
    }
  
    /** Create a display item for the specified key and {@link ValueProvider}. */
    public static ItemSpec<?> item(String key, @Nullable ValueProvider<?> value) {
      if (value == null) {
        return item(key, Type.STRING, null);
      }
      if (value.isAccessible()) {
        Object got = value.get();
        if (got == null) {
          return item(key, Type.STRING, null);
        }
        Type type = inferType(got);
        if (type != null) {
          return item(key, type, got);
        }
      }
      // General case: not null and type not inferable. Fall back to toString of the VP itself.
      return item(key, Type.STRING, String.valueOf(value));
    }
  
    /** Create a display item for the specified key and integer value. */
    public static ItemSpec<Integer> item(String key, @Nullable Integer value) {
      return item(key, Type.INTEGER, value);
    }
  
    /** Create a display item for the specified key and integer value. */
    public static ItemSpec<Long> item(String key, @Nullable Long value) {
      return item(key, Type.INTEGER, value);
    }
  
    /** Create a display item for the specified key and floating point value. */
    public static ItemSpec<Float> item(String key, @Nullable Float value) {
      return item(key, Type.FLOAT, value);
    }
  
    /** Create a display item for the specified key and floating point value. */
    public static ItemSpec<Double> item(String key, @Nullable Double value) {
      return item(key, Type.FLOAT, value);
    }
  
    /** Create a display item for the specified key and boolean value. */
    public static ItemSpec<Boolean> item(String key, @Nullable Boolean value) {
      return item(key, Type.BOOLEAN, value);
    }
  
    /** Create a display item for the specified key and timestamp value. */
    public static ItemSpec<Instant> item(String key, @Nullable Instant value) {
      return item(key, Type.TIMESTAMP, value);
    }
  
    /** Create a display item for the specified key and duration value. */
    public static ItemSpec<Duration> item(String key, @Nullable Duration value) {
      return item(key, Type.DURATION, value);
    }
  
    /** Create a display item for the specified key and class value. */
    public static <T> ItemSpec<Class<T>> item(String key, @Nullable Class<T> value) {
      return item(key, Type.JAVA_CLASS, value);
    }
  
    /**
     * Create a display item for the specified key, type, and value. This method should be used if the
     * type of the input value can only be determined at runtime. Otherwise, {@link HasDisplayData}
     * implementors should call one of the typed factory methods, such as {@link #item(String,
     * String)} or {@link #item(String, Integer)}.
     *
     * @throws ClassCastException if the value cannot be formatted as the given type.
     * @see Type#inferType(Object)
     */
    public static <T> ItemSpec<T> item(String key, Type type, @Nullable T value) {
      checkNotNull(key, "key argument cannot be null");
      checkNotNull(type, "type argument cannot be null");
  
      return ItemSpec.create(key, type, value);
    }
  }
  