ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/CollectionCloner.java
2018-09-22 17:48:06 -04:00

229 lines
7.3 KiB
Java

/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.reflect.cloning;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import com.google.common.collect.BiMap;
/**
* 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) || Map.class.isAssignableFrom(clazz) || clazz.isArray();
}
@Override
@SuppressWarnings("unchecked")
public Object clone(Object source) {
if (source == null)
throw new IllegalArgumentException("source cannot be NULL.");
Class<?> clazz = source.getClass();
try {
if (source instanceof Collection) {
Collection<Object> copy = cloneConstructor(Collection.class, clazz, source);
// Next, clone each element in the collection
try {
copy.clear();
for (Object element : (Collection<Object>) source) {
copy.add(getClone(element, source));
}
} catch (UnsupportedOperationException e) {
// Immutable - we can't do much about that
}
return copy;
} else if (source instanceof Map) {
Map<Object, Object> copy;
if (source instanceof BiMap) {
BiMap original = (BiMap) source;
if (clazz.getDeclaringClass() != null) {
Method create = clazz.getDeclaringClass().getMethod("create", int.class);
copy = ((BiMap) create.invoke(null, original.size())).inverse();
} else {
Method create = clazz.getMethod("create", int.class);
copy = (Map) create.invoke(null, original.size());
}
} else {
copy = cloneConstructor(Map.class, clazz, source);
}
// Next, clone each element in the collection
try {
copy.clear();
for (Entry<Object, Object> element : ((Map<Object, Object>) source).entrySet()) {
Object key = getClone(element.getKey(), source);
Object value = getClone(element.getValue(), source);
copy.put(key, value);
}
} catch (UnsupportedOperationException e) {
// Immutable - we can't do much about that
}
return copy;
} else if (clazz.isArray()) {
// Get the length
int length = 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(), length);
// Set each element
for (int i = 0; i < length; 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;
}
} catch (Exception ex) {
throw new RuntimeException("Failed to clone " + source + " (" + source.getClass() + ")", ex);
}
throw new IllegalArgumentException(source + " is not an array nor a Collection.");
}
/**
* Clone an element using the default cloner.
* @param element - the element to clone.
* @param container - where the element is stored.
* @return The cloned element.
*/
private Object getClone(Object element, Object container) {
if (defaultCloner.canClone(element))
return defaultCloner.clone(element);
else
throw new IllegalArgumentException("Cannot clone " + element + " in container " + container);
}
/**
* 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 its clone constructor, or alternatively, a "clone" method.
* @param superclass - the superclass we expect in the clone constructor.
* @param clazz - the class of the object.
* @param source - the object itself.
* @return A cloned object.
*/
@SuppressWarnings("unchecked")
private <T> T cloneConstructor(Class<?> superclass, Class<?> clazz, Object source) {
// Not all collections or maps implement "clone", but most *do* implement the "copy constructor" pattern
try {
Constructor<?> constructCopy = clazz.getConstructor(superclass);
return (T) constructCopy.newInstance(source);
} catch (NoSuchMethodException e) {
if (source instanceof Serializable)
return (T) new SerializableCloner().clone(source);
// Delegate to serializable if possible
return (T) cloneObject(clazz, source);
} catch (Exception e) {
throw new RuntimeException("Cannot construct collection.", e);
}
}
/**
* Clone an object by calling "clone" using reflection.
* @param clazz - the class type.
* @param source - 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;
}
}