MethodCache.java

package Hoot.Runtime.Maps;

import java.util.*;
import java.lang.reflect.*;
import java.util.stream.Collectors;

import Hoot.Runtime.Faces.Valued;
import Hoot.Runtime.Names.Keyword;
import Hoot.Runtime.Names.Selector;
import Hoot.Runtime.Blocks.Enclosure;
import static Hoot.Runtime.Functions.Utils.*;

/**
 * Caches method references for a Java class.
 *
 * @author nik <nikboyd@sonic.net>
 * @see "Copyright 2010,2021 Nikolas S Boyd."
 * @see "Permission is granted to copy this work provided this copyright statement is retained in all copies."
 */
public class MethodCache implements Valued {

    public MethodCache(Class aClass) { cacheClass = aClass; }
    public Class<?> cachedType() { return this.cacheClass; }
    private final Class cacheClass;

    private boolean loaded = false;
    private final HashMap<String, Method> contents = emptyMap(Method.class);
    public void loadAllMethods() {
        if (loaded) return;
        Method[] methods = cachedType().getMethods();
        for (Method method : methods) {
            Class[] types = method.getParameterTypes();
            String methodName = method.getName();
            if (qualifiesForLoading(methodName, types)) {
                contents.put(methodName, method);
            }
        }
        loaded = true;
    }

    public HashSet<String> loadMethods() {
        HashSet<String> result = emptySet(String.class);
        Method[] methods = cachedType().getDeclaredMethods();
        for (Method method : methods) {
            Class[] types = method.getParameterTypes();
            String methodName = Keyword.from(method.getName(), types.length);
            if (qualifiesForLoading(methodName, types)) {
                contents.put(methodName, method);
                result.add(methodName);
            }
        }
        return result;
    }

    public void flush() {
        contents.clear();
        loaded = false;
    }

    public Set<Selector> allSelectors() {
        loadAllMethods();
        return contents.keySet().stream()
                .map(name -> Selector.named(name))
                .collect(Collectors.toSet());
    }

    public Set<Selector> selectors() {
        return mapSet(loadMethods(), name -> Selector.named(name)); }

    public final Method methodNamed(String selector, int argumentCount) {

        // if found previously, return cached method
        Method aMethod = contents.get(selector);
        if (aMethod != null) return aMethod;

        // check special selectors for argument classes
        Class arguments[] = specialSelector(selector);

        // determine method argument classes
        if (arguments == null) {
            if (selector.endsWith(IfAbsent)) {
                arguments = classesWithBlock(argumentCount, 1);
            }
            else {
                arguments = objectClasses(argumentCount);
            }
        }

        // find the method and cache its reference
        String methodName = Keyword.with(selector).methodName();
        try {
            aMethod = cachedType().getMethod(methodName, arguments);
            contents.put(selector, aMethod);
            return aMethod;
        }
        catch(NoSuchMethodException e) {
            error(format(MissingMethod, cachedType().getName(), selector, argumentCount));
            return null;
        }
    }

    public final Constructor constructorFor(int argumentCount) {
        Class argumentsTypes[] = objectClasses(argumentCount);
        try {
            return cachedType().getConstructor(argumentsTypes);
        }
        catch( NoSuchMethodException e ) {
            error(format(MissingConstruct, cachedType().getName(), argumentCount));
            return null;
        }
    }

    protected boolean qualifiesForLoading(String methodName, Class[] types) {
        return qualifiesAsSpecial(methodName, types)
            || qualifiesAsAbsentia(methodName, types)
            || qualifiesAsNormal(methodName, types); }

    static final HashMap<String, Class[]> SpecialSelectors = emptyMap(Class[].class);
    protected static Class[] specialSelector(String methodName) { return SpecialSelectors.get(methodName); }
    protected boolean qualifiesAsSpecial(String methodName, Class[] types) {
        Class[] arguments = specialSelector(methodName);
        if (arguments == null || types.length != arguments.length) return false;
        for(int t = 0; t < types.length; t++) if (types[t] != arguments[t]) return false;
        return true;
    }

    static final String IfAbsent = ":ifAbsent:";
    protected boolean qualifiesAsAbsentia(String methodName, Class[] types) {
        if (!methodName.endsWith(IfAbsent)) return false;

        Class[] arguments = classesWithBlock(types.length, 1);
        for(int i = 0; i < types.length; i++) {
            if (types[i] != arguments[i]) return false;
        }

        return true;
    }

    protected boolean qualifiesAsNormal(String methodName, Class[] types) {
        return matchAll(wrap(types), type -> type.equals(Object.class)); }

    static final int CacheSize = 11;
    static Class[][] ObjectClasses = new Class[CacheSize][];
    protected static Class[] objectClasses(int argumentCount) {
        return (argumentCount < CacheSize) ? ObjectClasses[argumentCount] : objectClassArray(argumentCount); }

    static Class[] BlockClasses = { Enclosure.class, Enclosure.class, Enclosure.class };
    protected static Class[] classesWithBlock(int methodArgumentCount, int blockArgumentCount) {
        Class result[] = objectClassArray(methodArgumentCount);
        result[methodArgumentCount - 1] = BlockClasses[blockArgumentCount];
        return result;
    }

    protected static Class[] objectClassArray(int argumentCount) {
        Class result[] = new Class[argumentCount];
        for(int i = 0; i < argumentCount; i++) result[i] = Object.class;
        return result;
    }

    static {
        SpecialSelectors.put("do:", classesWithBlock(1, 1));
        SpecialSelectors.put("to:do:", classesWithBlock(2, 1));
        SpecialSelectors.put("from:to:do:", classesWithBlock(3, 1));
        SpecialSelectors.put("to:by:do:", classesWithBlock(3, 1));

        SpecialSelectors.put("detect:", classesWithBlock(1, 1));
        SpecialSelectors.put("select:", classesWithBlock(1, 1));
        SpecialSelectors.put("reject:", classesWithBlock(1, 1));
        SpecialSelectors.put("collect:", classesWithBlock(1, 1));
        SpecialSelectors.put("inject:into:", classesWithBlock(2, 2));

        SpecialSelectors.put("ensure:", classesWithBlock(1, 0));
        SpecialSelectors.put("ifCurtailed:", classesWithBlock(1, 0));

        for(int i = 0; i < CacheSize; i++) {
            ObjectClasses[i] = objectClassArray(i);
        }
    }

    private static final String MissingMethod = "%s does not understand %s with %d argument(s)";
    private static final String MissingConstruct = "%s does not construct with %d argument(s)";
}