ProtocolLib/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java

688 lines
22 KiB
Java
Raw Normal View History

/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
2015-06-17 20:25:39 +02:00
* 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.
*
2015-06-17 20:25:39 +02:00
* 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.
*
2015-06-17 20:25:39 +02:00
* 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.*;
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;
2016-05-16 22:27:05 +02:00
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.
2015-06-17 20:25:39 +02:00
* @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.
2015-06-17 20:25:39 +02:00
* @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.
*/
2015-06-17 20:25:39 +02:00
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);
2016-03-20 18:35:59 +01:00
if (result.size() > 0) {
return result.get(0);
2016-03-20 18:35:59 +01:00
} else {
throw new IllegalArgumentException("Unable to find a method that matches " + matcher);
2016-03-20 18:35:59 +01:00
}
}
2016-03-20 18:35:59 +01:00
/**
* 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()) {
2015-06-17 20:25:39 +02:00
// Right - this is probably it.
return method;
}
}
2015-06-17 20:25:39 +02:00
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());
}
Significantly reduce the possibility of a race condition. The vanilla server bootstrap (ServerConnectionChannel) is executed asynchronously when a new channel object has been registered in an event loop, much before it is ready to accept a new client connection. It is here the whole channel handler pipeline is set up, along with a NetworkManager responsible for reading and writing packets. The trouble starts when the bootstrap class adds the created NetworkManager to a list (f) of managers in ServerConnection. This list is regularly inspected by the main thread (in order to process packets on the main thread) and includes a clean up procedure (ServerConnection#61) in case it detects a disconnected network manager. Unfortunately, the network manager IS considered disconnected for a moment when its added to the list, so the main thread MAY end up getting to the network manager before Netty has connected the channel. This is still very rare under normal circumstances, but because ProtocolLib does a lot of initial processing in the channel handler, the asynchronous thread gets hold up for a long while the first time a player connects to the server, allowing the main thread sufficient time to catch up and evict the network manager. The partial solution here is to synchronize on the network manager list, stopping the main thread from processing network managers when we are preparing our new connection. But I suspect the best solution would be to correct the root of the problem, and correct the race condition in the server itself. This could be done by only adding the network manager when the channel is active (see ChannelInboundHandler.channelActive).
2013-12-14 04:05:12 +01:00
/**
* 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()) {
2015-06-17 20:25:39 +02:00
// Right - this is probably it.
return field;
}
}
// Looks like we're outdated. Too bad.
2015-06-17 20:25:39 +02:00
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;
}
/**
* Retrieves a field with a given type and parameters. This is most useful
* when dealing with Collections.
*
* @param fieldType Type of the field
* @param params Variable length array of type parameters
* @return The field
*
* @throws IllegalArgumentException If the field cannot be found
*/
public Field getParameterizedField(Class<?> fieldType, Class<?>... params) {
for (Field field : getFields()) {
if (field.getType().equals(fieldType)) {
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
if (Arrays.equals(((ParameterizedType) type).getActualTypeArguments(), params))
return field;
}
}
}
throw new IllegalArgumentException("Unable to find a field with type " + fieldType + " and params " + Arrays.toString(params));
}
/**
* 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>
2015-06-17 20:25:39 +02:00
* 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.
2015-06-17 20:25:39 +02:00
throw new IllegalArgumentException("Unable to find a field with the type " +
typeRegex + " in " + source.getName());
}
/**
* Retrieves a field by type.
* <p>
2015-06-17 20:25:39 +02:00
* 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.
2015-06-17 20:25:39 +02:00
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);
}
/**
2015-06-17 20:25:39 +02:00
* 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() {
2016-05-16 22:27:05 +02:00
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;
}
}