LinkAdditionRemovalReorderingDetection.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2018 wcm.io
 * %%
 * Licensed 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.
 * #L%
 */
package io.wcm.caravan.hal.comparison.impl.links.steps;

import java.util.List;
import java.util.function.Function;

import org.apache.commons.lang3.StringUtils;

import io.wcm.caravan.hal.comparison.HalComparisonContext;
import io.wcm.caravan.hal.comparison.HalDifference;
import io.wcm.caravan.hal.comparison.impl.context.HalComparisonContextImpl;
import io.wcm.caravan.hal.comparison.impl.difference.HalDifferenceListBuilder;
import io.wcm.caravan.hal.comparison.impl.links.LinkProcessingStep;
import io.wcm.caravan.hal.comparison.impl.matching.MatchingAlgorithm;
import io.wcm.caravan.hal.comparison.impl.matching.MatchingResult;
import io.wcm.caravan.hal.comparison.impl.matching.SimpleIdMatchingAlgorithm;
import io.wcm.caravan.hal.resource.Link;

/**
 * Determines if links have been added/removed/reordered, and ensures that the following steps
 * will only find pairs of matching expected/actual links in their lists.
 */
public class LinkAdditionRemovalReorderingDetection implements LinkProcessingStep {

  @Override
  public List<HalDifference> apply(HalComparisonContextImpl context, List<Link> expected, List<Link> actual) {

    MatchingResult<Link> matchingResult = applyMatchingAlgorithm(context, expected, actual);

    // collect the re-ordering, addition and removal differences
    HalDifferenceListBuilder diffs = findDifferences(context, expected, actual, matchingResult);

    // all following processing steps should only process the lniks that were successfully
    // matched (and re-ordered), so the given lists will be completely replaced with the ones from the
    // matching result
    expected.clear();
    expected.addAll(matchingResult.getMatchedExpected());

    actual.clear();
    actual.addAll(matchingResult.getMatchedActual());

    return diffs.build();
  }

  private MatchingResult<Link> applyMatchingAlgorithm(HalComparisonContext context, List<Link> expected, List<Link> actual) {

    Function<Link, String> idProvider = new DefaultIdProvider(expected, actual);

    MatchingAlgorithm<Link> algorithm = new SimpleIdMatchingAlgorithm<>(idProvider);

    return algorithm.findMatchingItems(expected, actual);
  }

  private HalDifferenceListBuilder findDifferences(HalComparisonContextImpl context, List<Link> expected, List<Link> actual,
      MatchingResult<Link> matchingResult) {

    HalDifferenceListBuilder diffs = new HalDifferenceListBuilder(context);
    String relation = context.getLastRelation();
    boolean reorderingRequired = matchingResult.areMatchesReordered();

    if (reorderingRequired) {
      String msg = "The " + relation + " links have a different order in the actual resource";
      diffs.reportReorderedLinks(msg, expected, actual);
    }

    for (Link removed : matchingResult.getRemovedExpected()) {
      String msg = "A " + relation + " link" + getLinkNameOrTitle(removed) + " is missing in the actual resource";
      diffs.reportMissingLink(msg, removed, expected.indexOf(removed));
    }

    for (Link added : matchingResult.getAddedActual()) {
      String msg = "An additional " + relation + " link" + getLinkNameOrTitle(added) + " is present in the actual resource";
      diffs.reportAdditionalLink(msg, added, actual.indexOf(added));
    }

    return diffs;
  }

  private static String getLinkNameOrTitle(Link link) {

    String name = link.getName();
    if (name != null) {
      return " with name '" + name + "'";
    }

    String title = link.getTitle();
    if (title != null) {
      return " with title '" + title + "'";
    }

    return "";
  }


  static class DefaultIdProvider implements Function<Link, String> {

    private final boolean useLinkName;

    DefaultIdProvider(List<Link> expected, List<Link> actual) {
      // only use names as an ID if they are used in the expected and actual resources
      // otherwise you would get confusing results if link names were only added
      // in one of the versions to be compared
      useLinkName = namesAreUsedIn(expected) && namesAreUsedIn(actual);
    }

    private static boolean namesAreUsedIn(List<Link> expected) {
      return expected.stream()
          .anyMatch(link -> link.getName() != null);
    }

    @Override
    public String apply(Link link) {
      if (useLinkName) {
        return StringUtils.trimToEmpty(link.getName());
      }

      // otherwise use a blank ID, which results in links being matched on their index only
      return "";
    }
  }
}