Allow for plugins/mods to register a clipboard format

*Use reflection to add new enum instances
This commit is contained in:
Jesse Boyd 2016-08-21 15:47:44 +10:00
parent b32b633248
commit 91b2347b96
4 changed files with 284 additions and 107 deletions

View File

@ -0,0 +1,24 @@
package com.boydti.fawe.object.clipboard;
import java.util.Arrays;
import java.util.HashSet;
public abstract class AbstractClipboardFormat implements IClipboardFormat {
private final String name;
private final HashSet<String> aliases;
public AbstractClipboardFormat(String name, String... aliases) {
this.name = name;
this.aliases = new HashSet<>(Arrays.asList(aliases));
}
@Override
public String getName() {
return name;
}
@Override
public HashSet<String> getAliases() {
return aliases;
}
}

View File

@ -0,0 +1,56 @@
package com.boydti.fawe.object.clipboard;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Set;
public interface IClipboardFormat {
/**
* Returns the name of this format.
* @return The name of the format
*/
String getName();
/**
* Create a reader.
*
* @param inputStream the input stream
* @return a reader
* @throws java.io.IOException thrown on I/O error
*/
ClipboardReader getReader(InputStream inputStream) throws IOException;
/**
* Create a writer.
*
* @param outputStream the output stream
* @return a writer
* @throws IOException thrown on I/O error
*/
ClipboardWriter getWriter(OutputStream outputStream) throws IOException;
/**
* Return whether the given file is of this format.
*
* @param file the file
* @return true if the given file is of this format
*/
boolean isFormat(File file);
/**
* Get the default extension
* @return
*/
String getExtension();
/**
* Get a set of aliases.
*
* @return a set of aliases
*/
Set<String> getAliases();
}

View File

