diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java index 373c7d98..eff03790 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java @@ -17,8 +17,6 @@ package com.comphenix.protocol.events; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; @@ -41,6 +39,7 @@ import com.comphenix.protocol.injector.StructureCache; import com.comphenix.protocol.reflect.EquivalentConverter; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.cloning.AggregateCloner; import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.wrappers.BukkitConverters; import com.comphenix.protocol.wrappers.ChunkPosition; @@ -370,35 +369,8 @@ public class PacketContainer implements Serializable { * @return A deep copy of the current packet. */ public PacketContainer deepClone() { - ObjectOutputStream output = null; - ObjectInputStream input = null; - - try { - // Use a small buffer of 32 bytes initially. - ByteArrayOutputStream bufferOut = new ByteArrayOutputStream(); - output = new ObjectOutputStream(bufferOut); - output.writeObject(this); - - ByteArrayInputStream bufferIn = new ByteArrayInputStream(bufferOut.toByteArray()); - input = new ObjectInputStream(bufferIn); - return (PacketContainer) input.readObject(); - - } catch (IOException e) { - throw new IllegalStateException("Unexpected error occured during object cloning.", e); - } catch (ClassNotFoundException e) { - // Cannot happen - throw new IllegalStateException("Unexpected failure with serialization.", e); - } finally { - try { - if (output != null) - output.close(); - if (input != null) - input.close(); - - } catch (IOException e) { - // STOP IT - } - } + Object clonedPacket = AggregateCloner.DEFAULT.clone(getHandle()); + return new PacketContainer(getID(), clonedPacket); } private void writeObject(ObjectOutputStream output) throws IOException { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/MinecraftRegistry.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/MinecraftRegistry.java index 5cdddd7c..7ad014cb 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/MinecraftRegistry.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/MinecraftRegistry.java @@ -182,6 +182,22 @@ class MinecraftRegistry { throw new IllegalArgumentException("The packet ID " + packetID + " is not registered."); } + /** + * Retrieve the packet ID of a given packet. + * @param packet - the type of packet to check. + * @return The ID of the given packet. + * @throws IllegalArgumentException If this is not a valid packet. + */ + public static int getPacketID(Class packet) { + if (packet == null) + throw new IllegalArgumentException("Packet type class cannot be NULL."); + if (!MinecraftReflection.getPacketClass().isAssignableFrom(packet)) + throw new IllegalArgumentException("Type must be a packet."); + + // The registry contains both the overridden and original packets + return getPacketToID().get(packet); + } + /** * Find the first superclass that is not a CBLib proxy object. * @param clazz - the class whose hierachy we're going to search through. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/StructureCache.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/StructureCache.java index bc4f7aa2..b76f2179 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/StructureCache.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/StructureCache.java @@ -64,6 +64,27 @@ public class StructureCache { return getStructure(id, true); } + /** + * Retrieve a cached structure modifier given a packet type. + * @param packetType - packet type. + * @return A structure modifier. + */ + public static StructureModifier getStructure(Class packetType) { + // Compile structures by default + return getStructure(packetType, true); + } + + /** + * Retrieve a cached structure modifier given a packet type. + * @param packetType - packet type. + * @param compile - whether or not to asynchronously compile the structure modifier. + * @return A structure modifier. + */ + public static StructureModifier getStructure(Class packetType, boolean compile) { + // Get the ID from the class + return getStructure(MinecraftRegistry.getPacketID(packetType), compile); + } + /** * Retrieve a cached structure modifier for the given packet id. * @param id - packet ID. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/ObjectWriter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/ObjectWriter.java index 8e7e1d1f..82224912 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/ObjectWriter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/ObjectWriter.java @@ -17,21 +17,63 @@ package com.comphenix.protocol.reflect; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import com.comphenix.protocol.injector.StructureCache; +import com.comphenix.protocol.reflect.cloning.Cloner; +import com.comphenix.protocol.reflect.cloning.IdentityCloner; +import com.comphenix.protocol.utility.MinecraftReflection; + /** * Can copy an object field by field. * * @author Kristian */ public class ObjectWriter { - // Cache structure modifiers @SuppressWarnings("rawtypes") private static ConcurrentMap> cache = new ConcurrentHashMap>(); + /** + * The default value cloner to use. + */ + private static final Cloner DEFAULT_CLONER = new IdentityCloner(); + + /** + * Retrieve a usable structure modifier for the given object type. + *

+ * Will attempt to reuse any other structure modifiers we have cached. + * @param type - the type of the object we are modifying. + * @return A structure modifier for the given type. + */ + private static StructureModifier getModifier(Class type) { + Class packetClass = MinecraftReflection.getPacketClass(); + + // Handle subclasses of the packet class with our custom structure cache + if (!type.equals(packetClass) && packetClass.isAssignableFrom(type)) { + // Delegate to our already existing registry of structure modifiers + return StructureCache.getStructure(type); + } + + StructureModifier modifier = cache.get(type); + + // Create the structure modifier if we haven't already + if (modifier == null) { + StructureModifier value = new StructureModifier(type, null, false); + modifier = cache.putIfAbsent(type, value); + + if (modifier == null) + modifier = value; + } + + // And we're done + return modifier; + } + /** * Copy every field in object A to object B. Each value is copied directly, and is not cloned. *

@@ -41,22 +83,31 @@ public class ObjectWriter { * @param commonType - type containing each field to copy. */ public static void copyTo(Object source, Object destination, Class commonType) { - + // Note that we indicate that public fields will be copied the first time around + copyToInternal(source, destination, commonType, DEFAULT_CLONER, true); + } + + /** + * Copy every field in object A to object B. Each value is copied using the supplied cloner. + *

+ * The two objects must have the same number of fields of the same type. + * @param source - fields to copy. + * @param destination - fields to copy to. + * @param commonType - type containing each field to copy. + * @param valueCloner - a object responsible for copying the content of each field. + */ + public static void copyTo(Object source, Object destination, Class commonType, Cloner valueCloner) { + copyToInternal(source, destination, commonType, valueCloner, true); + } + + // Internal method that will actually implement the recursion + private static void copyToInternal(Object source, Object destination, Class commonType, Cloner valueCloner, boolean copyPublic) { if (source == null) throw new IllegalArgumentException("Source cannot be NULL"); if (destination == null) throw new IllegalArgumentException("Destination cannot be NULL"); - StructureModifier modifier = cache.get(commonType); - - // Create the structure modifier if we haven't already - if (modifier == null) { - StructureModifier value = new StructureModifier(commonType, null, false); - modifier = cache.putIfAbsent(commonType, value); - - if (modifier == null) - modifier = value; - } + StructureModifier modifier = getModifier(commonType); // Add target StructureModifier modifierSource = modifier.withTarget(source); @@ -65,20 +116,21 @@ public class ObjectWriter { // Copy every field try { for (int i = 0; i < modifierSource.size(); i++) { - if (!modifierDest.isReadOnly(i)) { - Object value = modifierSource.read(i); - modifierDest.write(i, value); - } + Field field = modifierSource.getField(i); + int mod = field.getModifiers(); - // System.out.println(String.format("Writing value %s to %s", - // value, modifier.getFields().get(i).getName())); + // Skip static fields. We also get the "public" field fairly often, so we'll skip that. + if (!Modifier.isStatic(mod) && (!Modifier.isPublic(mod) || copyPublic)) { + Object value = modifierSource.read(i); + modifierDest.write(i, valueCloner.clone(value)); + } } // Copy private fields underneath Class superclass = commonType.getSuperclass(); if (!superclass.equals(Object.class)) { - copyTo(source, destination, superclass); + copyToInternal(source, destination, superclass, valueCloner, false); } } catch (FieldAccessException e) { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java index 87a309fa..9d90d0b8 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java @@ -201,10 +201,16 @@ public class StructureModifier { * @return TRUE if the field by the given index is read-only, FALSE otherwise. */ public boolean isReadOnly(int fieldIndex) { - if (fieldIndex < 0 || fieldIndex >= data.size()) - throw new IllegalArgumentException("Index parameter is not within [0 - " + data.size() + ")"); - - return Modifier.isFinal(data.get(fieldIndex).getModifiers()); + return Modifier.isFinal(getField(fieldIndex).getModifiers()); + } + + /** + * Determine if a given field is public or not. + * @param fieldIndex - field index. + * @return TRUE if the field is public, FALSE otherwise. + */ + public boolean isPublic(int fieldIndex) { + return Modifier.isPublic(getField(fieldIndex).getModifiers()); } /** @@ -499,6 +505,19 @@ public class StructureModifier { return ImmutableList.copyOf(data); } + /** + * Retrieve a field by index. + * @param fieldIndex - index of the field to retrieve. + * @return The field represented with the given index. + * @throws IllegalArgumentException If no field with the given index can be found. + */ + public Field getField(int fieldIndex) { + if (fieldIndex < 0 || fieldIndex >= data.size()) + throw new IllegalArgumentException("Index parameter is not within [0 - " + data.size() + ")"); + + return data.get(fieldIndex); + } + /** * Retrieve every value stored in the fields of the current type. * @return Every field value. @@ -560,4 +579,6 @@ public class StructureModifier { return result; } + + } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/AggregateCloner.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/AggregateCloner.java new file mode 100644 index 00000000..233afbbb --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/AggregateCloner.java @@ -0,0 +1,236 @@ +package com.comphenix.protocol.reflect.cloning; + +import java.lang.ref.WeakReference; +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nullable; + +import com.comphenix.protocol.reflect.instances.DefaultInstances; +import com.comphenix.protocol.reflect.instances.ExistingGenerator; +import com.comphenix.protocol.reflect.instances.InstanceProvider; +import com.google.common.base.Function; +import com.google.common.collect.Lists; + +/** + * Implements a cloning procedure by trying multiple methods in turn until one is successful. + * + * @author Kristian + */ +public class AggregateCloner implements Cloner { + public static class BuilderParameters { + // Can only be modified by the builder + private InstanceProvider instanceProvider; + private Cloner aggregateCloner; + + // Used to construct the different types + private InstanceProvider typeConstructor; + + private BuilderParameters() { + // Only allow inner classes to construct it. + } + + /** + * Retrieve the instance provider last set in the builder. + * @return Current instance provider. + */ + public InstanceProvider getInstanceProvider() { + return instanceProvider; + } + + /** + * Retrieve the aggregate cloner that is being built. + * @return The parent cloner. + */ + public Cloner getAggregateCloner() { + return aggregateCloner; + } + } + + public static class Builder { + private List> factories = Lists.newArrayList(); + private BuilderParameters parameters; + + /** + * Create a new aggregate builder. + */ + public Builder() { + this.parameters = new BuilderParameters(); + } + + /** + * Set the instance provider supplied to all cloners in this builder. + * @param provider - new instance provider. + * @return The current builder. + */ + public Builder instanceProvider(InstanceProvider provider) { + this.parameters.instanceProvider = provider; + return this; + } + + /** + * Add the next cloner that will be considered in turn. + * @param type - the type of the next cloner. + * @return This builder. + */ + public Builder andThen(final Class type) { + // Use reflection to generate a factory on the fly + return orCloner(new Function() { + @Override + public Cloner apply(@Nullable BuilderParameters param) { + Object result = param.typeConstructor.create(type); + + if (result == null) { + throw new IllegalStateException("Constructed NULL instead of " + type); + } + + if (type.isAssignableFrom(result.getClass())) + return (Cloner) result; + else + throw new IllegalStateException("Constructed " + result.getClass() + " instead of " + type); + } + }); + } + + /** + * Add the next cloner that will be considered in turn. + * @param factory - factory constructing the next cloner. + * @return This builder. + */ + public Builder orCloner(Function factory) { + factories.add(factory); + return this; + } + + /** + * Build a new aggregate cloner using the supplied values. + * @return A new aggregate cloner. + */ + public AggregateCloner build() { + AggregateCloner newCloner = new AggregateCloner(); + + // The parameters we will pass to our cloners + Cloner paramCloner = new NullableCloner(newCloner); + InstanceProvider paramProvider = parameters.instanceProvider; + + // Initialize parameters + parameters.aggregateCloner = paramCloner; + parameters.typeConstructor = DefaultInstances.fromArray( + ExistingGenerator.fromObjectArray(new Object[] { paramCloner, paramProvider }) + ); + + // Build every cloner in the correct order + List cloners = Lists.newArrayList(); + + for (int i = 0; i < factories.size(); i++) { + Cloner cloner = factories.get(i).apply(parameters); + + // See if we were successful + if (cloner != null) + cloners.add(cloner); + else + throw new IllegalArgumentException( + String.format("Cannot create cloner from %s (%s)", factories.get(i), i) + ); + } + + // We're done + newCloner.setCloners(cloners); + return newCloner; + } + } + + /** + * Represents a default aggregate cloner. + */ + public static final AggregateCloner DEFAULT = newBuilder(). + instanceProvider(DefaultInstances.DEFAULT). + andThen(BukkitCloner.class). + andThen(ImmutableDetector.class). + andThen(CollectionCloner.class). + andThen(FieldCloner.class). + build(); + + // List of clone methods + private List cloners; + + private WeakReference lastObject; + private int lastResult; + + /** + * Begins constructing a new aggregate cloner. + * @return A builder for a new aggregate cloner. + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Construct a new, empty aggregate cloner. + */ + private AggregateCloner() { + // Only used by our builder above. + } + + /** + * Retrieves a view of the current list of cloners. + * @return Current cloners. + */ + public List getCloners() { + return Collections.unmodifiableList(cloners); + } + + /** + * Set the cloners that will be used. + * @param cloners - the cloners that will be used. + */ + private void setCloners(Iterable cloners) { + this.cloners = Lists.newArrayList(cloners); + } + + @Override + public boolean canClone(Object source) { + // Optimize a bit + lastResult = getFirstCloner(source); + lastObject = new WeakReference(source); + return lastResult >= 0 && lastResult < cloners.size(); + } + + /** + * Retrieve the index of the first cloner capable of cloning the given object. + *

+ * Returns an invalid index if no cloner is able to clone the object. + * @param source - the object to clone. + * @return The index of the cloner object. + */ + private int getFirstCloner(Object source) { + for (int i = 0; i < cloners.size(); i++) { + if (cloners.get(i).canClone(source)) + return i; + } + + return cloners.size(); + } + + @Override + public Object clone(Object source) { + if (source == null) + throw new IllegalAccessError("source cannot be NULL."); + int index = 0; + + // Are we dealing with the same object? + if (lastObject != null && lastObject.get() == source) { + index = lastResult; + } else { + index = getFirstCloner(source); + } + + // Make sure the object is valid + if (index < cloners.size()) { + return cloners.get(index).clone(source); + } + + // Damn - failure + throw new IllegalArgumentException("Cannot clone " + source + ": No cloner is sutable."); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/BukkitCloner.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/BukkitCloner.java new file mode 100644 index 00000000..290fb48a --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/BukkitCloner.java @@ -0,0 +1,56 @@ +package com.comphenix.protocol.reflect.cloning; + +import com.comphenix.protocol.reflect.EquivalentConverter; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.BukkitConverters; +import com.comphenix.protocol.wrappers.ChunkPosition; +import com.comphenix.protocol.wrappers.WrappedDataWatcher; + +/** + * Represents an object that can clone a specific list of Bukkit- and Minecraft-related objects. + * + * @author Kristian + */ +public class BukkitCloner implements Cloner { + // List of classes we support + private Class[] clonableClasses = { MinecraftReflection.getItemStackClass(), MinecraftReflection.getChunkPositionClass(), + MinecraftReflection.getDataWatcherClass() }; + + private int findMatchingClass(Class type) { + // See if is a subclass of any of our supported superclasses + for (int i = 0; i < clonableClasses.length; i++) { + if (clonableClasses[i].isAssignableFrom(type)) + return i; + } + + return -1; + } + + @Override + public boolean canClone(Object source) { + if (source == null) + return false; + + return findMatchingClass(source.getClass()) >= 0; + } + + @Override + public Object clone(Object source) { + if (source == null) + throw new IllegalArgumentException("source cannot be NULL."); + + // Convert to a wrapper + switch (findMatchingClass(source.getClass())) { + case 0: + return MinecraftReflection.getMinecraftItemStack(MinecraftReflection.getBukkitItemStack(source).clone()); + case 1: + EquivalentConverter chunkConverter = ChunkPosition.getConverter(); + return chunkConverter.getGeneric(clonableClasses[1], chunkConverter.getSpecific(source)); + case 2: + EquivalentConverter dataConverter = BukkitConverters.getDataWatcherConverter(); + return dataConverter.getGeneric(clonableClasses[2], dataConverter.getSpecific(source).deepClone()); + default: + throw new IllegalArgumentException("Cannot clone objects of type " + source.getClass()); + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/Cloner.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/Cloner.java new file mode 100644 index 00000000..c7fd0cf6 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/Cloner.java @@ -0,0 +1,25 @@ +package com.comphenix.protocol.reflect.cloning; + +/** + * Represents an object that is capable of cloning other objects. + * + * @author Kristian + */ +public interface Cloner { + /** + * Determine whether or not the current cloner can clone the given object. + * @param source - the object that is being considered. + * @return TRUE if this cloner can actually clone the given object, FALSE otherwise. + */ + public boolean canClone(Object source); + + /** + * Perform the clone. + *

+ * This method should never be called unless a corresponding {@link #canClone(Object)} returns TRUE. + * @param source - the value to clone. + * @return A cloned value. + * @throws IllegalArgumentException If this cloner cannot perform the clone. + */ + public Object clone(Object source); +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/CollectionCloner.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/CollectionCloner.java new file mode 100644 index 00000000..5fd9f3bc --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/CollectionCloner.java @@ -0,0 +1,146 @@ +package com.comphenix.protocol.reflect.cloning; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.util.Collection; + +/** + * Attempts to clone collection and array classes. + * + * @author Kristian + */ +public class CollectionCloner implements Cloner { + private final Cloner defaultCloner; + + /** + * Constructs a new collection and array cloner with the given inner element cloner. + * @param defaultCloner - default inner element cloner. + */ + public CollectionCloner(Cloner defaultCloner) { + this.defaultCloner = defaultCloner; + } + + @Override + public boolean canClone(Object source) { + if (source == null) + return false; + + Class clazz = source.getClass(); + return Collection.class.isAssignableFrom(clazz) || clazz.isArray(); + } + + @SuppressWarnings("unchecked") + @Override + public Object clone(Object source) { + if (source == null) + throw new IllegalArgumentException("source cannot be NULL."); + + Class clazz = source.getClass(); + + if (source instanceof Collection) { + Collection copy = null; + + // Not all collections implement "clone", but most *do* implement the "copy constructor" pattern + try { + Constructor constructCopy = clazz.getConstructor(Collection.class); + copy = (Collection) constructCopy.newInstance(source); + } catch (NoSuchMethodException e) { + copy = (Collection) cloneObject(clazz, source); + } catch (Exception e) { + throw new RuntimeException("Cannot construct collection.", e); + } + + // Next, clone each element in the collection + copy.clear(); + + for (Object element : (Collection) source) { + if (defaultCloner.canClone(element)) + copy.add(defaultCloner.clone(element)); + else + throw new IllegalArgumentException("Cannot clone " + element + " in collection " + source); + } + + return copy; + + // Second possibility + } else if (clazz.isArray()) { + // Get the length + int lenght = Array.getLength(source); + Class component = clazz.getComponentType(); + + // Can we speed things up by making a shallow copy instead? + if (ImmutableDetector.isImmutable(component)) { + return clonePrimitive(component, source); + } + + // Create a new copy + Object copy = Array.newInstance(clazz.getComponentType(), lenght); + + // Set each element + for (int i = 0; i < lenght; i++) { + Object element = Array.get(source, i); + + if (defaultCloner.canClone(element)) + Array.set(copy, i, defaultCloner.clone(element)); + else + throw new IllegalArgumentException("Cannot clone " + element + " in array " + source); + } + + // And we're done + return copy; + } + + throw new IllegalArgumentException(source + " is not an array nor a Collection."); + } + + /** + * Clone a primitive or immutable array by calling its clone method. + * @param component - the component type of the array. + * @param source - the array itself. + * @return The cloned array. + */ + private Object clonePrimitive(Class component, Object source) { + // Cast and call the correct version + if (byte.class.equals(component)) + return ((byte[]) source).clone(); + else if (short.class.equals(component)) + return ((short[]) source).clone(); + else if (int.class.equals(component)) + return ((int[]) source).clone(); + else if (long.class.equals(component)) + return ((long[]) source).clone(); + else if (float.class.equals(component)) + return ((float[]) source).clone(); + else if (double.class.equals(component)) + return ((double[]) source).clone(); + else if (char.class.equals(component)) + return ((char[]) source).clone(); + else if (boolean.class.equals(component)) + return ((boolean[]) source).clone(); + else + return ((Object[]) source).clone(); + } + + /** + * Clone an object by calling "clone" using reflection. + * @param clazz - the class type. + * @param obj - the object to clone. + * @return The cloned object. + */ + private Object cloneObject(Class clazz, Object source) { + // Try to clone it instead + try { + return clazz.getMethod("clone").invoke(source); + } catch (Exception e1) { + throw new RuntimeException("Cannot copy " + source + " (" + clazz + ")", e1); + } + } + + /** + * Retrieve the default cloner used to clone the content of each element in the collection. + * @return Cloner used to clone elements. + */ + public Cloner getDefaultCloner() { + return defaultCloner; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/FieldCloner.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/FieldCloner.java new file mode 100644 index 00000000..18d6554b --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/FieldCloner.java @@ -0,0 +1,61 @@ +package com.comphenix.protocol.reflect.cloning; + +import com.comphenix.protocol.reflect.ObjectWriter; +import com.comphenix.protocol.reflect.instances.InstanceProvider; + +/** + * Represents a class capable of cloning objects by deeply copying its fields. + * + * @author Kristian + */ +public class FieldCloner implements Cloner { + private final Cloner defaultCloner; + private final InstanceProvider instanceProvider; + + /** + * Constructs a field cloner that copies objects by reading and writing the internal fields directly. + * @param defaultCloner - the default cloner used while copying fields. + * @param instanceProvider - used to construct new, empty copies of a given type. + */ + public FieldCloner(Cloner defaultCloner, InstanceProvider instanceProvider) { + this.defaultCloner = defaultCloner; + this.instanceProvider = instanceProvider; + } + + @Override + public boolean canClone(Object source) { + if (source == null) + return false; + + // Attempt to create the type + return instanceProvider.create(source.getClass()) != null; + } + + @Override + public Object clone(Object source) { + if (source == null) + throw new IllegalArgumentException("source cannot be NULL."); + + Object copy = instanceProvider.create(source.getClass()); + + // Copy public and private fields alike. Skip static and transient fields. + ObjectWriter.copyTo(source, copy, source.getClass(), defaultCloner); + return copy; + } + + /** + * Retrieve the default cloner used to clone the content of each field. + * @return Cloner used to clone fields. + */ + public Cloner getDefaultCloner() { + return defaultCloner; + } + + /** + * Retrieve the instance provider this cloner is using to create new, empty classes. + * @return The instance provider in use. + */ + public InstanceProvider getInstanceProvider() { + return instanceProvider; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/IdentityCloner.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/IdentityCloner.java new file mode 100644 index 00000000..576baee1 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/IdentityCloner.java @@ -0,0 +1,18 @@ +package com.comphenix.protocol.reflect.cloning; + +/** + * Represents a cloner that simply returns the given object. + * + * @author Kristian + */ +public class IdentityCloner implements Cloner { + @Override + public boolean canClone(Object source) { + return true; + } + + @Override + public Object clone(Object source) { + return source; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/ImmutableDetector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/ImmutableDetector.java new file mode 100644 index 00000000..7ac3fee0 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/ImmutableDetector.java @@ -0,0 +1,70 @@ +package com.comphenix.protocol.reflect.cloning; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URL; +import java.util.Locale; +import java.util.UUID; + +import com.google.common.primitives.Primitives; + +/** + * Detects classes that are immutable, and thus doesn't require cloning. + *

+ * This ought to have no false positives, but plenty of false negatives. + * + * @author Kristian + */ +public class ImmutableDetector implements Cloner { + // Notable immutable classes we might encounter + private static final Class[] immutableClasses = { + StackTraceElement.class, BigDecimal.class, + BigInteger.class, Locale.class, UUID.class, + URL.class, URI.class, Inet4Address.class, + Inet6Address.class, InetSocketAddress.class + }; + + @Override + public boolean canClone(Object source) { + // Don't accept NULL + if (source == null) + return false; + + return isImmutable(source.getClass()); + } + + /** + * Determine if the given type is probably immutable. + * @param type - the type to check. + * @return TRUE if the type is immutable, FALSE otherwise. + */ + public static boolean isImmutable(Class type) { + // Cases that are definitely not true + if (type.isArray()) + return false; + + // All primitive types + if (Primitives.isWrapperType(type) || String.class.equals(type)) + return true; + // May not be true, but if so, that kind of code is broken anyways + if (type.isEnum()) + return true; + + for (Class clazz : immutableClasses) + if (clazz.equals(type)) + return true; + + // Probably not + return false; + } + + @Override + public Object clone(Object source) { + // Safe if the class is immutable + return source; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/NullableCloner.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/NullableCloner.java new file mode 100644 index 00000000..cfeaea55 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/NullableCloner.java @@ -0,0 +1,32 @@ +package com.comphenix.protocol.reflect.cloning; + +/** + * Creates a cloner wrapper that accepts and clones NULL values. + * + * @author Kristian + */ +public class NullableCloner implements Cloner { + protected Cloner wrapped; + + public NullableCloner(Cloner wrapped) { + this.wrapped = wrapped; + } + + @Override + public boolean canClone(Object source) { + return true; + } + + @Override + public Object clone(Object source) { + // Don't pass the NULL value to the cloner + if (source == null) + return null; + else + return wrapped.clone(source); + } + + public Cloner getWrapped() { + return wrapped; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java index 38066613..fe7233da 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java @@ -20,6 +20,8 @@ package com.comphenix.protocol.reflect.instances; import java.lang.reflect.Constructor; import java.util.*; +import javax.annotation.Nullable; + import net.sf.cglib.proxy.Enhancer; import com.google.common.base.Objects; @@ -30,7 +32,7 @@ import com.google.common.collect.ImmutableList; * @author Kristian * */ -public class DefaultInstances { +public class DefaultInstances implements InstanceProvider { /** * Standard default instance provider. @@ -326,4 +328,9 @@ public class DefaultInstances { } return false; } + + @Override + public Object create(@Nullable Class type) { + return getDefault(type); + } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/ExistingGenerator.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/ExistingGenerator.java index 43527d17..ca7d85d2 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/ExistingGenerator.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/ExistingGenerator.java @@ -18,13 +18,16 @@ package com.comphenix.protocol.reflect.instances; import java.lang.reflect.Field; +import java.util.Collection; import java.util.HashMap; +import java.util.LinkedList; import java.util.Map; import javax.annotation.Nullable; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; +import com.google.common.collect.Lists; /** * Provides instance constructors using a list of existing values. @@ -33,8 +36,52 @@ import com.comphenix.protocol.reflect.FuzzyReflection; * @author Kristian */ public class ExistingGenerator implements InstanceProvider { + /** + * Represents a single node in the tree of possible values. + * + * @author Kristian + */ + private static final class Node { + private Map, Node> children; + private Class key; + private Object value; + private int level; + + public Node(Class key, Object value, int level) { + this.children = new HashMap, Node>(); + this.key = key; + this.value = value; + this.level = level; + } - private Map existingValues = new HashMap(); + public Node addChild(Node node) { + children.put(node.key, node); + return node; + } + + public int getLevel() { + return level; + } + + public Collection getChildren() { + return children.values(); + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public Node getChild(Class clazz) { + return children.get(clazz); + } + } + + // Represents the root node + private Node root = new Node(null, null, 0); private ExistingGenerator() { // Only accessible to the constructors @@ -110,18 +157,94 @@ public class ExistingGenerator implements InstanceProvider { if (value == null) throw new IllegalArgumentException("Value cannot be NULL."); - existingValues.put(value.getClass().getName(), value); - } - - private void addObject(Class type, Object value) { - existingValues.put(type.getName(), value); + addObject(value.getClass(), value); } + private void addObject(Class type, Object value) { + Node node = getLeafNode(root, type, false); + + // Set the value + node.setValue(value); + } + + private Node getLeafNode(final Node start, Class type, boolean readOnly) { + Class[] path = getHierachy(type); + Node current = start; + + for (int i = 0; i < path.length; i++) { + Node next = getNext(current, path[i], readOnly); + + // Try every interface too + if (next == null && readOnly) { + current = null; + break; + } + + current = next; + } + + // And we're done + return current; + } + + private Node getNext(Node current, Class clazz, boolean readOnly) { + Node next = current.getChild(clazz); + + // Add a new node if needed + if (next == null && !readOnly) { + next = current.addChild(new Node(clazz, null, current.getLevel() + 1)); + } + + // Add interfaces + if (next != null && !readOnly && !clazz.isInterface()) { + for (Class clazzInterface : clazz.getInterfaces()) { + getLeafNode(root, clazzInterface, readOnly).addChild(next); + } + } + return next; + } + + private Node getLowestLeaf(Node current) { + Node candidate = current; + + // Depth-first search + for (Node child : current.getChildren()) { + Node subtree = getLowestLeaf(child); + + // Get the lowest node + if (subtree.getValue() != null && candidate.getLevel() < subtree.getLevel()) { + candidate = subtree; + } + } + + return candidate; + } + + private Class[] getHierachy(Class type) { + LinkedList> levels = Lists.newLinkedList(); + + // Add each class from the hierachy + for (; type != null; type = type.getSuperclass()) { + levels.addFirst(type); + } + + return levels.toArray(new Class[0]); + } + @Override public Object create(@Nullable Class type) { - Object value = existingValues.get(type.getName()); - + // Locate the type in the hierachy + Node node = getLeafNode(root, type, true); + + // Next, get the lowest leaf node + if (node != null) { + node = getLowestLeaf(node); + } + // NULL values indicate that the generator failed - return value; + if (node != null) + return node.getValue(); + else + return null; } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index 027d3f6a..b02e39a1 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -112,6 +112,24 @@ public class MinecraftReflection { return fullName.substring(0, fullName.lastIndexOf(".")); } + /** + * Dynamically retrieve the Bukkit entity from a given entity. + * @param nmsObject - the NMS entity. + * @return A bukkit entity. + * @throws RuntimeException If we were unable to retrieve the Bukkit entity. + */ + public static Object getBukkitEntity(Object nmsObject) { + if (nmsObject == null) + return null; + + // We will have to do this dynamically, unfortunately + try { + return nmsObject.getClass().getMethod("getBukkitEntity").invoke(nmsObject); + } catch (Exception e) { + throw new RuntimeException("Cannot get Bukkit entity from " + nmsObject, e); + } + } + /** * Determine if a given object can be found within the package net.minecraft.server. * @param obj - the object to test. @@ -138,25 +156,7 @@ public class MinecraftReflection { String javaName = obj.getClass().getName(); return javaName.startsWith(MINECRAFT_PREFIX_PACKAGE) && javaName.endsWith(className); } - - /** - * Dynamically retrieve the Bukkit entity from a given entity. - * @param nmsObject - the NMS entity. - * @return A bukkit entity. - * @throws RuntimeException If we were unable to retrieve the Bukkit entity. - */ - public static Object getBukkitEntity(Object nmsObject) { - if (nmsObject == null) - return null; - - // We will have to do this dynamically, unfortunately - try { - return nmsObject.getClass().getMethod("getBukkitEntity").invoke(nmsObject); - } catch (Exception e) { - throw new RuntimeException("Cannot get Bukkit entity from " + nmsObject, e); - } - } - + /** * Determine if a given object is a ChunkPosition. * @param obj - the object to test. diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java index ce907afe..1782518b 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/events/PacketContainerTest.java @@ -5,6 +5,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.lang.reflect.Array; import java.util.List; // Will have to be updated for every version though @@ -23,12 +24,15 @@ import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PrepareForTest; import com.comphenix.protocol.Packets; +import com.comphenix.protocol.reflect.EquivalentConverter; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.BukkitConverters; import com.comphenix.protocol.wrappers.ChunkPosition; import com.comphenix.protocol.wrappers.WrappedDataWatcher; import com.comphenix.protocol.wrappers.WrappedWatchableObject; + import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -36,6 +40,9 @@ import com.google.common.collect.Lists; @RunWith(org.powermock.modules.junit4.PowerMockRunner.class) @PrepareForTest(CraftItemFactory.class) public class PacketContainerTest { + // Helper converters + private EquivalentConverter watchConvert = BukkitConverters.getDataWatcherConverter(); + private EquivalentConverter itemConvert = BukkitConverters.getItemStackConverter(); @BeforeClass public static void initializeBukkit() throws IllegalAccessException { @@ -203,23 +210,15 @@ public class PacketContainerTest { } private boolean equivalentItem(ItemStack first, ItemStack second) { - if (first == null) + if (first == null) { return second == null; - else if (second == null) + } else if (second == null) { return false; - else + } else { return first.getType().equals(second.getType()); + } } - private boolean equivalentPacket(PacketContainer first, PacketContainer second) { - if (first == null) - return second == null; - else if (second == null) - return false; - else - return first.getModifier().getValues().equals(second.getModifier().getValues()); - } - @Test public void testGetWorldTypeModifier() { PacketContainer loginPacket = new PacketContainer(Packets.Server.LOGIN); @@ -324,7 +323,16 @@ public class PacketContainerTest { PacketContainer cloned = constructed.deepClone(); // Make sure they're equivalent - assertTrue("Packet " + id + " could not be cloned.", equivalentPacket(constructed, cloned)); + StructureModifier firstMod = constructed.getModifier(), secondMod = cloned.getModifier(); + assertEquals(firstMod.size(), secondMod.size()); + + // Make sure all the fields are equivalent + for (int i = 0; i < firstMod.size(); i++) { + if (firstMod.getField(i).getType().isArray()) + assertArrayEquals(getArray(firstMod.read(i)), getArray(secondMod.read(i))); + else + testEquality(firstMod.read(i), secondMod.read(i)); + } } catch (IllegalArgumentException e) { if (!registered) { @@ -337,4 +345,36 @@ public class PacketContainerTest { } } } + + // Convert to objects that support equals() + private void testEquality(Object a, Object b) { + if (a != null && b != null) { + if (MinecraftReflection.isDataWatcher(a)) { + a = watchConvert.getSpecific(a); + b = watchConvert.getSpecific(b); + } else if (MinecraftReflection.isItemStack(a)) { + a = itemConvert.getSpecific(a); + b = itemConvert.getSpecific(b); + } + } + + assertEquals(a, b); + } + + /** + * Get the underlying array as an object array. + * @param val - array wrapped as an Object. + * @return An object array. + */ + private Object[] getArray(Object val) { + if (val instanceof Object[]) + return (Object[]) val; + + int arrlength = Array.getLength(val); + Object[] outputArray = new Object[arrlength]; + + for (int i = 0; i < arrlength; ++i) + outputArray[i] = Array.get(val, i); + return outputArray; + } }