ObjectUtils.java

package it.fulminazzo.yagl.utils;

import it.fulminazzo.fulmicollection.objects.Refl;
import it.fulminazzo.fulmicollection.structures.tuples.Tuple;
import it.fulminazzo.fulmicollection.utils.ReflectionUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

/**
 * The type Object utils.
 */
@SuppressWarnings("unchecked")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ObjectUtils {
    private static final String EMPTY_IDENTIFIER = "";
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss MM-dd-yyyy");

    /**
     * Prints the given object in a JSON format.
     * If the object (or an object contained in it) is "empty",
     * it will be printed as {@link #EMPTY_IDENTIFIER}.
     *
     * @param object the object
     * @return the output
     */
    public static String printAsJSON(@Nullable Object object) {
        if (object == null) return EMPTY_IDENTIFIER;
        else if (object instanceof Enum<?>) return ((Enum<?>) object).name();
        else if (object instanceof String) {
            String s = object.toString();
            if (s.isEmpty()) return EMPTY_IDENTIFIER;
            else return String.format("\"%s\"", s);
        } else if (object instanceof Number) {
            // If number is 0, to avoid pollution, it will be hidden.
            Number n = (Number) object;
            if (n.doubleValue() != 0) return n.toString();
            else return EMPTY_IDENTIFIER;
        } else if (ReflectionUtils.isPrimitiveOrWrapper(object.getClass())) return object.toString();
        else if (object instanceof Collection) {
            Collection<?> collection = (Collection<?>) object;
            String output = collection.stream().map(ObjectUtils::printAsJSON).collect(Collectors.joining(", "));
            if (output.matches("(, )*")) return EMPTY_IDENTIFIER;
            else return String.format("[%s]", output);
        } else if (object instanceof UUID) return object.toString();
        else if (object instanceof Date) return DATE_FORMAT.format((Date) object);
        else if (!(object instanceof Map)) {
            Map<Object, Object> map = new LinkedHashMap<>();
            Refl<?> refl = new Refl<>(object);
            for (final Field field : refl.getNonStaticFields()) {
                Object obj = refl.getFieldObject(field);
                map.put(field.getName(), obj);
            }
            object = map;
        }
        Map<?, ?> map = (Map<?, ?>) object;
        StringBuilder output = new StringBuilder();
        map.entrySet().stream()
                .map(e -> new Tuple<>(printAsJSON(e.getKey()), printAsJSON(e.getValue())))
                .filter(t -> !t.getKey().equals(EMPTY_IDENTIFIER) && !t.getValue().equals(EMPTY_IDENTIFIER))
                .forEach(t -> output.append(t.getKey()).append(": ").append(t.getValue()).append(", "));
        String result = output.toString();
        if (result.matches("(: , )*")) return EMPTY_IDENTIFIER;
        else return String.format("{%s}", result.substring(0, result.length() - 2));
    }

    /**
     * Copies the given object to a new one.
     *
     * @param <T> the type of the object to copy from
     * @param t   the object to copy from
     * @return the copy
     */
    public static <T> T copy(final @NotNull T t) {
        Class<? extends T> clazz = (Class<? extends T>) t.getClass();
        try {
            ReflectionUtils.getConstructor(clazz);
        } catch (Exception e) {
            // Get the most abstract class
            Class<?> c = clazz;
            Class<?>[] interfaces;
            while ((interfaces = c.getInterfaces()).length != 0) {
                Class<?> tmp = interfaces[0];
                if (!tmp.equals(Iterable.class) && !tmp.getSimpleName().startsWith("Abstract"))
                    c = tmp;
                else break;
            }
            clazz = ReflectionUtils.getClass(c.getCanonicalName() + "Impl");
        }
        return copy(t, clazz);
    }

    /**
     * Copies the given object to a new one using the provided class.
     * If an interface is provided, it tries to convert it to a non-abstract implementation by appending <i>Impl</i>.
     * If no such class is found, an {@link IllegalArgumentException} is thrown.
     *
     * @param <T>   the type of the object to copy from
     * @param <O>   the type of the returned object
     * @param t     the object to copy from
     * @param clazz the class to copy to
     * @return the copy
     */
    public static <T, O extends T> O copy(final @NotNull T t, @NotNull Class<O> clazz) {
        if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers()))
            try {
                clazz = ReflectionUtils.getClass(clazz.getCanonicalName() + "Impl");
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException(String.format("Could not copy object to abstract class '%s': no '%sImpl' class found",
                        clazz.getCanonicalName(), clazz.getCanonicalName()));
            }

        final Refl<O> object = new Refl<>(clazz, new Object[0]);
        for (final Field field : object.getNonStaticFields())
            try {
                ReflectionUtils.get(field, t).map(obj1 -> {
                    if (obj1 == null) return null;
                    else if (obj1 instanceof Collection) return copyCollection(obj1);
                    else if (obj1 instanceof Map) return copyMap(obj1);
                    else if (obj1.getClass().isArray()) return copyArray(obj1);
                    else return copyWithMethod(obj1);
                }).ifPresent(obj1 -> object.setFieldObject(field, obj1));
            } catch (IllegalArgumentException e) {
                if (!e.getMessage().contains("Can not set")) throw e;
            }
        return object.getObject();
    }

    private static @NotNull Object[] copyArray(final @NotNull Object obj1) {
        Object[] tmp = (Object[]) obj1;
        Object[] arr = (Object[]) Array.newInstance(obj1.getClass().getComponentType(), tmp.length);
        for (int i = 0; i < tmp.length; i++) arr[i] = copyWithMethod(tmp[i]);
        return arr;
    }

    private static @NotNull Map<Object, Object> copyMap(final @NotNull Object obj1) {
        Map<Object, Object> map = new HashMap<>();
        ((Map<Object, Object>) obj1).forEach((k, v) ->
                map.put(copyWithMethod(k), copyWithMethod(v)));
        return map;
    }

    private static @NotNull Collection<?> copyCollection(final @NotNull Object obj1) {
        Class<?> tmpClass = obj1.getClass();
        // In the case of creation with Arrays.asList()
        if (tmpClass.getCanonicalName().equals(Arrays.class.getCanonicalName() + ".ArrayList"))
            tmpClass = ArrayList.class;
        Class<Collection<Object>> finalClass = (Class<Collection<Object>>) tmpClass;
        return ((Collection<?>) obj1).stream()
                .map(ObjectUtils::copyWithMethod)
                .collect(Collectors.toCollection(() -> new Refl<>(finalClass, new Object[0]).getObject()));
    }

    private static @Nullable Object copyWithMethod(final @Nullable Object obj1) {
        try {
            if (obj1 == null) return null;
            Method copy = obj1.getClass().getDeclaredMethod("copy");
            return ReflectionUtils.setAccessible(copy)
                    .map(m -> m.invoke(obj1))
                    .orElseGet(obj1);
        } catch (NoSuchMethodException e) {
            return obj1;
        }
    }

}