Added a generic cloning library to properly support deepClone().
Dieser Commit ist enthalten in:
Ursprung
fdbd3d6495
Commit
8b91e3034d
@ -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 {
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
In neuem Issue referenzieren
Einen Benutzer sperren