ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/AutoWrapper.java

201 lines
7.1 KiB
Java

/**
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2017 Dan Mulloy
*
* 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.wrappers;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
import com.google.common.base.Defaults;
import com.google.common.base.Preconditions;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import com.comphenix.protocol.reflect.EquivalentConverter;
import com.comphenix.protocol.utility.MinecraftReflection;
/**
* Automatically wraps an internal NMS class to a non-versioned, deofbuscated class.
* Requirements:
* <ul>
* <li>The wrapper must be public</li>
* <li>If the wrapper is an internal class, it must be static</li>
* <li>The wrapper must have one public constructor with no arguments (the default constructor is acceptable)</li>
* <li>The wrapper must have the the same number of fields as the NMS class</li>
* <li>Each field should correspond, in order, to its NMS counterpart</li>
* <li>Non-generic fields must have a converter</li>
* </ul>
*
* @author dmulloy2
*/
public class AutoWrapper<T> implements EquivalentConverter<T> {
private static final Object[] NO_ARGS = new Object[0];
private Map<Integer, Function<Object, Object>> wrappers = new HashMap<>();
private Map<Integer, Function<Object, Object>> unwrappers = new HashMap<>();
// lazy
private FieldAccessor[] nmsAccessors;
private FieldAccessor[] wrapperAccessors;
private Object[] nmsDefaultArgs;
private ConstructorAccessor nmsInstanceCreator;
private Class<T> wrapperClass;
private Class<?> nmsClass;
private AutoWrapper(Class<T> wrapperClass, Class<?> nmsClass) {
this.wrapperClass = wrapperClass;
this.nmsClass = nmsClass;
}
public static <T> AutoWrapper<T> wrap(Class<T> wrapperClass, Class<?> nmsClass) {
return new AutoWrapper<>(wrapperClass, nmsClass);
}
public static <T> AutoWrapper<T> wrap(Class<T> wrapperClass, String nmsClassName) {
return wrap(wrapperClass, MinecraftReflection.getMinecraftClass(nmsClassName));
}
public static <T> AutoWrapper<T> wrap(Class<T> wrapperClass, String nmsClassName, String... aliases) {
return wrap(wrapperClass, MinecraftReflection.getMinecraftClass(nmsClassName, aliases));
}
public AutoWrapper<T> field(int index, Function<Object, Object> wrapper, Function<Object, Object> unwrapper) {
wrappers.put(index, wrapper);
unwrappers.put(index, unwrapper);
return this;
}
public AutoWrapper<T> field(int index, EquivalentConverter converter) {
return field(index, converter::getSpecific, specific -> converter.getGeneric(specific));
}
public T wrap(Object nmsObject) {
Preconditions.checkNotNull(nmsObject);
T instance;
try {
instance = wrapperClass.newInstance();
} catch (ReflectiveOperationException ex) {
throw new InvalidWrapperException(wrapperClass.getSimpleName() + " is not accessible!", ex);
}
// ensures that all accessors are present
computeFieldAccessors();
for (int i = 0; i < wrapperAccessors.length; i++) {
FieldAccessor source = nmsAccessors[i];
FieldAccessor target = wrapperAccessors[i];
Object value = source.get(nmsObject);
if (wrappers.containsKey(i))
value = wrappers.get(i).apply(value);
target.set(instance, value);
}
return instance;
}
public Object unwrap(Object wrapper) {
Preconditions.checkNotNull(wrapper);
// ensures that all accessors are present
computeFieldAccessors();
computeNmsConstructorAccess();
Object instance = nmsInstanceCreator.invoke(nmsDefaultArgs);
for (int i = 0; i < wrapperAccessors.length; i++) {
FieldAccessor source = wrapperAccessors[i];
FieldAccessor target = nmsAccessors[i];
Object value = source.get(wrapper);
if (unwrappers.containsKey(i))
value = unwrappers.get(i).apply(value);
target.set(instance, value);
}
return instance;
}
private void computeFieldAccessors() {
if (nmsAccessors == null) {
nmsAccessors = Arrays
.stream(nmsClass.getDeclaredFields())
.filter(field -> !Modifier.isStatic(field.getModifiers()))
.map(Accessors::getFieldAccessor)
.toArray(FieldAccessor[]::new);
}
if (wrapperAccessors == null) {
wrapperAccessors = Arrays
.stream(wrapperClass.getDeclaredFields())
.filter(field -> !Modifier.isStatic(field.getModifiers()))
.map(Accessors::getFieldAccessor)
.toArray(FieldAccessor[]::new);
}
}
private void computeNmsConstructorAccess() {
if (nmsInstanceCreator == null) {
ConstructorAccessor noArgs = Accessors.getConstructorAccessorOrNull(nmsClass);
if (noArgs != null) {
// no args constructor is available - use it
nmsInstanceCreator = noArgs;
nmsDefaultArgs = NO_ARGS;
} else {
// use the first constructor of the class
nmsInstanceCreator = Accessors.getConstructorAccessor(nmsClass.getDeclaredConstructors()[0]);
nmsDefaultArgs = Arrays
.stream(nmsInstanceCreator.getConstructor().getParameterTypes())
.map(type -> type.isPrimitive() ? Defaults.defaultValue(type) : null)
.toArray(Object[]::new);
}
}
}
// ---- Equivalent conversion
@Override
public T getSpecific(Object generic) {
return wrap(generic);
}
@Override
public Object getGeneric(Object specific) {
return unwrap(specific);
}
@Override
public Class<T> getSpecificType() {
return wrapperClass;
}
public static class InvalidWrapperException extends RuntimeException {
private InvalidWrapperException(String message, Throwable cause) {
super(message, cause);
}
}
}