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

net.hamnaberg.json.codec.reflection.ReflectionCodec Maven / Gradle / Ivy

package net.hamnaberg.json.codec.reflection;

import io.vavr.collection.List;
import io.vavr.control.Option;
import net.hamnaberg.json.codec.Codecs;
import net.hamnaberg.json.codec.DecodeResult;
import net.hamnaberg.json.Json;
import net.hamnaberg.json.codec.JsonCodec;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.util.*;
import java.util.function.Predicate;

public final class ReflectionCodec implements JsonCodec {
    private final Class type;
    private final Map> codecs;
    private final List fields;
    private final Factory factory;

    private static Map, JsonCodec> defaultCodecs;

    static {
        Map, JsonCodec> codecs = new HashMap<>();
        codecs.put(String.class, Codecs.CString);
        codecs.put(Integer.class, Codecs.CInt);
        codecs.put(int.class, Codecs.CInt);
        codecs.put(double.class, Codecs.CDouble);
        codecs.put(long.class, Codecs.CLong);
        codecs.put(Long.class, Codecs.CLong);
        codecs.put(Double.class, Codecs.CDouble);
        codecs.put(BigInteger.class, Codecs.CNumber);
        codecs.put(Number.class, Codecs.CNumber);
        codecs.put(Boolean.class, Codecs.CBoolean);
        defaultCodecs = Collections.unmodifiableMap(codecs);
    }

    public ReflectionCodec(Class type) {
        this(type, Collections.emptyMap());
    }

    public ReflectionCodec(Class type, Map> codecs) {
        this(type, codecs, (p) -> true, Option.none());
    }

    public ReflectionCodec(Class type, Map> codecs, Predicate predicate) {
        this(type, codecs, predicate, Option.none());
    }

    public ReflectionCodec(Class type, Map> codecs, Predicate predicate, Option factoryMethod) {
        this.type = type;
        this.codecs = codecs;
        this.fields = getFields(type).filter(predicate);
        this.factory = factoryMethod.map(n -> Factory.factory(type, n, fields)).getOrElse(Factory.constructor(type, fields));
    }

    @Override
    public Json.JValue toJson(A value) {
        Map map = new LinkedHashMap<>();
        for (Param field : fields) {
            Option> codec = getCodec(field);
            Option optValue = field.get(value);
            Option opt = optValue.flatMap(o -> codec.map(c -> c.toJson(o)));
            opt.forEach(v -> map.put(field.getName(), v));
        }
        return map.isEmpty() ? Json.jNull() : Json.jObject(map);
    }

    @Override
    public DecodeResult fromJson(Json.JValue value) {
        Json.JObject object = value.asJsonObjectOrEmpty();
        ArrayList arguments = new ArrayList<>();

        for (Param field : fields) {
            JsonCodec codec = getCodec(field).getOrElseThrow(() -> {
                throw new NoSuchElementException("Missing codec for " + field.getName());
            });
            DecodeResult result = DecodeResult.decode(object, field.getName(), codec);
            result.forEach(arguments::add);
        }

        try {
            return DecodeResult.ok(this.factory.invoke(List.ofAll(arguments)));
        } catch (Exception e) {
            return DecodeResult.fail(e.getMessage());
        }
    }

    @SuppressWarnings("unchecked")
    private Option> getCodec(Param field) {
        return Option.of(
                (JsonCodec) codecs.getOrDefault(field.getName(), defaultCodecs.get(field.getType()))
        );
    }


    @Override
    public String toString() {
        return String.format("ReflectionCodec(%s)", type.getName());
    }

    private static String getterOf(String field) {
        return "get" + Character.toUpperCase(field.charAt(0)) + field.substring(1);
    }

    private static  List getFields(Class type) {
        Field[] declared = type.getDeclaredFields();
        ArrayList fields = new ArrayList<>(declared.length);
        for (Field field : declared) {
            int modifiers = field.getModifiers();
            if (Modifier.isFinal(modifiers) && Modifier.isPublic(modifiers)) {
                fields.add(new FieldParam(field.getName(), field));
            }
        }
        if (fields.isEmpty()) {
            for (Field field : declared) {
                try {
                    Method method = type.getDeclaredMethod(getterOf(field.getName()));
                    if (method != null) {
                        fields.add(new MethodParam(field.getName(), method));
                    }
                } catch (NoSuchMethodException ignore) {
                }
            }
        }
        return List.ofAll(fields);
    }
}