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

com.github.rschmitt.dynamicobject.DynamicObjectInvocationHandler Maven / Gradle / Ivy

package com.github.rschmitt.dynamicobject;

import clojure.lang.AFn;

import java.io.StringWriter;
import java.io.Writer;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import static com.github.rschmitt.dynamicobject.ClojureStuff.*;

class DynamicObjectInvocationHandler> implements InvocationHandler {
    private static final Object DEFAULT = new Object();
    private static final Object NULL = new Object();

    private final Object map;
    private final Class type;
    private final Constructor lookupConstructor;
    private final ConcurrentHashMap valueCache = new ConcurrentHashMap();

    DynamicObjectInvocationHandler(Object map, Class type, Constructor lookupConstructor) {
        this.map = map;
        this.type = type;
        this.lookupConstructor = lookupConstructor;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();

        if (isBuilderMethod(method)) {
            if (Reflection.isMetadataBuilder(method))
                return assocMeta(methodName, args[0]);
            String key = Reflection.getKeyNameForBuilder(method);
            return assoc(key, Conversions.javaToClojure(args[0]));
        }

        if (method.isDefault())
            return invokeDefaultMethod(proxy, method, args);

        switch (methodName) {
            case "getMap":
                return map;
            case "getType":
                return type;
            case "toString":
                return map.toString();
            case "hashCode":
                return map.hashCode();
            case "prettyPrint":
                PPRINT.invoke(map);
                return null;
            case "toFormattedString":
                Writer w = new StringWriter();
                PPRINT.invoke(map, w);
                return w.toString();
            case "merge":
                return merge((DynamicObject) args[0]);
            case "intersect":
                return union((DynamicObject) args[0]);
            case "subtract":
                return subtract((DynamicObject) args[0]);
            case "validate":
                validate();
                return null;
            case "equals":
                Object other = args[0];
                if (other instanceof DynamicObject)
                    return map.equals(((DynamicObject) other).getMap());
                else
                    return method.invoke(map, args);
            default:
                if (Reflection.isMetadataGetter(method))
                    return getMetadataFor(methodName);
                return getAndCacheValueFor(method);
        }
    }

    private Object union(DynamicObject arg) {
        return diff(arg, 2);
    }

    private Object subtract(DynamicObject arg) {
        return diff(arg, 0);
    }

    private Object diff(DynamicObject arg, int idx) {
        Object array = DIFF.invoke(map, arg.getMap());
        Object union = NTH.invoke(array, idx);
        if (union == null) union = EMPTY_MAP;
        union = Metadata.withTypeMetadata(union, type);
        return DynamicObject.wrap(union, type);
    }

    private T merge(DynamicObject other) {
        AFn ignoreNulls = new AFn() {
            public Object invoke(Object arg1, Object arg2) {
                return (arg2 == null) ? arg1 : arg2;
            }
        };
        Object mergedMap = MERGE_WITH.invoke(ignoreNulls, map, other.getMap());
        return DynamicObject.wrap(mergedMap, type);
    }

    private void validate() {
        Collection fields = Reflection.fieldGetters(type);
        Collection missingFields = new LinkedHashSet<>();
        Map> mismatchedFields = new HashMap<>();
        for (Method field : fields) {
            try {
                Object val = getAndCacheValueFor(field);
                if (Reflection.isRequired(field) && val == null)
                    missingFields.add(field);
                if (val != null) {
                    Class expectedType = Primitives.box(field.getReturnType());
                    Class actualType = val.getClass();
                    if (!expectedType.isAssignableFrom(actualType))
                        mismatchedFields.put(field, actualType);
                    if (val instanceof DynamicObject)
                        ((DynamicObject) val).validate();
                    else if (val instanceof List || val instanceof Set)
                        Validation.validateCollection((Collection) val, field.getGenericReturnType());
                    else if (val instanceof Map)
                        Validation.validateMap((Map) val, field.getGenericReturnType());
                }
            } catch (ClassCastException | AssertionError cce) {
                mismatchedFields.put(field, getRawValueFor(field).getClass());
            }
        }
        if (!missingFields.isEmpty() || !mismatchedFields.isEmpty())
            throw new IllegalStateException(Validation.getValidationErrorMessage(missingFields, mismatchedFields));
    }

    @SuppressWarnings("unchecked")
    private Object getAndCacheValueFor(Method method) {
        Object cachedValue = valueCache.getOrDefault(method, DEFAULT);
        if (cachedValue != DEFAULT) return cachedValue;
        if (cachedValue == NULL) return null;
        Object value = getValueFor(method);
        if (value == null)
            valueCache.putIfAbsent(method, NULL);
        else
            valueCache.putIfAbsent(method, value);
        return value;
    }

    private T assoc(String key, Object value) {
        if (value instanceof DynamicObject)
            value = ((DynamicObject) value).getMap();
        return DynamicObject.wrap(ASSOC.invoke(map, getMapKey(key), value), type);
    }

    private Object assocMeta(String key, Object value) {
        Object meta = META.invoke(map);
        if (meta == null)
            meta = EMPTY_MAP;
        meta = ASSOC.invoke(meta, key, value);
        return DynamicObject.wrap(WITH_META.invoke(map, meta), type);
    }

    private boolean isBuilderMethod(Method method) {
        return method.getReturnType().equals(type) && method.getParameterCount() == 1;
    }

    private Object getMetadataFor(String key) {
        Object meta = META.invoke(map);
        return GET.invoke(meta, key);
    }

    private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable {
        Class declaringClass = method.getDeclaringClass();
        int TRUSTED = -1;
        return lookupConstructor.newInstance(declaringClass, TRUSTED)
                .unreflectSpecial(method, declaringClass)
                .bindTo(proxy)
                .invokeWithArguments(args);
    }

    private Object getValueFor(Method method) {
        Object val = getRawValueFor(method);
        if (val == null) return null;
        Type genericReturnType = method.getGenericReturnType();
        return Conversions.clojureToJava(val, genericReturnType);
    }

    private Object getRawValueFor(Method method) {
        String keyName = Reflection.getKeyNameForGetter(method);
        Object keywordKey = getMapKey(keyName);
        return GET.invoke(map, keywordKey);
    }

    private static Object getMapKey(String keyName) {
        if (keyName.charAt(0) == ':')
            keyName = keyName.substring(1);
        return cachedRead(":" + keyName);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy