InternalUtils.java

package com.github.dakusui.pcond.internals;

import com.github.dakusui.pcond.core.Evaluable;
import com.github.dakusui.pcond.core.printable.PrintableFunction;
import com.github.dakusui.pcond.core.printable.PrintablePredicate;
import com.github.dakusui.pcond.forms.Functions;
import com.github.dakusui.pcond.forms.Printables;
import com.github.dakusui.pcond.validator.Explanation;
import com.github.dakusui.pcond.validator.Validator;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;

import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;

public enum InternalUtils {
  ;

  private static final Predicate<?>   DUMMY_PREDICATE = Printables.predicate("DUMMY_PREDICATE:ALWAYSTHROW", v -> {
    throw new UnsupportedOperationException("Testing: '" + v + "' was failed, because this is a dummy predicate.");
  });
  private static final Function<?, ?> DUMMY_FUNCTION  = Printables.function("DUMMY_FUNCTION:ALWAYSTHROW", v -> {
    throw new UnsupportedOperationException("Applying: '" + v + "' was failed, because this is a dummy predicate.");
  });

  public static String formatObject(Object value) {
    return formatObject(value, summarizedStringLength());
  }

  public static String formatObject(Object value, int maxLength) {
    return _formatObject(value, maxLength).replaceAll("[\\r\\n]", " ");
  }

  private static String _formatObject(Object value, int maxLength) {
    if (value == null)
      return "null";
    if (value instanceof Collection) {
      Collection<?> collection = (Collection<?>) value;
      if (collection.size() < 4)
        return format("[%s]",
            collection.stream()
                .map(InternalUtils::formatObject)
                .collect(joining(",")));
      Iterator<?> i = collection.iterator();
      return format("[%s,%s,%s...;%s]",
          formatObject(i.next()),
          formatObject(i.next()),
          formatObject(i.next()),
          collection.size()
      );
    }
    if (value instanceof Object[])
      return formatObject(asList((Object[]) value));
    if (value instanceof Formattable)
      return String.format("%s", value);
    if (value instanceof String) {
      String s = (String) value;
      s = summarizeString(s, maxLength);
      return format("\"%s\"", s);
    }
    if (value instanceof Throwable) {
      Throwable throwable = (Throwable) value;
      String simpleName = summarizeString(throwable.getClass().getSimpleName() + ":", maxLength);
      return simpleName +
          (simpleName.length() < Math.max(12, maxLength) ?
              formatObject(throwable.getMessage(), toNextEven(Math.max(12, maxLength - simpleName.length()))) :
              "");
    }
    if (isToStringOverridden(value))
      return summarizeString(
          value.toString(),
          maxLength + 2 /* 2 for margin for single quotes not necessary for non-strings */
      );
    return value.toString().substring(value.getClass().getPackage().getName().length() + 1);
  }

  public static String explainValue(Object value) {
    StringBuilder b = new StringBuilder();
    if (value instanceof Collection) {
      for (Object each : (Collection<?>) value) {
        explainValue(b, 0, each);
      }
    } else {
      explainValue(b, 0, value);
    }
    return b.toString().trim();
  }

  private static void explainValue(StringBuilder buffer, int level, Object value) {
    if (value instanceof Collection) {
      if (((Collection<?>) value).isEmpty())
        explainValue(buffer, level, "[]");
      else {
        for (Object each : (Collection<?>) value)
          explainValue(buffer, level + 1, each);
      }
    } else {
      buffer.append(String.format("%s%s%n", spaces(level * 2), value));
    }
  }

  private static String spaces(int spaces) {
    if (spaces <= 0)
      return "";
    return String.format("%-" + (spaces) + "s", "");
  }

  private static int toNextEven(int value) {
    if ((value & 1) == 0)
      return value;
    return value + 1;
  }

  private static String summarizeString(String s, int length) {
    assert (length & 1) == 0 : "The length must be an even int, but was <" + length + ">";
    assert length >= 12 : "The length must be greater than or equal to 12. Less than 20 is not recommended. But was <" + length + ">";
    if (s.length() > length) {
      int pre = length / 2 - 2;
      int post = length / 2 - 5;
      s = s.substring(0, length - pre) + "..." + s.substring(s.length() - post);
    }
    return s;
  }

  public static int summarizedStringLength() {
    return Validator.instance().configuration().summarizedStringLength();
  }

  private static boolean isToStringOverridden(Object object) {
    return getMethod(object.getClass(), "toString").getDeclaringClass() != Object.class;
  }

  /**
   * A method to check if assertion is enabled or not.
   *
   * @param v A boolean value to test.
   * @return {@code true} - assertion failed with the given value {@code v} / {@code false} - otherwise.
   */
  public static boolean assertFailsWith(boolean v) {
    boolean ret = false;
    try {
      assert v;
    } catch (AssertionError e) {
      ret = true;
    }
    return ret;
  }

