All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.remondis.remap.AssertConfiguration Maven / Gradle / Ivy

There is a newer version: 4.3.7
Show newest version
package com.remondis.remap;

import static com.remondis.remap.Lang.denyNull;
import static com.remondis.remap.MappingConfiguration.OMIT_FIELD_DEST;
import static com.remondis.remap.MappingConfiguration.OMIT_FIELD_SOURCE;
import static com.remondis.remap.MappingConfiguration.getPropertyFromFieldSelector;
import static com.remondis.remap.MappingConfiguration.getTypedPropertyFromFieldSelector;
import static com.remondis.remap.OmitTransformation.omitDestination;
import static com.remondis.remap.OmitTransformation.omitSource;
import static com.remondis.remap.ReassignBuilder.ASSIGN;
import static com.remondis.remap.ReplaceBuilder.TRANSFORM;

import java.beans.PropertyDescriptor;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Creates a test for a {@link Mapper} object to assert the mapping specification. The expected mapping is to be
 * configured on this object. The method {@link #ensure()} then performs the assertions against the actual configured
 * mapping configuration of the specified mapper and performs checks using the specified transformation functions.
 * Transformation functions specified for the `replace` operation are checked against null and sample
 * values. It is expected that those test invocations do not throw an exception.
 *
 * @param 
 *        The type of the source objects
 * @param 
 *        The type of the destination objects.
 *
 * @author schuettec
 */
public class AssertConfiguration {

  static final String DIFFERENT_NULL_STRATEGY = "The replace transformation specified by the mapper has a different "
      + "null value strategy than the expected transformation:\n";

  static final String UNEXPECTED_EXCEPTION = "Function threw an unexpected exception for transformation:\n";

  static final String NOT_NULL_SAFE = "The specified transformation function is not null-safe for operation:\n";

  static final String UNEXPECTED_TRANSFORMATION = "The following unexpected transformation "
      + "were specified on the mapping:\n";

  static final String EXPECTED_TRANSFORMATION = "The following expected transformation "
      + "were not specified on the mapping:\n";

  static final String TRANSFORMATION_ALREADY_ADDED = "The specified transformation was already added as an assertion";

  private Mapper mapper;

  private Set assertedTransformations;

  /**
   * Flag indicating that mappings that are not expected by asserts must be omitInSource mappings.
   */
  private boolean omitOthersSource = false;
  /**
   * Flag indicating that mappings that are not expected by asserts must be omitInDestination mappings.
   */
  private boolean omitOthersDestination = false;

  private Object expectN;

  private boolean expectNoImplicitMappings;
  private List verificaions;

  private boolean expectWriteNullIfSourceIsNull = false;

  AssertConfiguration(Mapper mapper) {
    denyNull("mapper", mapper);
    this.mapper = mapper;
    this.assertedTransformations = new HashSet<>();
    this.verificaions = new LinkedList<>();
  }

  /**
   * Specifies an assertion for a reassign operation.
   *
   * @param sourceSelector
   *        The source field selector.
   * @return Returns a {@link ReassignAssertBuilder} for further configuration.
   */
  public  ReassignAssertBuilder expectReassign(FieldSelector sourceSelector) {
    denyNull("sourceSelector", sourceSelector);
    PropertyDescriptor sourceProperty = getPropertyFromFieldSelector(Target.SOURCE, ASSIGN, getMapping().getSource(),
        sourceSelector);
    ReassignAssertBuilder reassignBuilder = new ReassignAssertBuilder(sourceProperty,
        getMapping().getDestination(), this);
    return reassignBuilder;
  }

  /**
   * Specifies an assertion for a replace operation.
   *
   * @param sourceSelector
   *        The source field selector.
   * @param destinationSelector
   *        The destination field selector.
   * @return Returns a {@link ReplaceAssertBuilder} for further configuration.
   */
  public  ReplaceAssertBuilder expectReplace(TypedSelector sourceSelector,
      TypedSelector destinationSelector) {
    denyNull("sourceSelector", sourceSelector);
    denyNull("destinationSelector", destinationSelector);

    TypedPropertyDescriptor sourceProperty = getTypedPropertyFromFieldSelector(Target.SOURCE, TRANSFORM,
        getMapping().getSource(), sourceSelector);
    TypedPropertyDescriptor destProperty = getTypedPropertyFromFieldSelector(Target.DESTINATION, TRANSFORM,
        getMapping().getDestination(), destinationSelector);

    ReplaceAssertBuilder builder = new ReplaceAssertBuilder<>(sourceProperty, destProperty, this);
    return builder;
  }

  /**
   * Specifies an assertion for a set operation.
   *
   * @param destinationSelector
   *        The destination field selector.
   * @return Returns a {@link SetAssertBuilder} for further configuration.
   */
  public  SetAssertBuilder expectSet(TypedSelector destinationSelector) {
    denyNull("destinationSelector", destinationSelector);

    TypedPropertyDescriptor destProperty = getTypedPropertyFromFieldSelector(Target.DESTINATION, TRANSFORM,
        getMapping().getDestination(), destinationSelector);
    SetAssertBuilder builder = new SetAssertBuilder<>(destProperty, this);
    return builder;
  }

  /**
   * Specifies an assertion for a restructure operation.
   *
   * @param destinationSelector
   *        The destination field selector.
   * @return Returns a {@link RestructureAssertBuilder} for further configuration.
   */
  public  RestructureAssertBuilder expectRestructure(TypedSelector destinationSelector) {
    denyNull("destinationSelector", destinationSelector);

    TypedPropertyDescriptor destProperty = getTypedPropertyFromFieldSelector(Target.DESTINATION, TRANSFORM,
        getMapping().getDestination(), destinationSelector);
    RestructureAssertBuilder builder = new RestructureAssertBuilder<>(destProperty, this);
    return builder;
  }

  /**
   * Specifies an assertion for a replace operation for collections.
   *
   * @param sourceSelector
   *        The source field selector.
   * @param destinationSelector
   *        The destination field selector.
   * @return Returns a {@link ReplaceCollectionAssertBuilder} for further configuration.
   */
  public  ReplaceCollectionAssertBuilder expectReplaceCollection(
      TypedSelector, S> sourceSelector, TypedSelector, D> destinationSelector) {
    denyNull("sourceSelector", sourceSelector);
    denyNull("destinationSelector", destinationSelector);
    TypedPropertyDescriptor> sourceProperty = getTypedPropertyFromFieldSelector(Target.SOURCE,
        ReplaceBuilder.TRANSFORM, getMapping().getSource(), sourceSelector);
    TypedPropertyDescriptor> destProperty = getTypedPropertyFromFieldSelector(Target.DESTINATION,
        ReplaceBuilder.TRANSFORM, getMapping().getDestination(), destinationSelector);

    ReplaceCollectionAssertBuilder builder = new ReplaceCollectionAssertBuilder<>(sourceProperty,
        destProperty, this);
    return builder;
  }

  private void _add(Transformation transformation) {
    if (assertedTransformations.contains(transformation)) {
      throw new AssertionError(TRANSFORMATION_ALREADY_ADDED);
    }
    assertedTransformations.add(transformation);
  }

  /**
   * Specifies an assertion for a source field to be omitted.
   *
   * @param sourceSelector
   *        The source field selector.
   * @return Returns a {@link AssertConfiguration} for further configuration.
   */
  public AssertConfiguration expectOmitInSource(FieldSelector sourceSelector) {
    denyNull("sourceSelector", sourceSelector);
    // Omit in destination
    PropertyDescriptor propertyDescriptor = getPropertyFromFieldSelector(Target.SOURCE, OMIT_FIELD_SOURCE,
        getMapping().getSource(), sourceSelector);
    OmitTransformation omitSource = omitSource(getMapping(), propertyDescriptor);
    _add(omitSource);
    return this;
  }

  /**
   * Expects the mapper to suppress creation of implicit mappings. Note: This requires the user to define the mappings
   * explicitly using {@link MappingConfiguration#reassign(FieldSelector)} or any other mapping operation. Therefore all
   * this
   * explicit
   * mappings must be backed by an assertion.
   *
   * @return Returns this instance for further configuration.
   */
  public AssertConfiguration expectNoImplicitMappings() {
    this.expectNoImplicitMappings = true;
    return this;
  }

  /**
   * Expects the mapper to suppress creation of implicit mappings. Note: This requires the user to define the mappings
   * explicitly using {@link MappingConfiguration#reassign(FieldSelector)} or any other mapping operation. Therefore all
   * this
   * explicit
   * mappings must be backed by an assertion.
   *
   * @return Returns this instance for further configuration.
   */
  public AssertConfiguration expectToWriteNullIfSourceIsNull() {
    this.expectWriteNullIfSourceIsNull = true;
    return this;
  }

  /**
   * Specifies an assertion for a destination field to be omitted.
   *
   * @param destinationSelector
   *        The destination field selector.
   * @return Returns a {@link AssertConfiguration} for further configuration.
   */
  public AssertConfiguration expectOmitInDestination(FieldSelector destinationSelector) {
    denyNull("destinationSelector", destinationSelector);
    PropertyDescriptor propertyDescriptor = getPropertyFromFieldSelector(Target.DESTINATION, OMIT_FIELD_DEST,
        getMapping().getDestination(), destinationSelector);
    OmitTransformation omitDestination = omitDestination(getMapping(), propertyDescriptor);
    _add(omitDestination);
    return this;
  }

  /**
   * Expects all other field to be omitted.
   *
   * @return Returns a {@link AssertConfiguration} for further configuration.
   */
  public AssertConfiguration expectOthersToBeOmitted() {
    expectOtherSourceFieldsToBeOmitted();
    expectOtherDestinationFieldsToBeOmitted();
    return this;
  }

  /**
   * Expects all other source fields to be omitted.
   *
   * @return Returns a {@link AssertConfiguration} for further configuration.
   */
  public AssertConfiguration expectOtherSourceFieldsToBeOmitted() {
    this.omitOthersSource = true;
    return this;
  }

  /**
   * Expects all other destination fields to be omitted.
   *
   * @return Returns a {@link AssertConfiguration} for further configuration.
   */
  public AssertConfiguration expectOtherDestinationFieldsToBeOmitted() {
    this.omitOthersDestination = true;
    return this;
  }

  /**
   * Performs the specified assertions against the specified mapper instance. If a replace operation was specified with
   * a transformation function to be also called for null values a null check is performed against the
   * function.
   *
   * @throws AssertionError
   *         Thrown if an assertion made about the {@link Mapper} object failed.
   */
  public void ensure() throws AssertionError {
    checkImplicitMappingStrategy();
    checkNullHandling();
    checkReplaceTransformations();

    checkVerifications();
    checkTransformations();
    checkReplaceFunctions();
  }

  private void checkVerifications() {
    verificaions.stream()
        .forEach(AssertVerification::verify);
  }

  private void checkImplicitMappingStrategy() {
    if (!mapper.getMapping()
        .isNoImplicitMappings() && expectNoImplicitMappings) {
      throw new AssertionError("The mapper was expected to create no implicit mappings but the actual mapper does.");
    } else if (mapper.getMapping()
        .isNoImplicitMappings() && !expectNoImplicitMappings) {
      throw new AssertionError("The mapper was expected to create implicit mappings but the actual mapper does not.");
    }
  }

  private void checkNullHandling() {
    if (!mapper.getMapping()
        .isWriteNull() && expectWriteNullIfSourceIsNull) {
      throw new AssertionError("The mapper was expected to write null values if the source value is null, "
          + "but the current mapper is configured to skip mappings if source value is null.");
    } else if (mapper.getMapping()
        .isWriteNull() && !expectWriteNullIfSourceIsNull) {
      throw new AssertionError("The mapper was expected to skip mapping if the source value is null, "
          + "but the current mapper is configured to write null if source value is null.");
    }
  }

  /**
   * This method checks the replace functions is null-safe if null strategy is not skip-when-null.
   */
  @SuppressWarnings("rawtypes")
  private void checkReplaceFunctions() {
    Set mappings = mapper.getMapping()
        .getMappings();
    mappings.stream()
        .filter(t -> {
          return (t instanceof SkipWhenNullTransformation);
        })
        .map(t -> {
          return (SkipWhenNullTransformation) t;
        })
        .forEach(r -> {
          Function transformation = r.getTransformation();
          if (!r.isSkipWhenNull()) {
            assertionErrorIfNullCheckFails(r, transformation);
          }
        });
  }

  /**
   * Throws an {@link AssertionError} if the specified {@link Function} is not null safe.
   *
   * @param r The {@link Transformation} that is validated.
   * @param transformation The {@link Function} that is null checked.
   * @throws AssertionError Thrown if the null check fails.
   */
  private static  void assertionErrorIfNullCheckFails(Transformation r, Function transformation)
      throws AssertionError {
    try {
      transformation.apply(null);
    } catch (NullPointerException t) {
      throw new AssertionError(NOT_NULL_SAFE + r.toString(), t);
    } catch (Throwable t) {
      throw new AssertionError(UNEXPECTED_EXCEPTION + r.toString(), t);
    }
  }

  /**
   * This method checks that the expected replace transformations and the actual replace transformations have equal null
   * strategies.
   */
  @SuppressWarnings("rawtypes")
  private void checkReplaceTransformations() {
    Set mappings = mapper.getMapping()
        .getMappings();

    mappings.stream()
        .filter(t -> {
          return (t instanceof SkipWhenNullTransformation);
        })
        .map(t -> {
          return (SkipWhenNullTransformation) t;
        })
        .forEach(replace -> {
          Optional sameTransformation = assertedTransformations().stream()
              .filter(t -> {
                return (t instanceof SkipWhenNullTransformation);
              })
              .map(t -> {
                return (SkipWhenNullTransformation) t;
              })
              .filter(r -> {
                return r.getSourceProperty()
                    .equals(replace.getSourceProperty());
              })
              .filter(r -> {
                return r.getDestinationProperty()
                    .equals(replace.getDestinationProperty());
              })
              .findFirst();
          if (sameTransformation.isPresent()) {
            SkipWhenNullTransformation assertedReplaceTransformation = sameTransformation.get();
            // Check if the configured replace transformation has the same skip-null configuration than the asserted
            // one and throw if not
            if (replace.isSkipWhenNull() != assertedReplaceTransformation.isSkipWhenNull()) {
              throw new AssertionError(DIFFERENT_NULL_STRATEGY + replace.toString());
            }
          }
        });
  }

  private void checkTransformations() {
    Set mappings = getMapping().getMappings();
    Set assertedTransformations = assertedTransformations();

    // we have to check that the mapping list contains all asserted transformations
    mappings.removeAll(assertedTransformations);
    assertedTransformations.removeAll(getMapping().getMappings());

    if (!assertedTransformations.isEmpty()) {
      throw new AssertionError(EXPECTED_TRANSFORMATION + listCollection(assertedTransformations));
    }

    if (!mappings.isEmpty()) {
      // if there are more elements left, the remaining transformations must be MapTransformations
      Stream tranformationStream = mappings.stream();

      // If omit others for destination, then all omitInDestination transformations are expected.
      if (omitOthersDestination) {
        tranformationStream = tranformationStream.filter(t -> {
          if (t instanceof OmitTransformation) {
            OmitTransformation omitTransformation = (OmitTransformation) t;
            return !(omitTransformation.isOmitInDestination());
          } else {
            return true;
          }
        });
      }
      // If omit others for source, then all omitInSource transformations are expected.
      if (omitOthersSource) {
        tranformationStream = tranformationStream.filter(t -> {
          if (t instanceof OmitTransformation) {
            OmitTransformation omitTransformation = (OmitTransformation) t;
            return !(omitTransformation.isOmitInSource());
          } else {
            return true;
          }
        });
      }

      // Ignore MapTransformations, because those are implicit transformations.
      tranformationStream = tranformationStream.filter(t -> {
        return !(t instanceof MapTransformation);
      });

      // All remaining transformations were not backed by an assert.
      Set unexpectedTransformations = tranformationStream.collect(Collectors.toSet());
      if (!unexpectedTransformations.isEmpty()) {
        throw new AssertionError(UNEXPECTED_TRANSFORMATION + listCollection(unexpectedTransformations));
      }
    }
  }

  private String listCollection(Set transformations) {
    StringBuilder b = new StringBuilder();
    transformations.stream()
        .forEach(t -> {
          b.append("- " + t.toString())
              .append("\n");
        });
    return b.toString();
  }

  private Set assertedTransformations() {
    return new HashSet<>(assertedTransformations);
  }

  void addAssertion(Transformation transformation) {
    _add(transformation);
  }

  MappingConfiguration getMapping() {
    return mapper.getMapping();
  }

  /**
   * Method to add custom verifications, that cannot be performed by a comparision of {@link Transformation} using
   * equals.
   */
  void addVerification(AssertVerification verification) {
    denyNull("verification", verification);
    this.verificaions.add(verification);
  }
}