Morph.java

package org.klojang.convert;

import org.klojang.check.Check;

import java.lang.reflect.Array;
import java.nio.charset.StandardCharsets;
import java.util.Collection;

import static org.klojang.check.CommonChecks.notNull;
import static org.klojang.util.ClassMethods.*;

/**
 * Performs a wide variety of type conversions.
 *
 * @param <T> The type to which incoming values will be converted
 * @author Ayco Holleman
 * @see NumberMethods#convert(Number, Class)
 * @see NumberMethods#parse(String, Class)
 * @see Bool
 * @see EnumParser
 */
@SuppressWarnings({"unchecked", "rawtypes"})
public class Morph<T> {

  /**
   * Converts the specified object to the specified type.
   *
   * @param <U> The target type
   * @param obj The value to convert
   * @param targetType The {@code Class} object corresponding to the target type
   * @return The converted value
   * @throws TypeConversionException If the conversion did not succeed
   */
  public static <U> U convert(Object obj, Class<U> targetType) {
    return new Morph<>(targetType).convert(obj);
  }

  private final Class<T> targetType;

  /**
   * Creates a new {@code Morph} instance that will convert values to the specified
   * type.
   *
   * @param targetType The type to which to convert values
   */
  public Morph(Class<T> targetType) {
    this.targetType = Check.notNull(targetType).ok();
  }

  /**
   * Converts the specified object into an instance of the type specified through the
   * constructor.
   *
   * @param obj The value to convert
   * @return An instance of the target type
   * @throws TypeConversionException If the conversion did not succeed
   */
  public T convert(Object obj) throws TypeConversionException {
    Class<T> toType = this.targetType;
    if (obj == null) {
      return getTypeDefault(toType);
    } else if (toType.isInstance(obj)) {
      return (T) obj;
    } else if (toType.isPrimitive() && isAutoUnboxedAs(obj.getClass(), toType)) {
      return (T) obj;
    } else if (toType == String.class) {
      if (obj instanceof byte[] bytes) {
        return (T) new String(bytes, StandardCharsets.UTF_8);
      } else if (obj instanceof char[] chars) {
        return (T) new String(chars);
      }
      return (T) obj.toString();
    } else if (toType.isArray()) {
      return MorphToArray.morph(obj, toType);
    } else if (isSubtype(toType, Collection.class)) {
      return MorphToCollection.morph(obj, toType);
    }
    Class myType = obj.getClass();
    if (myType.isArray()) {
      return Array.getLength(obj) == 0
          ? getTypeDefault(toType)
          : convert(Array.get(obj, 0), toType);
    } else if (isSubtype(myType, Collection.class)) {
      Collection coll = (Collection) obj;
      return coll.isEmpty()
          ? getTypeDefault(toType)
          : convert(coll.iterator().next(), toType);
    }
    Object out = MorphToNumber.morph(obj, toType);
    if (out != null) {
      return (T) out;
    } else if (toType.isEnum()) {
      return (T) MorphToEnum.morph(obj, toType);
    }
    throw new TypeConversionException(obj, toType);
  }

  static String stringify(Object obj) {
    return Check.that(obj.toString())
        .is(notNull(), "obj.toString() must not return null")
        .ok();
  }

}