com.comphenix.protocol.reflect.FuzzyReflection.java Source code

Java tutorial

Introduction

Here is the source code for com.comphenix.protocol.reflect.FuzzyReflection.java

Source

/*
 *  ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
 *  Copyright (C) 2012 Kristian S. Stangeland
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the
 *  GNU General Public License as published by the Free Software Foundation; either version 2 of
 *  the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with this program;
 *  if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
 *  02111-1307 USA
 */

package com.comphenix.protocol.reflect;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.commons.lang.Validate;

import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.fuzzy.AbstractFuzzyMatcher;
import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

/**
 * Retrieves fields and methods by signature, not just name.
 * 
 * @author Kristian
 */
public class FuzzyReflection {
    // The class we're actually representing
    private Class<?> source;

    // Whether or not to lookup private members
    private boolean forceAccess;

    public FuzzyReflection(Class<?> source, boolean forceAccess) {
        this.source = source;
        this.forceAccess = forceAccess;
    }

    /**
     * Retrieves a fuzzy reflection instance from a given class.
     * @param source - the class we'll use.
     * @return A fuzzy reflection instance.
     */
    public static FuzzyReflection fromClass(Class<?> source) {
        return fromClass(source, false);
    }

    /**
     * Retrieves a fuzzy reflection instance from a given class.
     * @param source - the class we'll use.
     * @param forceAccess - whether or not to override scope restrictions.
     * @return A fuzzy reflection instance.
     */
    public static FuzzyReflection fromClass(Class<?> source, boolean forceAccess) {
        return new FuzzyReflection(source, forceAccess);
    }

    /**
     * Retrieves a fuzzy reflection instance from an object.
     * @param reference - the object we'll use.
     * @return A fuzzy reflection instance that uses the class of the given object.
     */
    public static FuzzyReflection fromObject(Object reference) {
        return new FuzzyReflection(reference.getClass(), false);
    }

    /**
     * Retrieves a fuzzy reflection instance from an object.
     * @param reference - the object we'll use.
     * @param forceAccess - whether or not to override scope restrictions.
     * @return A fuzzy reflection instance that uses the class of the given object.
     */
    public static FuzzyReflection fromObject(Object reference, boolean forceAccess) {
        return new FuzzyReflection(reference.getClass(), forceAccess);
    }

    /**
     * Retrieve the value of the first field of the given type.
     * @param <T> Type
     * @param instance - the instance to retrieve from.
     * @param fieldClass - type of the field to retrieve.
     * @param forceAccess - whether or not to look for private and protected fields.
     * @return The value of that field.
     * @throws IllegalArgumentException If the field cannot be found.
     */
    public static <T> T getFieldValue(Object instance, Class<T> fieldClass, boolean forceAccess) {
        @SuppressWarnings("unchecked")
        T result = (T) Accessors.getFieldAccessor(instance.getClass(), fieldClass, forceAccess).get(instance);
        return result;
    }

    /**
     * Retrieves the underlying class.
     * @return The underlying class.
     */
    public Class<?> getSource() {
        return source;
    }

    /**
     * Retrieve the singleton instance of a class, from a method or field.
     * @return The singleton instance.
     * @throws IllegalStateException If the class has no singleton.
     */
    public Object getSingleton() {
        Method method = null;
        Field field = null;

        try {
            method = getMethod(FuzzyMethodContract.newBuilder().parameterCount(0).returnDerivedOf(source)
                    .requireModifier(Modifier.STATIC).build());
        } catch (IllegalArgumentException e) {
            // Try getting the field instead
            // Note that this will throw an exception if not found
            field = getFieldByType("instance", source);
        }

        // Convert into unchecked exceptions
        if (method != null) {
            try {
                method.setAccessible(true);
                return method.invoke(null);
            } catch (Exception e) {
                throw new RuntimeException("Cannot invoke singleton method " + method, e);
            }
        }
        if (field != null) {
            try {
                field.setAccessible(true);
                return field.get(null);
            } catch (Exception e) {
                throw new IllegalArgumentException("Cannot get content of singleton field " + field, e);
            }
        }
        // We should never get to this point
        throw new IllegalStateException("Impossible.");
    }

