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.hamcrest.Matchers.allOf;
  import static org.hamcrest.Matchers.is;
  
  import java.util.Arrays;
  import java.util.Collection;
  import org.apache.beam.sdk.transforms.display.DisplayData.Item;
  import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets;
  import org.hamcrest.CustomTypeSafeMatcher;
  import org.hamcrest.Description;
  import org.hamcrest.FeatureMatcher;
  import org.hamcrest.Matcher;
  import org.hamcrest.Matchers;
  import org.hamcrest.TypeSafeDiagnosingMatcher;
  import org.joda.time.Duration;
  import org.joda.time.Instant;
  
  /** Hamcrest matcher for making assertions on {@link DisplayData} instances. */
  public class DisplayDataMatchers {
    /** Do not instantiate. */
    private DisplayDataMatchers() {}
  
    /** Creates a matcher that matches if the examined {@link DisplayData} contains any items. */
    public static Matcher<DisplayData> hasDisplayItem() {
      return new FeatureMatcher<DisplayData, Collection<DisplayData.Item>>(
          Matchers.not(Matchers.empty()), "DisplayData", "DisplayData") {
        @Override
        protected Collection<Item> featureValueOf(DisplayData actual) {
          return actual.items();
        }
      };
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData} contains an item with the
     * specified key.
     */
    public static Matcher<DisplayData> hasDisplayItem(String key) {
      return hasDisplayItem(hasKey(key));
    }
  
    /**
     * Create a matcher that matches if the examined {@link DisplayData} contains an item with the
     * specified key and String value.
     */
    public static Matcher<DisplayData> hasDisplayItem(String key, String value) {
      return hasDisplayItem(key, DisplayData.Type.STRING, value);
    }
  
    /**
     * Create a matcher that matches if the examined {@link DisplayData} contains an item with the
     * specified key and Boolean value.
     */
    public static Matcher<DisplayData> hasDisplayItem(String key, Boolean value) {
      return hasDisplayItem(key, DisplayData.Type.BOOLEAN, value);
    }
  
    /**
     * Create a matcher that matches if the examined {@link DisplayData} contains an item with the
     * specified key and Duration value.
     */
    public static Matcher<DisplayData> hasDisplayItem(String key, Duration value) {
      return hasDisplayItem(key, DisplayData.Type.DURATION, value);
    }
  
    /**
     * Create a matcher that matches if the examined {@link DisplayData} contains an item with the
     * specified key and Float value.
     */
    public static Matcher<DisplayData> hasDisplayItem(String key, double value) {
      return hasDisplayItem(key, DisplayData.Type.FLOAT, value);
    }
  
    /**
     * Create a matcher that matches if the examined {@link DisplayData} contains an item with the
     * specified key and Integer value.
     */
    public static Matcher<DisplayData> hasDisplayItem(String key, long value) {
      return hasDisplayItem(key, DisplayData.Type.INTEGER, value);
    }
  
    /**
     * Create a matcher that matches if the examined {@link DisplayData} contains an item with the
     * specified key and Class value.
     */
    public static Matcher<DisplayData> hasDisplayItem(String key, Class<?> value) {
      return hasDisplayItem(key, DisplayData.Type.JAVA_CLASS, value);
    }
  
    /**
     * Create a matcher that matches if the examined {@link DisplayData} contains an item with the
     * specified key and Timestamp value.
     */
    public static Matcher<DisplayData> hasDisplayItem(String key, Instant value) {
      return hasDisplayItem(key, DisplayData.Type.TIMESTAMP, value);
    }
  
    private static Matcher<DisplayData> hasDisplayItem(
        String key, DisplayData.Type type, Object value) {
      DisplayData.FormattedItemValue formattedValue = type.format(value);
      return hasDisplayItem(
          allOf(hasKey(key), hasType(type), hasValue(formattedValue.getLongValue())));
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData} contains any item matching
     * the specified {@code itemMatcher}.
     */
    public static Matcher<DisplayData> hasDisplayItem(Matcher<DisplayData.Item> itemMatcher) {
      return new HasDisplayDataItemMatcher(itemMatcher);
    }
  
    private static class HasDisplayDataItemMatcher extends TypeSafeDiagnosingMatcher<DisplayData> {
      private final Matcher<Item> itemMatcher;
  
      private HasDisplayDataItemMatcher(Matcher<DisplayData.Item> itemMatcher) {
        this.itemMatcher = itemMatcher;
      }
  
      @Override
      public void describeTo(Description description) {
        description.appendText("display data with item: ");
        itemMatcher.describeTo(description);
      }
  
      @Override
      protected boolean matchesSafely(DisplayData data, Description mismatchDescription) {
        Collection<Item> items = data.items();
        boolean isMatch = Matchers.hasItem(itemMatcher).matches(items);
        if (!isMatch) {
          mismatchDescription.appendText("found " + items.size() + " non-matching item(s):\n");
          mismatchDescription.appendValue(data);
        }
  
        return isMatch;
      }
    }
  
    /**
     * Create a matcher that matches if the examined {@link DisplayData} contains all display data
     * registered from the specified subcomponent and namespace.
     */
    public static Matcher<DisplayData> includesDisplayDataFor(
        final String path, final HasDisplayData subComponent) {
      return new CustomTypeSafeMatcher<DisplayData>("includes subcomponent") {
        @Override
        protected boolean matchesSafely(DisplayData displayData) {
          DisplayData subComponentData = subComponentData(path);
          if (subComponentData.items().isEmpty()) {
            throw new UnsupportedOperationException(
                "subComponent contains no display data; " + "cannot verify whether it is included");
          }
  
          DisplayDataComparison comparison = checkSubset(displayData, subComponentData, path);
          return comparison.missingItems.isEmpty();
        }
  
        @Override
        protected void describeMismatchSafely(
            DisplayData displayData, Description mismatchDescription) {
          DisplayData subComponentDisplayData = subComponentData(path);
          DisplayDataComparison comparison = checkSubset(displayData, subComponentDisplayData, path);
  
          mismatchDescription
              .appendText("did not include:\n")
              .appendValue(comparison.missingItems)
              .appendText("\nNon-matching items:\n")
              .appendValue(comparison.unmatchedItems);
        }
  
        private DisplayData subComponentData(final String path) {
          return DisplayData.from(builder -> builder.include(path, subComponent));
        }
  
        private DisplayDataComparison checkSubset(
            DisplayData displayData, DisplayData included, String path) {
          DisplayDataComparison comparison = new DisplayDataComparison(displayData.items());
          for (Item item : included.items()) {
            Item matchedItem =
                displayData
                    .asMap()
                    .get(
                        DisplayData.Identifier.of(
                            DisplayData.Path.absolute(path), item.getNamespace(), item.getKey()));
  
            if (matchedItem != null) {
              comparison.matched(matchedItem);
            } else {
              comparison.missing(item);
            }
          }
  
          return comparison;
        }
  
        class DisplayDataComparison {
          Collection<Item> missingItems;
          Collection<Item> unmatchedItems;
  
          DisplayDataComparison(Collection<Item> superset) {
            missingItems = Sets.newHashSet();
            unmatchedItems = Sets.newHashSet(superset);
          }
  
          void matched(Item supersetItem) {
            unmatchedItems.remove(supersetItem);
          }
  
          void missing(Item subsetItem) {
            missingItems.add(subsetItem);
          }
        }
      };
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData.Item} contains a key with the
     * specified value.
     */
    public static Matcher<DisplayData.Item> hasKey(String key) {
      return hasKey(is(key));
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData.Item} contains a key matching
     * the specified key matcher.
     */
    public static Matcher<DisplayData.Item> hasKey(Matcher<String> keyMatcher) {
      return new FeatureMatcher<DisplayData.Item, String>(keyMatcher, "with key", "key") {
        @Override
        protected String featureValueOf(DisplayData.Item actual) {
          return actual.getKey();
        }
      };
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData.Item} contains a path
     * matching the specified namespace.
     */
    public static Matcher<DisplayData.Item> hasPath(String... paths) {
      DisplayData.Path path =
          (paths.length == 0)
              ? DisplayData.Path.root()
              : DisplayData.Path.absolute(paths[0], Arrays.copyOfRange(paths, 1, paths.length));
      return new FeatureMatcher<DisplayData.Item, DisplayData.Path>(
          is(path), " with namespace", "namespace") {
        @Override
        protected DisplayData.Path featureValueOf(DisplayData.Item actual) {
          return actual.getPath();
        }
      };
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData.Item} contains the specified
     * namespace.
     */
    public static Matcher<DisplayData.Item> hasNamespace(Class<?> namespace) {
      return hasNamespace(Matchers.<Class<?>>is(namespace));
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData.Item} contains a namespace
     * matching the specified namespace matcher.
     */
    public static Matcher<DisplayData.Item> hasNamespace(Matcher<Class<?>> namespaceMatcher) {
      return new FeatureMatcher<DisplayData.Item, Class<?>>(
          namespaceMatcher, " with namespace", "namespace") {
        @Override
        protected Class<?> featureValueOf(DisplayData.Item actual) {
          return actual.getNamespace();
        }
      };
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData.Item} matches the specified
     * type.
     */
    public static Matcher<DisplayData.Item> hasType(DisplayData.Type type) {
      return hasType(is(type));
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData.Item} has a type matching the
     * specified type matcher.
     */
    public static Matcher<DisplayData.Item> hasType(Matcher<DisplayData.Type> typeMatcher) {
      return new FeatureMatcher<DisplayData.Item, DisplayData.Type>(
          typeMatcher, "with type", "type") {
        @Override
        protected DisplayData.Type featureValueOf(DisplayData.Item actual) {
          return actual.getType();
        }
      };
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData.Item} has the specified
     * value.
     */
    public static Matcher<DisplayData.Item> hasValue(Object value) {
      return hasValue(is(value));
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData.Item} contains a value
     * matching the specified value matcher.
     */
    public static <T> Matcher<DisplayData.Item> hasValue(Matcher<T> valueMatcher) {
      return new FeatureMatcher<DisplayData.Item, T>(valueMatcher, "with value", "value") {
        @Override
        protected T featureValueOf(DisplayData.Item actual) {
          @SuppressWarnings("unchecked")
          T value = (T) actual.getValue();
          return value;
        }
      };
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData.Item} has the specified
     * label.
     */
    public static Matcher<DisplayData.Item> hasLabel(String label) {
      return hasLabel(is(label));
    }
  
    /**
     * Creates a matcher that matches if the examined {@link DisplayData.Item} has a label matching
     * the specified label matcher.
     */
    public static Matcher<DisplayData.Item> hasLabel(Matcher<String> labelMatcher) {
      return new FeatureMatcher<DisplayData.Item, String>(
          labelMatcher, "display item with label", "label") {
        @Override
        protected String featureValueOf(DisplayData.Item actual) {
          return actual.getLabel();
        }
      };
    }
  }
  