From dbb8b3974cac93e454f28a987ee1c390d138b140 Mon Sep 17 00:00:00 2001 From: nahkd123 Date: Thu, 9 Nov 2023 23:50:27 +0700 Subject: [PATCH] Nahara's Toolkit Reflective: Reflection for mad lads! --- common/reflective/build.gradle | 18 +++ .../java/nahara/common/reflective/Handle.java | 21 ++++ .../common/reflective/HandleFactory.java | 14 +++ .../java/nahara/common/reflective/Method.java | 5 + .../common/reflective/MethodFactory.java | 9 ++ .../nahara/common/reflective/Reflective.java | 105 ++++++++++++++++ .../impl/FieldHandleFactoryImpl.java | 53 ++++++++ .../reflective/impl/MethodFactoryImpl.java | 52 ++++++++ .../impl/MethodHandleFactoryImpl.java | 58 +++++++++ .../reflective/impl/ReflectiveImpl.java | 115 ++++++++++++++++++ .../common/reflective/ReflectiveTest.java | 30 +++++ .../nahara/common/reflective/SampleClass.java | 15 +++ settings.gradle | 1 + 13 files changed, 496 insertions(+) create mode 100644 common/reflective/build.gradle create mode 100644 common/reflective/src/main/java/nahara/common/reflective/Handle.java create mode 100644 common/reflective/src/main/java/nahara/common/reflective/HandleFactory.java create mode 100644 common/reflective/src/main/java/nahara/common/reflective/Method.java create mode 100644 common/reflective/src/main/java/nahara/common/reflective/MethodFactory.java create mode 100644 common/reflective/src/main/java/nahara/common/reflective/Reflective.java create mode 100644 common/reflective/src/main/java/nahara/common/reflective/impl/FieldHandleFactoryImpl.java create mode 100644 common/reflective/src/main/java/nahara/common/reflective/impl/MethodFactoryImpl.java create mode 100644 common/reflective/src/main/java/nahara/common/reflective/impl/MethodHandleFactoryImpl.java create mode 100644 common/reflective/src/main/java/nahara/common/reflective/impl/ReflectiveImpl.java create mode 100644 common/reflective/src/test/java/nahara/common/reflective/ReflectiveTest.java create mode 100644 common/reflective/src/test/java/nahara/common/reflective/SampleClass.java diff --git a/common/reflective/build.gradle b/common/reflective/build.gradle new file mode 100644 index 0000000..86e290f --- /dev/null +++ b/common/reflective/build.gradle @@ -0,0 +1,18 @@ +plugins { +} + +eclipse { + project.name = "Nahara Toolkit - Common - Reflective" +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/common/reflective/src/main/java/nahara/common/reflective/Handle.java b/common/reflective/src/main/java/nahara/common/reflective/Handle.java new file mode 100644 index 0000000..c6aa404 --- /dev/null +++ b/common/reflective/src/main/java/nahara/common/reflective/Handle.java @@ -0,0 +1,21 @@ +package nahara.common.reflective; + +import java.util.function.Consumer; +import java.util.function.UnaryOperator; + +/** + *

Handles allows you to get/set objects using either field accessing or getter and setter.

