Archiviert
13
0

Added a generic cloning library to properly support deepClone().

Dieser Commit ist enthalten in:
Kristian S. Stangeland 2012-12-27 09:23:07 +01:00
Ursprung fdbd3d6495
Commit 8b91e3034d
17 geänderte Dateien mit 992 neuen und 96 gelöschten Zeilen

Datei anzeigen

@ -17,8 +17,6 @@
package com.comphenix.protocol.events; package com.comphenix.protocol.events;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; 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.EquivalentConverter;
import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.reflect.cloning.AggregateCloner;
import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.BukkitConverters; import com.comphenix.protocol.wrappers.BukkitConverters;
import com.comphenix.protocol.wrappers.ChunkPosition; import com.comphenix.protocol.wrappers.ChunkPosition;
@ -370,35 +369,8 @@ public class PacketContainer implements Serializable {
* @return A deep copy of the current packet. * @return A deep copy of the current packet.
*/ */
public PacketContainer deepClone() { public PacketContainer deepClone() {
ObjectOutputStream output = null; Object clonedPacket = AggregateCloner.DEFAULT.clone(getHandle());
ObjectInputStream input = null; return new PacketContainer(getID(), clonedPacket);
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
}
}
} }
private void writeObject(ObjectOutputStream output) throws IOException { private void writeObject(ObjectOutputStream output) throws IOException {

Datei anzeigen

@ -182,6 +182,22 @@ class MinecraftRegistry {
throw new IllegalArgumentException("The packet ID " + packetID + " is not registered."); 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. * Find the first superclass that is not a CBLib proxy object.
* @param clazz - the class whose hierachy we're going to search through. * @param clazz - the class whose hierachy we're going to search through.

Datei anzeigen

@ -64,6 +64,27 @@ public class StructureCache {
return getStructure(id, true); return getStructure(id, true);
} }
/**
* Retrieve a cached structure modifier given a packet type.
* @param packetType - packet type.
* @return A structure modifier.
*/
public static StructureModifier<Object> 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<Object> 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. * Retrieve a cached structure modifier for the given packet id.
* @param id - packet ID. * @param id - packet ID.

Datei anzeigen

@ -17,21 +17,63 @@
package com.comphenix.protocol.reflect; package com.comphenix.protocol.reflect;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; 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. * Can copy an object field by field.
* *
* @author Kristian * @author Kristian
*/ */
public class ObjectWriter { public class ObjectWriter {
// Cache structure modifiers // Cache structure modifiers
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
private static ConcurrentMap<Class, StructureModifier<Object>> cache = private static ConcurrentMap<Class, StructureModifier<Object>> cache =
new ConcurrentHashMap<Class, StructureModifier<Object>>(); new ConcurrentHashMap<Class, StructureModifier<Object>>();
/**
* The default value cloner to use.
*/
private static final Cloner DEFAULT_CLONER = new IdentityCloner();
/**
* Retrieve a usable structure modifier for the given object type.
* <p>
* 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<Object> 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<Object> modifier = cache.get(type);
// Create the structure modifier if we haven't already
if (modifier == null) {
StructureModifier<Object> value = new StructureModifier<Object>(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. * Copy every field in object A to object B. Each value is copied directly, and is not cloned.
* <p> * <p>
@ -41,22 +83,31 @@ public class ObjectWriter {
* @param commonType - type containing each field to copy. * @param commonType - type containing each field to copy.
*/ */
public static void copyTo(Object source, Object destination, Class<?> commonType) { 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.
* <p>
* 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) if (source == null)
throw new IllegalArgumentException("Source cannot be NULL"); throw new IllegalArgumentException("Source cannot be NULL");
if (destination == null) if (destination == null)
throw new IllegalArgumentException("Destination cannot be NULL"); throw new IllegalArgumentException("Destination cannot be NULL");
StructureModifier<Object> modifier = cache.get(commonType); StructureModifier<Object> modifier = getModifier(commonType);
// Create the structure modifier if we haven't already
if (modifier == null) {
StructureModifier<Object> value = new StructureModifier<Object>(commonType, null, false);
modifier = cache.putIfAbsent(commonType, value);
if (modifier == null)
modifier = value;
}
// Add target // Add target
StructureModifier<Object> modifierSource = modifier.withTarget(source); StructureModifier<Object> modifierSource = modifier.withTarget(source);
@ -65,20 +116,21 @@ public class ObjectWriter {
// Copy every field // Copy every field
try { try {
for (int i = 0; i < modifierSource.size(); i++) { for (int i = 0; i < modifierSource.size(); i++) {
if (!modifierDest.isReadOnly(i)) { Field field = modifierSource.getField(i);
Object value = modifierSource.read(i); int mod = field.getModifiers();
modifierDest.write(i, value);
}
// System.out.println(String.format("Writing value %s to %s", // Skip static fields. We also get the "public" field fairly often, so we'll skip that.
// value, modifier.getFields().get(i).getName())); if (!Modifier.isStatic(mod) && (!Modifier.isPublic(mod) || copyPublic)) {
Object value = modifierSource.read(i);
modifierDest.write(i, valueCloner.clone(value));
}
} }
// Copy private fields underneath // Copy private fields underneath
Class<?> superclass = commonType.getSuperclass(); Class<?> superclass = commonType.getSuperclass();
if (!superclass.equals(Object.class)) { if (!superclass.equals(Object.class)) {
copyTo(source, destination, superclass); copyToInternal(source, destination, superclass, valueCloner, false);
} }
} catch (FieldAccessException e) { } catch (FieldAccessException e) {

Datei anzeigen

@ -201,10 +201,16 @@ public class StructureModifier<TField> {
* @return TRUE if the field by the given index is read-only, FALSE otherwise. * @return TRUE if the field by the given index is read-only, FALSE otherwise.
*/ */
public boolean isReadOnly(int fieldIndex) { public boolean isReadOnly(int fieldIndex) {
if (fieldIndex < 0 || fieldIndex >= data.size()) return Modifier.isFinal(getField(fieldIndex).getModifiers());
throw new IllegalArgumentException("Index parameter is not within [0 - " + data.size() + ")"); }
return Modifier.isFinal(data.get(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<TField> {
return ImmutableList.copyOf(data); 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. * Retrieve every value stored in the fields of the current type.
* @return Every field value. * @return Every field value.
@ -560,4 +579,6 @@ public class StructureModifier<TField> {
return result; return result;
} }
} }

Datei anzeigen

@ -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<Function<BuilderParameters, Cloner>> 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<? extends Cloner> type) {
// Use reflection to generate a factory on the fly
return orCloner(new Function<BuilderParameters, Cloner>() {
@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<BuilderParameters, Cloner> 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<Cloner> 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<Cloner> cloners;
private WeakReference<Object> 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<Cloner> getCloners() {
return Collections.unmodifiableList(cloners);
}
/**
* Set the cloners that will be used.
* @param cloners - the cloners that will be used.
*/
private void setCloners(Iterable<? extends Cloner> cloners) {
this.cloners = Lists.newArrayList(cloners);
}
@Override
public boolean canClone(Object source) {
// Optimize a bit
lastResult = getFirstCloner(source);
lastObject = new WeakReference<Object>(source);
return lastResult >= 0 && lastResult < cloners.size();
}
/**
* Retrieve the index of the first cloner capable of cloning the given object.
* <p>
* 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.");
}
}

Datei anzeigen

@ -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<ChunkPosition> chunkConverter = ChunkPosition.getConverter();
return chunkConverter.getGeneric(clonableClasses[1], chunkConverter.getSpecific(source));
case 2:
EquivalentConverter<WrappedDataWatcher> dataConverter = BukkitConverters.getDataWatcherConverter();
return dataConverter.getGeneric(clonableClasses[2], dataConverter.getSpecific(source).deepClone());
default:
throw new IllegalArgumentException("Cannot clone objects of type " + source.getClass());
}
}
}

Datei anzeigen

@ -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.
* <p>
* 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);
}

Datei anzeigen

@ -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<Object> copy = null;
// Not all collections implement "clone", but most *do* implement the "copy constructor" pattern
try {
Constructor<?> constructCopy = clazz.getConstructor(Collection.class);
copy = (Collection<Object>) constructCopy.newInstance(source);
} catch (NoSuchMethodException e) {
copy = (Collection<Object>) 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<Object>) 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;
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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.
* <p>
* 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;
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -20,6 +20,8 @@ package com.comphenix.protocol.reflect.instances;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.util.*; import java.util.*;
import javax.annotation.Nullable;
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.Enhancer;
import com.google.common.base.Objects; import com.google.common.base.Objects;
@ -30,7 +32,7 @@ import com.google.common.collect.ImmutableList;
* @author Kristian * @author Kristian
* *
*/ */
public class DefaultInstances { public class DefaultInstances implements InstanceProvider {
/** /**
* Standard default instance provider. * Standard default instance provider.
@ -326,4 +328,9 @@ public class DefaultInstances {
} }
return false; return false;
} }
@Override
public Object create(@Nullable Class<?> type) {
return getDefault(type);
}
} }

Datei anzeigen

@ -18,13 +18,16 @@
package com.comphenix.protocol.reflect.instances; package com.comphenix.protocol.reflect.instances;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map; import java.util.Map;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.FuzzyReflection;
import com.google.common.collect.Lists;
/** /**
* Provides instance constructors using a list of existing values. * Provides instance constructors using a list of existing values.
@ -33,8 +36,52 @@ import com.comphenix.protocol.reflect.FuzzyReflection;
* @author Kristian * @author Kristian
*/ */
public class ExistingGenerator implements InstanceProvider { public class ExistingGenerator implements InstanceProvider {
/**
* Represents a single node in the tree of possible values.
*
* @author Kristian
*/
private static final class Node {
private Map<Class<?>, Node> children;
private Class<?> key;
private Object value;
private int level;
public Node(Class<?> key, Object value, int level) {
this.children = new HashMap<Class<?>, Node>();
this.key = key;
this.value = value;
this.level = level;
}
private Map<String, Object> existingValues = new HashMap<String, Object>(); public Node addChild(Node node) {
children.put(node.key, node);
return node;
}
public int getLevel() {
return level;
}
public Collection<Node> 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() { private ExistingGenerator() {
// Only accessible to the constructors // Only accessible to the constructors
@ -110,18 +157,94 @@ public class ExistingGenerator implements InstanceProvider {
if (value == null) if (value == null)
throw new IllegalArgumentException("Value cannot be NULL."); throw new IllegalArgumentException("Value cannot be NULL.");
existingValues.put(value.getClass().getName(), value); addObject(value.getClass(), value);
}
private void addObject(Class<?> type, Object value) {
existingValues.put(type.getName(), 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<Class<?>> 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 @Override
public Object create(@Nullable Class<?> type) { 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 // NULL values indicate that the generator failed
return value; if (node != null)
return node.getValue();
else
return null;
} }
} }

Datei anzeigen

@ -112,6 +112,24 @@ public class MinecraftReflection {
return fullName.substring(0, fullName.lastIndexOf(".")); 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. * Determine if a given object can be found within the package net.minecraft.server.
* @param obj - the object to test. * @param obj - the object to test.
@ -138,25 +156,7 @@ public class MinecraftReflection {
String javaName = obj.getClass().getName(); String javaName = obj.getClass().getName();
return javaName.startsWith(MINECRAFT_PREFIX_PACKAGE) && javaName.endsWith(className); 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. * Determine if a given object is a ChunkPosition.
* @param obj - the object to test. * @param obj - the object to test.

Datei anzeigen

@ -5,6 +5,7 @@ import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.lang.reflect.Array;
import java.util.List; import java.util.List;
// Will have to be updated for every version though // 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 org.powermock.core.classloader.annotations.PrepareForTest;
import com.comphenix.protocol.Packets; import com.comphenix.protocol.Packets;
import com.comphenix.protocol.reflect.EquivalentConverter;
import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.BukkitConverters;
import com.comphenix.protocol.wrappers.ChunkPosition; import com.comphenix.protocol.wrappers.ChunkPosition;
import com.comphenix.protocol.wrappers.WrappedDataWatcher; import com.comphenix.protocol.wrappers.WrappedDataWatcher;
import com.comphenix.protocol.wrappers.WrappedWatchableObject; import com.comphenix.protocol.wrappers.WrappedWatchableObject;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
@ -36,6 +40,9 @@ import com.google.common.collect.Lists;
@RunWith(org.powermock.modules.junit4.PowerMockRunner.class) @RunWith(org.powermock.modules.junit4.PowerMockRunner.class)
@PrepareForTest(CraftItemFactory.class) @PrepareForTest(CraftItemFactory.class)
public class PacketContainerTest { public class PacketContainerTest {
// Helper converters
private EquivalentConverter<WrappedDataWatcher> watchConvert = BukkitConverters.getDataWatcherConverter();
private EquivalentConverter<ItemStack> itemConvert = BukkitConverters.getItemStackConverter();
@BeforeClass @BeforeClass
public static void initializeBukkit() throws IllegalAccessException { public static void initializeBukkit() throws IllegalAccessException {
@ -203,23 +210,15 @@ public class PacketContainerTest {
} }
private boolean equivalentItem(ItemStack first, ItemStack second) { private boolean equivalentItem(ItemStack first, ItemStack second) {
if (first == null) if (first == null) {
return second == null; return second == null;
else if (second == null) } else if (second == null) {
return false; return false;
else } else {
return first.getType().equals(second.getType()); 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 @Test
public void testGetWorldTypeModifier() { public void testGetWorldTypeModifier() {
PacketContainer loginPacket = new PacketContainer(Packets.Server.LOGIN); PacketContainer loginPacket = new PacketContainer(Packets.Server.LOGIN);
@ -324,7 +323,16 @@ public class PacketContainerTest {
PacketContainer cloned = constructed.deepClone(); PacketContainer cloned = constructed.deepClone();
// Make sure they're equivalent // Make sure they're equivalent
assertTrue("Packet " + id + " could not be cloned.", equivalentPacket(constructed, cloned)); StructureModifier<Object> 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) { } catch (IllegalArgumentException e) {
if (!registered) { 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;
}
} }