    /**
     * Retrieve the first method that matches.
     * <p>
     * ForceAccess must be TRUE in order for this method to access private, protected and package level method.
     * @param matcher - the matcher to use.
     * @return The first method that satisfies the given matcher.
     * @throws IllegalArgumentException If the method cannot be found.
     */
    public Method getMethod(AbstractFuzzyMatcher<MethodInfo> matcher) {
        List<Method> result = getMethodList(matcher);

        if (result.size() > 0) {
            return result.get(0);
        } else {
            throw new IllegalArgumentException("Unable to find a method that matches " + matcher);
        }
    }

    /**
     * Retrieve a method that matches. If there are multiple methods that match, the first one with the preferred
     * name is selected.
     * <p>
     * ForceAccess must be TRUE in order for this method to access private, protected and package level method.
     * @param matcher - the matcher to use.
     * @param preferred - the preferred name.
     * @return The first method that satisfies the given matcher.
     * @throws IllegalArgumentException If the method cannot be found.
     */
    public Method getMethod(AbstractFuzzyMatcher<MethodInfo> matcher, String preferred) {
        List<Method> result = getMethodList(matcher);

        if (result.size() > 1) {
            for (Method method : result) {
                if (method.getName().equals(preferred)) {
                    return method;
                }
            }
        }

        if (result.size() > 0) {
            return result.get(0);
        } else {
            throw new IllegalArgumentException("Unable to find a method that matches " + matcher);
        }
    }

    /**
     * Retrieve a list of every method that matches the given matcher.
     * <p>
     * ForceAccess must be TRUE in order for this method to access private, protected and package level methods.
     * @param matcher - the matcher to apply.
     * @return List of found methods.
     */
    public List<Method> getMethodList(AbstractFuzzyMatcher<MethodInfo> matcher) {
        List<Method> methods = Lists.newArrayList();

        // Add all matching fields to the list
        for (Method method : getMethods()) {
            if (matcher.isMatch(MethodInfo.fromMethod(method), source)) {
                methods.add(method);
            }
        }
        return methods;
    }

    /**
     * Retrieves a method by looking at its name.
     * @param nameRegex -  regular expression that will match method names.
     * @return The first method that satisfies the regular expression.
     * @throws IllegalArgumentException If the method cannot be found.
     */
    public Method getMethodByName(String nameRegex) {
        Pattern match = Pattern.compile(nameRegex);

        for (Method method : getMethods()) {
            if (match.matcher(method.getName()).matches()) {
                // Right - this is probably it.
                return method;
            }
        }

        throw new IllegalArgumentException(
                "Unable to find a method with the pattern " + nameRegex + " in " + source.getName());
    }

    /**
     * Retrieves a method by looking at the parameter types only.
     * @param name - potential name of the method. Only used by the error mechanism.
     * @param args - parameter types of the method to find.
     * @return The first method that satisfies the parameter types.
     * @throws IllegalArgumentException If the method cannot be found.
     */
    public Method getMethodByParameters(String name, Class<?>... args) {
        // Find the correct method to call
        for (Method method : getMethods()) {
            if (Arrays.equals(method.getParameterTypes(), args)) {
                return method;
            }
        }

        // That sucks
        throw new IllegalArgumentException("Unable to find " + name + " in " + source.getName());
    }

    /**
     * Retrieves a method by looking at the parameter types and return type only.
     * @param name - potential name of the method. Only used by the error mechanism.
     * @param returnType - return type of the method to find.
     * @param args - parameter types of the method to find.
     * @return The first method that satisfies the parameter types.
     * @throws IllegalArgumentException If the method cannot be found.
     */
    public Method getMethodByParameters(String name, Class<?> returnType, Class<?>[] args) {
        // Find the correct method to call
        List<Method> methods = getMethodListByParameters(returnType, args);

        if (methods.size() > 0) {
            return methods.get(0);
        } else {
            // That sucks
            throw new IllegalArgumentException("Unable to find " + name + " in " + source.getName());
        }
    }

