Created
June 24, 2017 21:51
-
-
Save Alvin-LB/ac5626f861be7523a8c11338db3afb59 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.google.common.collect.Lists; | |
import com.google.common.collect.Maps; | |
import com.mojang.authlib.GameProfile; | |
import io.netty.channel.*; | |
import org.bukkit.Bukkit; | |
import org.bukkit.entity.Player; | |
import org.bukkit.event.EventHandler; | |
import org.bukkit.event.HandlerList; | |
import org.bukkit.event.Listener; | |
import org.bukkit.event.player.PlayerLoginEvent; | |
import org.bukkit.event.player.PlayerQuitEvent; | |
import org.bukkit.plugin.Plugin; | |
import org.bukkit.scheduler.BukkitRunnable; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.Method; | |
import java.util.*; | |
import java.util.stream.Collectors; | |
/** | |
* A packet listener using netty channel injection. | |
* | |
* Based off of TinyProtocol by dmulloy2: | |
* https://github.com/aadnk/ProtocolLib/blob/master/modules/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/ | |
* | |
* @author AlvinB | |
*/ | |
@SuppressWarnings({"SameParameterValue", "WeakerAccess", "SameReturnValue", "unchecked"}) | |
public abstract class PacketInterceptor implements Listener { | |
private static final Method GET_HANDLE = ReflectUtil.getMethod(ReflectUtil.getCBClass("entity.CraftPlayer").getOrThrow(), "getHandle").getOrThrow(); | |
private static final Field PLAYER_CONNECTION = ReflectUtil.getFieldByType(ReflectUtil.getNMSClass("EntityPlayer").getOrThrow(), ReflectUtil.getNMSClass("PlayerConnection").getOrThrow(), 0).getOrThrow(); | |
private static final Class<?> NETWORK_MANAGER_CLASS = ReflectUtil.getNMSClass("NetworkManager").getOrThrow(); | |
private static final Field NETWORK_MANAGER = ReflectUtil.getFieldByType(ReflectUtil.getNMSClass("PlayerConnection").getOrThrow(), NETWORK_MANAGER_CLASS, 0).getOrThrow(); | |
private static final Field CHANNEL = ReflectUtil.getFieldByType(NETWORK_MANAGER_CLASS, Channel.class, 0).getOrThrow(); | |
private static final Method GET_MINECRAFT_SERVER = ReflectUtil.getMethodByType(ReflectUtil.getCBClass("CraftServer").getOrThrow(), ReflectUtil.getNMSClass("MinecraftServer").getOrThrow(), 0).getOrThrow(); | |
private static final Class<?> SERVER_CONNECTION_CLASS = ReflectUtil.getNMSClass("ServerConnection").getOrThrow(); | |
private static final Field SERVER_CONNECTION = ReflectUtil.getDeclaredFieldByType(ReflectUtil.getNMSClass("MinecraftServer").getOrThrow(), SERVER_CONNECTION_CLASS, 0, true).getOrThrow(); | |
private static final Class<?> PACKET_LOGIN_START = ReflectUtil.getNMSClass("PacketLoginInStart").getOrThrow(); | |
private static final Method GET_GAME_PROFILE = ReflectUtil.getMethodByType(PACKET_LOGIN_START, GameProfile.class, 0).getOrThrow(); | |
private static Field NETWORK_MANAGERS = null; | |
private static Field CHANNEL_FUTURES = null; | |
private static int id = 0; | |
private final Set<String> packets; | |
private final boolean blackList; | |
private final Plugin plugin; | |
private final String handlerName; | |
private final List<Channel> serverChannels = Lists.newArrayList(); | |
// We have to store player names instead of UUIDs as PacketLoginInStart ignores UUIDs when in offline mode. Name changing should not be an issue, | |
// as the player is only kept in this list while they are online. | |
private final Map<String, Channel> injectedPlayerChannels = Maps.newHashMap(); | |
private ChannelInboundHandlerAdapter serverChannelHandler; | |
public PacketInterceptor(Plugin plugin) { | |
this(plugin, true); | |
} | |
public PacketInterceptor(Plugin plugin, String... packets) { | |
this(plugin, false, packets); | |
} | |
public PacketInterceptor(Plugin plugin, boolean blackList, String... packets) { | |
this.packets = Arrays.stream(packets).collect(Collectors.toSet()); | |
this.blackList = blackList; | |
this.plugin = plugin; | |
this.handlerName = "packet_interceptor_" + plugin.getName() + "_" + id++; | |
Bukkit.getPluginManager().registerEvents(this, plugin); | |
injectServer(); | |
for (Player player : Bukkit.getOnlinePlayers()) { | |
if (!injectedPlayerChannels.containsKey(player.getName())) { | |
injectPlayer(player); | |
} | |
} | |
} | |
@EventHandler | |
public void onPlayerJoin(PlayerLoginEvent e) { | |
injectPlayer(e.getPlayer()); | |
} | |
@EventHandler | |
public void onPlayerLeave(PlayerQuitEvent e) { | |
if (injectedPlayerChannels.containsKey(e.getPlayer().getName())) { | |
injectedPlayerChannels.remove(e.getPlayer().getName()); | |
} | |
} | |
private void injectServer() { | |
Object minecraftServer = ReflectUtil.invokeMethod(Bukkit.getServer(), GET_MINECRAFT_SERVER).getOrThrow(); | |
Object serverConnection = ReflectUtil.getFieldValue(minecraftServer, SERVER_CONNECTION).getOrThrow(); | |
for (int i = 0; NETWORK_MANAGERS == null || CHANNEL_FUTURES == null; i++) { | |
Field field = ReflectUtil.getDeclaredFieldByType(SERVER_CONNECTION_CLASS, List.class, i, true).getOrThrow(); | |
List<Object> list = (List<Object>) ReflectUtil.getFieldValue(serverConnection, field).getOrThrow(); | |
for (Object object : list) { | |
if (NETWORK_MANAGERS == null && NETWORK_MANAGER_CLASS.isInstance(object)) { | |
NETWORK_MANAGERS = field; | |
} | |
if (CHANNEL_FUTURES == null && ChannelFuture.class.isInstance(object)) { | |
CHANNEL_FUTURES = field; | |
} | |
} | |
if (CHANNEL_FUTURES != null && NETWORK_MANAGERS == null) { | |
NETWORK_MANAGERS = field; | |
} | |
} | |
List<Object> networkManagers = (List<Object>) ReflectUtil.getFieldValue(serverConnection, NETWORK_MANAGERS).getOrThrow(); | |
List<ChannelFuture> channelFutures = (List<ChannelFuture>) ReflectUtil.getFieldValue(serverConnection, CHANNEL_FUTURES).getOrThrow(); | |
ChannelInitializer<Channel> channelInitializer = new ChannelInitializer<Channel>() { | |
@Override | |
protected void initChannel(Channel channel) throws Exception { | |
try { | |
synchronized (networkManagers) { | |
channel.eventLoop().submit(() -> { | |
injectChannel(channel, null); | |
}); | |
} | |
} catch (Exception e) { | |
plugin.getLogger().severe("Failed to inject Channel " + channel + " due to " + e + "!"); | |
} | |
} | |
}; | |
ChannelInitializer<Channel> channelPreInitializer = new ChannelInitializer<Channel>() { | |
@Override | |
protected void initChannel(Channel channel) throws Exception { | |
channel.pipeline().addLast(channelInitializer); | |
} | |
}; | |
serverChannelHandler = new ChannelInboundHandlerAdapter() { | |
@Override | |
public void channelRead(ChannelHandlerContext context, Object message) throws Exception { | |
Channel channel = (Channel) message; | |
channel.pipeline().addFirst(channelPreInitializer); | |
context.fireChannelRead(message); | |
} | |
}; | |
for (ChannelFuture channelFuture : channelFutures) { | |
Channel channel = channelFuture.channel(); | |
serverChannels.add(channel); | |
channel.pipeline().addFirst(serverChannelHandler); | |
} | |
} | |
private void injectPlayer(Player player) { | |
Channel channel; | |
if (injectedPlayerChannels.containsKey(player.getName())) { | |
channel = injectedPlayerChannels.get(player.getName()); | |
} else { | |
Object handle = ReflectUtil.invokeMethod(player, GET_HANDLE).getOrThrow(); | |
Object playerConnection = ReflectUtil.getFieldValue(handle, PLAYER_CONNECTION).getOrThrow(); | |
if (playerConnection == null) { | |
plugin.getLogger().warning("Failed to inject Channel for player " + player.getName() + "!"); | |
return; | |
} | |
Object networkManager = ReflectUtil.getFieldValue(playerConnection, NETWORK_MANAGER).getOrThrow(); | |
channel = (Channel) ReflectUtil.getFieldValue(networkManager, CHANNEL).getOrThrow(); | |
} | |
injectChannel(channel, player); | |
if (!injectedPlayerChannels.containsKey(player.getName())) { | |
injectedPlayerChannels.put(player.getName(), channel); | |
} | |
} | |
private void injectChannel(Channel channel, Player player) { | |
ChannelInterceptor handler = (ChannelInterceptor) channel.pipeline().get(handlerName); | |
if (handler == null) { | |
handler = new ChannelInterceptor(); | |
channel.pipeline().addBefore("packet_handler", handlerName, handler); | |
} | |
if (player != null) { | |
handler.player = player; | |
} | |
} | |
public void close() { | |
for (Channel channel : injectedPlayerChannels.values()) { | |
try { | |
channel.eventLoop().execute(() -> channel.pipeline().remove(handlerName)); | |
} catch (NoSuchElementException ignored) { | |
} | |
} | |
injectedPlayerChannels.clear(); | |
for (Channel channel : serverChannels) { | |
channel.pipeline().remove(serverChannelHandler); | |
} | |
HandlerList.unregisterAll(this); | |
} | |
private boolean doSyncRead() { | |
try { | |
// Given that this will only execute on the subscribed packets, we can get away with a reflection call. | |
// It's also a relatively cheap reflection call. | |
return this.getClass().getMethod("packetReading", Player.class, Object.class, String.class).getDeclaringClass() != PacketInterceptor.class; | |
} catch (NoSuchMethodException e) { | |
// Should not happen | |
e.printStackTrace(); | |
return false; | |
} | |
} | |
private boolean doSyncWrite() { | |
try { | |
// Given that this will only execute on the subscribed packets, we can get away with a reflection call. | |
// It's also a relatively cheap reflection call. | |
return this.getClass().getMethod("packetSending", Player.class, Object.class, String.class).getDeclaringClass() != PacketInterceptor.class; | |
} catch (NoSuchMethodException e) { | |
// Should not happen | |
e.printStackTrace(); | |
return false; | |
} | |
} | |
public boolean packetSendingAsync(Player player, Object packet, String packetName) { | |
return true; | |
} | |
public boolean packetReadingAsync(Player player, Object packet, String packetName) { | |
return true; | |
} | |
public boolean packetSending(Player player, Object packet, String packetName) { | |
return true; | |
} | |
public boolean packetReading(Player player, Object packet, String packetName) { | |
return true; | |
} | |
private class ChannelInterceptor extends ChannelDuplexHandler { | |
private Player player; | |
@Override | |
public void write(ChannelHandlerContext context, Object message, ChannelPromise promise) throws Exception { | |
if ((blackList && packets.contains(message.getClass().getSimpleName())) || (!blackList && !packets.contains(message.getClass().getSimpleName()))) { | |
super.write(context, message, promise); | |
return; | |
} | |
if (doSyncWrite()) { | |
final boolean[] result = new boolean[2]; | |
new BukkitRunnable() { | |
@Override | |
public void run() { | |
try { | |
result[0] = packetSending(player, message, message.getClass().getSimpleName()); | |
} catch (Exception e) { | |
System.out.println("An error occurred while plugin " + plugin.getName() + " was handling packet " + message.getClass().getSimpleName() + "!"); | |
e.printStackTrace(); | |
result[0] = true; | |
} | |
result[1] = true; | |
synchronized (result) { | |
result.notifyAll(); | |
} | |
} | |
}.runTask(plugin); | |
synchronized (result) { | |
while (!result[1]) { | |
result.wait(); | |
} | |
} | |
if (result[0]) { | |
super.write(context, message, promise); | |
} | |
} else { | |
try { | |
if (packetSendingAsync(player, message, message.getClass().getSimpleName())) { | |
super.write(context, message, promise); | |
} | |
} catch (Exception e) { | |
System.out.println("An error occurred while plugin " + plugin.getName() + " was handling packet " + message.getClass().getSimpleName() + "!"); | |
e.printStackTrace(); | |
super.write(context, message, promise); | |
} | |
} | |
} | |
@Override | |
public void channelRead(ChannelHandlerContext context, Object message) throws Exception { | |
if (PACKET_LOGIN_START.isInstance(message)) { | |
injectedPlayerChannels.put(((GameProfile) ReflectUtil.invokeMethod(message, GET_GAME_PROFILE).getOrThrow()).getName(), context.channel()); | |
} | |
if ((blackList && packets.contains(message.getClass().getSimpleName())) || (!blackList && !packets.contains(message.getClass().getSimpleName()))) { | |
super.channelRead(context, message); | |
return; | |
} | |
if (doSyncRead()) { | |
final boolean[] result = new boolean[2]; | |
new BukkitRunnable() { | |
@Override | |
public void run() { | |
try { | |
result[0] = packetReading(player, message, message.getClass().getSimpleName()); | |
} catch (Exception e) { | |
System.out.println("An error occurred while plugin " + plugin.getName() + " was handling packet " + message.getClass().getSimpleName() + "!"); | |
e.printStackTrace(); | |
result[0] = true; | |
} | |
result[1] = true; | |
synchronized (result) { | |
result.notifyAll(); | |
} | |
} | |
}.runTask(plugin); | |
synchronized (result) { | |
while (!result[1]) { | |
result.wait(); | |
} | |
} | |
if (result[0]) { | |
super.channelRead(context, message); | |
} | |
} else { | |
try { | |
if (packetReadingAsync(player, message, message.getClass().getSimpleName())) { | |
super.channelRead(context, message); | |
} | |
} catch (Exception e) { | |
System.out.println("An error occurred while plugin " + plugin.getName() + " was handling packet " + message.getClass().getSimpleName() + "!"); | |
e.printStackTrace(); | |
super.channelRead(context, message); | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.google.common.base.Joiner; | |
import com.google.common.base.MoreObjects; | |
import com.google.common.collect.Lists; | |
import com.google.common.collect.Maps; | |
import com.google.common.collect.Multimap; | |
import org.apache.commons.lang.ArrayUtils; | |
import org.apache.commons.lang.Validate; | |
import org.apache.commons.lang3.ClassUtils; | |
import org.bukkit.Bukkit; | |
import java.lang.reflect.*; | |
import java.util.*; | |
import java.util.function.Predicate; | |
import java.util.stream.Collectors; | |
/** | |
* A utility class for performing various reflection operations. | |
* | |
* Partly inspired by I Al Istannen's ReflectionUtil: | |
* https://github.com/PerceiveDev/PerceiveCore/blob/master/Reflection/src/main/java/com/perceivedev/perceivecore/reflection/ReflectionUtil.java | |
* | |
* @author AlvinB | |
*/ | |
@SuppressWarnings({"SameParameterValue", "WeakerAccess", "unused"}) | |
public class ReflectUtil { | |
public static final String NMS_PACKAGE = "net.minecraft.server" + Bukkit.getServer().getClass().getPackage().getName().substring(Bukkit.getServer().getClass().getPackage().getName().lastIndexOf(".")); | |
public static final String CB_PACKAGE = Bukkit.getServer().getClass().getPackage().getName(); | |
private static final Field MODIFIERS_FIELD = getDeclaredField(Field.class, "modifiers", true).getOrThrow(); | |
public static ReflectionResponse<Class<?>> getNMSClass(String clazz) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
return getClass(NMS_PACKAGE + "." + clazz); | |
} | |
public static ReflectionResponse<Class<?>> getCBClass(String clazz) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
return getClass(CB_PACKAGE + "." + clazz); | |
} | |
public static ReflectionResponse<Class<?>> getClass(String clazz) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
try { | |
return new ReflectionResponse<>(Class.forName(clazz)); | |
} catch (ClassNotFoundException e) { | |
return new ReflectionResponse<>(e); | |
} | |
} | |
public static ReflectionResponse<Constructor<?>> getConstructor(Class<?> clazz, Class<?>... params) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
Validate.notNull(params, "params cannot be null"); | |
try { | |
return new ReflectionResponse<>(clazz.getConstructor(params)); | |
} catch (NoSuchMethodException e) { | |
return new ReflectionResponse<>(e); | |
} | |
} | |
public static ReflectionResponse<Field> getField(Class<?> clazz, String fieldName) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
Validate.notNull(fieldName, "fieldName cannot be null"); | |
try { | |
return new ReflectionResponse<>(clazz.getField(fieldName)); | |
} catch (NoSuchFieldException e) { | |
return new ReflectionResponse<>(e); | |
} | |
} | |
public static ReflectionResponse<Field> getDeclaredField(Class<?> clazz, String fieldName) { | |
return getDeclaredField(clazz, fieldName, false); | |
} | |
public static ReflectionResponse<Field> getDeclaredField(Class<?> clazz, String fieldName, boolean setAccessible) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
Validate.notNull(fieldName, "fieldName cannot be null"); | |
try { | |
Field field = clazz.getDeclaredField(fieldName); | |
field.setAccessible(setAccessible); | |
return new ReflectionResponse<>(field); | |
} catch (NoSuchFieldException e) { | |
return new ReflectionResponse<>(e); | |
} | |
} | |
public static ReflectionResponse<Field> getFieldByType(Class<?> clazz, Class<?> type, int index) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
Validate.notNull(type, "type cannot be null"); | |
Validate.isTrue(index >= 0, "index cannot be less than zero"); | |
int curIndex = 0; | |
for (Field field : clazz.getFields()) { | |
if (field.getType() == type) { | |
if (curIndex == index) { | |
return new ReflectionResponse<>(field); | |
} | |
curIndex++; | |
} | |
} | |
return new ReflectionResponse<>(new NoSuchFieldException("No field with type " + type + " and index" + index + " in " + clazz)); | |
} | |
public static ReflectionResponse<Field> getModifiableFinalStaticField(Class<?> clazz, String fieldName) { | |
ReflectionResponse<Field> response = getField(clazz, fieldName); | |
if (!response.hasResult()) { | |
return response; | |
} | |
Field field = response.getValue(); | |
ReflectionResponse<Void> voidResponse = makeFinalStaticFieldModifiable(field); | |
if (!voidResponse.hasResult()) { | |
return new ReflectionResponse<>(voidResponse.getException()); | |
} | |
return new ReflectionResponse<>(field); | |
} | |
public static ReflectionResponse<Field> getModifiableDeclaredFinalStaticField(Class<?> clazz, String fieldName, boolean setAccessible) { | |
ReflectionResponse<Field> response = getDeclaredField(clazz, fieldName, setAccessible); | |
if (!response.hasResult()) { | |
return response; | |
} | |
Field field = response.getValue(); | |
ReflectionResponse<Void> voidResponse = makeFinalStaticFieldModifiable(field); | |
if (!voidResponse.hasResult()) { | |
return new ReflectionResponse<>(voidResponse.getException()); | |
} | |
return new ReflectionResponse<>(field); | |
} | |
public static ReflectionResponse<Void> makeFinalStaticFieldModifiable(Field field) { | |
Validate.notNull(field, "field cannot be null"); | |
Validate.isTrue(Modifier.isStatic(field.getModifiers()), "field is not static"); | |
Validate.isTrue(Modifier.isFinal(field.getModifiers()), "field is not final"); | |
return setFieldValue(field, MODIFIERS_FIELD, field.getModifiers() & ~Modifier.FINAL); | |
} | |
public static ReflectionResponse<Field> getDeclaredFieldByType(Class<?> clazz, Class<?> type, int index) { | |
return getDeclaredFieldByType(clazz, type, index, false); | |
} | |
public static ReflectionResponse<Field> getDeclaredFieldByType(Class<?> clazz, Class<?> type, int index, boolean setAccessible) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
Validate.notNull(type, "type cannot be null"); | |
Validate.isTrue(index >= 0, "index cannot be less than zero"); | |
int curIndex = 0; | |
for (Field field : clazz.getDeclaredFields()) { | |
if (field.getType() == type) { | |
if (curIndex == index) { | |
field.setAccessible(setAccessible); | |
return new ReflectionResponse<>(field); | |
} | |
curIndex++; | |
} | |
} | |
return new ReflectionResponse<>(new NoSuchFieldException("No declared field with type " + type + " and index " + index + " in " + clazz)); | |
} | |
public static ReflectionResponse<Method> getMethod(Class<?> clazz, String methodName, Class<?>... params) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
Validate.notNull(methodName, "methodName cannot be null"); | |
Validate.notNull(params, "params cannot be null"); | |
try { | |
return new ReflectionResponse<>(clazz.getMethod(methodName, params)); | |
} catch (NoSuchMethodException e) { | |
return new ReflectionResponse<>(e); | |
} | |
} | |
public static ReflectionResponse<Method> getMethodByType(Class<?> clazz, Class<?> type, int index) { | |
return getMethodByPredicate(clazz, new MethodPredicate().withReturnType(type), index); | |
} | |
public static ReflectionResponse<Method> getMethodByParams(Class<?> clazz, int index, Class<?>... params) { | |
return getMethodByPredicate(clazz, new MethodPredicate().withParams(params), index); | |
} | |
public static ReflectionResponse<Method> getMethodByTypeAndParams(Class<?> clazz, Class<?> type, int index, Class<?>... params) { | |
return getMethodByPredicate(clazz, new MethodPredicate().withReturnType(type).withParams(params), index); | |
} | |
public static ReflectionResponse<Method> getMethodByPredicate(Class<?> clazz, Predicate<Method> predicate, int index) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
Validate.isTrue(index >= 0, "index cannot be less than zero"); | |
int curIndex = 0; | |
for (Method method : clazz.getMethods()) { | |
if (predicate == null || predicate.test(method)) { | |
if (curIndex == index) { | |
return new ReflectionResponse<>(method); | |
} | |
curIndex++; | |
} | |
} | |
return new ReflectionResponse<>(new NoSuchMethodException("No method matching " + (predicate instanceof MethodPredicate ? predicate : "specified predicate") + " in " + clazz)); | |
} | |
public static ReflectionResponse<Method> getDeclaredMethodByType(Class<?> clazz, Class<?> type, int index) { | |
return getDeclaredMethodByType(clazz, type, index, false); | |
} | |
public static ReflectionResponse<Method> getDeclaredMethodByType(Class<?> clazz, Class<?> type, int index, boolean setAccessible) { | |
return getDeclaredMethodByPredicate(clazz, new MethodPredicate().withReturnType(type), 0, setAccessible); | |
} | |
public static ReflectionResponse<Method> getDeclaredMethodByPredicate(Class<?> clazz, Predicate<Method> predicate, int index, boolean setAccessible) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
Validate.isTrue(index >= 0, "index cannot be less than zero"); | |
int curIndex = 0; | |
for (Method method : clazz.getDeclaredMethods()) { | |
if (predicate == null || predicate.test(method)) { | |
if (curIndex == index) { | |
method.setAccessible(setAccessible); | |
return new ReflectionResponse<>(method); | |
} | |
curIndex++; | |
} | |
} | |
return new ReflectionResponse<>(new NoSuchMethodException("No method matching " + (predicate instanceof MethodPredicate ? predicate : "specified predicate") + " in " + clazz)); | |
} | |
public static ReflectionResponse<Object> getFieldValue(Object object, Field field) { | |
Validate.notNull(field, "field cannot be null"); | |
Validate.isTrue(object != null || Modifier.isStatic(field.getModifiers()), "object cannot be null"); | |
try { | |
return new ReflectionResponse<>(field.get(object)); | |
} catch (IllegalAccessException e) { | |
return new ReflectionResponse<>(e); | |
} | |
} | |
public static ReflectionResponse<Object> getEnumConstant(Class<?> clazz, String constant) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
Validate.isTrue(clazz.isEnum(), "clazz is not an Enum"); | |
Validate.notNull(constant, "constant cannot be null"); | |
try { | |
Field field = clazz.getField(constant); | |
return new ReflectionResponse<>(field.get(null)); | |
} catch (NoSuchFieldException | IllegalAccessException e) { | |
return new ReflectionResponse<>(e); | |
} | |
} | |
public static ReflectionResponse<Void> setFieldValue(Object object, Field field, Object newValue) { | |
Validate.notNull(field, "field cannot be null"); | |
Validate.isTrue(object != null || Modifier.isStatic(field.getModifiers())); | |
try { | |
field.set(object, newValue); | |
return new ReflectionResponse<>((Void) null); | |
} catch (IllegalAccessException e) { | |
return new ReflectionResponse<>(e); | |
} | |
} | |
public static ReflectionResponse<Object> invokeMethod(Object object, Method method, Object... params) { | |
Validate.notNull(method, "method cannot be null"); | |
Validate.isTrue(object != null || Modifier.isStatic(method.getModifiers()), "object cannot be null"); | |
Validate.notNull(params, "params cannot be null"); | |
try { | |
return new ReflectionResponse<>(method.invoke(object, params)); | |
} catch (IllegalAccessException | InvocationTargetException e) { | |
return new ReflectionResponse<>(e); | |
} | |
} | |
public static ReflectionResponse<Object> invokeConstructor(Constructor<?> constructor, Object... params) { | |
Validate.notNull(constructor, "constructor cannot be null"); | |
Validate.notNull(params, "params cannot be null"); | |
try { | |
return new ReflectionResponse<>(constructor.newInstance(params)); | |
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { | |
return new ReflectionResponse<>(e); | |
} | |
} | |
public static ReflectionResponse<Map<String, String>> getPrintableFields(Object object, Class<?>... toStringExceptions) { | |
return getPrintableFields(object, true, toStringExceptions); | |
} | |
public static ReflectionResponse<Map<String, String>> getPrintableFields(Object object, boolean useToString, Class<?>... toStringExceptions) { | |
Validate.notNull(object, "object cannot be null"); | |
return getPrintableFields(object, object.getClass(), useToString, toStringExceptions); | |
} | |
public static ReflectionResponse<Map<String, String>> getPrintableFields(Object object, Class<?> clazz, boolean useToString, Class<?>... toStringExceptions) { | |
Validate.notNull(clazz, "clazz cannot be null"); | |
Map<String, String> fields = Maps.newHashMap(); | |
try { | |
for (Field field : clazz.getFields()) { | |
if (!Modifier.isStatic(field.getModifiers())) { | |
ReflectionResponse<String> response = getStringRepresentation(field.get(object), useToString, toStringExceptions); | |
if (!response.hasResult()) { | |
return new ReflectionResponse<>(response.getException()); | |
} | |
fields.put(field.getName(), response.getValue()); | |
} | |
} | |
for (Field field : clazz.getDeclaredFields()) { | |
if (!Modifier.isStatic(field.getModifiers())) { | |
if (clazz.getEnclosingClass() != null && field.getType() == clazz.getEnclosingClass()) { | |
/* Inner classes contain a reference to their outer class instance and not ignoring it would | |
cause the code to recurse infinitely and cause a StackOverflowError. | |
This field is normally named 'this$0' (or with added $'s if a field with that name already exists), | |
but Mojang's obfuscation tool obfuscates this field and renames it 'a'. */ | |
if (field.getName().startsWith("this$0") || (clazz.getPackage().getName().equals(NMS_PACKAGE) && field.getName().equals("a"))) { | |
continue; | |
} | |
} | |
field.setAccessible(true); | |
ReflectionResponse<String> response = getStringRepresentation(field.get(object), useToString, toStringExceptions); | |
if (!response.hasResult()) { | |
return new ReflectionResponse<>(response.getException()); | |
} | |
fields.put(field.getName(), response.getValue()); | |
} | |
} | |
} catch (IllegalAccessException e) { | |
return new ReflectionResponse<>(e); | |
} | |
return new ReflectionResponse<>(fields); | |
} | |
public static ReflectionResponse<String> getStringRepresentation(Object object, boolean useToString, Class<?>... toStringExceptions) { | |
try { | |
if (object == null) { | |
return new ReflectionResponse<>("null"); | |
} | |
// Multimaps don't extend Map apparently. | |
if (object instanceof Multimap) { | |
object = ((Multimap) object).asMap(); | |
} | |
// Using toString() (or Arrays.toString) on Collections/Maps/arrays would use | |
// toString() on the contained Objects, which is why we make our own implementation. | |
if (object instanceof Map) { | |
String str = "{"; | |
for (Map.Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) { | |
ReflectionResponse<String> firstResponse = getStringRepresentation(entry.getKey(), useToString, toStringExceptions); | |
ReflectionResponse<String> secondResponse = getStringRepresentation(entry.getValue(), useToString, toStringExceptions); | |
if (!firstResponse.hasResult()) { | |
// getStringRepresentation caught an Exception, so we abort. | |
return firstResponse; | |
} | |
if (!secondResponse.hasResult()) { | |
// getStringRepresentation caught an Exception, so we abort. | |
return secondResponse; | |
} | |
str += firstResponse.getValue() + "=" + secondResponse.getValue() + ","; | |
} | |
// Remove last comma | |
str = str.substring(0, str.length() - 1) + "}"; | |
return new ReflectionResponse<>(str); | |
} | |
if (object instanceof Collection) { | |
String str = "["; | |
for (Object listEntry : (Collection) object) { | |
ReflectionResponse<String> response = getStringRepresentation(listEntry, useToString, toStringExceptions); | |
if (!response.hasResult()) { | |
// getStringRepresentation caught an Exception, so we abort. | |
return response; | |
} | |
str += response.getValue() + ","; | |
} | |
// Remove last comma | |
str = str.substring(0, str.length() - 1) + "]"; | |
return new ReflectionResponse<>(str); | |
} | |
if (object.getClass().isArray()) { | |
String str = "["; | |
for (int i = 0; i < Array.getLength(object); i++) { | |
ReflectionResponse<String> response = getStringRepresentation(Array.get(object, i), useToString, toStringExceptions); | |
if (!response.hasResult()) { | |
// getStringRepresentation caught an Exception, so we abort. | |
return response; | |
} | |
str += response.getValue() + ","; | |
} | |
// Remove last comma | |
str = str.substring(0, str.length() - 1) + "]"; | |
return new ReflectionResponse<>(str); | |
} | |
if (useToString) { | |
if (object.getClass().getMethod("toString").getDeclaringClass() != Object.class && !ArrayUtils.contains(toStringExceptions, object.getClass())) { | |
return new ReflectionResponse<>(object.toString()); | |
} else { | |
ReflectionResponse<Map<String, String>> response = getPrintableFields(object, true, toStringExceptions); | |
if (!response.hasResult()) { | |
// getPrintableFields caught an Exception, so we abort. | |
return new ReflectionResponse<>(response.getException()); | |
} | |
return new ReflectionResponse<>(object.getClass().getSimpleName() + response.getValue()); | |
} | |
} else { | |
if (ClassUtils.isPrimitiveWrapper(object.getClass()) || object instanceof String || object instanceof Enum || ArrayUtils.contains(toStringExceptions, object.getClass())) { | |
// Even though useToString is false, we call toString on primitive wrappers, Strings, Enums and the specified exceptions. | |
return new ReflectionResponse<>(object.toString()); | |
} else { | |
ReflectionResponse<Map<String, String>> response = getPrintableFields(object, false, toStringExceptions); | |
if (!response.hasResult()) { | |
// getPrintableFields caught an Exception, so we abort. | |
return new ReflectionResponse<>(response.getException()); | |
} | |
return new ReflectionResponse<>(object.getClass().getSimpleName() + response.getValue()); | |
} | |
} | |
} catch (NoSuchMethodException e) { | |
return new ReflectionResponse<>(e); | |
} | |
} | |
@SuppressWarnings("unused") | |
public static class ReflectionResponse<T> { | |
private final T value; | |
private final Exception exception; | |
private final boolean hasResult; | |
private ReflectionResponse(T value, boolean hasResult, Exception exception) { | |
this.value = value; | |
this.hasResult = hasResult; | |
this.exception = exception; | |
} | |
private ReflectionResponse(T value) { | |
this(value, true, null); | |
} | |
private ReflectionResponse(Exception exception) { | |
this(null, false, exception); | |
} | |
public T getValue() { | |
return value; | |
} | |
public boolean hasResult() { | |
return hasResult; | |
} | |
public Exception getException() { | |
return exception; | |
} | |
public T getOrThrow() { | |
if (hasResult) { | |
return value; | |
} else { | |
throw new RuntimeException(exception); | |
} | |
} | |
@Override | |
public String toString() { | |
return "ReflectionResponse{value=" + value + ",exception=" + exception + ",hasResult=" + hasResult + "}"; | |
} | |
} | |
public static class MethodPredicate implements Predicate<Method> { | |
private Class<?> returnType; | |
private Class<?>[] params; | |
private List<Integer> withModifiers; | |
private List<Integer> withoutModifiers; | |
private Predicate<Method> predicate; | |
private String name; | |
public MethodPredicate withReturnType(Class<?> returnType) { | |
this.returnType = returnType; | |
return this; | |
} | |
public MethodPredicate withParams(Class<?>... params) { | |
this.params = params; | |
return this; | |
} | |
public MethodPredicate withModifiers(int... modifiers) { | |
this.withModifiers = Arrays.stream(modifiers).boxed().collect(Collectors.toList()); | |
return this; | |
} | |
public MethodPredicate withModifiers(Collection<Integer> modifiers) { | |
this.withModifiers = new ArrayList<>(modifiers); | |
return this; | |
} | |
public MethodPredicate withoutModifiers(int... modifiers) { | |
this.withoutModifiers = Arrays.stream(modifiers).boxed().collect(Collectors.toList()); | |
return this; | |
} | |
public MethodPredicate withoutModifiers(Collection<Integer> modifiers) { | |
this.withoutModifiers = new ArrayList<>(modifiers); | |
return this; | |
} | |
public MethodPredicate withPredicate(Predicate<Method> predicate) { | |
this.predicate = predicate; | |
return this; | |
} | |
public MethodPredicate withName(String name) { | |
this.name = name; | |
return this; | |
} | |
@SuppressWarnings("RedundantIfStatement") | |
@Override | |
public boolean test(Method method) { | |
if (returnType != null && method.getReturnType() != returnType) { | |
return false; | |
} | |
if (params != null && !Arrays.equals(method.getParameterTypes(), params)) { | |
return false; | |
} | |
if (withModifiers != null) { | |
int modifiers = method.getModifiers(); | |
for (int bitMask : withModifiers) { | |
if ((modifiers & bitMask) == 0) { | |
return false; | |
} | |
} | |
} | |
if (withoutModifiers != null) { | |
int modifiers = method.getModifiers(); | |
for (int bitMask : withoutModifiers) { | |
if ((modifiers & bitMask) != 0) { | |
return false; | |
} | |
} | |
} | |
if (predicate != null && !predicate.test(method)) { | |
return false; | |
} | |
if (name != null && !method.getName().equals(name)) { | |
return false; | |
} | |
return true; | |
} | |
@Override | |
public String toString() { | |
List<String> args = Lists.newArrayList(); | |
if (returnType != null) { | |
args.add("return type " + returnType); | |
} | |
if (params != null) { | |
args.add("params " + Arrays.toString(params)); | |
} | |
if (withModifiers != null) { | |
args.add("with modifiers (bitmasks) " + withModifiers); | |
} | |
if (withoutModifiers != null) { | |
args.add("without modifiers (bitmasks) " + withoutModifiers); | |
} | |
if (predicate != null) { | |
args.add("specified predicate"); | |
} | |
if (name != null) { | |
args.add("with name " + name); | |
} | |
return Joiner.on(", ").join(args.subList(0, args.size() - 1)) + ", and " + args.get(args.size() - 1); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment