| /* |
| * 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.v20_0.com.google.common.base.Preconditions.checkArgument; |
| import static org.apache.beam.vendor.guava.v20_0.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 javax.annotation.Nullable; |
| import org.apache.beam.sdk.options.ValueProvider; |
| import org.apache.beam.sdk.transforms.PTransform; |
| import org.apache.beam.vendor.guava.v20_0.com.google.common.base.Joiner; |
| import org.apache.beam.vendor.guava.v20_0.com.google.common.collect.ImmutableList; |
| import org.apache.beam.vendor.guava.v20_0.com.google.common.collect.ImmutableMap; |
| import org.apache.beam.vendor.guava.v20_0.com.google.common.collect.Maps; |
| import org.apache.beam.vendor.guava.v20_0.com.google.common.collect.Sets; |
| 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. |
| */ |
| 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, |
| */ |
| @Nullable |
| public static 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(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. */ |
| @Nullable |
| @JsonIgnore |
| public abstract Path getPath(); |
| |
| /** |
| * The namespace for the display item. The namespace defaults to the component which the display |
| * item belongs to. |
| */ |
| @Nullable |
| @JsonGetter("namespace") |
| 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 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) |
| @Nullable |
| public abstract 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) |
| @Nullable |
| public abstract 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) |
| @Nullable |
| public abstract 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 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. |
| */ |
| @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}. |
| */ |
| 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}. |
| */ |
| @Nullable |
| public abstract 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}. |
| */ |
| @Nullable |
| public abstract 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. |
| */ |
| @Nullable |
| public abstract 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. |
| */ |
| @Nullable |
| public abstract 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 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 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(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); |
| } |
| |
| @Nullable |
| private static 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); |
| |
| @Nullable private final Object shortValue; |
| @Nullable private final 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; |
| |
| @Nullable private Path latestPath; |
| @Nullable private 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 { |
| namespace = |
| Class.forName(namespace.getCanonicalName().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); |
| } |
| } |