    /**
     * Retrieves a method by looking at the parameter types and return type only.
     * @param name - potential name of the method. Only used by the error mechanism.
     * @param returnTypeRegex - regular expression matching the return type of the method to find.
     * @param argsRegex - regular expressions of the matching parameter types.
     * @return The first method that satisfies the parameter types.
     * @throws IllegalArgumentException If the method cannot be found.
     */
    public Method getMethodByParameters(String name, String returnTypeRegex, String[] argsRegex) {
        Pattern match = Pattern.compile(returnTypeRegex);
        Pattern[] argMatch = new Pattern[argsRegex.length];

        for (int i = 0; i < argsRegex.length; i++) {
            argMatch[i] = Pattern.compile(argsRegex[i]);
        }

        // Find the correct method to call
        for (Method method : getMethods()) {
            if (match.matcher(method.getReturnType().getName()).matches()) {
                if (matchParameters(argMatch, method.getParameterTypes()))
                    return method;
            }
        }

        // That sucks
        throw new IllegalArgumentException("Unable to find " + name + " in " + source.getName());
    }

    /**
     * Invoke a method by return type and parameters alone.
     * <p>
     * The parameters must be non-null for this to work.
     * @param target - the instance.
     * @param name - the name of the method - for debugging.
     * @param returnType - the expected return type.
     * @param parameters - the parameters.
     * @return The return value, or NULL.
     */
    public Object invokeMethod(Object target, String name, Class<?> returnType, Object... parameters) {
        Class<?>[] types = new Class<?>[parameters.length];

        for (int i = 0; i < types.length; i++) {
            types[i] = parameters[i].getClass();
        }
        return Accessors.getMethodAccessor(getMethodByParameters(name, returnType, types)).invoke(target,
                parameters);
    }

    private boolean matchParameters(Pattern[] parameterMatchers, Class<?>[] argTypes) {
        if (parameterMatchers.length != argTypes.length)
            throw new IllegalArgumentException("Arrays must have the same cardinality.");

        // Check types against the regular expressions
        for (int i = 0; i < argTypes.length; i++) {
            if (!parameterMatchers[i].matcher(argTypes[i].getName()).matches())
                return false;
        }

        return true;
    }

    /**
     * Retrieves every method that has the given parameter types and return type.
     * @param returnType - return type of the method to find.
     * @param args - parameter types of the method to find.
     * @return Every method that satisfies the given constraints.
     */
    public List<Method> getMethodListByParameters(Class<?> returnType, Class<?>[] args) {
        List<Method> methods = new ArrayList<Method>();

        // Find the correct method to call
        for (Method method : getMethods()) {
            if (method.getReturnType().equals(returnType) && Arrays.equals(method.getParameterTypes(), args)) {
                methods.add(method);
            }
        }
        return methods;
    }

    /**
     * Retrieves a field by name.
     * @param nameRegex - regular expression that will match a field name.
     * @return The first field to match the given expression.
     * @throws IllegalArgumentException If the field cannot be found.
     */
    public Field getFieldByName(String nameRegex) {
        Pattern match = Pattern.compile(nameRegex);

        for (Field field : getFields()) {
            if (match.matcher(field.getName()).matches()) {
                // Right - this is probably it.
                return field;
            }
        }

        // Looks like we're outdated. Too bad.
        throw new IllegalArgumentException(
                "Unable to find a field with the pattern " + nameRegex + " in " + source.getName());
    }

    /**
     * Retrieves the first field with a type equal to or more specific to the given type.
     * @param name - name the field probably is given. This will only be used in the error message.
     * @param type - type of the field to find.
     * @return The first field with a type that is an instance of the given type.
     */
    public Field getFieldByType(String name, Class<?> type) {
        List<Field> fields = getFieldListByType(type);

        if (fields.size() > 0) {
            return fields.get(0);
        } else {
            // Looks like we're outdated. Too bad.
            throw new IllegalArgumentException(String.format("Unable to find a field %s with the type %s in %s",
                    name, type.getName(), source.getName()));
        }
    }

