commit c65f6b006f38c0e8ca919c846ee9605ef54433bf Author: Kristian S. Stangeland Date: Wed Sep 12 19:04:53 2012 +0200 Initial commit. diff --git a/ProtocolLib/.classpath b/ProtocolLib/.classpath new file mode 100644 index 00000000..0bc481ca --- /dev/null +++ b/ProtocolLib/.classpath @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/ProtocolLib/.project b/ProtocolLib/.project new file mode 100644 index 00000000..dd7e4061 --- /dev/null +++ b/ProtocolLib/.project @@ -0,0 +1,17 @@ + + + ProtocolLib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/ProtocolLib/.settings/org.eclipse.jdt.core.prefs b/ProtocolLib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..54e493c0 --- /dev/null +++ b/ProtocolLib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,11 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/ProtocolLib/Java.xml b/ProtocolLib/Java.xml new file mode 100644 index 00000000..33c14105 --- /dev/null +++ b/ProtocolLib/Java.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/ProtocolLib/lib/cglib-nodep-2.2.3.jar b/ProtocolLib/lib/cglib-nodep-2.2.3.jar new file mode 100644 index 00000000..35d6d7c3 Binary files /dev/null and b/ProtocolLib/lib/cglib-nodep-2.2.3.jar differ diff --git a/ProtocolLib/src/com/comphenix/protocol/Application.java b/ProtocolLib/src/com/comphenix/protocol/Application.java new file mode 100644 index 00000000..e4c6ab64 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/Application.java @@ -0,0 +1,8 @@ +package com.comphenix.protocol; + +public class Application { + public static void main(String[] args) { + // For now, though we might consider making a proper application + System.out.println("This is a Bukkit library. Place it in the plugin-folder and restart the server!"); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java new file mode 100644 index 00000000..3b2711c9 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java @@ -0,0 +1,63 @@ +package com.comphenix.protocol; + +import java.util.logging.Logger; + +import org.bukkit.Server; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.java.JavaPlugin; + +import com.comphenix.protocol.injector.PacketFilterManager; + +public class ProtocolLibrary extends JavaPlugin { + + // There should only be one protocol manager, so we'll make it static + private static PacketFilterManager protocolManager; + + // Error logger + private Logger logger; + + @Override + public void onLoad() { + logger = getLoggerSafely(); + protocolManager = new PacketFilterManager(getClassLoader(), logger); + } + + @Override + public void onEnable() { + Server server = getServer(); + PluginManager manager = server.getPluginManager(); + + // Player login and logout events + protocolManager.registerEvents(manager, this); + protocolManager.initializePlayers(server.getOnlinePlayers()); + } + + @Override + public void onDisable() { + protocolManager.close(); + } + + /** + * Retrieves the packet protocol manager. + * @return Packet protocol manager. + */ + public static ProtocolManager getProtocolManager() { + return protocolManager; + } + + // Get the Bukkit logger first, before we try to create our own + private Logger getLoggerSafely() { + + Logger log = null; + + try { + log = getLogger(); + } catch (Throwable e) { + // We'll handle it + } + + if (log == null) + log = Logger.getLogger("Minecraft"); + return log; + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java b/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java new file mode 100644 index 00000000..3e37fc2d --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java @@ -0,0 +1,90 @@ +package com.comphenix.protocol; + +import java.lang.reflect.InvocationTargetException; +import java.util.Set; + +import org.bukkit.entity.Player; + +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketListener; +import com.google.common.collect.ImmutableSet; + +public interface ProtocolManager { + + /** + * Retrieves a list of every registered packet listener. + * @return Every registered packet listener. + */ + public abstract ImmutableSet getPacketListeners(); + + /** + * Adds a packet listener. + * @param listener - new packet listener. + */ + public abstract void addPacketListener(PacketListener listener); + + /** + * Removes a given packet listener. + * @param listener - the packet listener to remove. + */ + public abstract void removePacketListener(PacketListener listener); + + /** + * Send a packet to the given player. + * @param reciever - the reciever. + * @param packet - packet to send. + * @throws InvocationTargetException - if an error occured when sending the packet. + */ + public void sendServerPacket(Player reciever, PacketContainer packet) + throws InvocationTargetException; + + /** + * Send a packet to the given player. + * @param reciever - the reciever. + * @param packet - packet to send. + * @param filters - whether or not to invoke any packet filters. + * @throws InvocationTargetException - if an error occured when sending the packet. + */ + public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) + throws InvocationTargetException; + + /** + * Simulate recieving a certain packet from a given player. + * @param sender - the sender. + * @param packet - the packet that was sent. + * @throws InvocationTargetException If the reflection machinery failed. + * @throws IllegalAccessException If the underlying method caused an error. + */ + public void recieveClientPacket(Player sender, PacketContainer packet) + throws IllegalAccessException, InvocationTargetException; + + /** + * Simulate recieving a certain packet from a given player. + * @param sender - the sender. + * @param packet - the packet that was sent. + * @param filters - whether or not to invoke any packet filters. + * @throws InvocationTargetException If the reflection machinery failed. + * @throws IllegalAccessException If the underlying method caused an error. + */ + public void recieveClientPacket(Player sender, PacketContainer packet, boolean filters) + throws IllegalAccessException, InvocationTargetException; + + /** + * Constructs a new encapsulated Minecraft packet with the given ID. + * @param id - packet ID. + * @return New encapsulated Minecraft packet. + */ + public PacketContainer createPacket(int id); + + /** + * Retieves a set of every enabled packet. + * @return Every packet filter. + */ + public Set getPacketFilters(); + + /** + * Determines whether or not is protocol mananger has been disabled. + * @return TRUE if it has, FALSE otherwise. + */ + public boolean isClosed(); +} \ No newline at end of file diff --git a/ProtocolLib/src/com/comphenix/protocol/events/ConnectionSide.java b/ProtocolLib/src/com/comphenix/protocol/events/ConnectionSide.java new file mode 100644 index 00000000..5e8d1b2e --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/events/ConnectionSide.java @@ -0,0 +1,31 @@ +package com.comphenix.protocol.events; + +/** + * Used to set a packet filter. + * + * @author Kristian + */ +public enum ConnectionSide { + /** + * Listen for server side packets that will invoke onPacketSending(). + */ + SERVER_SIDE, + + /** + * Listen for client side packets that will invoke onPacketReceiving(). + */ + CLIENT_SIDE, + + /** + * Listen for both client and server side packets. + */ + BOTH; + + public boolean isForClient() { + return this == CLIENT_SIDE || this == BOTH; + } + + public boolean isForServer() { + return this == SERVER_SIDE || this == BOTH; + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/events/PacketAdapter.java b/ProtocolLib/src/com/comphenix/protocol/events/PacketAdapter.java new file mode 100644 index 00000000..16a11659 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/events/PacketAdapter.java @@ -0,0 +1,60 @@ +package com.comphenix.protocol.events; + +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.bukkit.plugin.java.JavaPlugin; + +import com.google.common.collect.Sets; + +/** + * Represents a packet listener with useful constructors. + * + * @author Kristian + */ +public abstract class PacketAdapter implements PacketListener { + + protected JavaPlugin plugin; + protected Set packetsID; + protected ConnectionSide connectionSide; + + /** + * Initialize a packet listener. + * @param plugin - the plugin that spawned this listener. + * @param connectionSide - the packet type the listener is looking for. + * @param packets - the packet IDs the listener is looking for. + */ + public PacketAdapter(JavaPlugin plugin, ConnectionSide connectionSide, Integer... packets) { + this.plugin = plugin; + this.connectionSide = connectionSide; + this.packetsID = Sets.newHashSet(packets); + } + + @Override + public void onPacketReceiving(PacketEvent event) { + // Default is to do nothing + } + + @Override + public void onPacketSending(PacketEvent event) { + // And here too + } + + @Override + public ConnectionSide getConnectionSide() { + return connectionSide; + } + + @Override + public Set getPacketsID() { + return packetsID; + } + + @Override + public String toString() { + // This is used by the error reporter + return String.format("PacketAdapter[plugin=%s, side=%s, packets=%s]", + plugin.getName(), getConnectionSide().name(), + StringUtils.join(packetsID, ", ")); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/events/PacketContainer.java b/ProtocolLib/src/com/comphenix/protocol/events/PacketContainer.java new file mode 100644 index 00000000..5971e974 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/events/PacketContainer.java @@ -0,0 +1,126 @@ +package com.comphenix.protocol.events; + +import org.apache.commons.lang.NullArgumentException; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.inventory.ItemStack; + +import com.comphenix.protocol.injector.StructureCache; +import com.comphenix.protocol.reflect.EquivalentConverter; +import com.comphenix.protocol.reflect.StructureModifier; + +import net.minecraft.server.Packet; + +/** + * Represents a Minecraft packet indirectly. + * + * @author Kristian + */ +public class PacketContainer { + + protected Packet handle; + protected int id; + + // Current structure modifier + protected StructureModifier structureModifier; + + /** + * Creates a packet container for a new packet. + * @param id - ID of the packet to create. + */ + public PacketContainer(int id) { + this(id, StructureCache.newPacket(id)); + } + + /** + * Creates a packet container for an existing packet. + * @param id - ID of the given packet. + * @param handle - contained packet. + */ + public PacketContainer(int id, Packet handle) { + this(id, handle, StructureCache.getStructure(id).withTarget(handle)); + } + + /** + * Creates a packet container for an existing packet. + * @param id - ID of the given packet. + * @param handle - contained packet. + * @param structure - structure modifier. + */ + public PacketContainer(int id, Packet handle, StructureModifier structure) { + if (handle == null) + throw new NullArgumentException("handle"); + + this.id = id; + this.handle = handle; + this.structureModifier = structure; + } + + /** + * Retrieves the underlying Minecraft packet. + * @return Underlying Minecraft packet. + */ + public Packet getHandle() { + return handle; + } + + /** + * Retrieves the generic structure modifier for this packet. + * @return Structure modifier. + */ + public StructureModifier getModifier() { + return structureModifier; + } + + public StructureModifier getPrimitiveModifier(Class primitiveType) { + return structureModifier.withType(primitiveType); + } + + public StructureModifier getItemModifier() { + // Convert from and to the Bukkit wrapper + return structureModifier.withType(net.minecraft.server.ItemStack.class, new EquivalentConverter() { + public Object getGeneric(ItemStack specific) { + return ((CraftItemStack) specific).getHandle(); + } + + @Override + public ItemStack getSpecific(Object generic) { + return new CraftItemStack((net.minecraft.server.ItemStack) generic); + } + }); + } + + public StructureModifier getItemArrayModifier() { + // Convert to and from the Bukkit wrapper + return structureModifier.withType(net.minecraft.server.ItemStack[].class, new EquivalentConverter() { + public Object getGeneric(ItemStack[] specific) { + net.minecraft.server.ItemStack[] result = new net.minecraft.server.ItemStack[specific.length]; + + // Unwrap every item + for (int i = 0; i < result.length; i++) { + result[i] = ((CraftItemStack) specific[i]).getHandle(); + } + return result; + } + + @Override + public ItemStack[] getSpecific(Object generic) { + net.minecraft.server.ItemStack[] input = (net.minecraft.server.ItemStack[]) generic; + ItemStack[] result = new ItemStack[input.length]; + + // Add the wrapper + for (int i = 0; i < result.length; i++) { + result[i] = new CraftItemStack(input[i]); + } + return result; + } + }); + } + + /** + * Retrieves the ID of this packet. + * @return Packet ID. + */ + public int getID() { + return id; + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/events/PacketEvent.java b/ProtocolLib/src/com/comphenix/protocol/events/PacketEvent.java new file mode 100644 index 00000000..e403f658 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/events/PacketEvent.java @@ -0,0 +1,119 @@ +package com.comphenix.protocol.events; + +import java.util.EventObject; + +import org.bukkit.entity.Player; + +public class PacketEvent extends EventObject { + /** + * Automatically generated by Eclipse. + */ + private static final long serialVersionUID = -5360289379097430620L; + + private PacketContainer packet; + private Player sender; + private Player reciever; + private boolean cancel; + + /** + * Use the static constructors to create instances of this event. + * @param source - the event source. + */ + public PacketEvent(Object source) { + super(source); + } + + private PacketEvent(Object source, PacketContainer packet, Player sender, Player reciever) { + super(source); + this.packet = packet; + this.sender = sender; + this.reciever = reciever; + } + + /** + * Creates an event representing a client packet transmission. + * @param source - the event source. + * @param packet - the packet. + * @param client - the client that sent the packet. + * @return The event. + */ + public static PacketEvent fromClient(Object source, PacketContainer packet, Player client) { + return new PacketEvent(source, packet, client, null); + } + + /** + * Creates an event representing a server packet transmission. + * @param source - the event source. + * @param packet - the packet. + * @param recipient - the client that will receieve the packet. + * @return The event. + */ + public static PacketEvent fromServer(Object source, PacketContainer packet, Player recipient) { + return new PacketEvent(source, packet, null, recipient); + } + + /** + * Retrieves the packet that will be sent to the player. + * @return Packet to send to the player. + */ + public PacketContainer getPacket() { + return packet; + } + + /** + * Replace the packet that will be sent to the player. + * @param packet - the packet that will be sent instead. + */ + public void setPacket(PacketContainer packet) { + this.packet = packet; + } + + /** + * Retrieves the packet ID. + * @return The current packet ID. + */ + public int getPacketID() { + return packet.getID(); + } + + /** + * Retrieves whether or not the packet should be cancelled. + * @return TRUE if it should be cancelled, FALSE otherwise. + */ + public boolean isCancelled() { + return cancel; + } + + /** + * Sets whether or not the packet should be cancelled. + * @param cancel - TRUE if it should be cancelled, FALSE otherwise. + */ + public void setCancelled(boolean cancel) { + this.cancel = cancel; + } + + /** + * Retrieves the player that has sent the packet. + * @return The sender, or NULL if the server is sending the packet. + */ + public Player getSender() { + return sender; + } + + /** + * Retrieves the player that will recieve the packet. + * @return The reciever, or NULL if the server is recieving the packet. + */ + public Player getReciever() { + return reciever; + } + + + /** + * Whether or not this packet was created by the server. + * @return TRUE if the packet was created by the server, FALSE if it was created by a client. + */ + public boolean isServerPacket() { + return getReciever() != null; + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/events/PacketListener.java b/ProtocolLib/src/com/comphenix/protocol/events/PacketListener.java new file mode 100644 index 00000000..6b0b7fc0 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/events/PacketListener.java @@ -0,0 +1,34 @@ +package com.comphenix.protocol.events; + +import java.util.Set; + + +public interface PacketListener { + + /** + * Invoked right before a packet is transmitted from the server to the client. + *

+ * Note that the packet may be replaced, if needed. + * + * @param event - the packet that should be sent. + */ + public void onPacketSending(PacketEvent event); + + /** + * Invoked right before a recieved packet from a client is being processed. + * @param event - the packet that has been recieved. + */ + public void onPacketReceiving(PacketEvent event); + + /** + * Retrieve whether or not we're listening for client or server packets. + * @return The type of packets we expect. + */ + public ConnectionSide getConnectionSide(); + + /** + * Set of packet ids we expect to recieve. + * @return Packets IDs. + */ + public Set getPacketsID(); +} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/MinecraftRegistry.java b/ProtocolLib/src/com/comphenix/protocol/injector/MinecraftRegistry.java new file mode 100644 index 00000000..9c8d4fe6 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/MinecraftRegistry.java @@ -0,0 +1,88 @@ +package com.comphenix.protocol.injector; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang.ObjectUtils; + +import net.minecraft.server.Packet; + +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; + +/** + * Static registries in Minecraft. + * + * @author Kristian + */ +@SuppressWarnings("rawtypes") +class MinecraftRegistry { + + // The packet class to packet ID translator + private static Map packetToID; + + // New proxy values + private static Map overwrittenPackets = new HashMap(); + + // Vanilla packets + private static Map previousValues = new HashMap(); + + @SuppressWarnings({ "unchecked" }) + public static Map getPacketToID() { + // Initialize it, if we haven't already + if (packetToID == null) { + try { + Field packetsField = FuzzyReflection.fromClass(Packet.class, true).getFieldByType("java\\.util\\.Map"); + packetToID = (Map) FieldUtils.readStaticField(packetsField, true); + + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to retrieve the packetClassToIdMap", e); + } + } + + return packetToID; + } + + public static Map getOverwrittenPackets() { + return overwrittenPackets; + } + + public static Map getPreviousPackets() { + return previousValues; + } + + /** + * Retrieves the correct packet class from a given packet ID. + * @param packetID - the packet ID. + * @return The associated class. + */ + public static Class getPacketClassFromID(int packetID) { + return getPacketClassFromID(packetID, false); + } + + /** + * Retrieves the correct packet class from a given packet ID. + * @param packetID - the packet ID. + * @param vanilla - whether or not to look for vanilla classes, not injected classes. + * @return The associated class. + */ + public static Class getPacketClassFromID(int packetID, boolean forceVanilla) { + + Map lookup = forceVanilla ? previousValues : overwrittenPackets; + + // Optimized lookup + if (lookup.containsKey(packetToID)) { + return lookup.get(packetToID); + } + + // Will most likely not be used + for (Map.Entry entry : getPacketToID().entrySet()) { + if (ObjectUtils.equals(entry.getValue(), packetID)) { + return entry.getKey(); + } + } + + throw new IllegalArgumentException("The packet ID " + packetID + " is not registered."); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java new file mode 100644 index 00000000..c96e5cea --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java @@ -0,0 +1,336 @@ +package com.comphenix.protocol.injector; + +import java.io.DataInputStream; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.minecraft.server.Packet; + +import org.apache.commons.lang.NullArgumentException; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginManager; + +import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.events.ConnectionSide; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.events.PacketListener; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; + +public final class PacketFilterManager implements ProtocolManager { + + private Set packetListeners = new CopyOnWriteArraySet(); + + // Player injection + private Map connectionLookup = new HashMap(); + private Map playerInjection = new HashMap(); + + // Packet injection + private PacketInjector packetInjector; + + // Enabled packet filters + private Set packetFilters = new HashSet(); + + // Whether or not this class has been closed + private boolean hasClosed; + + // Error logger + private Logger logger; + + /** + * Only create instances of this class if protocol lib is disabled. + */ + public PacketFilterManager(ClassLoader classLoader, Logger logger) { + if (logger == null) + throw new NullArgumentException("logger"); + if (classLoader == null) + throw new NullArgumentException("classLoader"); + + try { + // Initialize values + this.logger = logger; + this.packetInjector = new PacketInjector(classLoader, this, connectionLookup); + } catch (IllegalAccessException e) { + logger.log(Level.SEVERE, "Unable to initialize packet injector.", e); + } + } + + public Logger getLogger() { + return logger; + } + + @Override + public ImmutableSet getPacketListeners() { + return ImmutableSet.copyOf(packetListeners); + } + + @Override + public void addPacketListener(PacketListener listener) { + if (listener == null) + throw new NullArgumentException("listener"); + + packetListeners.add(listener); + enablePacketFilters(listener.getConnectionSide(), + listener.getPacketsID()); + } + + @Override + public void removePacketListener(PacketListener listener) { + if (listener == null) + throw new NullArgumentException("listener"); + + packetListeners.remove(listener); + disablePacketFilters(listener.getConnectionSide(), + listener.getPacketsID()); + } + + /** + * Invokes the given packet event for every registered listener. + * @param event - the packet event to invoke. + */ + public void invokePacketRecieving(PacketEvent event) { + for (PacketListener listener : packetListeners) { + try { + if (canHandlePacket(listener, event)) + listener.onPacketReceiving(event); + } catch (Exception e) { + // Minecraft doesn't want your Exception. + logger.log(Level.SEVERE, "Exception occured in onPacketReceiving() for " + listener.toString(), e); + } + } + } + + /** + * Invokes the given packet event for every registered listener. + * @param event - the packet event to invoke. + */ + public void invokePacketSending(PacketEvent event) { + for (PacketListener listener : packetListeners) { + try { + if (canHandlePacket(listener, event)) + listener.onPacketSending(event); + } catch (Exception e) { + logger.log(Level.SEVERE, "Exception occured in onPacketReceiving() for " + listener.toString(), e); + } + } + } + + private boolean canHandlePacket(PacketListener listener, PacketEvent event) { + // Make sure the listener is looking for this packet + if (!listener.getPacketsID().contains(event.getPacket().getID())) + return false; + + // And this type of packet + if (event.isServerPacket()) + return listener.getConnectionSide().isForServer(); + else + return listener.getConnectionSide().isForClient(); + } + + /** + * Enables packet events for a given packet ID. + *

+ * Note that all packets are disabled by default. + * + * @param side - which side the event will arrive from. + * @param packets - the packet id(s). + */ + private void enablePacketFilters(ConnectionSide side, Set packets) { + if (side == null) + throw new NullArgumentException("side"); + + for (int packetID : packets) { + if (side.isForServer()) + packetFilters.add(packetID); + if (side.isForClient() && packetInjector != null) + packetInjector.addPacketHandler(packetID); + } + } + + /** + * Disables packet events from a given packet ID. + * @param packets - the packet id(s). + * @param side - which side the event no longer should arrive from. + */ + private void disablePacketFilters(ConnectionSide side, Set packets) { + if (side == null) + throw new NullArgumentException("side"); + + for (int packetID : packets) { + if (side.isForServer()) + packetFilters.remove(packetID); + if (side.isForClient() && packetInjector != null) + packetInjector.removePacketHandler(packetID); + } + } + + @Override + public void sendServerPacket(Player reciever, PacketContainer packet) throws InvocationTargetException { + sendServerPacket(reciever, packet, true); + } + + @Override + public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException { + if (reciever == null) + throw new NullArgumentException("reciever"); + if (packet == null) + throw new NullArgumentException("packet"); + + getInjector(reciever).sendServerPacket(packet.getHandle(), filters); + } + + @Override + public void recieveClientPacket(Player sender, PacketContainer packet) throws IllegalAccessException, InvocationTargetException { + recieveClientPacket(sender, packet, true); + } + + @Override + public void recieveClientPacket(Player sender, PacketContainer packet, boolean filters) throws IllegalAccessException, InvocationTargetException { + + if (sender == null) + throw new NullArgumentException("sender"); + if (packet == null) + throw new NullArgumentException("packet"); + + PlayerInjector injector = getInjector(sender); + Packet mcPacket = packet.getHandle(); + + if (filters) { + mcPacket = injector.handlePacketRecieved(mcPacket); + } + + injector.processPacket(mcPacket); + } + + @Override + public PacketContainer createPacket(int id) { + return new PacketContainer(id); + } + + @Override + public Set getPacketFilters() { + if (packetInjector != null) + return Sets.union(packetFilters, packetInjector.getPacketHandlers()); + else + return packetFilters; + } + + /** + * Initialize the packet injection for every player. + * @param players - list of players to inject. + */ + public void initializePlayers(Player[] players) { + for (Player player : players) + injectPlayer(player); + } + + private void injectPlayer(Player player) { + // Don't inject if the class has closed + if (!hasClosed && player != null && !playerInjection.containsKey(player)) { + try { + PlayerInjector injector = new PlayerInjector(player, this, packetFilters); + + injector.injectManager(); + playerInjection.put(player, injector); + connectionLookup.put(injector.getInputStream(false), player); + + } catch (IllegalAccessException e) { + // Mark this injection attempt as a failure + playerInjection.put(player, null); + logger.log(Level.SEVERE, "Unable to access fields.", e); + } + } + } + + /** + * Register this protocol manager on Bukkit. + * @param manager - Bukkit plugin manager that provides player join/leave events. + * @param plugin - the parent plugin. + */ + public void registerEvents(PluginManager manager, Plugin plugin) { + manager.registerEvents(new Listener() { + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onPlayerJoin(PlayerJoinEvent event) { + injectPlayer(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onPlayerQuit(PlayerQuitEvent event) { + uninjectPlayer(event.getPlayer()); + } + }, plugin); + } + + private void uninjectPlayer(Player player) { + if (!hasClosed && player != null) { + + PlayerInjector injector = playerInjection.get(player); + DataInputStream input = injector.getInputStream(true); + + if (injector != null) { + injector.cleanupAll(); + + playerInjection.remove(injector); + connectionLookup.remove(input); + } + } + } + + private PlayerInjector getInjector(Player player) { + if (!playerInjection.containsKey(player)) { + // What? Try to inject again. + injectPlayer(player); + } + + PlayerInjector injector = playerInjection.get(player); + + // Check that the injector was sucessfully added + if (injector != null) + return injector; + else + throw new IllegalArgumentException("Player has no injected handler."); + } + + @Override + public boolean isClosed() { + return hasClosed; + } + + public void close() { + // Guard + if (hasClosed) + return; + + // Remove everything + for (PlayerInjector injection : playerInjection.values()) { + injection.cleanupAll(); + } + + // Remove packet handlers + if (packetInjector != null) + packetInjector.cleanupAll(); + + playerInjection.clear(); + connectionLookup.clear(); + hasClosed = true; + } + + @Override + protected void finalize() throws Throwable { + close(); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/PacketInjector.java b/ProtocolLib/src/com/comphenix/protocol/injector/PacketInjector.java new file mode 100644 index 00000000..f143b0d8 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/PacketInjector.java @@ -0,0 +1,177 @@ +package com.comphenix.protocol.injector; + +import java.io.DataInputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; + +import org.bukkit.entity.Player; + +import net.minecraft.server.Packet; +import net.sf.cglib.proxy.Callback; +import net.sf.cglib.proxy.Enhancer; + +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.reflect.FuzzyReflection; + +/** + * This class is responsible for adding or removing proxy objects that intercepts recieved packets. + * + * @author Kristian + */ +class PacketInjector { + + // The "put" method that associates a packet ID with a packet class + private static Method putMethod; + private static Object intHashMap; + + // The packet filter manager + private PacketFilterManager manager; + + // Allows us to determine the sender + private Map playerLookup; + + // Class loader + private ClassLoader classLoader; + + public PacketInjector(ClassLoader classLoader, PacketFilterManager manager, + Map playerLookup) throws IllegalAccessException { + + this.classLoader = classLoader; + this.manager = manager; + this.playerLookup = playerLookup; + initialize(); + } + + private void initialize() throws IllegalAccessException { + if (intHashMap == null) { + // We're looking for the first static field with a Minecraft-object. This should be a IntHashMap. + Field intHashMapField = FuzzyReflection.fromClass(Packet.class).getFieldByType(FuzzyReflection.MINECRAFT_OBJECT); + + try { + intHashMap = intHashMapField.get(null); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Minecraft is incompatible.", e); + } + + // Now, get the "put" method. + putMethod = FuzzyReflection.fromObject(intHashMap).getMethodByParameters("put", int.class, Object.class); + } + } + + @SuppressWarnings("rawtypes") + public boolean addPacketHandler(int packetID) { + if (hasPacketHandler(packetID)) + return false; + + Enhancer ex = new Enhancer(); + + // Unfortunately, we can't easily distinguish between these two functions: + // * Object lookup(int par1) + // * Object removeObject(int par1) + + // So, we'll use the classMapToInt registry instead. + Map overwritten = MinecraftRegistry.getOverwrittenPackets(); + Map previous = MinecraftRegistry.getPreviousPackets(); + Map registry = MinecraftRegistry.getPacketToID(); + Class old = MinecraftRegistry.getPacketClassFromID(packetID); + + // Check for previous injections + if (!old.getName().startsWith("net.minecraft.")) { + throw new IllegalStateException("Packet " + packetID + " has already been injected."); + } + + // Subclass the specific packet class + ex.setSuperclass(old); + ex.setCallbackType(ReadPacketModifier.class); + ex.setUseCache(false); + ex.setClassLoader(classLoader); + Class proxy = ex.createClass(); + + // Add a static reference + Enhancer.registerStaticCallbacks(proxy, new Callback[] { + new ReadPacketModifier(packetID, this) + }); + + try { + // Override values + putMethod.invoke(intHashMap, packetID, proxy); + previous.put(packetID, old); + registry.put(proxy, packetID); + overwritten.put(packetID, proxy); + return true; + + } catch (IllegalArgumentException e) { + throw new RuntimeException("Illegal argument.", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot access method.", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Exception occured in IntHashMap.put.", e); + } + } + + @SuppressWarnings("rawtypes") + public boolean removePacketHandler(int packetID) { + if (!hasPacketHandler(packetID)) + return false; + + Map registry = MinecraftRegistry.getPacketToID(); + Map previous = MinecraftRegistry.getPreviousPackets(); + Map overwritten = MinecraftRegistry.getOverwrittenPackets(); + + // Use the old class definition + try { + Class old = previous.get(packetID); + Class proxy = MinecraftRegistry.getPacketClassFromID(packetID); + + putMethod.invoke(intHashMap, packetID, old); + previous.remove(packetID); + registry.remove(proxy); + overwritten.remove(packetID); + return true; + + // Handle some problems + } catch (IllegalArgumentException e) { + return false; + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot access method.", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Exception occured in IntHashMap.put.", e); + } + } + + public boolean hasPacketHandler(int packetID) { + return MinecraftRegistry.getPreviousPackets().containsKey(packetID); + } + + public Set getPacketHandlers() { + return MinecraftRegistry.getPreviousPackets().keySet(); + } + + // Called from the ReadPacketModified monitor + PacketEvent packetRecieved(PacketContainer packet, DataInputStream input) { + + Player client = playerLookup.get(input); + PacketEvent event = PacketEvent.fromClient((Object) manager, packet, client); + + manager.invokePacketRecieving(event); + return event; + } + + @SuppressWarnings("rawtypes") + public void cleanupAll() { + Map overwritten = MinecraftRegistry.getOverwrittenPackets(); + Map previous = MinecraftRegistry.getPreviousPackets(); + + // Remove every packet handler + for (Integer id : previous.keySet()) { + removePacketHandler(id); + } + + overwritten.clear(); + previous.clear(); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/PlayerInjector.java b/ProtocolLib/src/com/comphenix/protocol/injector/PlayerInjector.java new file mode 100644 index 00000000..dcbefb72 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/PlayerInjector.java @@ -0,0 +1,261 @@ +package com.comphenix.protocol.injector; + +import java.io.DataInputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Set; + +import net.minecraft.server.EntityPlayer; +import net.minecraft.server.Packet; + +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.VolatileField; + +class PlayerInjector { + + // Cache previously retrieved fields + private static Field serverHandlerField; + private static Field networkManagerField; + private static Field inputField; + private static Field netHandlerField; + + // And methods + private static Method queueMethod; + private static Method processMethod; + + private Player player; + private boolean hasInitialized; + + // Reference to the player's network manager + private VolatileField networkManager; + + // The packet manager and filters + private PacketFilterManager manager; + private Set packetFilters; + + // Previous data input + private DataInputStream cachedInput; + + // Current net handler + private Object netHandler; + + public PlayerInjector(Player player, PacketFilterManager manager, Set packetFilters) throws IllegalAccessException { + this.player = player; + this.manager = manager; + this.packetFilters = packetFilters; + initialize(); + } + + private void initialize() throws IllegalAccessException { + + CraftPlayer craft = (CraftPlayer) player; + EntityPlayer notchEntity = craft.getHandle(); + + if (!hasInitialized) { + // Do this first, in case we encounter an exception + hasInitialized = true; + + // Retrieve the server handler + if (serverHandlerField == null) + serverHandlerField = FuzzyReflection.fromObject(notchEntity).getFieldByType(".*NetServerHandler"); + Object serverHandler = FieldUtils.readField(serverHandlerField, notchEntity); + + // Next, get the network manager + if (networkManagerField == null) + networkManagerField = FuzzyReflection.fromObject(serverHandler).getFieldByType(".*NetworkManager"); + networkManager = new VolatileField(networkManagerField, serverHandler); + + // And the queue method + if (queueMethod == null) + queueMethod = FuzzyReflection.fromClass(networkManagerField.getType()). + getMethodByParameters("queue", Packet.class ); + + // And the data input stream that we'll use to identify a player + if (inputField == null) + inputField = FuzzyReflection.fromObject(networkManager.getOldValue(), true). + getFieldByType("java\\.io\\.DataInputStream"); + } + } + + /** + * Retrieves the current net handler for this player. + * @return Current net handler. + * @throws IllegalAccessException Unable to find or retrieve net handler. + */ + private Object getNetHandler() throws IllegalAccessException { + + // What a mess + try { + if (netHandlerField == null) + netHandlerField = FuzzyReflection.fromClass(networkManagerField.getType(), true). + getFieldByType("net\\.minecraft\\.NetHandler"); + } catch (RuntimeException e1) { + try { + // Well, that sucks. Try just Minecraft objects then. + netHandlerField = FuzzyReflection.fromClass(networkManagerField.getType(), true). + getFieldByType(FuzzyReflection.MINECRAFT_OBJECT); + + } catch (RuntimeException e2) { + return new IllegalAccessException("Cannot locate net handler. " + e2.getMessage()); + } + } + + // Get the handler + if (netHandler != null) + netHandler = FieldUtils.readField(netHandlerField, networkManager.getOldValue(), true); + return netHandler; + } + + /** + * Processes the given packet as if it was transmitted by the current player. + * @param packet - packet to process. + * @throws IllegalAccessException If the reflection machinery failed. + * @throws InvocationTargetException If the underlying method caused an error. + */ + public void processPacket(Packet packet) throws IllegalAccessException, InvocationTargetException { + + Object netHandler = getNetHandler(); + + // Get the process method + if (processMethod == null) { + try { + processMethod = FuzzyReflection.fromClass(Packet.class). + getMethodByParameters("processPacket", netHandlerField.getType()); + } catch (RuntimeException e) { + throw new IllegalArgumentException("Cannot locate process packet method: " + e.getMessage()); + } + } + + // We're ready + try { + processMethod.invoke(packet, netHandler); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Method " + processMethod.getName() + " is not compatible."); + } catch (InvocationTargetException e) { + throw e; + } + } + + /** + * Send a packet to the client. + * @param packet - server packet to send. + * @param filtered - whether or not the packet will be filtered by our listeners. + * @param InvocationTargetException If an error occured when sending the packet. + */ + public void sendServerPacket(Packet packet, boolean filtered) throws InvocationTargetException { + Object networkDelegate = filtered ? networkManager.getValue() : networkManager.getOldValue(); + + if (networkDelegate != null) { + try { + // Note that invocation target exception is a wrapper for a checked exception + queueMethod.invoke(networkDelegate, packet); + + } catch (IllegalArgumentException e) { + throw e; + } catch (InvocationTargetException e) { + throw e; + } catch (IllegalAccessException e) { + throw new IllegalStateException("Unable to access queue method.", e); + } + } else { + throw new IllegalStateException("Unable to load network mananager. Cannot send packet."); + } + } + + public void injectManager() { + + if (networkManager != null) { + final Class networkInterface = networkManagerField.getType(); + final Object networkDelegate = networkManager.getOldValue(); + + // Create our proxy object + Object networkProxy = Proxy.newProxyInstance(networkInterface.getClassLoader(), + new Class[] { networkInterface }, new InvocationHandler() { + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // OH OH! The queue method! + if (method.equals(queueMethod)) { + Packet packet = (Packet) args[0]; + + if (packet != null) { + packet = handlePacketRecieved(packet); + + // A NULL packet indicate cancelling + if (packet != null) + args[0] = packet; + else + return null; + } + } + + // Delegate to our underlying class + try { + return method.invoke(networkDelegate, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + }); + + // Inject it, if we can. + networkManager.setValue(networkProxy); + } + } + + /** + * Allows a packet to be recieved by the listeners. + * @param packet - packet to recieve. + * @return The given packet, or the packet replaced by the listeners. + */ + Packet handlePacketRecieved(Packet packet) { + // Get the packet ID too + Integer id = MinecraftRegistry.getPacketToID().get(packet.getClass()); + + // Make sure we're listening + if (packetFilters.contains(id)) { + // A packet has been sent guys! + PacketContainer container = new PacketContainer(id, packet); + PacketEvent event = PacketEvent.fromServer(manager, container, player); + manager.invokePacketSending(event); + + // Cancelling is pretty simple. Just ignore the packet. + if (event.isCancelled()) + return null; + + // Right, remember to replace the packet again + return event.getPacket().getHandle(); + } + + return packet; + } + + public DataInputStream getInputStream(boolean cache) { + // Get the associated input stream + try { + if (cache && cachedInput != null) + return cachedInput; + + // Save to cache + cachedInput = (DataInputStream) FieldUtils.readField(inputField, networkManager.getOldValue(), true); + return cachedInput; + + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to read input stream.", e); + } + } + + public void cleanupAll() { + // Clean up + networkManager.revertValue(); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/ReadPacketModifier.java b/ProtocolLib/src/com/comphenix/protocol/injector/ReadPacketModifier.java new file mode 100644 index 00000000..a3c59f95 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/ReadPacketModifier.java @@ -0,0 +1,84 @@ +package com.comphenix.protocol.injector; + +import java.io.DataInputStream; +import java.lang.reflect.Method; +import java.util.WeakHashMap; + +import org.apache.commons.lang.ArrayUtils; + +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; + +import net.minecraft.server.Packet; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +class ReadPacketModifier implements MethodInterceptor { + + @SuppressWarnings("rawtypes") + private static Class[] parameters = { DataInputStream.class }; + + // Common for all packets of the same type + private PacketInjector packetInjector; + private int packetID; + + // Whether or not a packet has been cancelled + private static WeakHashMap override = new WeakHashMap(); + + public ReadPacketModifier(int packetID, PacketInjector packetInjector) { + this.packetID = packetID; + this.packetInjector = packetInjector; + } + + @Override + public Object intercept(Object thisObj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + + Object returnValue = null; + String methodName = method.getName(); + + // We always pass these down (otherwise, we'll end up with a infinite loop) + if (methodName.equals("hashCode") || methodName.equals("equals") || methodName.equals("toString")) { + return proxy.invokeSuper(thisObj, args); + } + + if (override.containsKey(thisObj)) { + Object overridenObject = override.get(thisObj); + + // Cancel EVERYTHING, including "processPacket" + if (overridenObject == null) + return null; + + returnValue = proxy.invokeSuper(overridenObject, args); + } else { + returnValue = proxy.invokeSuper(thisObj, args); + } + + // Is this a readPacketData method? + if (returnValue == null && + ArrayUtils.isEquals(method.getParameterTypes(), parameters)) { + + // We need this in order to get the correct player + DataInputStream input = (DataInputStream) args[0]; + + // Let the people know + PacketContainer container = new PacketContainer(packetID, (Packet) thisObj); + PacketEvent event = packetInjector.packetRecieved(container, input); + Packet result = event.getPacket().getHandle(); + + // Handle override + if (event != null) { + if (event.isCancelled()) { + override.put(thisObj, null); + } else if (!objectEquals(thisObj, result)) { + override.put(thisObj, result); + } + } + } + + return returnValue; + } + + private boolean objectEquals(Object a, Object b) { + return System.identityHashCode(a) != System.identityHashCode(b); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/StructureCache.java b/ProtocolLib/src/com/comphenix/protocol/injector/StructureCache.java new file mode 100644 index 00000000..9aab6ab2 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/StructureCache.java @@ -0,0 +1,46 @@ +package com.comphenix.protocol.injector; + +import java.util.HashMap; +import java.util.Map; + +import net.minecraft.server.Packet; + +import com.comphenix.protocol.reflect.StructureModifier; + +public class StructureCache { + // Structure modifiers + private static Map> structureModifiers = new HashMap>(); + + /** + * Creates an empty Minecraft packet of the given ID. + * @param id - packet ID. + * @return Created packet. + */ + public static Packet newPacket(int id) { + try { + return (Packet) MinecraftRegistry.getPacketClassFromID(id, true).newInstance(); + } catch (InstantiationException e) { + return null; + } catch (IllegalAccessException e) { + throw new RuntimeException("Access denied.", e); + } + } + + /** + * Retrieve a cached structure modifier for the given packet id. + * @param id - packet ID. + * @return A structure modifier. + */ + public static StructureModifier getStructure(int id) { + + StructureModifier result = structureModifiers.get(id); + + // Use the vanilla class definition + if (result == null) { + result = new StructureModifier(MinecraftRegistry.getPacketClassFromID(id, true)); + structureModifiers.put(id, result); + } + + return result; + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/EquivalentConverter.java b/ProtocolLib/src/com/comphenix/protocol/reflect/EquivalentConverter.java new file mode 100644 index 00000000..a55db066 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/EquivalentConverter.java @@ -0,0 +1,12 @@ +package com.comphenix.protocol.reflect; + +/** + * Interface that converts generic objects into types and back. + * + * @author Kristian + * @param The specific type. + */ +public interface EquivalentConverter { + public TType getSpecific(Object generic); + public Object getGeneric(TType specific); +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/FieldUtils.java b/ProtocolLib/src/com/comphenix/protocol/reflect/FieldUtils.java new file mode 100644 index 00000000..28b9ebf8 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/FieldUtils.java @@ -0,0 +1,473 @@ +package com.comphenix.protocol.reflect; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Modifier; +import java.util.Iterator; + +import org.apache.commons.lang.ClassUtils; + +/** + * Utilities for working with fields by reflection. Adapted and refactored from + * the dormant [reflect] Commons sandbox component. + *

+ * The ability is provided to break the scoping restrictions coded by the + * programmer. This can allow fields to be changed that shouldn't be. This + * facility should be used with care. + * + * @author Apache Software Foundation + * @author Matt Benson + * @since 2.5 + * @version $Id: FieldUtils.java 1057009 2011-01-09 19:48:06Z niallp $ + */ +@SuppressWarnings("rawtypes") +public class FieldUtils { + + /** + * FieldUtils instances should NOT be constructed in standard programming. + *

+ * This constructor is public to permit tools that require a JavaBean + * instance to operate. + */ + public FieldUtils() { + super(); + } + + /** + * Gets an accessible Field by name respecting scope. + * Superclasses/interfaces will be considered. + * + * @param cls the class to reflect, must not be null + * @param fieldName the field name to obtain + * @return the Field object + * @throws IllegalArgumentException if the class or field name is null + */ + public static Field getField(Class cls, String fieldName) { + Field field = getField(cls, fieldName, false); + MemberUtils.setAccessibleWorkaround(field); + return field; + } + + /** + * Gets an accessible Field by name breaking scope if + * requested. Superclasses/interfaces will be considered. + * + * @param cls the class to reflect, must not be null + * @param fieldName the field name to obtain + * @param forceAccess whether to break scope restrictions using the + * setAccessible method. False will + * only match public fields. + * @return the Field object + * @throws IllegalArgumentException if the class or field name is null + */ + public static Field getField(final Class cls, String fieldName, boolean forceAccess) { + if (cls == null) { + throw new IllegalArgumentException("The class must not be null"); + } + if (fieldName == null) { + throw new IllegalArgumentException("The field name must not be null"); + } + // Sun Java 1.3 has a bugged implementation of getField hence we write + // the + // code ourselves + + // getField() will return the Field object with the declaring class + // set correctly to the class that declares the field. Thus requesting + // the + // field on a subclass will return the field from the superclass. + // + // priority order for lookup: + // searchclass private/protected/package/public + // superclass protected/package/public + // private/different package blocks access to further superclasses + // implementedinterface public + + // check up the superclass hierarchy + for (Class acls = cls; acls != null; acls = acls.getSuperclass()) { + try { + Field field = acls.getDeclaredField(fieldName); + // getDeclaredField checks for non-public scopes as well + // and it returns accurate results + if (!Modifier.isPublic(field.getModifiers())) { + if (forceAccess) { + field.setAccessible(true); + } else { + continue; + } + } + return field; + } catch (NoSuchFieldException ex) { + // ignore + } + } + // check the public interface case. This must be manually searched for + // incase there is a public supersuperclass field hidden by a + // private/package + // superclass field. + Field match = null; + for (Iterator intf = ClassUtils.getAllInterfaces(cls).iterator(); intf.hasNext();) { + try { + Field test = ((Class) intf.next()).getField(fieldName); + if (match != null) { + throw new IllegalArgumentException("Reference to field " + fieldName + + " is ambiguous relative to " + cls + + "; a matching field exists on two or more implemented interfaces."); + } + match = test; + } catch (NoSuchFieldException ex) { + // ignore + } + } + return match; + } + + /** + * Read an accessible static Field. + * + * @param field to read + * @return the field value + * @throws IllegalArgumentException if the field is null or not static + * @throws IllegalAccessException if the field is not accessible + */ + public static Object readStaticField(Field field) throws IllegalAccessException { + return readStaticField(field, false); + } + + /** + * Read a static Field. + * + * @param field to read + * @param forceAccess whether to break scope restrictions using the + * setAccessible method. + * @return the field value + * @throws IllegalArgumentException if the field is null or not static + * @throws IllegalAccessException if the field is not made accessible + */ + public static Object readStaticField(Field field, boolean forceAccess) + throws IllegalAccessException { + if (field == null) { + throw new IllegalArgumentException("The field must not be null"); + } + if (!Modifier.isStatic(field.getModifiers())) { + throw new IllegalArgumentException("The field '" + field.getName() + "' is not static"); + } + return readField(field, (Object) null, forceAccess); + } + + /** + * Read the named public static field. Superclasses will be considered. + * + * @param cls the class to reflect, must not be null + * @param fieldName the field name to obtain + * @return the value of the field + * @throws IllegalArgumentException if the class or field name is null + * @throws IllegalAccessException if the field is not accessible + */ + public static Object readStaticField(Class cls, String fieldName) throws IllegalAccessException { + return readStaticField(cls, fieldName, false); + } + + /** + * Read the named static field. Superclasses will be considered. + * + * @param cls the class to reflect, must not be null + * @param fieldName the field name to obtain + * @param forceAccess whether to break scope restrictions using the + * setAccessible method. False will + * only match public fields. + * @return the Field object + * @throws IllegalArgumentException if the class or field name is null + * @throws IllegalAccessException if the field is not made accessible + */ + public static Object readStaticField(Class cls, String fieldName, boolean forceAccess) + throws IllegalAccessException { + Field field = getField(cls, fieldName, forceAccess); + if (field == null) { + throw new IllegalArgumentException("Cannot locate field " + fieldName + " on " + cls); + } + // already forced access above, don't repeat it here: + return readStaticField(field, false); + } + + /** + * Read an accessible Field. + * + * @param field the field to use + * @param target the object to call on, may be null for static fields + * @return the field value + * @throws IllegalArgumentException if the field is null + * @throws IllegalAccessException if the field is not accessible + */ + public static Object readField(Field field, Object target) throws IllegalAccessException { + return readField(field, target, false); + } + + /** + * Read a Field. + * + * @param field the field to use + * @param target the object to call on, may be null for static fields + * @param forceAccess whether to break scope restrictions using the + * setAccessible method. + * @return the field value + * @throws IllegalArgumentException if the field is null + * @throws IllegalAccessException if the field is not made accessible + */ + public static Object readField(Field field, Object target, boolean forceAccess) throws IllegalAccessException { + if (field == null) + throw new IllegalArgumentException("The field must not be null"); + + if (forceAccess && !field.isAccessible()) { + field.setAccessible(true); + } else { + MemberUtils.setAccessibleWorkaround(field); + } + return field.get(target); + } + + /** + * Read the named public field. Superclasses will be considered. + * + * @param target the object to reflect, must not be null + * @param fieldName the field name to obtain + * @return the value of the field + * @throws IllegalArgumentException if the class or field name is null + * @throws IllegalAccessException if the named field is not public + */ + public static Object readField(Object target, String fieldName) throws IllegalAccessException { + return readField(target, fieldName, false); + } + + /** + * Read the named field. Superclasses will be considered. + * + * @param target the object to reflect, must not be null + * @param fieldName the field name to obtain + * @param forceAccess whether to break scope restrictions using the + * setAccessible method. False will + * only match public fields. + * @return the field value + * @throws IllegalArgumentException if the class or field name is null + * @throws IllegalAccessException if the named field is not made accessible + */ + public static Object readField(Object target, String fieldName, boolean forceAccess) + throws IllegalAccessException { + if (target == null) { + throw new IllegalArgumentException("target object must not be null"); + } + Class cls = target.getClass(); + Field field = getField(cls, fieldName, forceAccess); + if (field == null) { + throw new IllegalArgumentException("Cannot locate field " + fieldName + " on " + cls); + } + // already forced access above, don't repeat it here: + return readField(field, target); + } + + /** + * Write a public static Field. + * + * @param field to write + * @param value to set + * @throws IllegalArgumentException if the field is null or not static + * @throws IllegalAccessException if the field is not public or is final + */ + public static void writeStaticField(Field field, Object value) throws IllegalAccessException { + writeStaticField(field, value, false); + } + + /** + * Write a static Field. + * + * @param field to write + * @param value to set + * @param forceAccess whether to break scope restrictions using the + * setAccessible method. False will + * only match public fields. + * @throws IllegalArgumentException if the field is null or not static + * @throws IllegalAccessException if the field is not made accessible or is + * final + */ + public static void writeStaticField(Field field, Object value, boolean forceAccess) + throws IllegalAccessException { + if (field == null) { + throw new IllegalArgumentException("The field must not be null"); + } + if (!Modifier.isStatic(field.getModifiers())) { + throw new IllegalArgumentException("The field '" + field.getName() + "' is not static"); + } + writeField(field, (Object) null, value, forceAccess); + } + + /** + * Write a named public static Field. Superclasses will be considered. + * + * @param cls Class on which the Field is to be found + * @param fieldName to write + * @param value to set + * @throws IllegalArgumentException if the field cannot be located or is not + * static + * @throws IllegalAccessException if the field is not public or is final + */ + public static void writeStaticField(Class cls, String fieldName, Object value) + throws IllegalAccessException { + writeStaticField(cls, fieldName, value, false); + } + + /** + * Write a named static Field. Superclasses will be considered. + * + * @param cls Class on which the Field is to be found + * @param fieldName to write + * @param value to set + * @param forceAccess whether to break scope restrictions using the + * setAccessible method. False will + * only match public fields. + * @throws IllegalArgumentException if the field cannot be located or is not + * static + * @throws IllegalAccessException if the field is not made accessible or is + * final + */ + public static void writeStaticField(Class cls, String fieldName, Object value, + boolean forceAccess) throws IllegalAccessException { + Field field = getField(cls, fieldName, forceAccess); + if (field == null) { + throw new IllegalArgumentException("Cannot locate field " + fieldName + " on " + cls); + } + // already forced access above, don't repeat it here: + writeStaticField(field, value); + } + + /** + * Write an accessible field. + * + * @param field to write + * @param target the object to call on, may be null for static fields + * @param value to set + * @throws IllegalArgumentException if the field is null + * @throws IllegalAccessException if the field is not accessible or is final + */ + public static void writeField(Field field, Object target, Object value) + throws IllegalAccessException { + writeField(field, target, value, false); + } + + /** + * Write a field. + * + * @param field to write + * @param target the object to call on, may be null for static fields + * @param value to set + * @param forceAccess whether to break scope restrictions using the + * setAccessible method. False will + * only match public fields. + * @throws IllegalArgumentException if the field is null + * @throws IllegalAccessException if the field is not made accessible or is + * final + */ + public static void writeField(Field field, Object target, Object value, boolean forceAccess) + throws IllegalAccessException { + if (field == null) { + throw new IllegalArgumentException("The field must not be null"); + } + if (forceAccess && !field.isAccessible()) { + field.setAccessible(true); + } else { + MemberUtils.setAccessibleWorkaround(field); + } + field.set(target, value); + } + + /** + * Write a public field. Superclasses will be considered. + * + * @param target the object to reflect, must not be null + * @param fieldName the field name to obtain + * @param value to set + * @throws IllegalArgumentException if target or + * fieldName is null + * @throws IllegalAccessException if the field is not accessible + */ + public static void writeField(Object target, String fieldName, Object value) + throws IllegalAccessException { + writeField(target, fieldName, value, false); + } + + /** + * Write a field. Superclasses will be considered. + * + * @param target the object to reflect, must not be null + * @param fieldName the field name to obtain + * @param value to set + * @param forceAccess whether to break scope restrictions using the + * setAccessible method. False will + * only match public fields. + * @throws IllegalArgumentException if target or + * fieldName is null + * @throws IllegalAccessException if the field is not made accessible + */ + public static void writeField(Object target, String fieldName, Object value, boolean forceAccess) + throws IllegalAccessException { + if (target == null) { + throw new IllegalArgumentException("target object must not be null"); + } + Class cls = target.getClass(); + Field field = getField(cls, fieldName, forceAccess); + if (field == null) { + throw new IllegalArgumentException("Cannot locate declared field " + cls.getName() + + "." + fieldName); + } + // already forced access above, don't repeat it here: + writeField(field, target, value); + } + + // Useful member methods + private static class MemberUtils { + + private static final int ACCESS_TEST = Modifier.PUBLIC | Modifier.PROTECTED + | Modifier.PRIVATE; + + public static void setAccessibleWorkaround(AccessibleObject o) { + if (o == null || o.isAccessible()) { + return; + } + Member m = (Member) o; + if (Modifier.isPublic(m.getModifiers()) + && isPackageAccess(m.getDeclaringClass().getModifiers())) { + try { + o.setAccessible(true); + } catch (SecurityException e) { // NOPMD + // ignore in favor of subsequent IllegalAccessException + } + } + } + + /** + * Returns whether a given set of modifiers implies package access. + * + * @param modifiers to test + * @return true unless package/protected/private modifier detected + */ + public static boolean isPackageAccess(int modifiers) { + return (modifiers & ACCESS_TEST) == 0; + } + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java b/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java new file mode 100644 index 00000000..2f729329 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java @@ -0,0 +1,218 @@ +package com.comphenix.protocol.reflect; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Pattern; + +import org.apache.commons.lang.ArrayUtils; + +/** + * Retrieves fields and methods by signature, not just name. + * + * @author Kristian + */ +public class FuzzyReflection { + + /** + * Matches a Minecraft object. + */ + public static String MINECRAFT_OBJECT = "net\\.minecraft(\\.\\w+)+"; + + // 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); + } + + /** + * Retrieves the underlying class. + */ + public Class getSource() { + return source; + } + + /** + * 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. + */ + 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 RuntimeException("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. + */ + public Method getMethodByParameters(String name, Class... args) { + + // Find the correct method to call + for (Method method : getMethods()) { + if (ArrayUtils.isEquals(method.getParameterTypes(), args)) { + return method; + } + } + + // That sucks + throw new RuntimeException("Unable to find " + name + " in " + source.getName()); + } + + /** + * Retrieves a field by name. + * @param nameRegex - regular expression that will match a field name. + * @return The first field to match the given expression. + */ + 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 RuntimeException("Unable to find a field with the pattern " + + nameRegex + " in " + source.getName()); + } + + /** + * Retrieves a field by type. + *

+ * Note that the type is matched using the full canonical representation, i.e.: + *

    + *
  • java.util.List
  • + *
  • net.comphenix.xp.ExperienceMod
  • + *
+ * @param typeRegex - regular expression that will match the field type. + * @return The first field with a type that matches the given regular expression. + */ + public Field getFieldByType(String typeRegex) { + + Pattern match = Pattern.compile(typeRegex); + + // Like above, only here we test the field type + for (Field field : getFields()) { + if (match.matcher(field.getType().getName()).matches()) { + return field; + } + } + + // Looks like we're outdated. Too bad. + throw new RuntimeException("Unable to find a field with the type " + + typeRegex + " in " + source.getName()); + } + + /** + * Retrieves all private and public fields in declared order (after JDK 1.5). + * @return Every field. + */ + public Set getFields() { + // 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 methods in declared order (after JDK 1.5). + * @return Every method. + */ + public Set getMethods() { + // We will only consider private methods in the declared class + if (forceAccess) + return setUnion(source.getDeclaredMethods(), source.getMethods()); + else + return setUnion(source.getMethods()); + } + + // Prevent duplicate fields + private static Set setUnion(T[]... array) { + Set result = new LinkedHashSet(); + + 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; + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/MethodUtils.java b/ProtocolLib/src/com/comphenix/protocol/reflect/MethodUtils.java new file mode 100644 index 00000000..324c823d --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/MethodUtils.java @@ -0,0 +1,1325 @@ +package com.comphenix.protocol.reflect; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.logging.Logger; + +import org.bukkit.Bukkit; + +/** + *

Utility reflection methods focussed on methods in general rather than properties in particular.

+ * + *

Known Limitations

+ *

Accessing Public Methods In A Default Access Superclass

+ *

There is an issue when invoking public methods contained in a default access superclass. + * Reflection locates these methods fine and correctly assigns them as public. + * However, an IllegalAccessException is thrown if the method is invoked.

+ * + *

MethodUtils contains a workaround for this situation. + * It will attempt to call setAccessible on this method. + * If this call succeeds, then the method can be invoked as normal. + * This call will only succeed when the application has sufficient security privilages. + * If this call fails then a warning will be logged and the method may fail.

+ * + * @author Craig R. McClanahan + * @author Ralph Schaer + * @author Chris Audley + * @author Rey François + * @author Gregor Raýman + * @author Jan Sorensen + * @author Robert Burrell Donkin + */ + +@SuppressWarnings("rawtypes") +public class MethodUtils { + + // --------------------------------------------------------- Private Methods + + /** + * Only log warning about accessibility work around once. + *

+ * Note that this is broken when this class is deployed via a shared + * classloader in a container, as the warning message will be emitted + * only once, not once per webapp. However making the warning appear + * once per webapp means having a map keyed by context classloader + * which introduces nasty memory-leak problems. As this warning is + * really optional we can ignore this problem; only one of the webapps + * will get the warning in its logs but that should be good enough. + */ + private static boolean loggedAccessibleWarning = false; + + /** + * Indicates whether methods should be cached for improved performance. + *

+ * Note that when this class is deployed via a shared classloader in + * a container, this will affect all webapps. However making this + * configurable per webapp would mean having a map keyed by context classloader + * which may introduce memory-leak problems. + */ + private static boolean CACHE_METHODS = true; + + /** An empty class array */ + private static final Class[] EMPTY_CLASS_PARAMETERS = new Class[0]; + /** An empty object array */ + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + /** + * Stores a cache of MethodDescriptor -> Method in a WeakHashMap. + *

+ * The keys into this map only ever exist as temporary variables within + * methods of this class, and are never exposed to users of this class. + * This means that the WeakHashMap is used only as a mechanism for + * limiting the size of the cache, ie a way to tell the garbage collector + * that the contents of the cache can be completely garbage-collected + * whenever it needs the memory. Whether this is a good approach to + * this problem is doubtful; something like the commons-collections + * LRUMap may be more appropriate (though of course selecting an + * appropriate size is an issue). + *

+ * This static variable is safe even when this code is deployed via a + * shared classloader because it is keyed via a MethodDescriptor object + * which has a Class as one of its members and that member is used in + * the MethodDescriptor.equals method. So two components that load the same + * class via different classloaders will generate non-equal MethodDescriptor + * objects and hence end up with different entries in the map. + */ + @SuppressWarnings("unchecked") + private static final Map cache = Collections.synchronizedMap(new WeakHashMap()); + + // --------------------------------------------------------- Public Methods + + /** + * Set whether methods should be cached for greater performance or not, + * default is true. + * + * @param cacheMethods true if methods should be + * cached for greater performance, otherwise false + * @since 1.8.0 + */ + public static synchronized void setCacheMethods(boolean cacheMethods) { + CACHE_METHODS = cacheMethods; + if (!CACHE_METHODS) { + clearCache(); + } + } + + /** + * Clear the method cache. + * @return the number of cached methods cleared + * @since 1.8.0 + */ + public static synchronized int clearCache() { + int size = cache.size(); + cache.clear(); + return size; + } + + /** + *

Invoke a named method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than invokeExactMethod(). + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeMethod(Object object,String methodName,Object [] args)}. + *

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param arg use this argument + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeMethod( + Object object, + String methodName, + Object arg) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + Object[] args = {arg}; + return invokeMethod(object, methodName, args); + + } + + + /** + *

Invoke a named method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link #invokeExactMethod(Object object,String methodName,Object [] args)}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeMethod(Object object,String methodName,Object [] args,Class[] parameterTypes)}. + *

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeMethod( + Object object, + String methodName, + Object[] args) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeMethod(object, methodName, args, parameterTypes); + + } + + + /** + *

Invoke a named method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link + * #invokeExactMethod(Object object,String methodName,Object [] args,Class[] parameterTypes)}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeMethod( + Object object, + String methodName, + Object[] args, + Class[] parameterTypes) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (parameterTypes == null) { + parameterTypes = EMPTY_CLASS_PARAMETERS; + } + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + + Method method = getMatchingAccessibleMethod( + object.getClass(), + methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on object: " + object.getClass().getName()); + } + return method.invoke(object, args); + } + + + /** + *

Invoke a method whose parameter type matches exactly the object + * type.

+ * + *

This is a convenient wrapper for + * {@link #invokeExactMethod(Object object,String methodName,Object [] args)}. + *

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param arg use this argument + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactMethod( + Object object, + String methodName, + Object arg) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + Object[] args = {arg}; + return invokeExactMethod(object, methodName, args); + + } + + + /** + *

Invoke a method whose parameter types match exactly the object + * types.

+ * + *

This uses reflection to invoke the method obtained from a call to + * getAccessibleMethod().

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactMethod( + Object object, + String methodName, + Object[] args) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeExactMethod(object, methodName, args, parameterTypes); + + } + + + /** + *

Invoke a method whose parameter types match exactly the parameter + * types given.

+ * + *

This uses reflection to invoke the method obtained from a call to + * getAccessibleMethod().

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactMethod( + Object object, + String methodName, + Object[] args, + Class[] parameterTypes) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + + if (parameterTypes == null) { + parameterTypes = EMPTY_CLASS_PARAMETERS; + } + + Method method = getAccessibleMethod( + object.getClass(), + methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on object: " + object.getClass().getName()); + } + return method.invoke(object, args); + + } + + /** + *

Invoke a static method whose parameter types match exactly the parameter + * types given.

+ * + *

This uses reflection to invoke the method obtained from a call to + * {@link #getAccessibleMethod(Class, String, Class[])}.

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + * @since 1.8.0 + */ + public static Object invokeExactStaticMethod( + Class objectClass, + String methodName, + Object[] args, + Class[] parameterTypes) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + + if (parameterTypes == null) { + parameterTypes = EMPTY_CLASS_PARAMETERS; + } + + Method method = getAccessibleMethod( + objectClass, + methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on class: " + objectClass.getName()); + } + return method.invoke(null, args); + + } + + /** + *

Invoke a named static method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link #invokeExactMethod(Object, String, Object[], Class[])}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeStaticMethod(Class objectClass,String methodName,Object [] args)}. + *

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param arg use this argument + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + * @since 1.8.0 + */ + public static Object invokeStaticMethod( + Class objectClass, + String methodName, + Object arg) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + Object[] args = {arg}; + return invokeStaticMethod (objectClass, methodName, args); + + } + + + /** + *

Invoke a named static method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link #invokeExactMethod(Object object,String methodName,Object [] args)}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeStaticMethod(Class objectClass,String methodName,Object [] args,Class[] parameterTypes)}. + *

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + * @since 1.8.0 + */ + public static Object invokeStaticMethod( + Class objectClass, + String methodName, + Object[] args) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeStaticMethod (objectClass, methodName, args, parameterTypes); + + } + + + /** + *

Invoke a named static method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link + * #invokeExactStaticMethod(Class objectClass,String methodName,Object [] args,Class[] parameterTypes)}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + * @since 1.8.0 + */ + public static Object invokeStaticMethod( + Class objectClass, + String methodName, + Object[] args, + Class[] parameterTypes) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (parameterTypes == null) { + parameterTypes = EMPTY_CLASS_PARAMETERS; + } + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + + Method method = getMatchingAccessibleMethod( + objectClass, + methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on class: " + objectClass.getName()); + } + return method.invoke(null, args); + } + + + /** + *

Invoke a static method whose parameter type matches exactly the object + * type.

+ * + *

This is a convenient wrapper for + * {@link #invokeExactStaticMethod(Class objectClass,String methodName,Object [] args)}. + *

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param arg use this argument + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + * @since 1.8.0 + */ + public static Object invokeExactStaticMethod( + Class objectClass, + String methodName, + Object arg) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + Object[] args = {arg}; + return invokeExactStaticMethod (objectClass, methodName, args); + + } + + + /** + *

Invoke a static method whose parameter types match exactly the object + * types.

+ * + *

This uses reflection to invoke the method obtained from a call to + * {@link #getAccessibleMethod(Class, String, Class[])}.

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + * @since 1.8.0 + */ + public static Object invokeExactStaticMethod( + Class objectClass, + String methodName, + Object[] args) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeExactStaticMethod(objectClass, methodName, args, parameterTypes); + + } + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) with given name and parameters. If no such method + * can be found, return null. + * This is just a convenient wrapper for + * {@link #getAccessibleMethod(Method method)}.

+ * + * @param clazz get method from this class + * @param methodName get method with this name + * @param parameterTypes with these parameters types + * @return The accessible method + */ + @SuppressWarnings("unchecked") + public static Method getAccessibleMethod( + Class clazz, + String methodName, + Class[] parameterTypes) { + + try { + MethodDescriptor md = new MethodDescriptor(clazz, methodName, parameterTypes, true); + // Check the cache first + Method method = getCachedMethod(md); + if (method != null) { + return method; + } + + method = getAccessibleMethod + (clazz, clazz.getMethod(methodName, parameterTypes)); + cacheMethod(md, method); + return method; + } catch (NoSuchMethodException e) { + return (null); + } + + } + + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) that implements the specified Method. If no such method + * can be found, return null.

+ * + * @param method The method that we wish to call + * @return The accessible method + */ + public static Method getAccessibleMethod(Method method) { + + // Make sure we have a method to check + if (method == null) { + return (null); + } + + return getAccessibleMethod(method.getDeclaringClass(), method); + + } + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) that implements the specified Method. If no such method + * can be found, return null.

+ * + * @param clazz The class of the object + * @param method The method that we wish to call + * @return The accessible method + * @since 1.8.0 + */ + public static Method getAccessibleMethod(Class clazz, Method method) { + + // Make sure we have a method to check + if (method == null) { + return (null); + } + + // If the requested method is not public we cannot call it + if (!Modifier.isPublic(method.getModifiers())) { + return (null); + } + + boolean sameClass = true; + if (clazz == null) { + clazz = method.getDeclaringClass(); + } else { + sameClass = clazz.equals(method.getDeclaringClass()); + if (!method.getDeclaringClass().isAssignableFrom(clazz)) { + throw new IllegalArgumentException(clazz.getName() + + " is not assignable from " + method.getDeclaringClass().getName()); + } + } + + // If the class is public, we are done + if (Modifier.isPublic(clazz.getModifiers())) { + if (!sameClass && !Modifier.isPublic(method.getDeclaringClass().getModifiers())) { + setMethodAccessible(method); // Default access superclass workaround + } + return (method); + } + + String methodName = method.getName(); + Class[] parameterTypes = method.getParameterTypes(); + + // Check the implemented interfaces and subinterfaces + method = + getAccessibleMethodFromInterfaceNest(clazz, + methodName, + parameterTypes); + + // Check the superclass chain + if (method == null) { + method = getAccessibleMethodFromSuperclass(clazz, + methodName, + parameterTypes); + } + + return (method); + + } + + + // -------------------------------------------------------- Private Methods + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) by scanning through the superclasses. If no such method + * can be found, return null.

+ * + * @param clazz Class to be checked + * @param methodName Method name of the method we wish to call + * @param parameterTypes The parameter type signatures + */ + @SuppressWarnings("unchecked") + private static Method getAccessibleMethodFromSuperclass + (Class clazz, String methodName, Class[] parameterTypes) { + + Class parentClazz = clazz.getSuperclass(); + while (parentClazz != null) { + if (Modifier.isPublic(parentClazz.getModifiers())) { + try { + return parentClazz.getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException e) { + return null; + } + } + parentClazz = parentClazz.getSuperclass(); + } + return null; + } + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) that implements the specified method, by scanning through + * all implemented interfaces and subinterfaces. If no such method + * can be found, return null.

+ * + *

There isn't any good reason why this method must be private. + * It is because there doesn't seem any reason why other classes should + * call this rather than the higher level methods.

+ * + * @param clazz Parent class for the interfaces to be checked + * @param methodName Method name of the method we wish to call + * @param parameterTypes The parameter type signatures + */ + @SuppressWarnings("unchecked") + private static Method getAccessibleMethodFromInterfaceNest + (Class clazz, String methodName, Class[] parameterTypes) { + + Method method = null; + + // Search up the superclass chain + for (; clazz != null; clazz = clazz.getSuperclass()) { + + // Check the implemented interfaces of the parent class + Class[] interfaces = clazz.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + + // Is this interface public? + if (!Modifier.isPublic(interfaces[i].getModifiers())) { + continue; + } + + // Does the method exist on this interface? + try { + method = interfaces[i].getDeclaredMethod(methodName, + parameterTypes); + } catch (NoSuchMethodException e) { + /* Swallow, if no method is found after the loop then this + * method returns null. + */ + } + if (method != null) { + return method; + } + + // Recursively check our parent interfaces + method = + getAccessibleMethodFromInterfaceNest(interfaces[i], + methodName, + parameterTypes); + if (method != null) { + return method; + } + + } + + } + + // We did not find anything + return (null); + + } + + /** + *

Find an accessible method that matches the given name and has compatible parameters. + * Compatible parameters mean that every method parameter is assignable from + * the given parameters. + * In other words, it finds a method with the given name + * that will take the parameters given.

+ * + *

This method is slightly undeterminstic since it loops + * through methods names and return the first matching method.

+ * + *

This method is used by + * {@link + * #invokeMethod(Object object,String methodName,Object [] args,Class[] parameterTypes)}. + * + *

This method can match primitive parameter by passing in wrapper classes. + * For example, a Boolean will match a primitive boolean + * parameter. + * + * @param clazz find method in this class + * @param methodName find method with this name + * @param parameterTypes find method with compatible parameters + * @return The accessible method + */ + @SuppressWarnings("unchecked") + public static Method getMatchingAccessibleMethod( + Class clazz, + String methodName, + Class[] parameterTypes) { + + MethodDescriptor md = new MethodDescriptor(clazz, methodName, parameterTypes, false); + Logger log = tryGetLogger(); + + // see if we can find the method directly + // most of the time this works and it's much faster + try { + // Check the cache first + Method method = getCachedMethod(md); + if (method != null) { + return method; + } + + method = clazz.getMethod(methodName, parameterTypes); + + setMethodAccessible(method); // Default access superclass workaround + + cacheMethod(md, method); + return method; + + } catch (NoSuchMethodException e) { /* SWALLOW */ } + + // search through all methods + int paramSize = parameterTypes.length; + Method bestMatch = null; + Method[] methods = clazz.getMethods(); + float bestMatchCost = Float.MAX_VALUE; + float myCost = Float.MAX_VALUE; + for (int i = 0, size = methods.length; i < size ; i++) { + if (methods[i].getName().equals(methodName)) { + + // compare parameters + Class[] methodsParams = methods[i].getParameterTypes(); + int methodParamSize = methodsParams.length; + if (methodParamSize == paramSize) { + boolean match = true; + for (int n = 0 ; n < methodParamSize; n++) { + if (!isAssignmentCompatible(methodsParams[n], parameterTypes[n])) { + match = false; + break; + } + } + + if (match) { + // get accessible version of method + Method method = getAccessibleMethod(clazz, methods[i]); + if (method != null) { + setMethodAccessible(method); // Default access superclass workaround + myCost = getTotalTransformationCost(parameterTypes,method.getParameterTypes()); + if ( myCost < bestMatchCost ) { + bestMatch = method; + bestMatchCost = myCost; + } + } + + if (log != null) { + //log.severe("Couldn't find accessible method."); + } + } + } + } + } + if ( bestMatch != null ){ + cacheMethod(md, bestMatch); + } else { + if (log != null) { + log.severe("No match found."); + } + } + + return bestMatch; + } + + /** + * Attempt to get the default logger from Bukkit. + * @return Bukkit default logger. + */ + private static Logger tryGetLogger() { + try { + return Bukkit.getLogger(); + } catch (Exception e) { + return null; + } + } + + /** + * Try to make the method accessible + * @param method The source arguments + */ + private static void setMethodAccessible(Method method) { + try { + // + // + // When a public class has a default access superclass + // with public methods, these methods are accessible. + // Calling them from compiled code works fine. + // + // Unfortunately, using reflection to invoke these methods + // seems to (wrongly) to prevent access even when the method + // modifer is public. + // + // The following workaround solves the problem but will only + // work from sufficiently privilages code. + // + // Better workarounds would be greatfully accepted. + // + if (!method.isAccessible()) { + method.setAccessible(true); + } + + } catch (SecurityException se) { + + if (!loggedAccessibleWarning) { + boolean vulnerableJVM = false; + try { + String specVersion = System.getProperty("java.specification.version"); + if (specVersion.charAt(0) == '1' && + (specVersion.charAt(2) == '0' || + specVersion.charAt(2) == '1' || + specVersion.charAt(2) == '2' || + specVersion.charAt(2) == '3')) { + + vulnerableJVM = true; + } + } catch (SecurityException e) { + // don't know - so display warning + vulnerableJVM = true; + } + if (vulnerableJVM && tryGetLogger() != null) { + tryGetLogger().info("Vulnerable JVM!"); + } + + loggedAccessibleWarning = true; + } + } + } + + /** + * Returns the sum of the object transformation cost for each class in the source + * argument list. + * @param srcArgs The source arguments + * @param destArgs The destination arguments + * @return The total transformation cost + */ + private static float getTotalTransformationCost(Class[] srcArgs, Class[] destArgs) { + + float totalCost = 0.0f; + for (int i = 0; i < srcArgs.length; i++) { + Class srcClass, destClass; + srcClass = srcArgs[i]; + destClass = destArgs[i]; + totalCost += getObjectTransformationCost(srcClass, destClass); + } + + return totalCost; + } + + /** + * Gets the number of steps required needed to turn the source class into the + * destination class. This represents the number of steps in the object hierarchy + * graph. + * @param srcClass The source class + * @param destClass The destination class + * @return The cost of transforming an object + */ + private static float getObjectTransformationCost(Class srcClass, Class destClass) { + float cost = 0.0f; + while (destClass != null && !destClass.equals(srcClass)) { + if (destClass.isInterface() && isAssignmentCompatible(destClass,srcClass)) { + // slight penalty for interface match. + // we still want an exact match to override an interface match, but + // an interface match should override anything where we have to get a + // superclass. + cost += 0.25f; + break; + } + cost++; + destClass = destClass.getSuperclass(); + } + + /* + * If the destination class is null, we've travelled all the way up to + * an Object match. We'll penalize this by adding 1.5 to the cost. + */ + if (destClass == null) { + cost += 1.5f; + } + + return cost; + } + + + /** + *

Determine whether a type can be used as a parameter in a method invocation. + * This method handles primitive conversions correctly.

+ * + *

In order words, it will match a Boolean to a boolean, + * a Long to a long, + * a Float to a float, + * a Integer to a int, + * and a Double to a double. + * Now logic widening matches are allowed. + * For example, a Long will not match a int. + * + * @param parameterType the type of parameter accepted by the method + * @param parameterization the type of parameter being tested + * + * @return true if the assignement is compatible. + */ + @SuppressWarnings("unchecked") + public static final boolean isAssignmentCompatible(Class parameterType, Class parameterization) { + // try plain assignment + if (parameterType.isAssignableFrom(parameterization)) { + return true; + } + + if (parameterType.isPrimitive()) { + // this method does *not* do widening - you must specify exactly + // is this the right behaviour? + Class parameterWrapperClazz = getPrimitiveWrapper(parameterType); + if (parameterWrapperClazz != null) { + return parameterWrapperClazz.equals(parameterization); + } + } + + return false; + } + + /** + * Gets the wrapper object class for the given primitive type class. + * For example, passing boolean.class returns Boolean.class + * @param primitiveType the primitive type class for which a match is to be found + * @return the wrapper type associated with the given primitive + * or null if no match is found + */ + public static Class getPrimitiveWrapper(Class primitiveType) { + // does anyone know a better strategy than comparing names? + if (boolean.class.equals(primitiveType)) { + return Boolean.class; + } else if (float.class.equals(primitiveType)) { + return Float.class; + } else if (long.class.equals(primitiveType)) { + return Long.class; + } else if (int.class.equals(primitiveType)) { + return Integer.class; + } else if (short.class.equals(primitiveType)) { + return Short.class; + } else if (byte.class.equals(primitiveType)) { + return Byte.class; + } else if (double.class.equals(primitiveType)) { + return Double.class; + } else if (char.class.equals(primitiveType)) { + return Character.class; + } else { + + return null; + } + } + + /** + * Gets the class for the primitive type corresponding to the primitive wrapper class given. + * For example, an instance of Boolean.class returns a boolean.class. + * @param wrapperType the + * @return the primitive type class corresponding to the given wrapper class, + * null if no match is found + */ + public static Class getPrimitiveType(Class wrapperType) { + // does anyone know a better strategy than comparing names? + if (Boolean.class.equals(wrapperType)) { + return boolean.class; + } else if (Float.class.equals(wrapperType)) { + return float.class; + } else if (Long.class.equals(wrapperType)) { + return long.class; + } else if (Integer.class.equals(wrapperType)) { + return int.class; + } else if (Short.class.equals(wrapperType)) { + return short.class; + } else if (Byte.class.equals(wrapperType)) { + return byte.class; + } else if (Double.class.equals(wrapperType)) { + return double.class; + } else if (Character.class.equals(wrapperType)) { + return char.class; + } else { + return null; + } + } + + /** + * Find a non primitive representation for given primitive class. + * + * @param clazz the class to find a representation for, not null + * @return the original class if it not a primitive. Otherwise the wrapper class. Not null + */ + public static Class toNonPrimitiveClass(Class clazz) { + if (clazz.isPrimitive()) { + Class primitiveClazz = MethodUtils.getPrimitiveWrapper(clazz); + // the above method returns + if (primitiveClazz != null) { + return primitiveClazz; + } else { + return clazz; + } + } else { + return clazz; + } + } + + + /** + * Return the method from the cache, if present. + * + * @param md The method descriptor + * @return The cached method + */ + private static Method getCachedMethod(MethodDescriptor md) { + if (CACHE_METHODS) { + Reference methodRef = (Reference)cache.get(md); + if (methodRef != null) { + return (Method)methodRef.get(); + } + } + return null; + } + + /** + * Add a method to the cache. + * + * @param md The method descriptor + * @param method The method to cache + */ + @SuppressWarnings("unchecked") + private static void cacheMethod(MethodDescriptor md, Method method) { + if (CACHE_METHODS) { + if (method != null) { + cache.put(md, new WeakReference(method)); + } + } + } + + /** + * Represents the key to looking up a Method by reflection. + */ + private static class MethodDescriptor { + private Class cls; + private String methodName; + private Class[] paramTypes; + private boolean exact; + private int hashCode; + + /** + * The sole constructor. + * + * @param cls the class to reflect, must not be null + * @param methodName the method name to obtain + * @param paramTypes the array of classes representing the paramater types + * @param exact whether the match has to be exact. + */ + public MethodDescriptor(Class cls, String methodName, Class[] paramTypes, boolean exact) { + if (cls == null) { + throw new IllegalArgumentException("Class cannot be null"); + } + if (methodName == null) { + throw new IllegalArgumentException("Method Name cannot be null"); + } + if (paramTypes == null) { + paramTypes = EMPTY_CLASS_PARAMETERS; + } + + this.cls = cls; + this.methodName = methodName; + this.paramTypes = paramTypes; + this.exact= exact; + + this.hashCode = methodName.length(); + } + /** + * Checks for equality. + * @param obj object to be tested for equality + * @return true, if the object describes the same Method. + */ + public boolean equals(Object obj) { + if (!(obj instanceof MethodDescriptor)) { + return false; + } + MethodDescriptor md = (MethodDescriptor)obj; + + return ( + exact == md.exact && + methodName.equals(md.methodName) && + cls.equals(md.cls) && + java.util.Arrays.equals(paramTypes, md.paramTypes) + ); + } + /** + * Returns the string length of method name. I.e. if the + * hashcodes are different, the objects are different. If the + * hashcodes are the same, need to use the equals method to + * determine equality. + * @return the string length of method name. + */ + public int hashCode() { + return hashCode; + } + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java b/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java new file mode 100644 index 00000000..62faff1a --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java @@ -0,0 +1,218 @@ +package com.comphenix.protocol.reflect; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.base.Function; + +import net.minecraft.server.Packet; + +@SuppressWarnings("rawtypes") +public class StructureModifier { + + // Object and its type + private Class targetType; + private Object target; + + // Converter. May be NULL. + private EquivalentConverter converter; + + // The fields to read in order + private Class fieldType; + private List data = new ArrayList(); + + // Cache of previous types + private Map subtypeCache; + + public StructureModifier(Class targetType) { + this(targetType, Object.class, getFields(targetType), null, new HashMap()); + } + + private StructureModifier(Class targetType, Class fieldType, List data, + EquivalentConverter converter, Map subTypeCache) { + this.targetType = targetType; + this.fieldType = fieldType; + this.data = data; + this.converter = converter; + this.subtypeCache = subTypeCache; + } + + private StructureModifier(StructureModifier other, Object target) { + this(other.targetType, other.fieldType, other.data, other.converter, other.subtypeCache); + this.target = target; + } + + /** + * Reads the value of a field given its index. + * @param fieldIndex - index of the field. + * @return Value of the field. + * @throws IllegalAccessException If we're unable to read the field due to a security limitation. + */ + @SuppressWarnings("unchecked") + public TField read(int fieldIndex) throws IllegalAccessException { + if (fieldIndex < 0 || fieldIndex >= data.size()) + throw new IllegalArgumentException("Field index must be within 0 - count"); + if (target == null) + throw new IllegalStateException("Cannot read from a NULL target."); + + Object result = FieldUtils.readField(data.get(fieldIndex), target, true); + + // Use the converter, if we have it + if (converter != null) + return converter.getSpecific(result); + else + return (TField) result; + } + + /** + * Writes the value of a field given its index. + * @param fieldIndex - index of the field. + * @param value - new value of the field. + * @return This structure modifier - for chaining. + * @throws IllegalAccessException If we're unable to write to the field due to a security limitation. + */ + public StructureModifier write(int fieldIndex, TField value) throws IllegalAccessException { + if (fieldIndex < 0 || fieldIndex >= data.size()) + throw new IllegalArgumentException("Field index must be within 0 - count"); + if (target == null) + throw new IllegalStateException("Cannot write to a NULL target."); + + // Use the converter, if it exists + Object obj = converter != null ? converter.getGeneric(value) : value; + FieldUtils.writeField(data.get(fieldIndex), target, obj, true); + + // Make this method chainable + return this; + } + + /** + * Correctly modifies the value of a field. + * @param fieldIndex - index of the field to modify. + * @param select - the function that modifies the field value. + * @return This structure modifier - for chaining. + * @throws IllegalAccessException + */ + public StructureModifier modify(int fieldIndex, Function select) throws IllegalAccessException { + TField value = read(fieldIndex); + return write(fieldIndex, select.apply(value)); + } + + /** + * Retrieves a structure modifier that only reads and writes fields of a given type. + * @param fieldType - the type, or supertype, of every field to modify. + * @return A structure modifier for fields of this type. + */ + public StructureModifier withType(Class fieldType) { + return withType(fieldType, null); + } + + /** + * Retrieves a structure modifier that only reads and writes fields of a given type. + * @param fieldType - the type, or supertype, of every field to modify. + * @param converter - converts objects into the given type. + * @return A structure modifier for fields of this type. + */ + @SuppressWarnings("unchecked") + public StructureModifier withType(Class fieldType, EquivalentConverter converter) { + + StructureModifier result = subtypeCache.get(fieldType); + + if (fieldType.equals(this.fieldType)) { + + // We're dealing with the exact field type. + return withConverter(converter); + + } else if (result == null) { + List filtered = new ArrayList(); + + for (Field field : data) { + if (fieldType.isAssignableFrom(field.getType())) { + filtered.add(field); + } + } + + // Cache structure modifiers + result = new StructureModifier(targetType, fieldType, filtered, + converter, new HashMap()); + subtypeCache.put(fieldType, result); + } + + // Add the target too + return result.withTarget(target); + } + + /** + * Retrieves the common type of each field. + * @return Common type of each field. + */ + public Class getFieldType() { + return fieldType; + } + + /** + * Retrieves the type of the object we're modifying. + * @return Type of the object. + */ + public Class getTargetType() { + return targetType; + } + + /** + * Retrieves the object we're currently modifying. + * @return Object we're modifying. + */ + public Object getTarget() { + return target; + } + + /** + * Retrieve the number of readable types. + * @return Readable types. + */ + public int size() { + return data.size(); + } + + /** + * Retrieves a structure modifier of the same type for a different object target. + * @param target - different target of the same type. + * @return Structure modifier with the new target. + */ + public StructureModifier withTarget(Object target) { + return new StructureModifier(this, target); + } + + /** + * Retrieves a structure modifier with the same type and target, but using a new object converter. + * @param converter- the object converter to use. + * @return Structure modifier with the new converter. + */ + @SuppressWarnings("unchecked") + private StructureModifier withConverter(EquivalentConverter converter) { + StructureModifier copy = new StructureModifier(this, target); + + copy.converter = converter; + return copy; + } + + // Used to filter out irrelevant fields + private static List getFields(Class type) { + List result = new ArrayList(); + + // Retrieve every private and public field + for (Field field : FuzzyReflection.fromClass(type, true).getFields()) { + int mod = field.getModifiers(); + + // Ignore static, final and "abstract packet" fields + if (!Modifier.isFinal(mod) && !Modifier.isStatic(mod) && !field.getDeclaringClass().equals(Packet.class)) { + result.add(field); + } + } + + return result; + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/VolatileField.java b/ProtocolLib/src/com/comphenix/protocol/reflect/VolatileField.java new file mode 100644 index 00000000..b6259332 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/VolatileField.java @@ -0,0 +1,158 @@ +package com.comphenix.protocol.reflect; + +import java.lang.reflect.Field; + + +/** + * Represents a field that will revert to its original state when this class is garbaged collected. + * + * @author Kristian + */ +public class VolatileField { + + private Field field; + private Object container; + + // The current and previous values + private Object previous; + private Object current; + + // Whether or not we must reset or load + private boolean previousLoaded; + private boolean currentSet; + + // Whether or not to break access restrictions + private boolean forceAccess; + + /** + * Initializes a volatile field with an associated object. + * @param field - the field. + * @param container - the object this field belongs to. + */ + public VolatileField(Field field, Object container) { + this.field = field; + this.container = container; + } + + /** + * Initializes a volatile field with an associated object. + * @param field - the field. + * @param container - the object this field belongs to. + * @param forceAccess - whether or not to override any scope restrictions. + */ + public VolatileField(Field field, Object container, boolean forceAccess) { + this.field = field; + this.container = container; + this.forceAccess = forceAccess; + } + + /** + * Retrieves the current field. + * @return The stored field. + */ + public Field getField() { + return field; + } + + /** + * Retrieves the object the field is stored. + * @return The reference object. + */ + public Object getContainer() { + return container; + } + + /** + * 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; + } + + /** + * Retrieves the current field value. + * @return The current field value. + */ + public Object getValue() { + // Retrieve the correct value + if (!currentSet) { + ensureLoaded(); + return previous; + } else { + return current; + } + } + + /** + * Retrieves the field value before the previous setValue(), unless saveValue() has been called. + * @return Previous value. + */ + public Object getOldValue() { + ensureLoaded(); + return previous; + } + + /** + * Sets the current value. This will be reverted unless saveValue() is called. + * @param newValue - new field value. + */ + public void setValue(Object newValue) { + + // Remember to safe the previous value + ensureLoaded(); + + try { + FieldUtils.writeField(field, container, newValue, forceAccess); + current = newValue; + currentSet = true; + + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to read field " + field.getName(), e); + } + } + + /** + * Ensure that the current value is still set after this class has been garbaged collected. + */ + public void saveValue() { + previous = current; + currentSet = false; + } + + /** + * Revert to the previously set value. + */ + public void revertValue() { + // Reset value. + if (currentSet) { + setValue(previous); + currentSet = false; + } + } + + private void ensureLoaded() { + // Load the value if we haven't already + if (!previousLoaded) { + try { + previous = FieldUtils.readField(field, container, forceAccess); + previousLoaded = true; + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to read field " + field.getName(), e); + } + } + } + + @Override + protected void finalize() throws Throwable { + revertValue(); + } +} diff --git a/ProtocolLib/src/plugin.yml b/ProtocolLib/src/plugin.yml new file mode 100644 index 00000000..aa0fa29f --- /dev/null +++ b/ProtocolLib/src/plugin.yml @@ -0,0 +1,8 @@ +name: ProtocolLib +version: 1.0.0 +description: Provides read/write access to the Minecraft protocol. +author: Comphenix +website: http://www.comphenix.net/ProtocolLib + +main: com.comphenix.protocol.ProtocolLibrary +database: false \ No newline at end of file diff --git a/ProtocolLib/test/com/comphenix/protocol/reflect/StructureModifierTest.java b/ProtocolLib/test/com/comphenix/protocol/reflect/StructureModifierTest.java new file mode 100644 index 00000000..d3168d87 --- /dev/null +++ b/ProtocolLib/test/com/comphenix/protocol/reflect/StructureModifierTest.java @@ -0,0 +1,24 @@ +package com.comphenix.protocol.reflect; + +import static org.junit.Assert.*; + +import net.minecraft.server.Packet103SetSlot; + +import org.junit.Test; + +import com.comphenix.protocol.reflect.StructureModifier; + +public class StructureModifierTest { + + @Test + public void test() throws IllegalAccessException { + + Packet103SetSlot move = new Packet103SetSlot(); + StructureModifier modifier = new StructureModifier(Packet103SetSlot.class); + + move.a = 1; + int value = (Integer) modifier.withTarget(move).withType(int.class).read(0); + + assertEquals(1, value); + } +}