diff --git a/sdks/java/core/src/main/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayData.java b/sdks/java/core/src/main/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayData.java index 63ec7fcde12c..d23fc0b797d4 100644 --- a/sdks/java/core/src/main/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayData.java +++ b/sdks/java/core/src/main/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayData.java @@ -76,6 +76,30 @@ public static DisplayData from(HasDisplayData component) { return InternalBuilder.forRoot(component).build(); } + /** + * Infer the {@link Type} for the given object. + * + *

Use this method if the type of metadata is not known at compile time. For example: + * + *

+   * {@code
+   * @Override
+   * public void populateDisplayData(DisplayData.Builder builder) {
+   *   Optional type = DisplayData.inferType(foo);
+   *   if (type.isPresent()) {
+   *     builder.add("foo", type.get(), foo);
+   *   }
+   * }
+   * }
+   * 
+ * + * @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); + } + public Collection items() { return entries.values(); } @@ -101,6 +125,10 @@ public String toString() { return builder.toString(); } + private static String namespaceOf(Class clazz) { + return clazz.getName(); + } + /** * Utility to build up display metadata from a component and its included * subcomponents. @@ -126,6 +154,20 @@ public interface Builder { */ ItemBuilder add(String key, String value); + /** + * Register the given string display data if the value is not null. + * + * @see DisplayData.Builder#add(String, String) + */ + ItemBuilder addIfNotNull(String key, @Nullable String value); + + /** + * Register the given string display data if the value is different than the specified default. + * + * @see DisplayData.Builder#add(String, String) + */ + ItemBuilder addIfNotDefault(String key, @Nullable String value, @Nullable String defaultValue); + /** * Register the given numeric display metadata. The metadata item will be registered with type * {@link DisplayData.Type#INTEGER}, and is identified by the specified key and namespace from @@ -133,6 +175,13 @@ public interface Builder { */ ItemBuilder add(String key, long value); + /** + * Register the given numeric display data if the value is different than the specified default. + * + * @see DisplayData.Builder#add(String, long) + */ + ItemBuilder addIfNotDefault(String key, long value, long defaultValue); + /** * Register the given floating point display metadata. The metadata item will be registered with * type {@link DisplayData.Type#FLOAT}, and is identified by the specified key and namespace @@ -141,12 +190,27 @@ public interface Builder { ItemBuilder add(String key, double value); /** - * Register the given floating point display metadata. The metadata item will be registered with + * Register the given floating point display data if the value is different than the specified + * default. + * + * @see DisplayData.Builder#add(String, double) + */ + ItemBuilder addIfNotDefault(String key, double value, double defaultValue); + + /** + * Register the given boolean display metadata. The metadata item will be registered with * type {@link DisplayData.Type#BOOLEAN}, and is identified by the specified key and namespace * from the current transform or component. */ ItemBuilder add(String key, boolean value); + /** + * Register the given boolean display data if the value is different than the specified default. + * + * @see DisplayData.Builder#add(String, boolean) + */ + ItemBuilder addIfNotDefault(String key, boolean value, boolean defaultValue); + /** * Register the given timestamp display metadata. The metadata item will be registered with type * {@link DisplayData.Type#TIMESTAMP}, and is identified by the specified key and namespace from @@ -154,6 +218,22 @@ public interface Builder { */ ItemBuilder add(String key, Instant value); + /** + * Register the given timestamp display data if the value is not null. + * + * @see DisplayData.Builder#add(String, Instant) + */ + ItemBuilder addIfNotNull(String key, @Nullable Instant value); + + /** + * Register the given timestamp display data if the value is different than the specified + * default. + * + * @see DisplayData.Builder#add(String, Instant) + */ + ItemBuilder addIfNotDefault( + String key, @Nullable Instant value, @Nullable Instant defaultValue); + /** * Register the given duration display metadata. The metadata item will be registered with type * {@link DisplayData.Type#DURATION}, and is identified by the specified key and namespace from @@ -161,12 +241,54 @@ public interface Builder { */ ItemBuilder add(String key, Duration value); + /** + * Register the given duration display data if the value is not null. + * + * @see DisplayData.Builder#add(String, Duration) + */ + ItemBuilder addIfNotNull(String key, @Nullable Duration value); + + /** + * Register the given duration display data if the value is different than the specified + * default. + * + * @see DisplayData.Builder#add(String, Duration) + */ + ItemBuilder addIfNotDefault( + String key, @Nullable Duration value, @Nullable Duration defaultValue); + /** * Register the given class display metadata. The metadata item will be registered with type * {@link DisplayData.Type#JAVA_CLASS}, and is identified by the specified key and namespace * from the current transform or component. */ ItemBuilder add(String key, Class value); + + /** + * Register the given class display data if the value is not null. + * + * @see DisplayData.Builder#add(String, Class) + */ + ItemBuilder addIfNotNull(String key, @Nullable Class value); + + /** + * Register the given class display data if the value is different than the specified default. + * + * @see DisplayData.Builder#add(String, Class) + */ + ItemBuilder addIfNotDefault( + String key, @Nullable Class value, @Nullable Class defaultValue); + + /** + * Register the given display metadata with the specified type. + * + *

The added display data is identified by the specified key and namespace from the current + * transform or component. + * + * @throws ClassCastException if the value cannot be safely cast to the specified type. + * @see DisplayData#inferType(Object) + */ + ItemBuilder add(String key, Type type, Object value); } /** @@ -190,6 +312,14 @@ public interface ItemBuilder extends Builder { *

Specifying a null value will clear the URL if it was previously defined. */ ItemBuilder withLinkUrl(@Nullable String url); + + /** + * Adds an explicit namespace to the most-recently added display metadata. The namespace + * and key uniquely identify the display metadata. + * + *

Leaving the namespace unspecified will default to the registering instance's class. + */ + ItemBuilder withNamespace(Class namespace); } /** @@ -207,10 +337,11 @@ public static class Item { private final String label; private final String url; - private static Item create(String namespace, String key, Type type, T value) { + private static Item create(Class nsClass, String key, Type type, Object value) { FormattedItemValue formatted = type.format(value); + String namespace = namespaceOf(nsClass); return new Item( - namespace, key, type, formatted.getLongValue(), formatted.getShortValue(), null, null); + namespace, key, type, formatted.getLongValue(), formatted.getShortValue(), null, null); } private Item( @@ -337,6 +468,12 @@ private Item withLabel(String label) { private Item withUrl(String url) { return new Item(this.ns, this.key, this.type, this.value, this.shortValue, url, this.label); } + + private Item withNamespace(Class nsClass) { + String namespace = namespaceOf(nsClass); + return new Item( + namespace, this.key, this.type, this.value, this.shortValue, this.url, this.label); + } } /** @@ -354,7 +491,7 @@ public static class Identifier { private final String key; public static Identifier of(Class namespace, String key) { - return of(namespace.getName(), key); + return of(namespaceOf(namespace), key); } public static Identifier of(String namespace, String key) { @@ -403,13 +540,14 @@ public enum Type { STRING { @Override FormattedItemValue format(Object value) { - return new FormattedItemValue((String) value); + return new FormattedItemValue(value.toString()); } }, INTEGER { @Override FormattedItemValue format(Object value) { - return new FormattedItemValue(Long.toString((long) value)); + Number number = (Number) value; + return new FormattedItemValue(Long.toString(number.longValue())); } }, FLOAT { @@ -451,6 +589,28 @@ FormattedItemValue format(Object value) { *

Internal-only. Value objects can be safely cast to the expected Java type. */ abstract FormattedItemValue format(Object value); + + @Nullable + private static Type tryInferFrom(@Nullable Object value) { + Type type; + 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 { @@ -480,8 +640,9 @@ private static class InternalBuilder implements ItemBuilder { private final Set visited; private Class latestNs; + + @Nullable private Item latestItem; - private Identifier latestIdentifier; private InternalBuilder() { this.entries = Maps.newHashMap(); @@ -503,6 +664,9 @@ public Builder include(HasDisplayData subComponent) { @Override public Builder include(HasDisplayData subComponent, Class namespace) { checkNotNull(subComponent); + checkNotNull(namespace); + + commitLatest(); boolean newComponent = visited.add(subComponent); if (newComponent) { Class prevNs = this.latestNs; @@ -517,75 +681,165 @@ public Builder include(HasDisplayData subComponent, Class namespace) { @Override public ItemBuilder add(String key, String value) { checkNotNull(value); - return addItem(key, Type.STRING, value); + return addItemIf(true, key, Type.STRING, value); + } + + @Override + public ItemBuilder addIfNotNull(String key, @Nullable String value) { + return addItemIf(value != null, key, Type.STRING, value); + } + + @Override + public ItemBuilder addIfNotDefault( + String key, @Nullable String value, @Nullable String defaultValue) { + return addItemIf(!Objects.equals(value, defaultValue), key, Type.STRING, value); } @Override public ItemBuilder add(String key, long value) { - return addItem(key, Type.INTEGER, value); + return addItemIf(true, key, Type.INTEGER, value); + } + + @Override + public ItemBuilder addIfNotDefault(String key, long value, long defaultValue) { + return addItemIf(value != defaultValue, key, Type.INTEGER, value); } @Override public ItemBuilder add(String key, double value) { - return addItem(key, Type.FLOAT, value); + return addItemIf(true, key, Type.FLOAT, value); + } + + @Override + public ItemBuilder addIfNotDefault(String key, double value, double defaultValue) { + return addItemIf(value != defaultValue, key, Type.FLOAT, value); } @Override public ItemBuilder add(String key, boolean value) { - return addItem(key, Type.BOOLEAN, value); + return addItemIf(true, key, Type.BOOLEAN, value); + } + + @Override + public ItemBuilder addIfNotDefault(String key, boolean value, boolean defaultValue) { + return addItemIf(value != defaultValue, key, Type.BOOLEAN, value); } @Override public ItemBuilder add(String key, Instant value) { checkNotNull(value); - return addItem(key, Type.TIMESTAMP, value); + return addItemIf(true, key, Type.TIMESTAMP, value); + } + + @Override + public ItemBuilder addIfNotNull(String key, @Nullable Instant value) { + return addItemIf(value != null, key, Type.TIMESTAMP, value); + } + + @Override + public ItemBuilder addIfNotDefault( + String key, @Nullable Instant value, @Nullable Instant defaultValue) { + return addItemIf(!Objects.equals(value, defaultValue), key, Type.TIMESTAMP, value); } @Override public ItemBuilder add(String key, Duration value) { checkNotNull(value); - return addItem(key, Type.DURATION, value); + return addItemIf(true, key, Type.DURATION, value); + } + + @Override + public ItemBuilder addIfNotNull(String key, @Nullable Duration value) { + return addItemIf(value != null, key, Type.DURATION, value); + } + + @Override + public ItemBuilder addIfNotDefault( + String key, @Nullable Duration value, @Nullable Duration defaultValue) { + return addItemIf(!Objects.equals(value, defaultValue), key, Type.DURATION, value); } @Override public ItemBuilder add(String key, Class value) { checkNotNull(value); - return addItem(key, Type.JAVA_CLASS, value); + return addItemIf(true, key, Type.JAVA_CLASS, value); + } + + @Override + public ItemBuilder addIfNotNull(String key, @Nullable Class value) { + return addItemIf(value != null, key, Type.JAVA_CLASS, value); + } + + @Override + public ItemBuilder addIfNotDefault( + String key, @Nullable Class value, @Nullable Class defaultValue) { + return addItemIf(!Objects.equals(value, defaultValue), key, Type.JAVA_CLASS, value); + } + + @Override + public ItemBuilder add(String key, Type type, Object value) { + checkNotNull(value); + checkNotNull(type); + return addItemIf(true, key, type, value); } - private ItemBuilder addItem(String key, Type type, T value) { + private ItemBuilder addItemIf(boolean condition, String key, Type type, Object value) { checkNotNull(key); checkArgument(!key.isEmpty()); - Identifier id = Identifier.of(latestNs, key); + commitLatest(); + if (condition) { + latestItem = Item.create(latestNs, key, type, value); + } + + return this; + } + + private void commitLatest() { + if (latestItem == null) { + return; + } + + Identifier id = Identifier.of(latestItem.getNamespace(), latestItem.getKey()); if (entries.containsKey(id)) { throw new IllegalArgumentException("DisplayData key already exists. All display data " + "for a component must be registered with a unique key.\nKey: " + id); } - Item item = Item.create(id.getNamespace(), key, type, value); - entries.put(id, item); - latestItem = item; - latestIdentifier = id; + entries.put(id, latestItem); + latestItem = null; + } + + @Override + public ItemBuilder withLabel(@Nullable String label) { + if (latestItem != null) { + latestItem = latestItem.withLabel(label); + } return this; } @Override - public ItemBuilder withLabel(String label) { - latestItem = latestItem.withLabel(label); - entries.put(latestIdentifier, latestItem); + public ItemBuilder withLinkUrl(@Nullable String url) { + if (latestItem != null) { + latestItem = latestItem.withUrl(url); + } + return this; } @Override - public ItemBuilder withLinkUrl(String url) { - latestItem = latestItem.withUrl(url); - entries.put(latestIdentifier, latestItem); + public ItemBuilder withNamespace(Class namespace) { + checkNotNull(namespace); + if (latestItem != null) { + latestItem = latestItem.withNamespace(namespace); + } + return this; } private DisplayData build() { + commitLatest(); return new DisplayData(this.entries); } } diff --git a/sdks/java/core/src/main/java/com/google/cloud/dataflow/sdk/transforms/display/HasDisplayData.java b/sdks/java/core/src/main/java/com/google/cloud/dataflow/sdk/transforms/display/HasDisplayData.java index 6ec1eca7078a..36f7a31e3d9e 100644 --- a/sdks/java/core/src/main/java/com/google/cloud/dataflow/sdk/transforms/display/HasDisplayData.java +++ b/sdks/java/core/src/main/java/com/google/cloud/dataflow/sdk/transforms/display/HasDisplayData.java @@ -40,6 +40,7 @@ public interface HasDisplayData { * builder * .include(subComponent) * .add("minFilter", 42) + * .addIfNotDefault("useTransactions", this.txn, false) * .add("topic", "projects/myproject/topics/mytopic") * .withLabel("Pub/Sub Topic") * .add("serviceInstance", "myservice.com/fizzbang") diff --git a/sdks/java/core/src/test/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayDataMatchers.java b/sdks/java/core/src/test/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayDataMatchers.java index d540b4b628a9..2832414256ca 100644 --- a/sdks/java/core/src/test/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayDataMatchers.java +++ b/sdks/java/core/src/test/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayDataMatchers.java @@ -248,10 +248,44 @@ protected String featureValueOf(DisplayData.Item actual) { }; } + /** + * Creates a matcher that matches if the examined {@link DisplayData.Item} contains the + * specified namespace. + */ + public static Matcher hasNamespace(Class namespace) { + return hasNamespace(Matchers.>is(namespace)); + } + + /** + * Creates a matcher that matches if the examined {@link DisplayData.Item} contains a namespace + * matching the specified namespace matcher. + */ + public static Matcher hasNamespace(Matcher> namespaceMatcher) { + return new FeatureMatcher>( + namespaceMatcher, "display item with namespace", "namespace") { + @Override + protected Class featureValueOf(DisplayData.Item actual) { + try { + return Class.forName(actual.getNamespace()); + } catch (ClassNotFoundException e) { + return null; + } + } + }; + } + + /** + * Creates a matcher that matches if the examined {@link DisplayData.Item} matches the + * specified type. + */ public static Matcher hasType(DisplayData.Type type) { return hasType(Matchers.is(type)); } + /** + * Creates a matcher that matches if the examined {@link DisplayData.Item} has a type + * matching the specified type matcher. + */ public static Matcher hasType(Matcher typeMatcher) { return new FeatureMatcher( typeMatcher, "with type", "type") { @@ -262,10 +296,19 @@ protected DisplayData.Type featureValueOf(DisplayData.Item actual) { }; } + /** + * Creates a matcher that matches if the examined {@link DisplayData.Item} has the specified + * value. + */ + public static Matcher hasValue(String value) { return hasValue(Matchers.is(value)); } + /** + * Creates a matcher that matches if the examined {@link DisplayData.Item} contains a value + * matching the specified value matcher. + */ public static Matcher hasValue(Matcher valueMatcher) { return new FeatureMatcher( valueMatcher, "with value", "value") { diff --git a/sdks/java/core/src/test/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayDataMatchersTest.java b/sdks/java/core/src/test/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayDataMatchersTest.java index 1b43ff7ea02c..f24288eb75a6 100644 --- a/sdks/java/core/src/test/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayDataMatchersTest.java +++ b/sdks/java/core/src/test/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayDataMatchersTest.java @@ -19,6 +19,7 @@ import static com.google.cloud.dataflow.sdk.transforms.display.DisplayDataMatchers.hasDisplayItem; import static com.google.cloud.dataflow.sdk.transforms.display.DisplayDataMatchers.hasKey; +import static com.google.cloud.dataflow.sdk.transforms.display.DisplayDataMatchers.hasNamespace; import static com.google.cloud.dataflow.sdk.transforms.display.DisplayDataMatchers.hasType; import static com.google.cloud.dataflow.sdk.transforms.display.DisplayDataMatchers.hasValue; import static com.google.cloud.dataflow.sdk.transforms.display.DisplayDataMatchers.includes; @@ -96,6 +97,15 @@ public void testHasValue() { assertThat(createDisplayDataWithItem("foo", "bar"), matcher); } + @Test + public void testHasNamespace() { + Matcher matcher = hasDisplayItem(hasNamespace(SampleTransform.class)); + + assertFalse(matcher.matches(DisplayData.from( + new PTransform, PCollection>(){}))); + assertThat(createDisplayDataWithItem("foo", "bar"), matcher); + } + @Test public void testIncludes() { final HasDisplayData subComponent = new HasDisplayData() { @@ -125,13 +135,23 @@ public void populateDisplayData(Builder builder) { assertThat(DisplayData.from(subComponent), matcher); } + private DisplayData createDisplayDataWithItem(final String key, final String value) { - return DisplayData.from( - new PTransform, PCollection>() { - @Override - public void populateDisplayData(Builder builder) { - builder.add(key, value); - } - }); + return DisplayData.from(new SampleTransform(key, value)); + } + + static class SampleTransform extends PTransform, PCollection> { + private final String key; + private final String value; + + SampleTransform(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public void populateDisplayData(Builder builder) { + builder.add(key, value); + } } } diff --git a/sdks/java/core/src/test/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayDataTest.java b/sdks/java/core/src/test/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayDataTest.java index 4d75e267197a..9f8d5097efc6 100644 --- a/sdks/java/core/src/test/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayDataTest.java +++ b/sdks/java/core/src/test/java/com/google/cloud/dataflow/sdk/transforms/display/DisplayDataTest.java @@ -19,6 +19,7 @@ import static com.google.cloud.dataflow.sdk.transforms.display.DisplayDataMatchers.hasDisplayItem; import static com.google.cloud.dataflow.sdk.transforms.display.DisplayDataMatchers.hasKey; +import static com.google.cloud.dataflow.sdk.transforms.display.DisplayDataMatchers.hasNamespace; import static com.google.cloud.dataflow.sdk.transforms.display.DisplayDataMatchers.hasType; import static com.google.cloud.dataflow.sdk.transforms.display.DisplayDataMatchers.hasValue; import static com.google.cloud.dataflow.sdk.transforms.display.DisplayDataMatchers.includes; @@ -86,20 +87,25 @@ public void populateDisplayData(DisplayData.Builder builder) { } }; + PTransform transform = new PTransform, PCollection>() { + final Instant defaultStartTime = new Instant(0); + Instant startTime = defaultStartTime; + @Override public void populateDisplayData(DisplayData.Builder builder) { builder .include(subComponent1) .include(subComponent2) - .add("MinSproggles", 200) - .withLabel("Mimimum Required Sproggles") - .add("FireLazers", true) - .add("TimeBomb", Instant.now().plus(Duration.standardDays(1))) - .add("FilterLogic", subComponent1.getClass()) - .add("ServiceUrl", "google.com/fizzbang") - .withLinkUrl("http://www.google.com/fizzbang"); + .add("minSproggles", 200) + .withLabel("Mimimum Required Sproggles") + .add("fireLazers", true) + .addIfNotDefault("startTime", startTime, defaultStartTime) + .add("timeBomb", Instant.now().plus(Duration.standardDays(1))) + .add("filterLogic", subComponent1.getClass()) + .add("serviceUrl", "google.com/fizzbang") + .withLinkUrl("http://www.google.com/fizzbang"); } }; @@ -158,14 +164,22 @@ public void populateDisplayData(DisplayData.Builder builder) { @Test public void testItemProperties() { final Instant value = Instant.now(); - DisplayData data = DisplayData.from(new ConcreteComponent(value)); + DisplayData data = DisplayData.from(new HasDisplayData() { + @Override + public void populateDisplayData(DisplayData.Builder builder) { + builder.add("now", value) + .withLabel("the current instant") + .withLinkUrl("http://time.gov") + .withNamespace(DisplayDataTest.class); + } + }); @SuppressWarnings("unchecked") DisplayData.Item item = (DisplayData.Item) data.items().toArray()[0]; assertThat( item, - allOf( - hasNamespace(Matchers.>is(ConcreteComponent.class)), + Matchers.allOf( + hasNamespace(DisplayDataTest.class), hasKey("now"), hasType(DisplayData.Type.TIMESTAMP), hasValue(ISO_FORMATTER.print(value)), @@ -174,19 +188,6 @@ public void testItemProperties() { hasUrl(is("http://time.gov")))); } - static class ConcreteComponent implements HasDisplayData { - private Instant value; - - ConcreteComponent(Instant value) { - this.value = value; - } - - @Override - public void populateDisplayData(DisplayData.Builder builder) { - builder.add("now", value).withLabel("the current instant").withLinkUrl("http://time.gov"); - } - } - @Test public void testUnspecifiedOptionalProperties() { DisplayData data = @@ -203,6 +204,54 @@ public void populateDisplayData(DisplayData.Builder builder) { hasDisplayItem(allOf(hasLabel(nullValue(String.class)), hasUrl(nullValue(String.class))))); } + @Test + public void testAddIfNotDefault() { + final int defaultValue = 10; + + DisplayData data = DisplayData.from(new HasDisplayData() { + @Override + public void populateDisplayData(Builder builder) { + builder + .addIfNotDefault("isDefault", defaultValue, defaultValue) + .addIfNotDefault("notDefault", defaultValue + 1, defaultValue); + } + }); + + assertThat(data, not(hasDisplayItem(hasKey("isDefault")))); + assertThat(data, hasDisplayItem("notDefault", defaultValue + 1)); + } + + @Test + public void testAddIfNotNull() { + DisplayData data = DisplayData.from(new HasDisplayData() { + @Override + public void populateDisplayData(Builder builder) { + builder + .addIfNotNull("isNull", (Class) null) + .addIfNotNull("notNull", DisplayDataTest.class); + } + }); + + assertThat(data, not(hasDisplayItem(hasKey("isNull")))); + assertThat(data, hasDisplayItem(hasKey("notNull"))); + } + + @Test + public void testModifyingConditionalItemIsSafe() { + HasDisplayData component = new HasDisplayData() { + @Override + public void populateDisplayData(Builder builder) { + builder.addIfNotNull("nullItem", (Class) null) + .withLinkUrl("http://abc") + .withNamespace(DisplayDataTest.class) + .withLabel("Null item shoudl be safe"); + } + }; + + DisplayData.from(component); // should not throw + } + + @Test public void testIncludes() { final HasDisplayData subComponent = @@ -250,6 +299,18 @@ public void populateDisplayData(DisplayData.Builder builder) { assertThat(data, includes(subComponent, namespaceOverride.getClass())); } + @Test + public void testNullNamespaceOverride() { + thrown.expect(NullPointerException.class); + + DisplayData.from(new HasDisplayData() { + @Override + public void populateDisplayData(Builder builder) { + builder.add("foo", "bar") + .withNamespace(null); + } + }); + } @Test public void testIdentifierEquality() { @@ -349,6 +410,22 @@ public void populateDisplayData(DisplayData.Builder builder) { }); } + @Test + public void testDuplicateKeyWithNamespaceOverrideDoesntThrow() { + DisplayData displayData = DisplayData.from( + new HasDisplayData() { + @Override + public void populateDisplayData(DisplayData.Builder builder) { + builder + .add("foo", "bar") + .add("foo", "baz") + .withNamespace(DisplayDataTest.class); + } + }); + + assertThat(displayData.items(), hasSize(2)); + } + @Test public void testToString() { HasDisplayData component = new HasDisplayData() { @@ -428,6 +505,7 @@ public int hashCode() { } @Override + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") public boolean equals(Object obj) { return true; } @@ -479,6 +557,50 @@ public void populateDisplayData(DisplayData.Builder builder) { items, hasItem(allOf(hasKey("duration"), hasType(DisplayData.Type.DURATION)))); } + @Test + public void testExplicitItemType() { + DisplayData data = DisplayData.from(new HasDisplayData() { + @Override + public void populateDisplayData(Builder builder) { + builder + .add("integer", DisplayData.Type.INTEGER, 1234) + .add("string", DisplayData.Type.STRING, "foobar"); + } + }); + + assertThat(data, hasDisplayItem("integer", 1234)); + assertThat(data, hasDisplayItem("string", "foobar")); + } + + @Test + public void testInvalidExplicitItemType() { + HasDisplayData component = new HasDisplayData() { + @Override + public void populateDisplayData(Builder builder) { + builder.add("integer", DisplayData.Type.INTEGER, "foobar"); + } + }; + + thrown.expect(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(); @@ -534,7 +656,7 @@ public void populateDisplayData(DisplayData.Builder builder) { hasItem( allOf( hasKey("alpha"), - hasNamespace(Matchers.>is(component.getClass()))))); + hasNamespace(component.getClass())))); } @Test @@ -555,6 +677,23 @@ public void populateDisplayData(Builder builder) { }); } + @Test + public void testIncludeNullNamespace() { + final HasDisplayData subComponent = new HasDisplayData() { + @Override + public void populateDisplayData(Builder builder) { + } + }; + + thrown.expect(NullPointerException.class); + DisplayData.from(new HasDisplayData() { + @Override + public void populateDisplayData(Builder builder) { + builder.include(subComponent, null); + } + }); + } + @Test public void testNullKey() { thrown.expect(NullPointerException.class); @@ -610,28 +749,15 @@ public void testAcceptsNullOptionalValues() { @Override public void populateDisplayData(Builder builder) { builder.add("key", "value") - .withLabel(null) - .withLinkUrl(null); + .withLabel(null) + .withLinkUrl(null) + .withNamespace(null); } }); // Should not throw } - private static Matcher hasNamespace(Matcher> nsMatcher) { - return new FeatureMatcher>( - nsMatcher, "display item with namespace", "namespace") { - @Override - protected Class featureValueOf(DisplayData.Item actual) { - try { - return Class.forName(actual.getNamespace()); - } catch (ClassNotFoundException e) { - return null; - } - } - }; - } - private static Matcher hasLabel(Matcher labelMatcher) { return new FeatureMatcher( labelMatcher, "display item with label", "label") {