    /**
     * Retrieves every field with a type equal to or more specific to the given type.
     * @param type - type of the fields to find.
     * @return Every field with a type that is an instance of the given type.
     */
    public List<Field> getFieldListByType(Class<?> type) {
        List<Field> fields = new ArrayList<Field>();

        // Field with a compatible type
        for (Field field : getFields()) {
            // A assignable from B -> B instanceOf A
            if (type.isAssignableFrom(field.getType())) {
                fields.add(field);
            }
        }

        return fields;
    }

    /**
     * Retrieve the first field that matches.
     * <p>
     * ForceAccess must be TRUE in order for this method to access private, protected and package level fields.
     * @param matcher - the matcher to use.
     * @return The first method that satisfies the given matcher.
     * @throws IllegalArgumentException If the method cannot be found.
     */
    public Field getField(AbstractFuzzyMatcher<Field> matcher) {
        List<Field> result = getFieldList(matcher);

        if (result.size() > 0)
            return result.get(0);
        else
            throw new IllegalArgumentException("Unable to find a field that matches " + matcher);
    }

    /**
     * Retrieve a list of every field that matches the given matcher.
     * <p>
     * ForceAccess must be TRUE in order for this method to access private, protected and package level fields.
     * @param matcher - the matcher to apply.
     * @return List of found fields.
     */
    public List<Field> getFieldList(AbstractFuzzyMatcher<Field> matcher) {
        List<Field> fields = Lists.newArrayList();

        // Add all matching fields to the list
        for (Field field : getFields()) {
            if (matcher.isMatch(field, source)) {
                fields.add(field);
            }
        }
        return fields;
    }

    /**
     * Retrieves a field by type.
     * <p>
     * Note that the type is matched using the full canonical representation, i.e.:
     * <ul>
     *     <li>java.util.List</li>
     *     <li>net.comphenix.xp.ExperienceMod</li>
     * </ul>
     * @param typeRegex - regular expression that will match the field type.
     * @return The first field with a type that matches the given regular expression.
     * @throws IllegalArgumentException If the field cannot be found.
     */
    public Field getFieldByType(String typeRegex) {

        Pattern match = Pattern.compile(typeRegex);

        // Like above, only here we test the field type
        for (Field field : getFields()) {
            String name = field.getType().getName();

            if (match.matcher(name).matches()) {
                return field;
            }
        }

        // Looks like we're outdated. Too bad.
        throw new IllegalArgumentException(
                "Unable to find a field with the type " + typeRegex + " in " + source.getName());
    }

    /**
     * Retrieves a field by type.
     * <p>
     * Note that the type is matched using the full canonical representation, i.e.:
     * <ul>
     *     <li>java.util.List</li>
     *     <li>net.comphenix.xp.ExperienceMod</li>
     * </ul>
     * @param typeRegex - regular expression that will match the field type.
     * @param ignored - types to ignore.
     * @return The first field with a type that matches the given regular expression.
     * @throws IllegalArgumentException If the field cannot be found.
     */
    @SuppressWarnings("rawtypes")
    public Field getFieldByType(String typeRegex, Set<Class> ignored) {

        Pattern match = Pattern.compile(typeRegex);

        // Like above, only here we test the field type
        for (Field field : getFields()) {
            Class type = field.getType();

            if (!ignored.contains(type) && match.matcher(type.getName()).matches()) {
                return field;
            }
        }

        // Looks like we're outdated. Too bad.
        throw new IllegalArgumentException(
                "Unable to find a field with the type " + typeRegex + " in " + source.getName());
    }