  @SuppressWarnings("unchecked")
  public static <T> T createInstanceFromClassName(Class<? super T> expectedClass, String requestedClassName, Object... args) {
    try {
      Class<?> loadedClass = Class.forName(requestedClassName);
      try {
        return (T) expectedClass.cast(loadedClass.getDeclaredConstructor(Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new)).newInstance(args));
      } catch (ClassCastException e) {
        throw executionFailure("The requested class:'" + requestedClassName +
                "' was found but not an instance of " + expectedClass.getCanonicalName() + ".: " +
                "It was '" + loadedClass.getCanonicalName() + "'.",
            e);
      } catch (NoSuchMethodException e) {
        throw executionFailure("Matching public constructor for " + Arrays.toString(args) + " was not found in " + requestedClassName, e);
      } catch (InvocationTargetException e) {
        throw executionFailure("Matching public constructor was found in " + requestedClassName + " but threw an exception", e.getCause());
      }
    } catch (InstantiationException | IllegalAccessException |
        ClassNotFoundException e) {
      throw executionFailure("The requested class was not found or not accessible.: " + requestedClassName, e);
    }
  }

  public static InternalException executionFailure(String message, Throwable cause) {
    throw executionFailure(Explanation.fromMessage(message), cause);
  }

  public static InternalException executionFailure(Explanation explanation, Throwable cause) {
    throw new InternalException(explanation.toString(), cause);
  }

  public static InternalException wrapIfNecessary(Throwable cause) {
    if (cause instanceof Error)
      throw (Error) cause;
    if (cause instanceof RuntimeException)
      throw (RuntimeException) cause;
    throw executionFailure(cause.getMessage(), cause);
  }

  public static List<? super Object> append(List<? super Object> list, Object p) {
    return unmodifiableList(new ArrayList<Object>(list) {{
      add(p);
    }});
  }

  @SuppressWarnings("unchecked")
  public static <T> Evaluable<T> toEvaluableIfNecessary(Predicate<? super T> p) {
    requireNonNull(p);
    if (p instanceof Evaluable)
      return (Evaluable<T>) p;
    // We know that Printable.predicate returns a PrintablePredicate object, which is an Evaluable.
    return (Evaluable<T>) Printables.predicate(p::toString, p);
  }

  public static <T> Evaluable<T> toEvaluableIfNecessary(Function<? super T, ?> f) {
    return toEvaluableWithFormatterIfNecessary(f, Object::toString);
  }

  @SuppressWarnings("unchecked")
  public static <T> Evaluable<T> toEvaluableWithFormatterIfNecessary(Function<? super T, ?> f, Function<Function<? super T, ?>, String> formatter) {
    requireNonNull(f);
    if (f instanceof Evaluable)
      return (Evaluable<T>) f;
    // We know that Printable.predicate returns a PrintableFunction object, which is an Evaluable.
    return (Evaluable<T>) Printables.function(() -> formatter.apply(f), f);
  }

  public static Class<?> wrapperClassOf(Class<?> clazz) {
    if (clazz == Integer.TYPE)
      return Integer.class;
    if (clazz == Long.TYPE)
      return Long.class;
    if (clazz == Boolean.TYPE)
      return Boolean.class;
    if (clazz == Byte.TYPE)
      return Byte.class;
    if (clazz == Character.TYPE)
      return Character.class;
    if (clazz == Float.TYPE)
      return Float.class;
    if (clazz == Double.TYPE)
      return Double.class;
    if (clazz == Short.TYPE)
      return Short.class;
    if (clazz == Void.TYPE)
      return Void.class;
    throw new IllegalArgumentException("Unsupported type:" + (clazz != null ? clazz.getName() : "null") + " was given.");
  }

  public static Method getMethod(Class<?> aClass, String methodName, Class<?>... parameterTypes) {
    try {
      return aClass.getMethod(methodName, parameterTypes);
    } catch (NoSuchMethodException e) {
      throw executionFailure(format("Requested method: %s(%s) was not found in %s", methodName, Arrays.stream(parameterTypes).map(Class::getName).collect(joining(",")), aClass.getName()), e);
    }
  }

  @SuppressWarnings("unchecked")
  public static <T> Predicate<? super T> dummyPredicate() {
    return (Predicate<? super T>) DUMMY_PREDICATE;
  }

  @SuppressWarnings("unchecked")
  public static <T, R> Function<T, R> dummyFunction() {
    return (Function<T, R>) DUMMY_FUNCTION;
  }

  public static boolean isDummyFunction(Function<?, ?> function) {
    return function == DUMMY_FUNCTION;
  }

  public static Object toNonStringObject(String s) {
    return new Object() {
      @Override
      public String toString() {
        return s;
      }
    };
  }

  public static String indent(int level) {
    return level == 0 ?
        "" :
        format("%" + (level * 2) + "s", "");
  }

  public static String newLine() {
    return format("%n");
  }

  /**
   * Marks "trivial" a given function.
   * A predicate marked trivial will not appear in an execution report.
   *
   * @param predicate A predicate to be marked.
   * @param <T>      Input type of the function.
   * @return A predicate marked trivial.
   */
  public static <T> Predicate<T> makeSquashable(Predicate<T> predicate) {
    return ((PrintablePredicate<T>) predicate).makeTrivial();
  }

  /**
   * Marks "trivial" given function.
   * A function marked trivial will not appear in an execution report.
   *
   * @param function A function to marked.
   * @param <T>      Input type of the function.
   * @param <R>      Output type of the function.
   * @return A function marked trivial.
   */
  public static <T, R> Function<T, R> makeSquashable(Function<T, R> function) {
    return ((PrintableFunction<T, R>) function).makeTrivial();
  }

  public static <T> Function<T, T> trivialIdentityFunction() {
    return Functions.identity();
  }
}