@ -1,9 +1,12 @@
package com.boydti.fawe.util;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -11,6 +14,9 @@ import java.util.List;
import java.util.Map;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import sun.reflect.ConstructorAccessor;
import sun.reflect.FieldAccessor;
import sun.reflect.ReflectionFactory;
/**
* @author DPOH-VAR
@ -58,29 +64,133 @@ public class ReflectionUtils {
}
}
public static <T, V> Map<T, V> getMap(Map<T, V> map) {
try {
Class<? extends Map> clazz = map.getClass();
Field m = clazz.getDeclaredField("m");
m.setAccessible(true);
return (Map<T, V>) m.get(map);
} catch (Throwable e) {
MainUtil.handleError(e);
return map;
}
}
@SuppressWarnings("unchecked")
public static <T extends Enum<?>> T addEnum(Class<T> enumType, String enumName) {
public static <T> List<T> getList(List<T> list) {
try {
Class<? extends List> clazz = (Class<? extends List>) Class.forName("java.util.Collections$UnmodifiableList");
Field m = clazz.getDeclaredField("list");
m.setAccessible(true);
return (List<T>) m.get(list);
} catch (Throwable e) {
MainUtil.handleError(e);
return list;
// 0. Sanity checks
if (!Enum.class.isAssignableFrom(enumType)) {
throw new RuntimeException("class " + enumType + " is not an instance of Enum");
}
// 1. Lookup "$VALUES" holder in enum class and get previous enum instances
Field valuesField = null;
Field[] fields = enumType.getDeclaredFields();
for (Field field : fields) {
if (field.getName().contains("$VALUES")) {
valuesField = field;
break;
}
}
AccessibleObject.setAccessible(new Field[] { valuesField }, true);
try {
// 2. Copy it
T[] previousValues = (T[]) valuesField.get(enumType);
List values = new ArrayList(Arrays.asList(previousValues));
// 3. build new enum
T newValue = (T) makeEnum(enumType, // The target enum class
enumName, // THE NEW ENUM INSTANCE TO BE DYNAMICALLY ADDED
values.size(),
new Class<?>[] {}, // can be used to pass values to the enum constuctor
new Object[] {}); // can be used to pass values to the enum constuctor
// 4. add new value
values.add(newValue);
// 5. Set new values field
setFailsafeFieldValue(valuesField, null,
values.toArray((T[]) Array.newInstance(enumType, 0)));
// 6. Clean enum cache
cleanEnumCache(enumType);
return newValue;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage(), e);
}
}
private static Object makeEnum(Class<?> enumClass, String value, int ordinal,
Class<?>[] additionalTypes, Object[] additionalValues) throws Exception {
Object[] parms = new Object[additionalValues.length + 2];
parms[0] = value;
parms[1] = Integer.valueOf(ordinal);
System.arraycopy(additionalValues, 0, parms, 2, additionalValues.length);
return enumClass.cast(getConstructorAccessor(enumClass, additionalTypes).newInstance(parms));
}
private static ConstructorAccessor getConstructorAccessor(Class<?> enumClass,
Class<?>[] additionalParameterTypes) throws NoSuchMethodException {
Class<?>[] parameterTypes = new Class[additionalParameterTypes.length + 2];
parameterTypes[0] = String.class;
parameterTypes[1] = int.class;
System.arraycopy(additionalParameterTypes, 0,
parameterTypes, 2, additionalParameterTypes.length);
return ReflectionFactory.getReflectionFactory().newConstructorAccessor(enumClass.getDeclaredConstructor(parameterTypes));
}
private static void setFailsafeFieldValue(Field field, Object target, Object value)
throws NoSuchFieldException, IllegalAccessException {
// let's make the field accessible
field.setAccessible(true);
// next we change the modifier in the Field instance to
// not be final anymore, thus tricking reflection into
// letting us modify the static final field
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
int modifiers = modifiersField.getInt(field);
// blank out the final bit in the modifiers int
modifiers &= ~Modifier.FINAL;
modifiersField.setInt(field, modifiers);
FieldAccessor fa = ReflectionFactory.getReflectionFactory().newFieldAccessor(field, false);
fa.set(target, value);
}
private static void blankField(Class<?> enumClass, String fieldName)
throws NoSuchFieldException, IllegalAccessException {
for (Field field : Class.class.getDeclaredFields()) {
if (field.getName().contains(fieldName)) {
AccessibleObject.setAccessible(new Field[] { field }, true);
setFailsafeFieldValue(field, enumClass, null);
break;
}
}
}
private static void cleanEnumCache(Class<?> enumClass)
throws NoSuchFieldException, IllegalAccessException {
blankField(enumClass, "enumConstantDirectory"); // Sun (Oracle?!?) JDK 1.5/6
blankField(enumClass, "enumConstants"); // IBM JDK
}
public static <T, V> Map<T, V> getMap(Map<T, V> map) {
try {
Class<? extends Map> clazz = map.getClass();
Field m = clazz.getDeclaredField("m");
m.setAccessible(true);
return (Map<T, V>) m.get(map);
} catch (Throwable e) {
MainUtil.handleError(e);
return map;
}
}
public static <T> List<T> getList(List<T> list) {
try {
Class<? extends List> clazz = (Class<? extends List>) Class.forName("java.util.Collections$UnmodifiableList");
Field m = clazz.getDeclaredField("list");
m.setAccessible(true);
return (List<T>) m.get(list);
} catch (Throwable e) {
MainUtil.handleError(e);
return list;
}
}
}
public static Class<?> getNmsClass(final String name) {
final String className = "net.minecraft.server." + getVersion() + "." + name;

View File

@ -20,12 +20,15 @@
package com.sk89q.worldedit.extent.clipboard.io;
import com.boydti.fawe.object.FaweOutputStream;
import com.boydti.fawe.object.clipboard.AbstractClipboardFormat;
import com.boydti.fawe.object.clipboard.DiskOptimizedClipboard;
import com.boydti.fawe.object.clipboard.IClipboardFormat;
import com.boydti.fawe.object.schematic.FaweFormat;
import com.boydti.fawe.object.schematic.PNGWriter;
import com.boydti.fawe.object.schematic.Schematic;
import com.boydti.fawe.object.schematic.StructureFormat;
import com.boydti.fawe.util.MainUtil;
import com.boydti.fawe.util.ReflectionUtils;
import com.sk89q.jnbt.NBTConstants;
import com.sk89q.jnbt.NBTInputStream;
import com.sk89q.jnbt.NBTOutputStream;
@ -37,13 +40,10 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.annotation.Nullable;
@ -59,7 +59,7 @@ public enum ClipboardFormat {
/**
* The Schematic format used by many software.
*/
SCHEMATIC("mcedit", "mce", "schematic") {
SCHEMATIC(new AbstractClipboardFormat("SCHEMATIC", "mcedit", "mce", "schematic") {
@Override
public ClipboardReader getReader(InputStream inputStream) throws IOException {
inputStream = new BufferedInputStream(inputStream);
@ -108,12 +108,13 @@ public enum ClipboardFormat {
public String getExtension() {
return "schematic";
}
},
}),
/**
* The structure block format:
* http://minecraft.gamepedia.com/Structure_block_file_format
*/
STRUCTURE("structure", "nbt") {
STRUCTURE(new AbstractClipboardFormat("STRUCTURE", "structure", "nbt") {
@Override
public ClipboardReader getReader(InputStream inputStream) throws IOException {
inputStream = new BufferedInputStream(inputStream);
@ -143,12 +144,13 @@ public enum ClipboardFormat {
public String getExtension() {
return "nbt";
}
},
}),
/**
* Isometric PNG writer
*/
PNG("png", "image") {
PNG(new AbstractClipboardFormat("PNG", "png", "image") {
@Override
public ClipboardReader getReader(InputStream inputStream) throws IOException {
return null;
@ -159,16 +161,16 @@ public enum ClipboardFormat {
return new PNGWriter(new BufferedOutputStream(outputStream));
}
@Override
public String getExtension() {
return "png";
}
@Override
public boolean isFormat(File file) {
return file.getName().endsWith(".png");
}
},
@Override
public String getExtension() {
return "png";
}
}),
/**
* The FAWE file format:
@ -185,64 +187,17 @@ public enum ClipboardFormat {
* FaweFormat: compression/mode -> Any/Any (slower)
*
*/
FAWE("fawe") {
/**
* Read a clipboard from a compressed stream (the first byte indicates the compression level)
* @param inputStream the input stream
* @return
* @throws IOException
*/
FAWE(new AbstractClipboardFormat("FAWE", "fawe") {
@Override
public ClipboardReader getReader(InputStream inputStream) throws IOException {
return new FaweFormat(MainUtil.getCompressedIS(inputStream));
}
/**
* Write a clipboard to a stream with compression level 8
* @param outputStream the output stream
* @return
* @throws IOException
*/
@Override
public ClipboardWriter getWriter(OutputStream outputStream) throws IOException {
return getWriter(outputStream, 8);
}
/**
* Write a clipboard to a stream
* @param os
* @param compression
* @return
* @throws IOException
*/
public ClipboardWriter getWriter(OutputStream os, int compression) throws IOException {
FaweFormat writer = new FaweFormat(new FaweOutputStream(os));
writer.compress(compression);
return writer;
}
/**
* Read or write blocks ids to a file
* @param file
* @return
* @throws IOException
*/
public DiskOptimizedClipboard getUncompressedReadWrite(File file) throws IOException {
return new DiskOptimizedClipboard(file);
}
/**
* Read or write block ids to a new file
* @param width
* @param height
* @param length
* @param file
* @return
*/
public DiskOptimizedClipboard createUncompressedReadWrite(int width, int height, int length, File file) {
return new DiskOptimizedClipboard(width, height, length, file);
}
@Override
public boolean isFormat(File file) {
return file.getName().endsWith(".fawe") || file.getName().endsWith(".bd");
@ -252,21 +207,45 @@ public enum ClipboardFormat {
public String getExtension() {
return "fawe";
}
},
public ClipboardWriter getWriter(OutputStream os, int compression) throws IOException {
FaweFormat writer = new FaweFormat(new FaweOutputStream(os));
writer.compress(compression);
return writer;
}
public DiskOptimizedClipboard getUncompressedReadWrite(File file) throws IOException {
return new DiskOptimizedClipboard(file);
}
public DiskOptimizedClipboard createUncompressedReadWrite(int width, int height, int length, File file) {
return new DiskOptimizedClipboard(width, height, length, file);
}
}),
;
private static final Map<String, ClipboardFormat> aliasMap = new HashMap<String, ClipboardFormat>();
private static final Map<String, ClipboardFormat> aliasMap;
static {
aliasMap = new ConcurrentHashMap<>();
for (ClipboardFormat emum : ClipboardFormat.values()) {
for (String alias : emum.getAliases()) {
aliasMap.put(alias, emum);
}
}
}
private IClipboardFormat format;
private final String[] aliases;
ClipboardFormat() {
/**
* Create a new instance.
*
* @param aliases an array of aliases by which this format may be referred to
*/
ClipboardFormat(String ... aliases) {
this.aliases = aliases;
}
ClipboardFormat(IClipboardFormat format) {
this.format = format;
}
public IClipboardFormat getFormat() {
return format;
}
/**
@ -275,7 +254,7 @@ public enum ClipboardFormat {
* @return a set of aliases
*/
public Set<String> getAliases() {
return Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(aliases)));
return format.getAliases();
}
/**
@ -285,7 +264,9 @@ public enum ClipboardFormat {
* @return a reader
* @throws IOException thrown on I/O error
*/
public abstract ClipboardReader getReader(InputStream inputStream) throws IOException;
public ClipboardReader getReader(InputStream inputStream) throws IOException {
return format.getReader(inputStream);
}
/**
* Create a writer.
@ -294,7 +275,9 @@ public enum ClipboardFormat {
* @return a writer
* @throws IOException thrown on I/O error
*/
public abstract ClipboardWriter getWriter(OutputStream outputStream) throws IOException;
public ClipboardWriter getWriter(OutputStream outputStream) throws IOException {
return format.getWriter(outputStream);
}
public Schematic load(File file) throws IOException {
return load(new FileInputStream(file));
@ -308,7 +291,9 @@ public enum ClipboardFormat {
* Get the file extension used
* @return file extension string
*/
public abstract String getExtension();
public String getExtension() {
return format.getExtension();
}
/**
* Return whether the given file is of this format.
@ -316,14 +301,8 @@ public enum ClipboardFormat {
* @param file the file
* @return true if the given file is of this format
*/
public abstract boolean isFormat(File file);
static {
for (ClipboardFormat format : EnumSet.allOf(ClipboardFormat.class)) {
for (String key : format.aliases) {
aliasMap.put(key, format);
}
}
public boolean isFormat(File file) {
return format.isFormat(file);
}
/**
@ -347,7 +326,6 @@ public enum ClipboardFormat {
@Nullable
public static ClipboardFormat findByFile(File file) {
checkNotNull(file);
for (ClipboardFormat format : EnumSet.allOf(ClipboardFormat.class)) {
if (format.isFormat(file)) {
return format;
@ -357,6 +335,15 @@ public enum ClipboardFormat {
return null;
}
public static ClipboardFormat addFormat(IClipboardFormat instance) {
ClipboardFormat newEnum = ReflectionUtils.addEnum(ClipboardFormat.class, instance.getName());
newEnum.format = instance;
for (String alias : newEnum.getAliases()) {
aliasMap.put(alias, newEnum);
}
return newEnum;
}
public static Class<?> inject() {
return ClipboardFormat.class;
}