+ * @param The type. + */ +public interface Handle { + public T get(); + public void set(T obj); + + default void apply(Consumer> applier) { + applier.accept(this); + } + + default void map(UnaryOperator mapper) { + set(mapper.apply(get())); + } +} diff --git a/common/reflective/src/main/java/nahara/common/reflective/HandleFactory.java b/common/reflective/src/main/java/nahara/common/reflective/HandleFactory.java new file mode 100644 index 0000000..d4c8705 --- /dev/null +++ b/common/reflective/src/main/java/nahara/common/reflective/HandleFactory.java @@ -0,0 +1,14 @@ +package nahara.common.reflective; + +/** + *

The factory for getting the handle from given object.

+ * @param The object type. + * @param The type for handle. + */ +public interface HandleFactory { + public Handle of(S obj); + + default Handle ofStatic() { + return of(null); + } +} diff --git a/common/reflective/src/main/java/nahara/common/reflective/Method.java b/common/reflective/src/main/java/nahara/common/reflective/Method.java new file mode 100644 index 0000000..3b9b92b --- /dev/null +++ b/common/reflective/src/main/java/nahara/common/reflective/Method.java @@ -0,0 +1,5 @@ +package nahara.common.reflective; + +public interface Method { + public T invoke(Object... args); +} diff --git a/common/reflective/src/main/java/nahara/common/reflective/MethodFactory.java b/common/reflective/src/main/java/nahara/common/reflective/MethodFactory.java new file mode 100644 index 0000000..7a0055e --- /dev/null +++ b/common/reflective/src/main/java/nahara/common/reflective/MethodFactory.java @@ -0,0 +1,9 @@ +package nahara.common.reflective; + +public interface MethodFactory { + public Method of(S obj); + + default Method ofStatic() { + return of(null); + } +} diff --git a/common/reflective/src/main/java/nahara/common/reflective/Reflective.java b/common/reflective/src/main/java/nahara/common/reflective/Reflective.java new file mode 100644 index 0000000..6178117 --- /dev/null +++ b/common/reflective/src/main/java/nahara/common/reflective/Reflective.java @@ -0,0 +1,105 @@ +package nahara.common.reflective; + +import java.lang.reflect.AccessibleObject; + +import nahara.common.reflective.impl.ReflectiveImpl; + +/** + *

"Once upon a time, there was a small pond in a middle of the forest. The pond water is said to be + * precious and shiny, which means it is highly reflective. Nahara saw the pond, got extremely + * curious, then decided to get closer to it.

+ *

Nahara stands still, staring at her reflection on the watery surface. "What is this all about?", + * "Why is the {@code PI} constant of universe becomes 4.0?", "Am I living in a simulation?", "Am I living + * in an illusion?", "Is this a dream?". Nahara begins questioning her existence as she is about to perform + * an act that no ones could have guessed: It's Java reflectin' time!"

+ * + *

This is Nahara's Toolkit for Reflections, a simple tool for your toolbox so that you can perform + * Java Reflection operations without dealing with nasty exceptions (I'm looking at you, + * {@link ReflectiveOperationException}). While the main point of this is to avoid catching exceptions, + * {@link Reflective} contains methods that helps you shorten the time it takes to use reflection.

+ *

First, some methods allows you to feed in multiple possible names for a field or method. This is + * to ensure no magic will happens in the production environment, especially in Fabric modding, where dev + * environment may have remapped names, but in prouction, it becomes "method_abcdef".

+ *

Second, some methods allows you to pick the only method with matching signature. There can be always + * more than 1 constructor or method with same name, but method signatures can't be the same.

+ *

Third, all {@link Handle}s and {@link Method}s will calls {@link AccessibleObject#setAccessible(boolean)} + * everytime you access the fields or methods. You don't have to set its accessibility everytime you need + * to, well, access them.

+ * + *

Now that I've done talkin' about {@link Reflective}, let's talk about "Where did the person named 'Nahara' + * comes from?". It might seems weird that I'm talking to you using Javadocs, but I kinda want to make this + * as a little "easter egg".

+ *

To say "it comes from my dream" wouldn't be entirely correct, but the whole idea is: every person should have + * someone to help with their job. But because you can't see Nahara in person (she isn't real), she decided to mail + * her toolbox to you, with the hope of improving your productivity. That's why "Nahara's Toolkit" was created: to + * help me write Java code faster and more enjoyable. And what is the result of enjoying? Being productive!

+ *

I don't expect this little dumb repository gets some attentions, but if you are using Nahara's toolkit in your + * project, consider making an "issue" in the Issues tab of my GitHub special repository for profile card. Nahara + * would likes to know her toolkit is actually useful!

+ * + * @see #constructor(Class...) + * @see #field(Class, String...) + * @see #method(Class, Class[], String...) + * @see #getterSetter(Class, String[], String[]) + */ +public interface Reflective { + /** + *

Combine getter and setter into a single handle.

+ * @param type The output type. + * @param getter All possible names for getter. Nahara will choose the method that is appeared first + * in this array that also present in the class. + * @param setter All possible names for setter. Nahara will choose the method that is appeared first + * in this array that also present in the class. + * @return The handle. + */ + public HandleFactory getterSetter(Class type, String[] getter, String[] setter); + + /** + *

Get a field as a handle.

+ * @param type The output type. + * @param names All possible names for the field. + * @return The handle. + */ + public HandleFactory field(Class type, String... names); + + /** + *

Find a method with specified name(s).

+ * @param returnType The return type of method. + * @param arguments Arguments to check, or {@code null} to ignore checking arguments. + * @param names All possible names for the method. + * @return The method. + */ + public MethodFactory method(Class returnType, Class[] arguments, String... names); + + /** + *

Find a constructor with given types for all arguments.

+ * @param arguments Arguments to check. + * @return The constructor described as a method. + */ + public Method constructor(Class... arguments); + + /** + *

Obtain a {@link Reflective} instance of a class.

+ * @param clazz The class. + * @return The {@link Reflective} instance. + */ + public static Reflective of(Class clazz) { + return new ReflectiveImpl<>(clazz); + } + + public static Reflective of(String className) { + try { + return of(Class.forName(className)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(); + } + } + + public static String[] names(String... names) { + return names; + } + + public static Class[] args(Class... args) { + return args; + } +} diff --git a/common/reflective/src/main/java/nahara/common/reflective/impl/FieldHandleFactoryImpl.java b/common/reflective/src/main/java/nahara/common/reflective/impl/FieldHandleFactoryImpl.java new file mode 100644 index 0000000..aa78aed --- /dev/null +++ b/common/reflective/src/main/java/nahara/common/reflective/impl/FieldHandleFactoryImpl.java @@ -0,0 +1,53 @@ +package nahara.common.reflective.impl; + +import java.lang.reflect.Field; + +import nahara.common.reflective.Handle; +import nahara.common.reflective.HandleFactory; + +public class FieldHandleFactoryImpl implements HandleFactory { + private Field field; + + public FieldHandleFactoryImpl(Field field) { + this.field = field; + } + + @Override + public Handle of(S obj) { + return new FieldHandleImpl<>(this, obj); + } + + public static class FieldHandleImpl implements Handle { + private FieldHandleFactoryImpl factory; + private S obj; + + public FieldHandleImpl(FieldHandleFactoryImpl factory, S obj) { + this.factory = factory; + this.obj = obj; + } + + @SuppressWarnings("unchecked") + @Override + public T get() { + try { + factory.field.setAccessible(true); + T t = (T) factory.field.get(obj); + factory.field.setAccessible(false); + return t; + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override + public void set(T obj) { + try { + factory.field.setAccessible(true); + factory.field.set(this.obj, obj); + factory.field.setAccessible(false); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/common/reflective/src/main/java/nahara/common/reflective/impl/MethodFactoryImpl.java b/common/reflective/src/main/java/nahara/common/reflective/impl/MethodFactoryImpl.java new file mode 100644 index 0000000..f4a00c6 --- /dev/null +++ b/common/reflective/src/main/java/nahara/common/reflective/impl/MethodFactoryImpl.java @@ -0,0 +1,52 @@ +package nahara.common.reflective.impl; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import nahara.common.reflective.Method; +import nahara.common.reflective.MethodFactory; + +public class MethodFactoryImpl implements MethodFactory { + private java.lang.reflect.Method method; + private Constructor constructor; + + public MethodFactoryImpl(java.lang.reflect.Method method, Constructor constructor) { + this.method = method; + this.constructor = constructor; + } + + @Override + public Method of(S obj) { + return new MethodImpl<>(this, obj); + } + + public static class MethodImpl implements Method { + private MethodFactoryImpl factory; + private S obj; + + public MethodImpl(MethodFactoryImpl factory, S obj) { + this.factory = factory; + this.obj = obj; + } + + @SuppressWarnings("unchecked") + @Override + public T invoke(Object... args) { + try { + if (factory.constructor != null) { + factory.constructor.setAccessible(true); + T t = (T) factory.constructor.newInstance(args); + factory.constructor.setAccessible(false); + return t; + } else { + factory.method.setAccessible(true); + T t = (T) factory.method.invoke(obj, args); + factory.method.setAccessible(false); + return t; + } + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/common/reflective/src/main/java/nahara/common/reflective/impl/MethodHandleFactoryImpl.java b/common/reflective/src/main/java/nahara/common/reflective/impl/MethodHandleFactoryImpl.java new file mode 100644 index 0000000..38662d5 --- /dev/null +++ b/common/reflective/src/main/java/nahara/common/reflective/impl/MethodHandleFactoryImpl.java @@ -0,0 +1,58 @@ +package nahara.common.reflective.impl; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import nahara.common.reflective.Handle; +import nahara.common.reflective.HandleFactory; + +public class MethodHandleFactoryImpl implements HandleFactory { + private Method getter; + private Method setter; + + public MethodHandleFactoryImpl(Method getter, Method setter) { + this.getter = getter; + this.setter = setter; + } + + @Override + public Handle of(S obj) { + return new MethodHandleImpl<>(this, obj); + } + + public static class MethodHandleImpl implements Handle { + private MethodHandleFactoryImpl factory; + private S obj; + + public MethodHandleImpl(MethodHandleFactoryImpl factory, S obj) { + this.factory = factory; + this.obj = obj; + } + + @SuppressWarnings("unchecked") + @Override + public T get() { + if (factory.getter == null) throw new RuntimeException("Handle does not link to a getter"); + try { + factory.getter.setAccessible(true); + T t = (T) factory.getter.invoke(obj); + factory.getter.setAccessible(false); + return t; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public void set(T obj) { + if (factory.getter == null) throw new RuntimeException("Handle does not link to a setter"); + try { + factory.getter.setAccessible(true); + factory.setter.invoke(this.obj, obj); + factory.getter.setAccessible(false); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/common/reflective/src/main/java/nahara/common/reflective/impl/ReflectiveImpl.java b/common/reflective/src/main/java/nahara/common/reflective/impl/ReflectiveImpl.java new file mode 100644 index 0000000..a7f454b --- /dev/null +++ b/common/reflective/src/main/java/nahara/common/reflective/impl/ReflectiveImpl.java @@ -0,0 +1,115 @@ +package nahara.common.reflective.impl; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import nahara.common.reflective.HandleFactory; +import nahara.common.reflective.MethodFactory; +import nahara.common.reflective.Reflective; + +public class ReflectiveImpl implements Reflective { + private Class clazz; + + public ReflectiveImpl(Class clazz) { + this.clazz = clazz; + } + + @Override + public HandleFactory getterSetter(Class type, String[] getter, String[] setter) { + Method mGetter = null, mSetter = null; + for (Method method : clazz.getDeclaredMethods()) { + if (!type.isAssignableFrom(method.getReturnType())) continue; + + if (mGetter == null && getter != null && getter.length > 0) { + for (String n : getter) { + if (method.getName().equals(n)) { + mGetter = method; + break; + } + } + } + + if (mSetter == null && setter != null && setter.length > 0) { + for (String n : setter) { + if (method.getName().equals(n)) { + mSetter = method; + break; + } + } + } + + if ( + (getter == null || getter.length == 0 || mGetter != null) && + (setter == null || setter.length == 0 || mSetter != null)) break; + } + + if (getter != null && getter.length > 0 && mGetter == null) { + throw new RuntimeException("Unable to find matching getter method in " + clazz + " (possible methods are " + String.join(", ", getter) + ")"); + } + + if (setter != null && setter.length > 0 && mSetter == null) { + throw new RuntimeException("Unable to find matching setter method in " + clazz + " (possible methods are " + String.join(", ", setter) + ")"); + } + + return new MethodHandleFactoryImpl<>(mGetter, mSetter); + } + + @Override + public MethodFactory method(Class returnType, Class[] arguments, String... names) { + outer: for (Method method : clazz.getDeclaredMethods()) { + if (!returnType.isAssignableFrom(method.getReturnType())) continue; + + for (String n : names) { + if (method.getName().equals(n)) { + if (arguments != null) { + Class[] params = method.getParameterTypes(); + if (params.length != arguments.length) continue outer; + + for (int i = 0; i < arguments.length; i++) { + Class param = params[i]; + Class arg = arguments[i]; + if (!param.isAssignableFrom(arg)) continue outer; + } + } + + return new MethodFactoryImpl<>(method, null); + } + } + } + + throw new RuntimeException("Unable to find matching field in " + clazz + " (possible fields are " + String.join(", ", names) + ")"); + } + + @SuppressWarnings("unchecked") + @Override + public nahara.common.reflective.Method constructor(Class... arguments) { + for (Constructor constructor : clazz.getDeclaredConstructors()) { + Class[] params = constructor.getParameterTypes(); + if (params.length != arguments.length) continue; + + for (int i = 0; i < arguments.length; i++) { + Class param = params[i]; + Class arg = arguments[i]; + if (!param.isAssignableFrom(arg)) continue; + } + + return new MethodFactoryImpl(null, (Constructor) constructor).ofStatic(); + } + + throw new RuntimeException("Unable to find matching constructor in " + clazz); + } + + @Override + public HandleFactory field(Class type, String... names) { + for (Field field : clazz.getDeclaredFields()) { + if (!type.isAssignableFrom(field.getType())) continue; + + for (String n : names) { + if (field.getName().equals(n)) return new FieldHandleFactoryImpl<>(field); + } + } + + throw new RuntimeException("Unable to find matching field in " + clazz + " (possible fields are " + String.join(", ", names) + ")"); + } +} diff --git a/common/reflective/src/test/java/nahara/common/reflective/ReflectiveTest.java b/common/reflective/src/test/java/nahara/common/reflective/ReflectiveTest.java new file mode 100644 index 0000000..affbac1 --- /dev/null +++ b/common/reflective/src/test/java/nahara/common/reflective/ReflectiveTest.java @@ -0,0 +1,30 @@ +package nahara.common.reflective; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ReflectiveTest { + @Test + void test() { + Reflective r = Reflective.of(SampleClass.class); + var ctorMethod = r.constructor(String.class, int.class); + var nameHandle = r.field(String.class, "name"); + var ageHandle = r.field(int.class, "age"); + var describeHandle = r.getterSetter(String.class, Reflective.names("describe"), null); + var describeMethod = r.method(String.class, Reflective.args(), "describe"); + + SampleClass john = new SampleClass("John", 42); + assertEquals("John is now 42 years old!", john.describe()); + + ageHandle.of(john).map(age -> age - 10); + assertEquals("John is now 32 years old!", john.describe()); + + nameHandle.of(john).set("Annie"); + assertEquals("Annie is now 32 years old!", john.describe()); + assertEquals("Annie is now 32 years old!", describeHandle.of(john).get()); + assertEquals("Annie is now 32 years old!", describeMethod.of(john).invoke()); + + assertEquals("Albert is now 69 years old!", ctorMethod.invoke("Albert", 69).describe()); + } +} diff --git a/common/reflective/src/test/java/nahara/common/reflective/SampleClass.java b/common/reflective/src/test/java/nahara/common/reflective/SampleClass.java new file mode 100644 index 0000000..2e020f5 --- /dev/null +++ b/common/reflective/src/test/java/nahara/common/reflective/SampleClass.java @@ -0,0 +1,15 @@ +package nahara.common.reflective; + +public class SampleClass { + private String name; + private int age; + + public SampleClass(String name, int age) { + this.name = name; + this.age = age; + } + + public String describe() { + return name + " is now " + age + " years old!"; + } +} diff --git a/settings.gradle b/settings.gradle index 18bb57a..17c0769 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,7 @@ plugins { 'common/localize', 'common/nbtstring', 'common/pipeline', + 'common/reflective', 'common/structures', 'common/tasks',