diff options
author | Dico200 <dico.karssiens@gmail.com> | 2018-07-22 03:25:13 +0200 |
---|---|---|
committer | Dico200 <dico.karssiens@gmail.com> | 2018-07-22 03:25:13 +0200 |
commit | dbcc90ac8a938e9098dc93206f061cc2a4765ad6 (patch) | |
tree | 2d14c4ce997a87303fffb434ef20ab825aa50acc | |
parent | 519f3f6b5c39eebace0247b34d56584db463f92b (diff) |
Add a few components of dicore3
12 files changed, 2514 insertions, 1 deletions
diff --git a/src/main/java/io/dico/dicore/Reflection.java b/src/main/java/io/dico/dicore/Reflection.java new file mode 100644 index 0000000..b7b1ed4 --- /dev/null +++ b/src/main/java/io/dico/dicore/Reflection.java @@ -0,0 +1,780 @@ +package io.dico.dicore; + +import io.dico.dicore.exceptions.ExceptionHandler; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Reflective utilities + */ +@SuppressWarnings("unchecked") +public class Reflection { + private static final ExceptionHandler exceptionHandler; + private static final Field fieldModifiersField = restrictedSearchField(Field.class, "modifiers"); + private static Consumer<String> errorTarget; + + private Reflection() { + + } + + static { + exceptionHandler = new ExceptionHandler() { + @Override + public void handle(Throwable ex) { + handleGenericException(ex); + } + + @Override + public Object handleGenericException(Throwable ex, Object... args) { + String action = args.length == 0 || !(args[0] instanceof String) ? "executing a reflective operation" : (String) args[0]; + ExceptionHandler.log(errorTarget, action, ex); + return null; + } + }; + + // don't use method reference here: the current reference in System.out would be cached. + setErrorTarget(msg -> System.out.println(msg)); + } + + /** + * Sets the output where ReflectiveOperationException's and similar are sent. + * This defaults to {@link System#out}. + * + * @param target The new output + * @throws NullPointerException if target is null + */ + public static void setErrorTarget(Consumer<String> target) { + errorTarget = Objects.requireNonNull(target); + } + + /** + * This search modifier tells the implementation that it should subsequently search superclasses for the field/method. + * Using this modifier means a call to {@link #deepSearchField(Class, String)} will be used instead of {@link #restrictedSearchField(Class, String)} + * and a call to {@link #deepSearchMethod(Class, String, Class[])} will be used instead of {@link #restrictedSearchMethod(Class, String, Class[])} + */ + public static final int DEEP_SEARCH = 0x1; + + /** + * This search modifier applies only to fields, and tells the implementation that a final modifier might be present on a found field, and that it should be removed. + */ + public static final int REMOVE_FINAL = 0x2; + + /** + * This search modifier applies only to methods, and tells the implementation that it should completely ignore parameter types and return the first method with a matching name + * The implementation uses {@link Class#getDeclaredMethods()} instead of {@link Class#getDeclaredMethod(String, Class[])} if this modifier is set. + */ + public static final int IGNORE_PARAMS = 0x2; + + /* + ### FIELD METHODS ### + */ + + /** + * Search a field of any accessibility within the class or any of its superclasses. + * The first field with the given name that is found will be returned. + * <p> + * If a field is found and it is not accessible, this method attempts to make it accessible. + * If a {@link SecurityException} is thrown in the process, that is ignored and the field will be returned nonetheless. + * <p> + * This method throws IllegalArgumentException if the Field is not found, because, in most cases, that should never happen, + * and it should simplify debugging. In some cases, if you want to know if the field exists, you'll have to use try/catch for that. + * + * @param clazz The lowest class in the ladder to start searching from + * @param fieldName The name of the field + * //@param fieldType the type of the field, or null if it can be any. + * @return The field + * @throws NullPointerException if clazz is null or fieldName is null + * @throws IllegalArgumentException if the field doesn't exist + * @see #restrictedSearchField(Class, String) + */ + public static Field deepSearchField(Class<?> clazz, String fieldName/*, Class<?> fieldType*/) { + Class<?> currentClass = clazz; + Field result; + do { + // throws NPE if class or fieldName is null + result = internalSearchField(clazz, fieldName); + if (result != null) { + return result; + } + currentClass = currentClass.getSuperclass(); + } while (currentClass != null); + + throw new IllegalArgumentException("field not found in " + clazz.getCanonicalName() + " and superclasses: " + fieldName); + } + + /** + * Search a field of any accessibility within the class, but not its superclasses. + * <p> + * If a field is found and it is not accessible, this method attempts to make it accessible. + * If a {@link SecurityException} is thrown in the process, that is ignored and the field will be returned nonetheless. + * <p> + * This method throws IllegalArgumentException if the Field is not found, because, in most cases, that should never happen, + * and it should simplify debugging. In some cases, if you want to know if the field exists, you'll have to use try/catch for that. + * + * @param clazz The only class to search for the field + * @param fieldName The name of the field + * @return The field + * @throws NullPointerException if clazz or fieldName is null + * @throws IllegalArgumentException if the field does not exist + */ + public static Field restrictedSearchField(Class<?> clazz, String fieldName) { + Field result = internalSearchField(clazz, fieldName); + if (result == null) { + throw new IllegalArgumentException("field not found in " + clazz.getCanonicalName() + ": " + fieldName); + } + return result; + } + + /** + * Searches for a field using the given search method. + * + * @param modifiers The modifiers for field search. Can have {@link #DEEP_SEARCH} and {@link #REMOVE_FINAL} + * @param clazz The class to search in/from + * @param fieldName Name of the field + * @return The field + * @throws NullPointerException if clazz or fieldName is null + * @throws IllegalArgumentException if the field is not found + */ + public static Field searchField(int modifiers, Class<?> clazz, String fieldName) { + Field result; + if ((modifiers & DEEP_SEARCH) != 0) { + result = deepSearchField(clazz, fieldName); + } else { + result = restrictedSearchField(clazz, fieldName); + } + if ((modifiers & REMOVE_FINAL) != 0) { + removeFinalModifier(result); + } + return result; + } + + /** + * @return The same as {@link #restrictedSearchField(Class, String)}, but returns null instead of throwing IllegalArgumentException + * @see #restrictedSearchField(Class, String) + */ + private static Field internalSearchField(Class<?> clazz, String fieldName) { + Field result; + try { + // throws NullPointerException if either clazz or fieldName are null. + result = clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException | SecurityException ex) { + return null; + } + + if (!result.isAccessible()) try { + result.setAccessible(true); + } catch (SecurityException ignored) { + + } + + return result; + } + + /** + * Attempts to remove existing final modifier of the given field + * This method should always return true. + * + * @param field The field whose final modifier to remove + * @return true if the field most definitely has no final modifier after this call + * @throws NullPointerException if field is null + */ + public static boolean removeFinalModifier(Field field) { + Objects.requireNonNull(field); + try { + int modifiers = (int) fieldModifiersField.get(field); + if (modifiers != (modifiers &= ~Modifier.FINAL)) { + fieldModifiersField.set(field, modifiers); + } + return true; + } catch (Exception ex) { + return false; + } + } + + /** + * Gets field value of the field named fieldName and the given instance + * To find the field, {@link #deepSearchField(Class, String)} is used (DEEP search method). + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param instance The instance whose field value to get + * @param fieldName the name of the field + * @param <T> The expected/known field type + * @return The field value + * @throws IllegalArgumentException if the field doesn't exist + * @see #deepSearchField(Class, String) + * @see #getFieldValue(Class, String, Object) + */ + public static <T> T getFieldValue(Object instance, String fieldName) { + return getFieldValue(deepSearchField(instance.getClass(), fieldName), instance); + } + + /** + * Gets field value of the field named fieldName and the given instance + * To find the field, {@link #restrictedSearchField(Class, String)} is used (RESTRICTED search method). + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param clazz The class to search for the field + * @param instance The instance whose field value to get + * @param fieldName the name of the field + * @param <T> The expected/known field type + * @return The field value + * @throws IllegalArgumentException if the field doesn't exist + * @see #restrictedSearchField(Class, String) + * @see #getFieldValue(Field, Object) + */ + public static <T> T getFieldValue(Class<?> clazz, String fieldName, Object instance) { + return getFieldValue(restrictedSearchField(clazz, fieldName), instance); + } + + /** + * Gets field value of the field named fieldName and the given instance + * To find the field, {@link #searchField(int, Class, String)} is used. + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param modifiers The modifiers for field search. Can have {@link #DEEP_SEARCH} and {@link #REMOVE_FINAL} + * @param clazz The class to search for the field + * @param instance The instance whose field value to get + * @param fieldName the name of the field + * @param <T> The expected/known field type + * @return The field value + * @throws IllegalArgumentException if the field doesn't exist + * @see #searchField(int, Class, String) + * @see #getFieldValue(Field, Object) + */ + public static <T> T getFieldValue(int modifiers, Class<?> clazz, String fieldName, Object instance) { + return getFieldValue(searchField(modifiers, clazz, fieldName), instance); + } + + /** + * Gets field value of the given field and the given instance + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param field the field + * @param instance The instance whose field value to get + * @param <T> The expected/known field type + * @return The field value + */ + public static <T> T getFieldValue(Field field, Object instance) { + return exceptionHandler.supplySafe(() -> (T) field.get(instance)); + } + + /** + * Gets static field value of the field named fieldName + * To find the field, {@link #restrictedSearchField(Class, String)} is used (RESTRICTED search method). + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param clazz The class to search for the field + * @param fieldName the name of the field + * @param <T> The expected/known field type + * @return The field value + * @throws IllegalArgumentException if the field doesn't exist + * @see #restrictedSearchField(Class, String) + * @see #getStaticFieldValue(Field) + */ + public static <T> T getStaticFieldValue(Class<?> clazz, String fieldName) { + return getStaticFieldValue(restrictedSearchField(clazz, fieldName)); + } + + /** + * Gets static field value of the field named fieldName + * To find the field, {@link #searchField(int, Class, String)} is used. + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param modifiers The modifiers for field search. Can have {@link #DEEP_SEARCH} and {@link #REMOVE_FINAL} + * @param clazz The class to search for the field + * @param fieldName the name of the field + * @param <T> The expected/known field type + * @return The field value + * @throws IllegalArgumentException if the field doesn't exist + * @see #deepSearchField(Class, String) + * @see #getStaticFieldValue(Field) + */ + public static <T> T getStaticFieldValue(int modifiers, Class<?> clazz, String fieldName) { + return getStaticFieldValue(searchField(modifiers, clazz, fieldName)); + } + + /** + * Gets static field value + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * <p> + * Equivalent to the call {@code getFieldValue(field, (Object) null)} + * + * @param field the field + * @param <T> The expected/known field type + * @return The field value + * @see #getFieldValue(Field, Object) + */ + public static <T> T getStaticFieldValue(Field field) { + return getFieldValue(field, (Object) null); + } + + /** + * Sets field value of the field named fieldName and the given instance + * To find the field, {@link #deepSearchField(Class, String)} is used (DEEP search method). + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param instance The instance whose field value to set + * @param fieldName the name of the field + * @param newValue the new field value + * @throws IllegalArgumentException if the field doesn't exist + * @see #deepSearchField(Class, String) + * @see #setFieldValue(Class, String, Object, Object) + */ + public static void setFieldValue(Object instance, String fieldName, Object newValue) { + setFieldValue(deepSearchField(instance.getClass(), fieldName), instance, newValue); + } + + /** + * Sets field value of the field named fieldName and the given instance + * To find the field, {@link #restrictedSearchField(Class, String)} is used (RESTRICTED search method). + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param clazz The class to search for the field + * @param fieldName the name of the field + * @param instance The field owner + * @param newValue The new field value + * @throws IllegalArgumentException if the field doesn't exist + * @see #restrictedSearchField(Class, String) + * @see #setFieldValue(Field, Object, Object) + */ + public static void setFieldValue(Class<?> clazz, String fieldName, Object instance, Object newValue) { + setFieldValue(restrictedSearchField(clazz, fieldName), instance, newValue); + } + + /** + * Sets field value of the field named fieldName and the given instance + * To find the field, {@link #searchField(int, Class, String)} is used. + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param modifiers The modifiers for field search. Can have {@link #DEEP_SEARCH} and {@link #REMOVE_FINAL} + * @param clazz The class to search for the field + * @param instance The instance whose field value to set + * @param fieldName the name of the field + * @param newValue The new field value + * @throws IllegalArgumentException if the field doesn't exist + * @see #searchField(int, Class, String) + * @see #setFieldValue(Field, Object, Object) + */ + public static void setFieldValue(int modifiers, Class<?> clazz, String fieldName, Object instance, Object newValue) { + setFieldValue(searchField(modifiers, clazz, fieldName), instance, newValue); + } + + /** + * Sets a field value + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param field The field + * @param instance The field owner + * @param newValue The new field value + */ + public static void setFieldValue(Field field, Object instance, Object newValue) { + exceptionHandler.runSafe(() -> field.set(instance, newValue)); + } + + /** + * Sets static field value of the field name fieldName + * To find the field, {@link #restrictedSearchField(Class, String)} is used (RESTRICTED search method). + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param clazz The class to search for the field + * @param fieldName the name of the field + * @param newValue The new field value + * @throws IllegalArgumentException if the field doesn't exist + * @see #restrictedSearchField(Class, String) + * @see #setStaticFieldValue(Field, Object) + */ + public static void setStaticFieldValue(Class<?> clazz, String fieldName, Object newValue) { + setStaticFieldValue(restrictedSearchField(clazz, fieldName), newValue); + } + + /** + * Sets static field value of the field named fieldName + * To find the field, {@link #searchField(int, Class, String)} is used. + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param modifiers The modifiers for field search. Can have {@link #DEEP_SEARCH} and {@link #REMOVE_FINAL} + * @param clazz The class to search for the field + * @param fieldName the name of the field + * @param newValue The new field value + * @throws IllegalArgumentException if the field doesn't exist + * @see #searchField(int, Class, String) + * @see #setStaticFieldValue(Field, Object) + */ + public static void setStaticFieldValue(int modifiers, Class<?> clazz, String fieldName, Object newValue) { + setStaticFieldValue(searchField(modifiers, clazz, fieldName), newValue); + } + + /** + * Sets a static field value + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param field The field + * @param newValue The new field value + */ + public static void setStaticFieldValue(Field field, Object newValue) { + setFieldValue(field, (Object) null, newValue); + } + + /* + ### METHOD METHODS ### + */ + + /** + * Search a method of any accessibility within the class or any of its superclasses. + * The first method with the given name that is found will be returned. + * <p> + * If a method is found and it is not accessible, this method attempts to make it accessible. + * If a {@link SecurityException} is thrown in the process, that is ignored and the method will be returned nonetheless. + * <p> + * This method throws IllegalArgumentException if the Method is not found, because, in most cases, that should never happen, + * and it should simplify debugging. In some cases, if you want to know if the method exists, you'll have to use try/catch for that. + * + * @param clazz The lowest class in the ladder to start searching from + * @param methodName The name of the method + * @param parameterTypes the parameter types of the sought method. + * @return The method + * @throws NullPointerException if clazz is null or methodName is null + * @throws IllegalArgumentException if the method doesn't exist + * @see #restrictedSearchMethod(Class, String, Class[]) + */ + public static Method deepSearchMethod(Class<?> clazz, String methodName, Class<?>... parameterTypes) { + return deepSearchMethod(0, clazz, methodName, parameterTypes); + } + + /** + * Search a method of any accessibility within the class or any of its superclasses. + * The first method with the given name that is found will be returned. + * <p> + * If a method is found and it is not accessible, this method attempts to make it accessible. + * If a {@link SecurityException} is thrown in the process, that is ignored and the method will be returned nonetheless. + * <p> + * This method throws IllegalArgumentException if the Method is not found, because, in most cases, that should never happen, + * and it should simplify debugging. In some cases, if you want to know if the method exists, you'll have to use try/catch for that. + * + * @param modifiers The modifiers for method search. Can have {@link #IGNORE_PARAMS} + * @param clazz The lowest class in the ladder to start searching from + * @param methodName The name of the method + * @param parameterTypes the parameter types of the sought method. + * @return The method + * @throws NullPointerException if clazz is null or methodName is null + * @throws IllegalArgumentException if the method doesn't exist + * @see #restrictedSearchMethod(Class, String, Class[]) + */ + public static Method deepSearchMethod(int modifiers, Class<?> clazz, String methodName, Class<?>... parameterTypes) { + Class<?> currentClass = clazz; + Method result; + do { + // throws NPE if class or methodName is null + result = internalSearchMethod(modifiers, currentClass, methodName, parameterTypes); + if (result != null) { + return result; + } + currentClass = currentClass.getSuperclass(); + } while (currentClass != null); + + throw new IllegalArgumentException("method not found in " + clazz.getCanonicalName() + " and superclasses: " + methodName); + } + + /** + * Search a method of any accessibility within the class, but not its superclasses. + * <p> + * If a method is found and it is not accessible, this method attempts to make it accessible. + * If a {@link SecurityException} is thrown in the process, that is ignored and the method will be returned nonetheless. + * <p> + * This method throws IllegalArgumentException if the Method is not found, because, in most cases, that should never happen, + * and it should simplify debugging. In some cases, if you want to know if the method exists, you'll have to use try/catch for that. + * + * @param clazz The only class to search for the method + * @param methodName The name of the method + * @param parameterTypes the parameter types of the sought method. + * @return The method + * @throws NullPointerException if clazz or methodName is null + * @throws IllegalArgumentException if the method does not exist + */ + public static Method restrictedSearchMethod(Class<?> clazz, String methodName, Class<?>... parameterTypes) { + return restrictedSearchMethod(0, clazz, methodName, parameterTypes); + } + + /** + * Search a method of any accessibility within the class, but not its superclasses. + * <p> + * If a method is found and it is not accessible, this method attempts to make it accessible. + * If a {@link SecurityException} is thrown in the process, that is ignored and the method will be returned nonetheless. + * <p> + * This method throws IllegalArgumentException if the Method is not found, because, in most cases, that should never happen, + * and it should simplify debugging. In some cases, if you want to know if the method exists, you'll have to use try/catch for that. + * + * @param modifiers The modifiers for method search. Can have {@link #IGNORE_PARAMS} + * @param clazz The only class to search for the method + * @param methodName The name of the method + * @param parameterTypes the parameter types of the sought method. + * @return The method + * @throws NullPointerException if clazz or methodName is null + * @throws IllegalArgumentException if the method does not exist + */ + public static Method restrictedSearchMethod(int modifiers, Class<?> clazz, String methodName, Class<?>... parameterTypes) { + Method result = internalSearchMethod(modifiers, clazz, methodName, parameterTypes); + if (result == null) { + throw new IllegalArgumentException("method not found in " + clazz.getCanonicalName() + ": " + methodName); + } + return result; + } + + /** + * Searches for a method using the given search method. + * <p> + * If a method is found and it is not accessible, this method attempts to make it accessible. + * If a {@link SecurityException} is thrown in the process, that is ignored and the method will be returned nonetheless. + * <p> + * This method throws IllegalArgumentException if the Method is not found, because, in most cases, that should never happen, + * and it should simplify debugging. In some cases, if you want to know if the method exists, you'll have to use try/catch for that. + * + * @param modifiers The modifiers for method search. Can have {@link #DEEP_SEARCH} and {@link #IGNORE_PARAMS} + * @param clazz The class to search in/from + * @param methodName Name of the method + * @param parameterTypes the parameter types of the sought method. + * @return The method + * @throws NullPointerException if clazz or methodName is null + * @throws IllegalArgumentException if the method is not found + */ + public static Method searchMethod(int modifiers, Class<?> clazz, String methodName, Class<?>... parameterTypes) { + if ((modifiers & DEEP_SEARCH) != 0) { + return deepSearchMethod(modifiers, clazz, methodName, parameterTypes); + } else { + return restrictedSearchMethod(modifiers, clazz, methodName, parameterTypes); + } + } + + /** + * @return The same as {@link #restrictedSearchMethod(Class, String, Class[]) }, but returns null instead of throwing IllegalArgumentException + * @see #restrictedSearchMethod(Class, String, Class[]) + */ + private static Method internalSearchMethod(int modifiers, Class<?> clazz, String methodName, Class<?>... parameterTypes) { + Method result = null; + + if ((modifiers & IGNORE_PARAMS) != 0) { + + // throws NullPointerException if either clazz or methodName are null. + methodName = methodName.intern(); + for (Method method : clazz.getDeclaredMethods()) { + // all method names are interned. Identity comparison is much faster. + if (method.getName() == methodName) { + result = method; + break; + } + } + + if (result == null) { + return null; + } + + } else { + + try { + // throws NullPointerException if either clazz or methodName are null. + result = clazz.getDeclaredMethod(methodName, parameterTypes); + } catch (NoSuchMethodException | SecurityException ex) { + return null; + } + + } + + if (!result.isAccessible()) try { + result.setAccessible(true); + } catch (SecurityException ignored) { + + } + + return result; + } + + /** + * Invokes the method named methodName with the given instance and arguments + * To find the method, {@link #searchMethod(int, Class, String, Class[])} is used with no type parameters, + * modifiers {@link #DEEP_SEARCH} and {@link #IGNORE_PARAMS}, and the class {@link Object#getClass() instance.getClass()} + * <p> + * To search the method with type parameters, you should search the method using {@link #searchMethod(int, Class, String, Class[])} or similar, + * and call {@link #invokeMethod(Method, Object, Object...)} + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param methodName Name of the method + * @param instance The instance to invoke the method on + * @param args The arguments to use in the method call + * @param <T> The expected/known method return type + * @return The result of calling the method + * @throws NullPointerException if instance or methodName is null + * @throws IllegalArgumentException if the method is not found + * @see #invokeMethod(Method, Object, Object...) + */ + public static <T> T invokeMethod(Object instance, String methodName, Object... args) { + return invokeMethod(searchMethod(DEEP_SEARCH | IGNORE_PARAMS, instance.getClass(), methodName), instance, args); + } + + /** + * Invokes the method named methodName with the given instance and arguments + * To find the method, {@link #searchMethod(int, Class, String, Class[])} is used with no type parameters, + * as well as the modifier {@link #IGNORE_PARAMS} + * <p> + * To search the method with type parameters, you should search the method using {@link #searchMethod(int, Class, String, Class[])} or similar, + * and call {@link #invokeMethod(Method, Object, Object...)} + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param clazz The class to search in/from + * @param methodName Name of the method + * @param instance The instance to invoke the method on + * @param args The arguments to use in the method call + * @param <T> The expected/known method return type + * @return The result of calling the method + * @throws NullPointerException if clazz or methodName is null + * @throws IllegalArgumentException if the method is not found + * @see #invokeMethod(Method, Object, Object...) + */ + public static <T> T invokeMethod(Class<?> clazz, String methodName, Object instance, Object... args) { + return invokeMethod(searchMethod(IGNORE_PARAMS, clazz, methodName), instance, args); + } + + /** + * Invokes the method named methodName with the given instance and arguments + * To find the method, {@link #searchMethod(int, Class, String, Class[])} is used with no type parameters. + * For this search, the result of calling {@link Object#getClass() instance.getClass()} is used. + * <p> + * To search the method with type parameters, you should search the method using {@link #searchMethod(int, Class, String, Class[])} or similar, + * and call {@link #invokeMethod(Method, Object, Object...)} + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param modifiers The modifiers for method search. Can have {@link #DEEP_SEARCH} and {@link #IGNORE_PARAMS} + * @param methodName Name of the method + * @param instance The instance to invoke the method on + * @param args The arguments to use in the method call + * @param <T> The expected/known method return type + * @return The result of calling the method + * @throws NullPointerException if instance or methodName is null + * @throws IllegalArgumentException if the method is not found + * @see #invokeMethod(Method, Object, Object...) + */ + public static <T> T invokeMethod(int modifiers, Object instance, String methodName, Object... args) { + return invokeMethod(searchMethod(modifiers, instance.getClass(), methodName), instance, args); + } + + /** + * Invokes the method named methodName with the given instance and arguments + * To find the method, {@link #searchMethod(int, Class, String, Class[])} is used with no type parameters. + * <p> + * To search the method with type parameters, you should search the method using {@link #searchMethod(int, Class, String, Class[])} or similar, + * and call {@link #invokeMethod(Method, Object, Object...)} + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param modifiers The modifiers for method search. Can have {@link #DEEP_SEARCH} and {@link #IGNORE_PARAMS} + * @param clazz The class to search in/from + * @param methodName Name of the method + * @param instance The instance to invoke the method on + * @param args The arguments to use in the method call + * @param <T> The expected/known method return type + * @return The result of calling the method + * @throws NullPointerException if clazz or methodName is null + * @throws IllegalArgumentException if the method is not found + * @see #invokeMethod(Method, Object, Object...) + */ + public static <T> T invokeMethod(int modifiers, Class<?> clazz, String methodName, Object instance, Object... args) { + return invokeMethod(searchMethod(modifiers, clazz, methodName), instance, args); + } + + /** + * Invokes the method with the given instance and arguments + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param method The method to invoke + * @param instance The instance to invoke the method on + * @param args The arguments to use in the method call + * @param <T> The expected/known method return type + * @return The result of calling the method + */ + public static <T> T invokeMethod(Method method, Object instance, Object... args) { + return exceptionHandler.supplySafe(() -> (T) method.invoke(instance, args)); + } + + /** + * Invokes the static method named methodName with the given arguments + * To find the method, {@link #searchMethod(int, Class, String, Class[])} is used with no type parameters, + * as well as the modifier {@link #IGNORE_PARAMS} + * <p> + * To search the method with type parameters, you should search the method using {@link #searchMethod(int, Class, String, Class[])} or similar, + * and call {@link #invokeMethod(Method, Object, Object...)} + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param clazz The class to search in/from + * @param methodName Name of the method + * @param args The arguments to use in the method call + * @param <T> The expected/known method return type + * @return The result of calling the method + * @throws NullPointerException if clazz or methodName is null + * @throws IllegalArgumentException if the method is not found + * @see #invokeStaticMethod(Method, Object...) + */ + public static <T> T invokeStaticMethod(Class<?> clazz, String methodName, Object... args) { + return invokeStaticMethod(searchMethod(IGNORE_PARAMS, clazz, methodName), args); + } + + /** + * Invokes the static method named methodName with the given arguments + * To find the method, {@link #searchMethod(int, Class, String, Class[])} is used with no type parameters. + * <p> + * To search the method with type parameters, you should search the method using {@link #searchMethod(int, Class, String, Class[])} or similar, + * and call {@link #invokeMethod(Method, Object, Object...)} + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param modifiers The modifiers for method search. Can have {@link #DEEP_SEARCH} and {@link #IGNORE_PARAMS} + * @param clazz The class to search in/from + * @param methodName Name of the method + * @param args The arguments to use in the method call + * @param <T> The expected/known method return type + * @return The result of calling the method + * @throws NullPointerException if clazz or methodName is null + * @throws IllegalArgumentException if the method is not found + * @see #invokeStaticMethod(Method, Object...) + */ + public static <T> T invokeStaticMethod(int modifiers, Class<?> clazz, String methodName, Object... args) { + return invokeStaticMethod(searchMethod(modifiers, clazz, methodName), args); + } + + /** + * Invokes the static method with the given arguments + * <p> + * If a {@link ReflectiveOperationException} occurs, this is printed to {@link System#out} + * + * @param method The method to invoke + * @param args The arguments to use in the method call + * @param <T> The expected/known method return type + * @return The result of calling the method + * @see #invokeMethod(Method, Object, Object...) + */ + public static <T> T invokeStaticMethod(Method method, Object... args) { + return invokeMethod(method, (Object) null, args); + } + +} diff --git a/src/main/java/io/dico/dicore/Registrator.java b/src/main/java/io/dico/dicore/Registrator.java new file mode 100644 index 0000000..9977418 --- /dev/null +++ b/src/main/java/io/dico/dicore/Registrator.java @@ -0,0 +1,987 @@ +package io.dico.dicore; + +import org.bukkit.Server; +import org.bukkit.event.*; +import org.bukkit.event.player.PlayerEvent; +import org.bukkit.event.player.PlayerKickEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.event.server.PluginEnableEvent; +import org.bukkit.plugin.EventExecutor; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.RegisteredListener; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.*; +import java.util.*; +import java.util.function.Consumer; + +/** + * This class acts as a utility to register event listeners in a functional manner. + * Listeners passed are always {@code <T> Consumer<T extends Event>} objects. + * <p> + * Registrations are made using its + * * {@link #registerListener(Class, Consumer)} + * * {@link #registerListener(Class, EventPriority, Consumer)} + * * {@link #registerListener(Class, boolean, Consumer)} + * * {@link #registerListener(Class, EventPriority, boolean, Consumer)} + * * {@link #registerListeners(Class)} + * * {@link #registerListeners(Object)} + * * {@link #registerListeners(Class, Object)} + * * {@link #registerPlayerQuitListener(Consumer)} + * methods. + * <p> + * Listeners registered in this way are generally a bit faster than when registered through {@link org.bukkit.plugin.PluginManager#registerEvents(Listener, Plugin)} + * Because it does not use reflection to call the event handlers. + * + * @implNote This class uses only one {@link Listener listener object} across all its instances, by fooling spigot into + * thinking they're all distinct ones (by violating the {@link Object#equals(Object)} contract). + * <p> + * Standard Registrator instances also use a fake plugin identity to register its listeners. + * You can use the {{@link #Registrator(Plugin)}} constructor to use real plugin identities. + */ +@SuppressWarnings("DanglingJavadoc") +public final class Registrator { + + // ############################################ + // # Public static methods + // ############################################ + + public static Registrator getInstance() { + return instance; + } + + // ############################################ + // # Static fields and initializer + // ############################################ + + private static final Registrator instance; + private static final Listener universalListenerObject; + private static final Plugin defaultFakePlugin; + private static final Map<Class<?>, HandlerListInfo> handlerListCache; + + static { + handlerListCache = new IdentityHashMap<>(); + defaultFakePlugin = new RegistratorPlugin(); + instance = new Registrator(); + universalListenerObject = new Listener() { + + //@formatter:off + /** return false here to fool the HandlerList into believing each registration is from another Listener. + * as a result, no exceptions will be thrown when registering multiple listeners for the same event and priority. + * + * Another option is to have this for each instance: + * + * + * <pre>{@code + private Listener getListenerFor(HandlerList list, EventPriority priority) { + int needed = (int) (listeners.get(list).stream().filter(listener -> listener.getPriority() == priority).count() + 1); + while (needed > myListeners.size()) { + myListeners.add(new Listener() {}); + } + return myListeners.get(needed - 1); + } + * }</pre> + * + * + * Where {@code myListeners} is a List<Listener> + * + * + */ + //@formatter:on + @Override + public boolean equals(Object obj) { + + return false; + } + }; + } + + // ############################################ + // # Instance fields and constructors + // ############################################ + + private final List<Registration> registrations; + private Plugin plugin; + private Registration pluginEnableListener; + private Registration pluginDisableListener; + private boolean enabled; + + /** + * Constructs a new instance using the {@link #defaultFakePlugin universal plugin object} + */ + public Registrator() { + this(false); + } + + /** + * Constructs a new instance using an artificial plugin. + * + * @param distinctPlugin true if the artificial plugin should be distinct from the {@link #defaultFakePlugin universal plugin object} + */ + public Registrator(boolean distinctPlugin) { + this(distinctPlugin ? new RegistratorPlugin() : defaultFakePlugin); + } + + /** + * Constructs a new instance using the given plugin + * + * @param plugin The plugin to register the listeners with + * @throws NullPointerException if plugin is null + */ + public Registrator(Plugin plugin) { + this.registrations = new ArrayList<>(); + setPlugin(plugin); + } + + // ############################################ + // # Internal static methods + // ############################################ + + /** + * static {@link EventExecutor} instantiator to make sure executors don't reference any objects unnecessarily. + */ + @SuppressWarnings("unchecked") + private static <T extends Event> EventExecutor newEventExecutor(Class<T> eventClass, Consumer<? super T> handler) { + if (getHandlerListInfoOf(eventClass).requiresFilter) { + return (ignored, event) -> { + if (eventClass.isInstance(event)) { + handler.accept((T) event); + } + }; + } + return (ignored, event) -> handler.accept((T) event); + } + + /** + * Reflectively acquire the HandlerList for the given event type. + * + * @param eventClass the Event type + * @return its HandlerList, or null if one can't be found + */ + private static HandlerList getHandlerListOf(Class<?> eventClass) { + try { + return getHandlerListInfoOf(eventClass).handlerList; + } catch (RuntimeException e) { + return null; + } + } + + private static HandlerListInfo getHandlerListInfoOf(Class<?> eventClass) { + return handlerListCache.computeIfAbsent(eventClass, clz -> { + Method method = Reflection.deepSearchMethod(clz, "getHandlerList"); + boolean requiresFilter = clz != method.getDeclaringClass(); + return new HandlerListInfo(Reflection.invokeStaticMethod(method), requiresFilter); + }); + } + + // ############################################ + // # Public instance methods + // ############################################ + + /** + * Change the plugin used by the listeners of this registrator. + * + * @param plugin the plugin to use + * @throws NullPointerException if plugin is null + * @throws IllegalStateException if this registrator was returned by {@link #getInstance()} + */ + public void setPlugin(Plugin plugin) { + Objects.requireNonNull(plugin); + if (this.plugin == plugin) { + return; + } + + if (this.plugin != null) { + if (this == instance) { + throw new IllegalStateException("You may not modify the plugin used by the universal Registrator instance"); + } + + setEnabled(false); + setPluginListenerRegisteredStates(false, false); + setListenersPluginTo(plugin); + } + + this.plugin = plugin; + initPluginListeners(); + updatePluginListeners(plugin.isEnabled()); + if (plugin.isEnabled()) { + setEnabled(true); + } + } + + /** + * @return The plugin object used when registering the listeners + */ + public Plugin getRegistrationPlugin() { + return plugin; + } + + /** + * @return True if the plugin used was artificial / not an actual plugin on the server / fooled the bukkit api + */ + public boolean hasFakePlugin() { + return plugin instanceof RegistratorPlugin; + } + + /** + * @return An unmodifiable view of the registrations made by this {@link Registrator} + */ + public List<Registration> getListeners() { + return Collections.unmodifiableList(registrations); + } + + /** + * Make a new listener handle for the given event type. + * The returned listener handle is not managed by this {@link Registrator}, and you must register it yourself. + * the event priority is set to {@link EventPriority#HIGHEST} + * the ignore cancelled flag is set to {@code true} + * + * @param eventClass The event type + * @param handler the listener + * @param <T> the event type + * @return this + * / + public <T extends Event> ListenerHandle makeListenerHandle(Class<T> eventClass, Consumer<? super T> handler) { + return makeListenerHandle(eventClass, EventPriority.HIGHEST, handler); + }/* */ + + /** + * Make a new listener handle for the given event type. + * The returned listener handle is not managed by this {@link Registrator}, and you must register it yourself. + * The ignoreCancelled flag is set to false if {@code priority} is {@link EventPriority#LOW} or {@link EventPriority#LOWEST} + * otherwise, it is set to true. + * + * @param eventClass The event type + * @param priority the event priority + * @param handler the listener + * @param <T> the event type + * @return this + * / + public <T extends Event> ListenerHandle makeListenerHandle(Class<T> eventClass, EventPriority priority, Consumer<? super T> handler) { + boolean ignoreCancelled = Cancellable.class.isAssignableFrom(eventClass) && priority.getSlot() > EventPriority.LOW.getSlot(); + return makeListenerHandle(eventClass, priority, ignoreCancelled, handler); + }/* */ + + /** + * Make a new listener handle for the given event type. + * The returned listener handle is not managed by this {@link Registrator}, and you must register it yourself. + * If {@code ignoreCancelled} is true, the event priority is set to {@link EventPriority#HIGHEST} + * Otherwise, it is set to {@link EventPriority#LOW} + * + * @param eventClass The event type + * @param ignoreCancelled the ignoreCancelled flag of the listener + * @param handler The listener + * @param <T> The event type + * @return this + * / + public <T extends Event> ListenerHandle makeListenerHandle(Class<T> eventClass, boolean ignoreCancelled, Consumer<? super T> handler) { + return makeListenerHandle(eventClass, ignoreCancelled ? EventPriority.HIGHEST : EventPriority.LOW, ignoreCancelled, handler); + }/* */ + + /** + * Make a new listener handle for the given event type. + * The returned listener handle is not managed by this {@link Registrator}, and you must register it yourself. + * + * @param eventClass The event type + * @param priority the event priority + * @param ignoreCancelled the ignoreCancelled flag of the listener + * @param handler the listener + * @param <T> the event type + * @return this + * / + public <T extends Event> ListenerHandle makeListenerHandle(Class<T> eventClass, EventPriority priority, boolean ignoreCancelled, Consumer<? super T> handler) { + return (ListenerHandle) createRegistration(true, priority, ignoreCancelled, eventClass, handler); + }/* */ + + /** + * Register a listener for the given event type. + * the event priority is set to {@link EventPriority#HIGHEST} + * the ignore cancelled flag is set to {@code true} + * + * @param eventClass The event type + * @param handler the listener + * @param <T> the event type + * @return this + */ + public <T extends Event> Registrator registerListener(Class<T> eventClass, Consumer<? super T> handler) { + return registerListener(eventClass, EventPriority.HIGHEST, handler); + } + + /** + * Register a listener for the given event type. + * The ignoreCancelled flag is set to false if {@code priority} is {@link EventPriority#LOW} or {@link EventPriority#LOWEST} + * otherwise, it is set to true. + * + * @param eventClass The event type + * @param priority the event priority + * @param handler the listener + * @param <T> the event type + * @return this + */ + public <T extends Event> Registrator registerListener(Class<T> eventClass, EventPriority priority, Consumer<? super T> handler) { + boolean ignoreCancelled = Cancellable.class.isAssignableFrom(eventClass) && priority.getSlot() > EventPriority.LOW.getSlot(); + return registerListener(eventClass, priority, ignoreCancelled, handler); + } + + /** + * Register a listener for the given event type. + * If {@code ignoreCancelled} is true, the event priority is set to {@link EventPriority#HIGHEST} + * Otherwise, it is set to {@link EventPriority#LOW} + * + * @param eventClass The event type + * @param ignoreCancelled the ignoreCancelled flag of the listener + * @param handler The listener + * @param <T> The event type + * @return this + */ + public <T extends Event> Registrator registerListener(Class<T> eventClass, boolean ignoreCancelled, Consumer<? super T> handler) { + return registerListener(eventClass, ignoreCancelled ? EventPriority.HIGHEST : EventPriority.LOW, ignoreCancelled, handler); + } + + /** + * Register a listener for the given event type. + * + * @param eventClass The event type + * @param priority the event priority + * @param ignoreCancelled the ignoreCancelled flag of the listener + * @param handler the listener + * @param <T> the event type + * @return this + */ + public <T extends Event> Registrator registerListener(Class<T> eventClass, EventPriority priority, boolean ignoreCancelled, Consumer<? super T> handler) { + registerListener(createRegistration(false, priority, ignoreCancelled, eventClass, handler)); + return this; + } + + public Registrator registerListeners(Class<?> clazz, Object instance) { + for (ListenerFieldInfo fieldInfo : getListenerFields(clazz, instance)) { + registerListener(fieldInfo.eventClass, fieldInfo.anno.priority(), fieldInfo.anno.ignoreCancelled(), fieldInfo.lambda); + } + return this; + } + + public Registrator registerListeners(Class<?> clazz) { + return registerListeners(clazz, null); + } + + public Registrator registerListeners(Object instance) { + return registerListeners(instance.getClass(), instance); + } + + /* * / + public ChainedListenerHandle makeChainedListenerHandle(Class<?> clazz, Object instance) { + ChainedListenerHandle rv = ChainedListenerHandles.empty(); + for (ListenerFieldInfo fieldInfo : getListenerFields(clazz, instance)) { + rv = rv.withElement(makeListenerHandle(fieldInfo.eventClass, fieldInfo.anno.priority(), fieldInfo.anno.ignoreCancelled(), fieldInfo.lambda)); + } + return rv; + } + + public ChainedListenerHandle makeChainedListenerHandle(Class<?> clazz) { + return makeChainedListenerHandle(clazz, null); + } + + public ChainedListenerHandle makeChainedListenerHandle(Object instance) { + return makeChainedListenerHandle(instance.getClass(), instance); + } + + public ListenerHandle makePlayerQuitListenerHandle(Consumer<? super PlayerEvent> handler) { + ListenerHandle first = makeListenerHandle(PlayerQuitEvent.class, EventPriority.NORMAL, handler); + ListenerHandle second = makeListenerHandle(PlayerKickEvent.class, EventPriority.NORMAL, handler); + return ChainedListenerHandles.singleton(first).withElement(second); + } + /* */ + + public Registrator registerPlayerQuitListener(Consumer<? super PlayerEvent> handler) { + registerListener(PlayerQuitEvent.class, EventPriority.NORMAL, handler); + return registerListener(PlayerKickEvent.class, EventPriority.NORMAL, handler); + } + + @Override + public String toString() { + return "Registrator{" + + "plugin: " + plugin + + ", enabled: " + enabled + + ", registrations: " + registrations.size() + + '}'; + } + + public String toStringWithAllRegistrations() { + StringBuilder sb = new StringBuilder("Registrator {"); + sb.append("\n plugin: ").append(plugin); + sb.append("\n enabled: ").append(enabled); + sb.append("\n registrations: ["); + + Iterator<Registration> iterator = registrations.iterator(); + if (iterator.hasNext()) { + sb.append("\n ").append(iterator.next().toString()); + } + while (iterator.hasNext()) { + sb.append(',').append("\n ").append(iterator.next().toString()); + } + if (!registrations.isEmpty()) { + sb.append("\n "); + } + sb.append("]\n}"); + return sb.toString(); + } + + // ############################################ + // # Public types + // ############################################ + + public interface IEventListener<T extends Event> extends Consumer<T> { + @Override + void accept(T event); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface ListenerInfo { + + String[] events() default {}; + + EventPriority priority() default EventPriority.HIGHEST; + + boolean ignoreCancelled() default true; + } + + public static class Registration extends RegisteredListener { + + private final EventExecutor executor; + private final Class<?> eventClass; + private final StackTraceElement caller; + private boolean registered; + + Registration(Class<?> eventClass, StackTraceElement caller, EventExecutor executor, EventPriority priority, Plugin plugin, boolean ignoreCancelled) { + super(universalListenerObject, executor, priority, plugin, ignoreCancelled); + this.executor = executor; + this.eventClass = eventClass; + this.caller = caller; + } + + Registration setPlugin(Plugin plugin) { + if (getPlugin() == plugin) { + return this; + } + boolean registered = this.registered; + unregister(); + Registration out = new Registration(eventClass, caller, executor, getPriority(), plugin, isIgnoringCancelled()); + if (registered) { + out.register(); + } + return out; + } + + public Class<?> getEventClass() { + return eventClass; + } + + public StackTraceElement getCaller() { + return caller; + } + + public boolean isRegistered() { + return registered; + } + + void register() { + if (!registered) { + registered = true; + getHandlerListOf(eventClass).register(this); + } + } + + void unregister() { + if (registered) { + registered = false; + getHandlerListOf(eventClass).unregister(this); + } + } + + @Override + public String toString() { + return "Listener for " + eventClass.getSimpleName() + (caller == null ? "" : " registered at " + caller.toString()); + } + + } + + // ############################################ + // # Internal instance methods + // ############################################ + + @SuppressWarnings("UnusedReturnValue") + private boolean setEnabled(boolean enabled) { + if (this.enabled != enabled) { + this.enabled = enabled; + if (enabled) { + registerAllListeners(); + } else { + unregisterAllListeners(); + } + return true; + } + return false; + } + + private void initPluginListeners() { + if (hasFakePlugin()) { + pluginEnableListener = pluginDisableListener = null; + } else { + if (pluginEnableListener != null) { + pluginEnableListener = pluginEnableListener.setPlugin(plugin); + } else { + pluginEnableListener = createRegistration(null, false, EventPriority.NORMAL, false, PluginEnableEvent.class, this::onPluginEnable); + } + if (pluginDisableListener != null) { + pluginDisableListener = pluginDisableListener.setPlugin(plugin); + } else { + pluginDisableListener = createRegistration(null, false, EventPriority.NORMAL, false, PluginDisableEvent.class, this::onPluginDisable); + } + } + } + + private void updatePluginListeners(boolean pluginEnabled) { + if (hasFakePlugin()) { + setPluginListenerRegisteredStates(false, false); + } else { + setPluginListenerRegisteredStates(!pluginEnabled, pluginEnabled); + } + } + + private void setPluginListenerRegisteredStates(boolean enableListenerRegistered, boolean disableListenerRegistered) { + if (pluginEnableListener != null) { + if (enableListenerRegistered) { + PluginEnableEvent.getHandlerList().register(pluginEnableListener); + } else { + PluginEnableEvent.getHandlerList().unregister(pluginEnableListener); + } + } + if (pluginDisableListener != null) { + if (disableListenerRegistered) { + PluginDisableEvent.getHandlerList().register(pluginDisableListener); + } else { + PluginDisableEvent.getHandlerList().unregister(pluginDisableListener); + } + } + } + + private void onPluginEnable(PluginEnableEvent event) { + if (event.getPlugin() == plugin) { + setEnabled(true); + updatePluginListeners(true); + } + } + + private void onPluginDisable(PluginDisableEvent event) { + if (event.getPlugin() == plugin) { + setEnabled(false); + updatePluginListeners(false); + } + } + + private void setListenersPluginTo(Plugin plugin) { + List<Registration> registrations = this.registrations; + for (int n = registrations.size(), i = 0; i < n; i++) { + registrations.set(i, registrations.get(i).setPlugin(plugin)); + } + } + + private void registerListener(Registration registration) { + registrations.add(registration); + if (enabled) { + registration.register(); + } + } + + private <T extends Event> Registration createRegistration(boolean asHandle, + EventPriority priority, + boolean ignoreCancelled, + Class<T> eventClass, + Consumer<? super T> handler) { + StackTraceElement caller = null; + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + if (stackTrace.length > 0) { + String className = Registrator.class.getName(); + for (StackTraceElement element : stackTrace) { + if (!element.getClassName().equals(className) && !element.getClassName().startsWith("java.lang")) { + caller = element; + break; + } + } + } + + return createRegistration(caller, asHandle, priority, ignoreCancelled, eventClass, handler); + } + + private <T extends Event> Registration createRegistration(StackTraceElement caller, + boolean asHandle, + EventPriority priority, + boolean ignoreCancelled, + Class<T> eventClass, + Consumer<? super T> handler) { + EventExecutor executor = newEventExecutor(eventClass, handler); + /* * / + if (asHandle) { + return new RegistrationWithHandle(eventClass, caller, executor, priority, plugin, ignoreCancelled); + } + /* */ + return new Registration(eventClass, caller, executor, priority, plugin, ignoreCancelled); + } + + private void registerAllListeners() { + for (Registration registration : registrations) { + registration.register(); + } + } + + private void unregisterAllListeners() { + for (Registration registration : registrations) { + registration.unregister(); + } + } + + @SuppressWarnings("unchecked") + private Collection<ListenerFieldInfo> getListenerFields(Class<?> clazz, Object instance) { + Collection<ListenerFieldInfo> rv = new ArrayList<>(); + + Field[] fields = clazz.getDeclaredFields(); + boolean isStatic = instance == null; + if (!isStatic && !clazz.isInstance(instance)) { + throw new IllegalArgumentException("Instance must be an instance of the given class"); + } + + fieldLoop: + for (Field f : fields) { + if (isStatic != Modifier.isStatic(f.getModifiers()) + || !f.isAnnotationPresent(ListenerInfo.class)) { + continue; + } + + if (!IEventListener.class.isAssignableFrom(f.getType())) { + handleListenerFieldError(new ListenerFieldError(f, "Field type cannot be assigned to IEventListener: " + f.getGenericType().getTypeName())); + continue; + } + + Type eventType = null; + if (f.getType() == IEventListener.class) { + + Type[] typeArgs; + if (!(f.getGenericType() instanceof ParameterizedType) + || (typeArgs = ((ParameterizedType) f.getGenericType()).getActualTypeArguments()).length != 1) { + // TODO: if its a TypeVariable, in some cases it might be possible to get the type. + // Log a warning or throw an exception + handleListenerFieldError(new ListenerFieldError(f, "Failed to recognize event class from field type: " + f.getGenericType().getTypeName())); + continue; + } + eventType = typeArgs[0]; + + } else { + // field type is subtype of IEventListener. + // TODO: link type arguments from field declaration (f.getGenericType()) to matching TypeVariables + Type[] interfaces = f.getType().getGenericInterfaces(); + for (Type itf : interfaces) { + Class<?> itfClass; + Type[] arguments = null; + if (itf instanceof ParameterizedType) { + if (!(((ParameterizedType) itf).getRawType() instanceof Class)) { + // Should not happen: throw error + throw new InternalError("rawType of ParameterizedType expected to be a Class"); + } + itfClass = (Class<?>) ((ParameterizedType) itf).getRawType(); + arguments = ((ParameterizedType) itf).getActualTypeArguments(); + } else if (itf instanceof Class<?>) { + itfClass = (Class<?>) itf; + } else { + // TypeVariable? Not sure + // Ignore + continue; + } + + if (itfClass == IEventListener.class) { + if (arguments == null || arguments.length != 1) { + // Log a warning or throw an exception + handleListenerFieldError(new ListenerFieldError(f, "")); + continue fieldLoop; + } + + eventType = arguments[0]; + break; + } + } + + if (eventType == null) { + // Log a warning or throw an exception + handleListenerFieldError(new ListenerFieldError(f, "Failed to recognize event class from field type: " + f.getGenericType().getTypeName())); + continue; + } + } + + if (!(eventType instanceof Class)) { + if (eventType instanceof ParameterizedType) { + Type rawType = ((ParameterizedType) eventType).getRawType(); + if (!(rawType instanceof Class)) { + // Log a warning or throw an exception + handleListenerFieldError(new ListenerFieldError(f, "Failed to recognize event class from a Type: " + eventType)); + continue; + } + eventType = rawType; + } else { + // Log a warning or throw an exception + handleListenerFieldError(new ListenerFieldError(f, "Failed to recognize event class from a Type: " + eventType)); + continue; + } + } + + Consumer<? super Event> lambda; + try { + f.setAccessible(true); + lambda = (Consumer<? super Event>) f.get(instance); + } catch (IllegalArgumentException | IllegalAccessException | ClassCastException e) { + // Log a warning or throw an exception + handleListenerFieldError(new ListenerFieldError(f, e)); + continue; + } + + Class<? extends Event> baseEventClass = (Class<? extends Event>) eventType; + + ListenerInfo anno = f.getAnnotation(ListenerInfo.class); + String[] eventClassNames = anno.events(); + if (eventClassNames.length > 0) { + + // The same field might get added here multiple times, to register it with multiple events. + // This list is used to prevent adding its listener to the same handler list multiple times. + // Allocation of a table is not necessary at this scale. + List<HandlerList> handlerLists = new ArrayList<>(); + + for (String eventClassName : eventClassNames) { + Class<? extends Event> eventClass = getEventClassByName(eventClassName); + if (eventClass != null && baseEventClass.isAssignableFrom(eventClass)) { + HandlerList handlerList = getHandlerListOf(eventClass); + if (handlerList == null) { + // multiple warnings could be raised here for the same field + handleListenerFieldError(new ListenerFieldError(f, "There is no HandlerList available for the event " + eventClass.getName())); + continue; + } + + if (handlerLists.contains(handlerList)) { + // Ignore: it will work as intended + continue; + } + + handlerLists.add(handlerList); + rv.add(new ListenerFieldInfo(eventClass, lambda, anno)); + } else { + // Error: event class string is not recognized or cannot be assigned to the event type + // Log a warning or throw an exception + String msg = String.format("Event class '%s', resolved to '%s', is unresolved or cannot be assigned to '%s'", + eventClassName, eventClass == null ? null : eventClass.getName(), baseEventClass.getName()); + handleListenerFieldError(new ListenerFieldError(f, msg)); + // Don't add the field to the result list + } + } + + } else { + rv.add(new ListenerFieldInfo(baseEventClass, lambda, anno)); + } + } + return rv; + } + + private static void handleListenerFieldError(ListenerFieldError error) { + // Log a warning or throw an exception. Behaviour can be changed. + throw error; + } + + private static Class<? extends Event> getEventClassByName(String name) { + try { + //noinspection unchecked + return (Class<? extends Event>) Class.forName("org.bukkit.event." + name); + } catch (ClassNotFoundException | ClassCastException e) { + return null; + } + } + + // ############################################ + // # Internal types + // ############################################ + + /* * / + private static final class RegistrationWithHandle extends Registration implements ListenerHandle { + RegistrationWithHandle(Class<?> eventClass, StackTraceElement caller, EventExecutor executor, EventPriority priority, Plugin plugin, boolean ignoreCancelled) { + super(eventClass, caller, executor, priority, plugin, ignoreCancelled); + } + + @Override + public void register() { + super.register(); + } + + @Override + public void unregister() { + super.unregister(); + } + } + /* */ + + private static final class HandlerListInfo { + final HandlerList handlerList; + // true if and only if the handler list resides in a super class of the event for which it was requested. + // the filter is needed to filter out event instances not of the requested class. + // See newEventExecutor(eventClass, handler) + final boolean requiresFilter; + + HandlerListInfo(HandlerList handlerList, boolean requiresFilter) { + this.handlerList = handlerList; + this.requiresFilter = requiresFilter; + } + } + + private static final class ListenerFieldInfo { + final Class<? extends Event> eventClass; + final Consumer<? super Event> lambda; + final ListenerInfo anno; + + ListenerFieldInfo(Class<? extends Event> eventClass, Consumer<? super Event> lambda, ListenerInfo anno) { + this.eventClass = eventClass; + this.lambda = lambda; + this.anno = anno; + } + } + + /** + * Error class to report fields that are intended to be listeners with illegal properties + */ + static final class ListenerFieldError extends Error { + private Field field; + + public ListenerFieldError(Field field, String message) { + super(message); + this.field = field; + } + + public ListenerFieldError(Field field, Throwable cause) { + super(cause); + this.field = field; + } + + public Field getField() { + return field; + } + } + + private static class RegistratorPlugin implements Plugin { + @Override + public java.io.File getDataFolder() { + return null; + } + + @Override + public org.bukkit.plugin.PluginDescriptionFile getDescription() { + return null; + } + + @Override + public org.bukkit.configuration.file.FileConfiguration getConfig() { + return null; + } + + @Override + public java.io.InputStream getResource(String s) { + return null; + } + + @Override + public void saveConfig() { + } + + @Override + public void saveDefaultConfig() { + } + + @Override + public void saveResource(String s, boolean b) { + } + + @Override + public void reloadConfig() { + } + + @Override + public org.bukkit.plugin.PluginLoader getPluginLoader() { + return null; + } + + @Override + public Server getServer() { + return null; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public void onDisable() { + } + + @Override + public void onLoad() { + } + + @Override + public void onEnable() { + } + + @Override + public boolean isNaggable() { + return false; + } + + @Override + public void setNaggable(boolean b) { + } + + /* * / + @Override + public com.avaje.ebean.EbeanServer getDatabase() { + return null; + } + /* */ + @Override + public org.bukkit.generator.ChunkGenerator getDefaultWorldGenerator(String s, String s1) { + return null; + } + + @Override + public java.util.logging.Logger getLogger() { + return null; + } + + @Override + public String getName() { + return null; + } + + @Override + public boolean onCommand(org.bukkit.command.CommandSender commandSender, org.bukkit.command.Command command, String s, String[] strings) { + return false; + } + + @Override + public List<String> onTabComplete(org.bukkit.command.CommandSender commandSender, org.bukkit.command.Command command, String s, String[] strings) { + return null; + } + + @Override + public boolean equals(Object obj) { + return this == obj; + } + } + +}
\ No newline at end of file diff --git a/src/main/java/io/dico/dicore/exceptions/ExceptionHandler.java b/src/main/java/io/dico/dicore/exceptions/ExceptionHandler.java new file mode 100644 index 0000000..002eaa0 --- /dev/null +++ b/src/main/java/io/dico/dicore/exceptions/ExceptionHandler.java @@ -0,0 +1,203 @@ +package io.dico.dicore.exceptions; + +import io.dico.dicore.exceptions.checkedfunctions.CheckedFunctionalObject; +import io.dico.dicore.exceptions.checkedfunctions.CheckedRunnable; +import io.dico.dicore.exceptions.checkedfunctions.CheckedSupplier; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.sql.SQLException; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +@FunctionalInterface +public interface ExceptionHandler { + + /** + * Handle the given exception according to this handler's implementation + * + * @param ex The exception to be handled + * @throws NullPointerException if ex is null, unless the implementation specifies otherwise + * @throws Error ex if ex is an instance of Error, unless the implementation specifies otherwise + */ + void handle(Throwable ex); + + /** + * Handle the given exception according to this handler's implementation + * This method is intended for use by {@link CheckedFunctionalObject} and subinterfaces. + * It supplies exception handlers the option to acquire more information, by overriding this method and calling it from {@link #handle(Throwable)} + * + * @param ex The exception to be handled + * @param args Any arguments passed, this is used by {@link CheckedFunctionalObject} and subinterfaces. + * @return {@code null} (unless specified otherwise by the implementation) + * @throws NullPointerException if ex is null, unless the implementation specifies otherwise + * @throws Error ex if ex is an instance of Error, unless the implementation specifies otherwise + */ + default Object handleGenericException(Throwable ex, Object... args) { + handle(ex); + return null; + } + + /** + * @return true if this {@link ExceptionHandler}'s {@link #handleGenericException(Throwable, Object...)} method is <b>never</b> expected to throw + * an unchecked exception other than {@link Error} + */ + default boolean isSafe() { + return true; + } + + /** + * Runs the given checked action, handling any thrown exceptions using this exception handler. + * <p> + * Any exceptions thrown by this handler are delegated to the caller. + * + * @param action The action to run + * @throws NullPointerException if action is null + */ + default void runSafe(CheckedRunnable<? extends Throwable> action) { + Objects.requireNonNull(action); + try { + action.checkedRun(); + } catch (Throwable ex) { + handle(ex); + } + } + + /** + * Computes the result of the given checked supplier, handling any thrown exceptions using this exception handler. + * <p> + * Any exceptions thrown by this handler are delegated to the caller. + * + * @param action The supplier whose result to compute + * @param <T> generic type parameter for the supplier and the result type of this method + * @return The result of this computation, or null if an error occurred + * @throws NullPointerException if action is null + */ + + default <T> T supplySafe(CheckedSupplier<T, ? extends Throwable> action) { + Objects.requireNonNull(action); + try { + return action.checkedGet(); + } catch (Throwable ex) { + handle(ex); + return null; + } + } + + /** + * @param action The action to wrap + * @return A runnable that wraps the given action using this handler's {@link #runSafe(CheckedRunnable)} method. + * @see #runSafe(CheckedRunnable) + */ + default Runnable safeRunnable(CheckedRunnable<? extends Throwable> action) { + return () -> runSafe(action); + } + + /** + * @param supplier The computation to wrap + * @return A supplier that wraps the given computation using this handler's {@link #supplySafe(CheckedSupplier)} method. + * @see #supplySafe(CheckedSupplier) + */ + default <T> Supplier<T> safeSupplier(CheckedSupplier<T, ? extends Throwable> supplier) { + return () -> supplySafe(supplier); + } + + /** + * Logs the given exception as an error to {@code out} + * <p> + * Format: Error occurred while {@code failedActivityDescription}, followed by additional details and a stack trace + * + * @param out The consumer to accept the error message, for instance {@code {@link java.util.logging.Logger logger}::warning}. + * @param failedActivityDescription A description of the activity that was being executed when the exception was thrown + * @param ex The exception that was thrown + * @throws NullPointerException if any argument is null + */ + static void log(Consumer<String> out, String failedActivityDescription, Throwable ex) { + if (out == null || failedActivityDescription == null || ex == null) { + throw new NullPointerException(); + } + + StringWriter msg = new StringWriter(1024); + msg.append("Error occurred while ").append(failedActivityDescription).append(':'); + + if (ex instanceof SQLException) { + SQLException sqlex = (SQLException) ex; + msg.append('\n').append("Error code: ").append(Integer.toString(sqlex.getErrorCode())); + msg.append('\n').append("SQL State: ").append(sqlex.getSQLState()); + } + + msg.append('\n').append("=======START STACK======="); + try (PrintWriter pw = new PrintWriter(msg)) { + ex.printStackTrace(pw); + } + msg.append('\n').append("========END STACK========"); + + out.accept(msg.toString()); + } + + /** + * @param activityDescription The activity description + * @return An ExceptionHandler that prints to {@link System#out} + * @see #log(Consumer, String) + */ + static ExceptionHandler log(String activityDescription) { + // A method reference would cache the current value in System.out + // This would update if the value in System.out is changed (by for example, applying a PrintStream that handles colours). + return log(msg -> System.out.println(msg), activityDescription); + } + + /** + * @param out The Consumer to be passed to {@link #log(Consumer, String, Throwable)} + * @param activityDescription The activity description to be passed to {@link #log(Consumer, String, Throwable)} + * @return An ExceptionHandler that passes exceptions with given arguments to {@link #log(Consumer, String, Throwable)} + * @see #log(Consumer, String, Throwable) + */ + static ExceptionHandler log(Consumer<String> out, String activityDescription) { + return ex -> log(out, activityDescription, ex); + } + + /** + * This ExceptionHandler turns any Throwable into an unchecked exception, then throws it again. + */ + ExceptionHandler UNCHECKED = new ExceptionHandler() { + @Override + public void handle(Throwable ex) { + errorFilter(ex); + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + throw new RuntimeException(ex); + } + + @Override + public boolean isSafe() { + return false; + } + }; + + /** + * This ExceptionHandler suppresses all exceptions, + * apart from {@link Error} because that is not usually supposed to be caught + */ + ExceptionHandler SUPPRESS = ExceptionHandler::errorFilter; + + /** + * This ExceptionHandler calls {@link Throwable#printStackTrace()} unless it is a {@link NullPointerException} or {@link Error}. + */ + ExceptionHandler PRINT_UNLESS_NP = ex -> { + errorFilter(ex); + if (!(ex instanceof NullPointerException)) { + ex.printStackTrace(); + } + }; + + static void errorFilter(Throwable ex) { + if (ex instanceof Error) { + throw (Error) ex; + } else if (ex == null) { + throw new NullPointerException(); + } + } + +} diff --git a/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedBiConsumer.java b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedBiConsumer.java new file mode 100644 index 0000000..badf05b --- /dev/null +++ b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedBiConsumer.java @@ -0,0 +1,69 @@ +package io.dico.dicore.exceptions.checkedfunctions; + +import io.dico.dicore.exceptions.ExceptionHandler; + +import java.util.function.BiConsumer; + +/** + * checked mimic of {@link BiConsumer} + * + * @param <TParam1> + * @param <TParam2> + * @param <TException> + */ +@FunctionalInterface +public interface CheckedBiConsumer<TParam1, TParam2, TException extends Throwable> + extends CheckedFunctionalObject<Void, TException>, BiConsumer<TParam1, TParam2> { + + /** + * The consuming action + * + * @param t the first argument to consume + * @param u the second argument to consume + * @throws TException if an exception occurs + */ + void checkedAccept(TParam1 t, TParam2 u) throws TException; + + /** + * unchecked wrapper for {@link #checkedAccept(Object, Object)} + * If a {@link TException} occurs, an unchecked one might be thrown by {@link #resultOnError(Throwable, Object...)} + * + * @param t the first input + * @param u the second input + * @see #checkedAccept(Object, Object) + * @see #resultOnError(Throwable, Object...) + */ + @Override + default void accept(TParam1 t, TParam2 u) { + try { + checkedAccept(t, u); + } catch (Throwable ex) { + handleGenericException(ex, t, u); + } + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedBiConsumer<TParam1, TParam2, TException> handleExceptionsWith(ExceptionHandler handler) { + return new CheckedBiConsumer<TParam1, TParam2, TException>() { + @Override + public void checkedAccept(TParam1 t, TParam2 u) throws TException { + CheckedBiConsumer.this.checkedAccept(t, u); + } + + @Override + public Void handleGenericException(Throwable thrown, Object... args) { + handler.handleGenericException(thrown, args); + return null; + } + + @Override + public CheckedBiConsumer<TParam1, TParam2, TException> handleExceptionsWith(ExceptionHandler handler) { + return CheckedBiConsumer.this.handleExceptionsWith(handler); + } + }; + } + +} diff --git a/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedBiFunction.java b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedBiFunction.java new file mode 100644 index 0000000..9b39325 --- /dev/null +++ b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedBiFunction.java @@ -0,0 +1,76 @@ +package io.dico.dicore.exceptions.checkedfunctions; + +import io.dico.dicore.exceptions.ExceptionHandler; + +import java.util.function.BiFunction; + +/** + * checked mimic of {@link BiFunction} + * + * @param <TParam1> + * @param <TParam2> + * @param <TResult> + * @param <TException> + */ +@FunctionalInterface +public interface CheckedBiFunction<TParam1, TParam2, TResult, TException extends Throwable> + extends CheckedFunctionalObject<TResult, TException>, BiFunction<TParam1, TParam2, TResult> { + + /** + * the functional method + * + * @param t the first input + * @param u the second input + * @return the function output + * @throws TException if an exception occurs + */ + TResult checkedApply(TParam1 t, TParam2 u) throws TException; + + /** + * unchecked wrapper for {@link #checkedApply(Object, Object)} + * If a {@link TException} occurs, an unchecked one might be thrown by {@link #resultOnError(Throwable, Object...)} + * + * @param t the first input + * @param u the second input + * @return the function output, or the result from {@link #resultOnError(Throwable, Object...)} if an exception was thrown + * @see #checkedApply(Object, Object) + * @see #resultOnError(Throwable, Object...) + */ + @Override + default TResult apply(TParam1 t, TParam2 u) { + try { + return checkedApply(t, u); + } catch (Throwable ex) { + return handleGenericException(ex, t, u); + } + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedBiFunction<TParam1, TParam2, TResult, TException> handleExceptionsWith(ExceptionHandler handler) { + return new CheckedBiFunction<TParam1, TParam2, TResult, TException>() { + @Override + public TResult checkedApply(TParam1 t, TParam2 u) throws TException { + return CheckedBiFunction.this.checkedApply(t, u); + } + + @Override + @SuppressWarnings("unchecked") + public TResult handleGenericException(Throwable thrown, Object... args) { + Object result = handler.handleGenericException(thrown, args); + try { + return (TResult) result; + } catch (Exception ex) { + return null; + } + } + + @Override + public CheckedBiFunction<TParam1, TParam2, TResult, TException> handleExceptionsWith(ExceptionHandler handler) { + return CheckedBiFunction.this.handleExceptionsWith(handler); + } + }; + } +} diff --git a/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedConsumer.java b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedConsumer.java new file mode 100644 index 0000000..fa48ae8 --- /dev/null +++ b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedConsumer.java @@ -0,0 +1,67 @@ +package io.dico.dicore.exceptions.checkedfunctions; + +import io.dico.dicore.exceptions.ExceptionHandler; + +import java.util.function.Consumer; + +/** + * checked mimic of {@link Consumer} + * + * @param <TParam> + * @param <TException> + */ +@FunctionalInterface +public interface CheckedConsumer<TParam, TException extends Throwable> + extends CheckedFunctionalObject<Void, TException>, Consumer<TParam> { + + /** + * The consuming action + * + * @param t the argument to consume + * @throws TException if an error occurs + */ + void checkedAccept(TParam t) throws TException; + + /** + * Unchecked version of {@link #checkedAccept(Object)} + * If a {@link TException} occurs, an unchecked one might be thrown by {@link #resultOnError(Throwable, Object...)} + * + * @param t the argument to consume + * @see #checkedAccept(Object) + * @see #resultOnError(Throwable, Object...) + */ + @Override + default void accept(TParam t) { + try { + checkedAccept(t); + } catch (Throwable ex) { + handleGenericException(ex, t); + } + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedConsumer<TParam, TException> handleExceptionsWith(ExceptionHandler handler) { + return new CheckedConsumer<TParam, TException>() { + @Override + public void checkedAccept(TParam t) throws TException { + CheckedConsumer.this.checkedAccept(t); + } + + @Override + @SuppressWarnings("unchecked") + public Void handleGenericException(Throwable thrown, Object... args) { + handler.handleGenericException(thrown, args); + return null; + } + + @Override + public CheckedConsumer<TParam, TException> handleExceptionsWith(ExceptionHandler handler) { + return CheckedConsumer.this.handleExceptionsWith(handler); + } + }; + } + +} diff --git a/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedFunction.java b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedFunction.java new file mode 100644 index 0000000..9ff806a --- /dev/null +++ b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedFunction.java @@ -0,0 +1,74 @@ +package io.dico.dicore.exceptions.checkedfunctions; + +import io.dico.dicore.exceptions.ExceptionHandler; + +import java.util.function.Function; + +/** + * checked mimic of {@link Function} + * + * @param <TParam> + * @param <TResult> + * @param <TException> + */ +@FunctionalInterface +public interface CheckedFunction<TParam, TResult, TException extends Throwable> + extends CheckedFunctionalObject<TResult, TException>, Function<TParam, TResult> { + + /** + * the functional method + * + * @param t the input + * @return the function output + * @throws TException if an exception occurs + */ + TResult checkedApply(TParam t) throws TException; + + /** + * unchecked wrapper for {@link #checkedApply(Object)} + * If a {@link TException} occurs, an unchecked one might be thrown by {@link #resultOnError(Throwable, Object...)} + * + * @param t the input + * @return the function output, or the result from {@link #resultOnError(Throwable, Object...)} if an exception was thrown + * @see #checkedApply(Object) + * @see #resultOnError(Throwable, Object...) + */ + @Override + default TResult apply(TParam t) { + try { + return checkedApply(t); + } catch (Throwable ex) { + return handleGenericException(ex, t); + } + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedFunction<TParam, TResult, TException> handleExceptionsWith(ExceptionHandler handler) { + return new CheckedFunction<TParam, TResult, TException>() { + @Override + public TResult checkedApply(TParam t) throws TException { + return CheckedFunction.this.checkedApply(t); + } + + @Override + @SuppressWarnings("unchecked") + public TResult handleGenericException(Throwable thrown, Object... args) { + Object result = handler.handleGenericException(thrown, args); + try { + return (TResult) result; + } catch (Exception ex) { + return null; + } + } + + @Override + public CheckedFunction<TParam, TResult, TException> handleExceptionsWith(ExceptionHandler handler) { + return CheckedFunction.this.handleExceptionsWith(handler); + } + }; + } + +} diff --git a/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedFunctionalObject.java b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedFunctionalObject.java new file mode 100644 index 0000000..e84b33d --- /dev/null +++ b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedFunctionalObject.java @@ -0,0 +1,85 @@ +package io.dico.dicore.exceptions.checkedfunctions; + +import io.dico.dicore.exceptions.ExceptionHandler; + +/** + * Base interface for all checked functional interfaces + * Most subinterfaces will mimic interfaces in the package {@link java.util.function} + * <p> + * Checked functional interfaces are functions with throws declarations. + * The name comes from the fact that they can throw <b>checked</b> exceptions. + * <p> + * They extend their non-checked counterparts, whose methods are implemented by + * returning the result of {@link #resultOnError(TException, Object...)} when an exception is thrown + * <p> + * Made public to allow more specialized checked functional interfaces to subclass it. + * Making primitive versions shouldn't provide a significant performance increase because we're checking for exceptions, + * the performance impact of which is probably a few magnitudes larger. Don't quote me on this. + * + * @param <TResult> The return type of this functional interface's method + * @param <TException> The type of exception that might be thrown + */ +public interface CheckedFunctionalObject<TResult, TException extends Throwable> extends ExceptionHandler { + + /** + * {@inheritDoc} + * + * @param ex The exception to be handled + */ + @Override + default void handle(Throwable ex) { + handleGenericException(ex); + } + + /** + * {@inheritDoc} + * <p> + * Method to handle exceptions thrown by the default implementations of subinterfaces. + * Since you can't catch a type parameter as exception, this code is in place to take care of it. + * <p> + * If the thrown exception is not a TException, an unchecked version is thrown by calling this method. + * + * @param thrown The thrown exception + * @param args the arguments supplied to the method that threw the exception, if any. These are not guaranteed to be given if parameters are present. + * @return The result computed by {@link #resultOnError(Throwable, Object...)} + * @see #resultOnError(Throwable, Object...) + */ + @Override + @SuppressWarnings("unchecked") + default TResult handleGenericException(Throwable thrown, Object... args) { + + // check if the throwable is a TException + TException castedException; + try { + castedException = (TException) thrown; + } catch (ClassCastException ex) { + // if not, throw an unchecked version of it + ExceptionHandler.UNCHECKED.handleGenericException(thrown); + // this code is never reached. + return null; + } + + // if it is a TException, use resultOnError to compute result. + return resultOnError(castedException, args); + } + + /** + * This method handles the exceptions thrown by the checked method of this object, when called by its unchecked wrapper. + * + * @param ex The exception thrown + * @param args The (typed) arguments passed, if any + * @return The result to return, if any + */ + default TResult resultOnError(TException ex, Object... args) { + return null; + } + + /** + * Creates a new functional object that uses the given exception handler to handle exceptions. + * + * @param handler The handler to handle exceptions + * @return a new functional object that uses the given exception handler to handle exceptions + */ + CheckedFunctionalObject<TResult, TException> handleExceptionsWith(ExceptionHandler handler); + +} diff --git a/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedRunnable.java b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedRunnable.java new file mode 100644 index 0000000..55de6f8 --- /dev/null +++ b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedRunnable.java @@ -0,0 +1,62 @@ +package io.dico.dicore.exceptions.checkedfunctions; + +import io.dico.dicore.exceptions.ExceptionHandler; + +/** + * checked mimic of {@link Runnable} + * + * @param <TException> + */ +@FunctionalInterface +public interface CheckedRunnable<TException extends Throwable> + extends CheckedFunctionalObject<Void, TException>, Runnable { + + /** + * The runnable action + * + * @throws TException if an exception occurs + */ + void checkedRun() throws TException; + + /** + * Unchecked version of {@link #checkedRun()} + * If a {@link TException} occurs, an unchecked one might be thrown by {@link #resultOnError(Throwable, Object...)} + * + * @see #checkedRun() + * @see #resultOnError(Throwable, Object...) + */ + @Override + default void run() { + try { + checkedRun(); + } catch (Throwable ex) { + handleGenericException(ex); + } + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedRunnable<TException> handleExceptionsWith(ExceptionHandler handler) { + return new CheckedRunnable<TException>() { + @Override + public void checkedRun() throws TException { + CheckedRunnable.this.checkedRun(); + } + + @Override + @SuppressWarnings("unchecked") + public Void handleGenericException(Throwable thrown, Object... args) { + handler.handleGenericException(thrown, args); + return null; + } + + @Override + public CheckedRunnable<TException> handleExceptionsWith(ExceptionHandler handler) { + return CheckedRunnable.this.handleExceptionsWith(handler); + } + }; + } + +} diff --git a/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedSupplier.java b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedSupplier.java new file mode 100644 index 0000000..dec2e7e --- /dev/null +++ b/src/main/java/io/dico/dicore/exceptions/checkedfunctions/CheckedSupplier.java @@ -0,0 +1,70 @@ +package io.dico.dicore.exceptions.checkedfunctions; + +import io.dico.dicore.exceptions.ExceptionHandler; + +import java.util.function.Supplier; + +/** + * checked mimic of {@link Supplier} + * + * @param <TResult> + * @param <TException> + */ +@FunctionalInterface +public interface CheckedSupplier<TResult, TException extends Throwable> + extends CheckedFunctionalObject<TResult, TException>, Supplier<TResult> { + + /** + * The computation + * + * @return the result of this computation + * @throws TException if an error occurs + */ + TResult checkedGet() throws TException; + + /** + * Unchecked version of {@link #checkedGet()} + * If a {@link TException} occurs, an unchecked one might be thrown by {@link #resultOnError(Throwable, Object...)} + * + * @return the result of this computation + * @see #checkedGet() + * @see #resultOnError(Throwable, Object...) + */ + @Override + default TResult get() { + try { + return checkedGet(); + } catch (Throwable ex) { + return handleGenericException(ex); + } + } + + /** + * {@inheritDoc} + */ + @Override + default CheckedSupplier<TResult, TException> handleExceptionsWith(ExceptionHandler handler) { + return new CheckedSupplier<TResult, TException>() { + @Override + public TResult checkedGet() throws TException { + return CheckedSupplier.this.checkedGet(); + } + + @Override + @SuppressWarnings("unchecked") + public TResult handleGenericException(Throwable thrown, Object... args) { + Object result = handler.handleGenericException(thrown, args); + try { + return (TResult) result; + } catch (Exception ex) { + return null; + } + } + + @Override + public CheckedSupplier<TResult, TException> handleExceptionsWith(ExceptionHandler handler) { + return CheckedSupplier.this.handleExceptionsWith(handler); + } + }; + } +} diff --git a/src/main/kotlin/io/dico/parcels2/Parcel.kt b/src/main/kotlin/io/dico/parcels2/Parcel.kt index 27b9268..6fd8cf3 100644 --- a/src/main/kotlin/io/dico/parcels2/Parcel.kt +++ b/src/main/kotlin/io/dico/parcels2/Parcel.kt @@ -8,7 +8,8 @@ import java.util.* class Parcel(val world: ParcelWorld, val pos: Vec2i, - val data: ParcelData = ParcelData()) { + var data: ParcelData = ParcelData()) { + } diff --git a/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt b/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt new file mode 100644 index 0000000..47bbf8c --- /dev/null +++ b/src/main/kotlin/io/dico/parcels2/util/PlayerExtensions.kt @@ -0,0 +1,39 @@ +package io.dico.parcels2.util + +import org.bukkit.entity.Player + +val Player.hasBanBypass get() = hasPermission("plots.admin.bypass.ban") +val Player.hasBuildAnywhere get() = hasPermission("plots.admin.bypass.build") +val Player.hasGamemodeBypass get() = hasPermission("plots.admin.bypass.gamemode") +val Player.hasAdminManage get() = hasPermission("plots.admin.manage") +val Player.hasPlotHomeOthers get() = hasPermission("plots.command.home.others") +val Player.hasRandomSpecific get() = hasPermission("plots.command.random.specific") +val Player.plotLimit: Int + get() { + for (info in effectivePermissions) { + val perm = info.permission + if (perm.startsWith("plots.limit.")) { + val limitString = perm.substring("plots.limit.".length) + if (limitString == "*") { + return Int.MAX_VALUE + } + return limitString.toIntOrNull() ?: DEFAULT_LIMIT.also { + Main.instance.logger.severe("$name has permission '$perm'. The suffix can not be parsed to an integer (or *).") + } + } + } + return DEFAULT_LIMIT + } + +val DEFAULT_LIMIT = 1 +internal val prefix = Formatting.translateChars('&', "&4[&c${Main.instance.name}&4] &a") + +fun Player.sendPlotMessage(except: Boolean = false, nopermit: Boolean = false, message: String) { + if (except) { + sendMessage(prefix + Formatting.YELLOW + Formatting.translateChars('&', message)) + } else if (nopermit) { + sendMessage(prefix + Formatting.RED + Formatting.translateChars('&', message)) + } else { + sendMessage(prefix + Formatting.translateChars('&', message)) + } +}
\ No newline at end of file |