HalComparisonContextImpl.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.context;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableMap;

import io.wcm.caravan.hal.comparison.HalComparisonContext;
import io.wcm.caravan.hal.comparison.impl.PairWithRelation;
import io.wcm.caravan.hal.resource.HalResource;
import io.wcm.caravan.hal.resource.Link;

/**
 * An immutable object that specifies in which part of the tree the comparison is currently being executed.
 */
public class HalComparisonContextImpl implements HalComparisonContext {

  private final HalPathImpl halPath;

  private final String expectedUrl;
  private final String actualUrl;

  private final Map<HalPathImpl, HalResource> parentResources;

  /**
   * This constructor shouldn't be used except for creating the initial context for the entry point. From then on, use
   * the #withXyz methods to build a new context
   * @param expectedUrl
   * @param actualUrl
   */
  public HalComparisonContextImpl(String expectedUrl, String actualUrl) {
    this.halPath = new HalPathImpl();
    this.expectedUrl = expectedUrl;
    this.actualUrl = actualUrl;
    this.parentResources = Collections.emptyMap();
  }

  private HalComparisonContextImpl(HalPathImpl halPath, String expectedUrl, String actualUrl, Map<HalPathImpl, HalResource> parentResources) {
    this.halPath = halPath;
    this.expectedUrl = expectedUrl;
    this.actualUrl = actualUrl;
    this.parentResources = ImmutableMap.copyOf(parentResources);
  }

  @Override
  public String getExpectedUrl() {
    return this.expectedUrl;
  }

  @Override
  public String getActualUrl() {
    return this.actualUrl;
  }

  /**
   * @param relation of the linked or embedded resource that is about to be processed
   * @return a new instance with an updated {@link HalPathImpl}
   */
  public HalComparisonContextImpl withAppendedHalPath(String relation, HalResource contextResource) {

    HalPathImpl newHalPath = halPath.append(relation, null, null);
    return new HalComparisonContextImpl(newHalPath, expectedUrl, actualUrl, createNewParentResourceMap(contextResource));
  }

  private Map<HalPathImpl, HalResource> createNewParentResourceMap(HalResource contextResource) {
    Map<HalPathImpl, HalResource> newParents = new HashMap<>(parentResources);
    newParents.put(halPath, contextResource);
    return newParents;
  }

  /**
   * @param pair of resources that is about to be compared
   * @param contextResource the resource from the expected tree that embeds the resource to be compared
   * @return a new instance with an updated {@link HalPathImpl} that adds array indices if required
   */
  public HalComparisonContextImpl withHalPathOfEmbeddedResource(PairWithRelation<HalResource> pair, HalResource contextResource) {

    String relation = pair.getRelation();
    List<HalResource> originalEmbedded = contextResource.getEmbedded(relation);

    Integer originalIndex = null;
    if (originalEmbedded.size() > 1) {
      originalIndex = findOriginalIndex(pair, originalEmbedded);
    }

    HalPathImpl newHalPath = halPath.append(relation, originalIndex, null);
    return new HalComparisonContextImpl(newHalPath, expectedUrl, actualUrl, createNewParentResourceMap(contextResource));
  }


  private Integer findOriginalIndex(PairWithRelation<HalResource> pair, List<HalResource> originalEmbedded) {
    for (int i = 0; i < originalEmbedded.size(); i++) {
      ObjectNode originalModel = originalEmbedded.get(i).getModel();
      if (originalModel == pair.getExpected().getModel()) {
        return i;
      }
    }
    throw new IllegalArgumentException("The resource from the given pair is not actually embedded in the context resource");
  }

  /**
   * @param pair of links that are about to be followed and compared
   * @param contextResource the resource from the expected tree that contains the links
   * @return a new instance with an updated {@link HalPathImpl} that adds array indices if required
   */
  public HalComparisonContextImpl withHalPathOfLinkedResource(PairWithRelation<Link> pair, HalResource contextResource) {

    String relation = pair.getRelation();
    List<Link> originalLinks = contextResource.getLinks(relation);

    Integer originalIndex = null;
    String name = pair.getExpected().getName();
    if (originalLinks.size() > 1) {
      originalIndex = originalLinks.indexOf(pair.getExpected());
    }

    HalPathImpl newHalPath = halPath.append(relation, originalIndex, name);
    Map<HalPathImpl, HalResource> newParents = new HashMap<>(parentResources);
    newParents.put(halPath, contextResource);
    return new HalComparisonContextImpl(newHalPath, expectedUrl, actualUrl, createNewParentResourceMap(contextResource));
  }

  /**
   * @param indexInArray the index of the next array entry to be compared
   * @return a new instance with an updated {@link HalPathImpl} that contains array indices
   */
  public HalComparisonContextImpl withHalPathIndex(int indexInArray) {
    HalPathImpl newHalPath = halPath.replaceHalPathIndex(indexInArray);
    return new HalComparisonContextImpl(newHalPath, expectedUrl, actualUrl, parentResources);
  }

  /**
   * @param fieldName the name of the JSON property to be compared
   * @return a new instance with an updated {@link HalPathImpl}
   */
  public HalComparisonContextImpl withAppendedJsonPath(String fieldName) {
    HalPathImpl newHalPath = halPath.appendJsonPath(fieldName);
    return new HalComparisonContextImpl(newHalPath, expectedUrl, actualUrl, parentResources);
  }

  /**
   * @param indexInArray the index of the next array entry to be compared
   * @return a new instance with an updated {@link HalPathImpl} that contains array indices
   */
  public HalComparisonContextImpl withJsonPathIndex(int indexInArray) {
    HalPathImpl newHalPath = halPath.replaceJsonPathIndex(indexInArray);
    return new HalComparisonContextImpl(newHalPath, expectedUrl, actualUrl, parentResources);
  }

  /**
   * @param newUrl of the expected resource that is about to be loaded
   * @return a new instance with an updated {@link #getExpectedUrl()} value
   */
  public HalComparisonContextImpl withNewExpectedUrl(String newUrl) {
    return new HalComparisonContextImpl(halPath, newUrl, actualUrl, parentResources);
  }

  /**
   * @param newUrl of the actual resource that is about to be loaded
   * @return a new instance with an updated {@link #getActualUrl()} value
   */
  public HalComparisonContextImpl withNewActualUrl(String newUrl) {
    return new HalComparisonContextImpl(halPath, expectedUrl, newUrl, parentResources);
  }

  @Override
  public String getLastRelation() {
    return halPath.getLastRelation();
  }

  @Override
  public List<String> getAllRelations() {
    return halPath.getAllRelations();
  }

  @Override
  public String getLastProperyName() {
    return halPath.getLastProperyName();
  }

  @Override
  public List<String> getAllPropertyNames() {
    return halPath.getAllPropertyNames();
  }

  @Override
  public HalResource getParentResourceWithRelation(String relation) {
    return parentResources.entrySet().stream()
        .filter(entry -> entry.getKey().getLastRelation().equals(relation))
        .sorted(HalComparisonContextImpl::longestHalPathFirstComparator)
        .map(Entry::getValue)
        .findFirst()
        .orElse(null);
  }

  private static int longestHalPathFirstComparator(Entry<HalPathImpl, HalResource> entry1, Entry<HalPathImpl, HalResource> entry2) {
    String firstHalPath = entry1.getKey().toString();
    String secondHalPath = entry2.getKey().toString();

    return -Integer.compare(firstHalPath.length(), secondHalPath.length());
  }

  @Override
  public String toString() {
    return halPath.toString();
  }
}