    /**
     * Retrieve the first constructor that matches.
     * <p>
     * ForceAccess must be TRUE in order for this method to access private, protected and package level constructors.
     * @param matcher - the matcher to use.
     * @return The first constructor that satisfies the given matcher.
     * @throws IllegalArgumentException If the constructor cannot be found.
     */
    public Constructor<?> getConstructor(AbstractFuzzyMatcher<MethodInfo> matcher) {
        List<Constructor<?>> result = getConstructorList(matcher);

        if (result.size() > 0)
            return result.get(0);
        else
            throw new IllegalArgumentException("Unable to find a method that matches " + matcher);
    }

    /**
     * Retrieve every method as a map over names.
     * <p>
     * Note that overloaded methods will only occur once in the resulting map.
     * @param methods - every method.
     * @return A map over every given method.
     */
    public Map<String, Method> getMappedMethods(List<Method> methods) {
        Map<String, Method> map = Maps.newHashMap();

        for (Method method : methods) {
            map.put(method.getName(), method);
        }
        return map;
    }

    /**
     * Retrieve a list of every constructor that matches the given matcher.
     * <p>
     * ForceAccess must be TRUE in order for this method to access private, protected and package level constructors.
     * @param matcher - the matcher to apply.
     * @return List of found constructors.
     */
    public List<Constructor<?>> getConstructorList(AbstractFuzzyMatcher<MethodInfo> matcher) {
        List<Constructor<?>> constructors = Lists.newArrayList();

        // Add all matching fields to the list
        for (Constructor<?> constructor : getConstructors()) {
            if (matcher.isMatch(MethodInfo.fromConstructor(constructor), source)) {
                constructors.add(constructor);
            }
        }
        return constructors;
    }

    /**
     * Retrieves all private and public fields in declared order (after JDK 1.5).
     * <p>
     * Private, protected and package fields are ignored if forceAccess is FALSE.
     * @return Every field.
     */
    public Set<Field> getFields() {
        Validate.notNull(source, "source cannot be null!");

        // We will only consider private fields in the declared class
        if (forceAccess)
            return setUnion(source.getDeclaredFields(), source.getFields());
        else
            return setUnion(source.getFields());
    }

    /**
     * Retrieves all private and public fields, up until a certain superclass.
     * @param excludeClass - the class (and its superclasses) to exclude from the search.
     * @return Every such declared field.
     */
    public Set<Field> getDeclaredFields(Class<?> excludeClass) {
        if (forceAccess) {
            Class<?> current = source;
            Set<Field> fields = Sets.newLinkedHashSet();

            while (current != null && current != excludeClass) {
                fields.addAll(Arrays.asList(current.getDeclaredFields()));
                current = current.getSuperclass();
            }
            return fields;
        }
        return getFields();
    }

    /**
     * Retrieves all private and public methods in declared order (after JDK 1.5).
     * <p>
     * Private, protected and package methods are ignored if forceAccess is FALSE.
     * @return Every method.
     */
    public Set<Method> getMethods() {
        // We will only consider private methods in the declared class
        if (forceAccess)
            return setUnion(source.getDeclaredMethods(), source.getMethods());
        else
            return setUnion(source.getMethods());
    }

    /**
     * Retrieves all private and public constructors in declared order (after JDK 1.5).
     * <p>
     * Private, protected and package constructors are ignored if forceAccess is FALSE.
     * @return Every constructor.
     */
    public Set<Constructor<?>> getConstructors() {
        if (forceAccess)
            return setUnion(source.getDeclaredConstructors());
        else
            return setUnion(source.getConstructors());
    }

    // Prevent duplicate fields

    @SafeVarargs
    private static <T> Set<T> setUnion(T[]... array) {
        Set<T> result = new LinkedHashSet<T>();

        for (T[] elements : array) {
            for (T element : elements) {
                result.add(element);
            }
        }
        return result;
    }

    /**
     * Retrieves whether or not not to override any scope restrictions.
     * @return TRUE if we override scope, FALSE otherwise.
     */
    public boolean isForceAccess() {
        return forceAccess;
    }

    /**
     * Sets whether or not not to override any scope restrictions.
     * @param forceAccess - TRUE if we override scope, FALSE otherwise.
     */
    public void setForceAccess(boolean forceAccess) {
        this.forceAccess = forceAccess;
    }
}