mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 20:44:08 +00:00
Field handler for automatic GUI creation
This commit is contained in:
1 parent
601b0d721e
commit
5182a3474e
9 files changed
+315
No files matched your search
@@ -0,0 +1,5 @@
|
||||
package somehussar.gui;
|
||||
|
||||
public interface FieldConstraint {
|
||||
void validate(Object value);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package somehussar.gui;
|
||||
|
||||
public final class FieldConstraints {
|
||||
|
||||
private static final FieldConstraints EMPTY =
|
||||
new FieldConstraints(new FieldConstraint[0]);
|
||||
|
||||
private final FieldConstraint[] constraints;
|
||||
|
||||
public FieldConstraints(FieldConstraint[] constraints) {
|
||||
this.constraints = constraints;
|
||||
}
|
||||
|
||||
public static FieldConstraints empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
public void validate(Object value) {
|
||||
for (FieldConstraint c : constraints) {
|
||||
c.validate(value);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return constraints.length == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package somehussar.gui;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface GuiEditable {
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface Field {
|
||||
String name() default "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package somehussar.gui;
|
||||
|
||||
import somehussar.gui.constraints.*;
|
||||
import somehussar.gui.field.EditableField;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class GuiFieldHandler {
|
||||
|
||||
private static final Map<Class<?>, ClassMetadata> CACHE = new HashMap<>();
|
||||
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
|
||||
|
||||
public static final Map<Class<? extends Annotation>, ConstraintFactory<?>> registry = new HashMap<>();
|
||||
|
||||
static {
|
||||
registry.put(NumericConstraint.class, new NumericConstraintFactory());
|
||||
}
|
||||
|
||||
|
||||
private GuiFieldHandler() {}
|
||||
|
||||
public static ClassMetadata getMetadata(Class<?> type) {
|
||||
return CACHE.computeIfAbsent(type, GuiFieldHandler::scanClass);
|
||||
}
|
||||
|
||||
|
||||
private static ClassMetadata scanClass(Class<?> type) {
|
||||
if (!type.isAnnotationPresent(GuiEditable.class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ClassMetadata parent = null;
|
||||
|
||||
Class<?> superCls = type.getSuperclass();
|
||||
if (superCls != null && superCls != Object.class) {
|
||||
parent = getMetadata(superCls);
|
||||
}
|
||||
|
||||
List<EditableField> list = new ArrayList<>();
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
EditableField editableField = createEditableField(field);
|
||||
|
||||
if (editableField != null) {
|
||||
list.add(editableField);
|
||||
}
|
||||
}
|
||||
|
||||
return new ClassMetadata(
|
||||
type,
|
||||
list.toArray(list.toArray(new EditableField[0])),
|
||||
parent
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"RawUseOfParameterized", "unchecked"})
|
||||
private static EditableField createEditableField(Field field) {
|
||||
try {
|
||||
GuiEditable.Field fieldConfig = field.getAnnotation(GuiEditable.Field.class);
|
||||
if (fieldConfig == null) return null;
|
||||
if (Modifier.isFinal(field.getModifiers())) return null;
|
||||
|
||||
field.setAccessible(true);
|
||||
EditableField.Builder builder = new EditableField.Builder();
|
||||
builder.name(fieldConfig.name());
|
||||
builder.type(field.getType());
|
||||
builder.getter(LOOKUP.unreflectGetter(field));
|
||||
builder.setter(LOOKUP.unreflectSetter(field));
|
||||
builder.constraints(buildConstraints(field));
|
||||
|
||||
|
||||
return builder.build();
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to bind field: " + field, e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"RawUseOfParameterized", "unchecked"})
|
||||
private static FieldConstraints buildConstraints(Field field) {
|
||||
List<FieldConstraint> list = new ArrayList<>();
|
||||
|
||||
for (Annotation ann : field.getAnnotations()) {
|
||||
ConstraintFactory factory = registry.get(ann.annotationType());
|
||||
if (factory != null) {
|
||||
FieldConstraint c = factory.create(field, ann);
|
||||
list.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (list.isEmpty()) return FieldConstraints.empty();
|
||||
return new FieldConstraints(list.toArray(new FieldConstraint[0]));
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings({"RawUseOfParameterized"})
|
||||
public static final class ClassMetadata {
|
||||
final Class<?> type;
|
||||
final EditableField[] fields; // only declared fields
|
||||
final ClassMetadata parent; // superclass metadata (nullable)
|
||||
|
||||
ClassMetadata(Class<?> type, EditableField[] fields, ClassMetadata parent) {
|
||||
this.type = type;
|
||||
this.fields = fields;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public ClassMetadata getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public EditableField[] getDeclaredFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
public Class<?> getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package somehussar.gui.constraints;
|
||||
|
||||
import somehussar.gui.FieldConstraint;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public interface ConstraintFactory<A extends Annotation> {
|
||||
FieldConstraint create(Field field, A annotation);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package somehussar.gui.constraints;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(value = RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface NumericConstraint {
|
||||
double min() default Double.NEGATIVE_INFINITY;
|
||||
double max() default Double.POSITIVE_INFINITY;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package somehussar.gui.constraints;
|
||||
|
||||
import somehussar.gui.FieldConstraint;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public class NumericConstraintFactory implements ConstraintFactory<NumericConstraint> {
|
||||
|
||||
@Override
|
||||
public FieldConstraint create(Field field, NumericConstraint ann) {
|
||||
return new NumericRangeConstraint(ann.min(), ann.max());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package somehussar.gui.constraints;
|
||||
|
||||
import somehussar.gui.FieldConstraint;
|
||||
|
||||
public final class NumericRangeConstraint implements FieldConstraint {
|
||||
|
||||
private final double min;
|
||||
private final double max;
|
||||
|
||||
public NumericRangeConstraint(double min, double max) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Object value) {
|
||||
if (!(value instanceof Number)) {
|
||||
throw new ConstraintViolationException("Expected number, got " + value);
|
||||
}
|
||||
|
||||
double v = ((Number) value).doubleValue();
|
||||
if (v < min || v > max) {
|
||||
throw new ConstraintViolationException(
|
||||
"Value " + v + " must be between " + min + " and " + max
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ConstraintViolationException extends RuntimeException {
|
||||
public ConstraintViolationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package somehussar.gui.field;
|
||||
|
||||
import somehussar.gui.FieldConstraints;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
|
||||
public final class EditableField<T> {
|
||||
|
||||
private final String name;
|
||||
private final Class<T> type;
|
||||
private final MethodHandle getter;
|
||||
private final MethodHandle setter;
|
||||
private final FieldConstraints constraints;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Class<?> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public T read(T instance) {
|
||||
try {
|
||||
return (T) getter.invoke(instance);
|
||||
} catch (Throwable t) {
|
||||
throw new RuntimeException("Failed to read field " + name, t);
|
||||
}
|
||||
}
|
||||
|
||||
public void write(T instance, T value) {
|
||||
constraints.validate(value);
|
||||
try {
|
||||
setter.invoke(instance, value);
|
||||
} catch (Throwable t) {
|
||||
throw new RuntimeException("Failed to write field " + name, t);
|
||||
}
|
||||
}
|
||||
|
||||
public FieldConstraints getConstraints() {
|
||||
return this.constraints;
|
||||
}
|
||||
|
||||
EditableField(Builder<T> b) {
|
||||
this.name = b.name;
|
||||
this.type = b.type;
|
||||
this.getter = b.getter;
|
||||
this.setter = b.setter;
|
||||
this.constraints = b.constraints;
|
||||
}
|
||||
|
||||
public static class Builder<T> {
|
||||
private String name;
|
||||
private Class<T> type;
|
||||
private MethodHandle getter;
|
||||
private MethodHandle setter;
|
||||
private FieldConstraints constraints;
|
||||
|
||||
public Builder<T> name(String v) { this.name = v; return this; }
|
||||
public Builder<T> type(Class<T> v) { this.type = v; return this; }
|
||||
public Builder<T> getter(MethodHandle v) { this.getter = v; return this; }
|
||||
public Builder<T> setter(MethodHandle v) { this.setter = v; return this; }
|
||||
public Builder<T> constraints(FieldConstraints v) { this.constraints = v; return this; }
|
||||
|
||||
public EditableField<T> build() {
|
||||
return new EditableField(this);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user